xref: /aosp_15_r20/external/swiftshader/tests/regres/git/git.go (revision 03ce13f70fcc45d86ee91b7ee4cab1936a95046e)
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