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