• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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