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 85// PushFlags advanced flags for Commit 86type PushFlags struct { 87 Username string // Used for authentication when uploading 88 Password string // Used for authentication when uploading 89} 90 91// Push pushes the local branch to remote. 92func Push(wd, remote, localBranch, remoteBranch string, flags PushFlags) error { 93 args := []string{} 94 if flags.Username != "" { 95 f, err := ioutil.TempFile("", "regres-cookies.txt") 96 if err != nil { 97 return fmt.Errorf("failed to create cookie file: %w", err) 98 } 99 defer f.Close() 100 defer os.Remove(f.Name()) 101 u, err := url.Parse(remote) 102 if err != nil { 103 return fmt.Errorf("failed to parse url '%v': %w", remote, err) 104 } 105 f.WriteString(fmt.Sprintf("%v FALSE / TRUE 2147483647 o %v=%v\n", u.Host, flags.Username, flags.Password)) 106 f.Close() 107 args = append(args, "-c", "http.cookiefile="+f.Name()) 108 } 109 args = append(args, "push", remote, localBranch+":"+remoteBranch) 110 return shell.Shell(gitTimeout, exe, wd, args...) 111} 112 113// CheckoutRemoteBranch performs a git fetch and checkout of the given branch into path. 114func CheckoutRemoteBranch(path, url string, branch string) error { 115 if err := os.MkdirAll(path, 0777); err != nil { 116 return fmt.Errorf("mkdir '"+path+"' failed: %w", err) 117 } 118 119 for _, cmds := range [][]string{ 120 {"init"}, 121 {"remote", "add", "origin", url}, 122 {"fetch", "origin", "--depth=1", branch}, 123 {"checkout", branch}, 124 } { 125 if err := shell.Shell(gitTimeout, exe, path, cmds...); err != nil { 126 os.RemoveAll(path) 127 return err 128 } 129 } 130 131 return nil 132} 133 134// CheckoutRemoteCommit performs a git fetch and checkout of the given commit into path. 135func CheckoutRemoteCommit(path, url string, commit Hash) error { 136 if err := os.MkdirAll(path, 0777); err != nil { 137 return fmt.Errorf("mkdir '"+path+"' failed: %w", err) 138 } 139 140 for _, cmds := range [][]string{ 141 {"init"}, 142 {"remote", "add", "origin", url}, 143 {"fetch", "origin", "--depth=1", commit.String()}, 144 {"checkout", commit.String()}, 145 } { 146 if err := shell.Shell(gitTimeout, exe, path, cmds...); err != nil { 147 os.RemoveAll(path) 148 return err 149 } 150 } 151 152 return nil 153} 154 155// CheckoutCommit performs a git checkout of the given commit. 156func CheckoutCommit(path string, commit Hash) error { 157 return shell.Shell(gitTimeout, exe, path, "checkout", commit.String()) 158} 159 160// Apply applys the patch file to the git repo at dir. 161func Apply(dir, patch string) error { 162 return shell.Shell(gitTimeout, exe, dir, "apply", patch) 163} 164 165// FetchRefHash returns the git hash of the given ref. 166func FetchRefHash(ref, url string) (Hash, error) { 167 out, err := shell.Exec(gitTimeout, exe, "", nil, "", "ls-remote", url, ref) 168 if err != nil { 169 return Hash{}, err 170 } 171 return ParseHash(string(out)), nil 172} 173 174type ChangeList struct { 175 Hash Hash 176 Date time.Time 177 Author string 178 Subject string 179 Description string 180} 181 182// Log returns the top count ChangeLists at HEAD. 183func Log(path string, count int) ([]ChangeList, error) { 184 return LogFrom(path, "HEAD", count) 185} 186 187// LogFrom returns the top count ChangeList starting from at. 188func LogFrom(path, at string, count int) ([]ChangeList, error) { 189 if at == "" { 190 at = "HEAD" 191 } 192 out, err := shell.Exec(gitTimeout, exe, "", nil, "", "log", at, "--pretty=format:"+prettyFormat, fmt.Sprintf("-%d", count), path) 193 if err != nil { 194 return nil, err 195 } 196 return parseLog(string(out)), nil 197} 198 199// Parent returns the parent ChangeList for cl. 200func Parent(cl ChangeList) (ChangeList, error) { 201 out, err := shell.Exec(gitTimeout, exe, "", nil, "", "log", "--pretty=format:"+prettyFormat, fmt.Sprintf("%v^", cl.Hash)) 202 if err != nil { 203 return ChangeList{}, err 204 } 205 cls := parseLog(string(out)) 206 if len(cls) == 0 { 207 return ChangeList{}, fmt.Errorf("Unexpected output") 208 } 209 return cls[0], nil 210} 211 212// HeadCL returns the HEAD ChangeList at the given commit/tag/branch. 213func HeadCL(path string) (ChangeList, error) { 214 cls, err := LogFrom(path, "HEAD", 1) 215 if err != nil { 216 return ChangeList{}, err 217 } 218 if len(cls) == 0 { 219 return ChangeList{}, fmt.Errorf("No commits found") 220 } 221 return cls[0], nil 222} 223 224// Show content of the file at path for the given commit/tag/branch. 225func Show(path, at string) ([]byte, error) { 226 return shell.Exec(gitTimeout, exe, "", nil, "", "show", at+":"+path) 227} 228 229const prettyFormat = "ǁ%Hǀ%cIǀ%an <%ae>ǀ%sǀ%b" 230 231func parseLog(str string) []ChangeList { 232 msgs := strings.Split(str, "ǁ") 233 cls := make([]ChangeList, 0, len(msgs)) 234 for _, s := range msgs { 235 if parts := strings.Split(s, "ǀ"); len(parts) == 5 { 236 cl := ChangeList{ 237 Hash: ParseHash(parts[0]), 238 Author: strings.TrimSpace(parts[2]), 239 Subject: strings.TrimSpace(parts[3]), 240 Description: strings.TrimSpace(parts[4]), 241 } 242 date, err := time.Parse(time.RFC3339, parts[1]) 243 if err != nil { 244 panic(err) 245 } 246 cl.Date = date 247 248 cls = append(cls, cl) 249 } 250 } 251 return cls 252} 253