You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

254 lines
6.7 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// Copyright 2019 The SwiftShader Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package git provides functions for interacting with Git.
package git
import (
"encoding/hex"
"fmt"
"io/ioutil"
"net/url"
"os"
"os/exec"
"strings"
"time"
"../cause"
"../shell"
)
const (
gitTimeout = time.Minute * 15 // timeout for a git operation
)
var exe string
func init() {
path, err := exec.LookPath("git")
if err != nil {
panic(cause.Wrap(err, "Couldn't find path to git executable"))
}
exe = path
}
// Hash is a 20 byte, git object hash.
type Hash [20]byte
func (h Hash) String() string { return hex.EncodeToString(h[:]) }
// ParseHash returns a Hash from a hexadecimal string.
func ParseHash(s string) Hash {
b, _ := hex.DecodeString(s)
h := Hash{}
copy(h[:], b)
return h
}
// Add calls 'git add <file>'.
func Add(wd, file string) error {
if err := shell.Shell(gitTimeout, exe, wd, "add", file); err != nil {
return cause.Wrap(err, "`git add %v` in working directory %v failed", file, wd)
}
return nil
}
// CommitFlags advanced flags for Commit
type CommitFlags struct {
Name string // Used for author and committer
Email string // Used for author and committer
}
// Commit calls 'git commit -m <msg> --author <author>'.
func Commit(wd, msg string, flags CommitFlags) error {
args := []string{}
if flags.Name != "" {
args = append(args, "-c", "user.name="+flags.Name)
}
if flags.Email != "" {
args = append(args, "-c", "user.email="+flags.Email)
}
args = append(args, "commit", "-m", msg)
return shell.Shell(gitTimeout, exe, wd, args...)
}
// PushFlags advanced flags for Commit
type PushFlags struct {
Username string // Used for authentication when uploading
Password string // Used for authentication when uploading
}
// Push pushes the local branch to remote.
func Push(wd, remote, localBranch, remoteBranch string, flags PushFlags) error {
args := []string{}
if flags.Username != "" {
f, err := ioutil.TempFile("", "regres-cookies.txt")
if err != nil {
return cause.Wrap(err, "Couldn't create cookie file")
}
defer f.Close()
defer os.Remove(f.Name())
u, err := url.Parse(remote)
if err != nil {
return cause.Wrap(err, "Couldn't parse url '%v'", remote)
}
f.WriteString(fmt.Sprintf("%v FALSE / TRUE 2147483647 o %v=%v\n", u.Host, flags.Username, flags.Password))
f.Close()
args = append(args, "-c", "http.cookiefile="+f.Name())
}
args = append(args, "push", remote, localBranch+":"+remoteBranch)
return shell.Shell(gitTimeout, exe, wd, args...)
}
// CheckoutRemoteBranch performs a git fetch and checkout of the given branch into path.
func CheckoutRemoteBranch(path, url string, branch string) error {
if err := os.MkdirAll(path, 0777); err != nil {
return cause.Wrap(err, "mkdir '"+path+"' failed")
}
for _, cmds := range [][]string{
{"init"},
{"remote", "add", "origin", url},
{"fetch", "origin", "--depth=1", branch},
{"checkout", branch},
} {
if err := shell.Shell(gitTimeout, exe, path, cmds...); err != nil {
os.RemoveAll(path)
return err
}
}
return nil
}
// CheckoutRemoteCommit performs a git fetch and checkout of the given commit into path.
func CheckoutRemoteCommit(path, url string, commit Hash) error {
if err := os.MkdirAll(path, 0777); err != nil {
return cause.Wrap(err, "mkdir '"+path+"' failed")
}
for _, cmds := range [][]string{
{"init"},
{"remote", "add", "origin", url},
{"fetch", "origin", "--depth=1", commit.String()},
{"checkout", commit.String()},
} {
if err := shell.Shell(gitTimeout, exe, path, cmds...); err != nil {
os.RemoveAll(path)
return err
}
}
return nil
}
// CheckoutCommit performs a git checkout of the given commit.
func CheckoutCommit(path string, commit Hash) error {
return shell.Shell(gitTimeout, exe, path, "checkout", commit.String())
}
// Apply applys the patch file to the git repo at dir.
func Apply(dir, patch string) error {
return shell.Shell(gitTimeout, exe, dir, "apply", patch)
}
// FetchRefHash returns the git hash of the given ref.
func FetchRefHash(ref, url string) (Hash, error) {
out, err := shell.Exec(gitTimeout, exe, "", nil, "ls-remote", url, ref)
if err != nil {
return Hash{}, err
}
return ParseHash(string(out)), nil
}
type ChangeList struct {
Hash Hash
Date time.Time
Author string
Subject string
Description string
}
// Log returns the top count ChangeLists at HEAD.
func Log(path string, count int) ([]ChangeList, error) {
return LogFrom(path, "HEAD", count)
}
// LogFrom returns the top count ChangeList starting from at.
func LogFrom(path, at string, count int) ([]ChangeList, error) {
if at == "" {
at = "HEAD"
}
out, err := shell.Exec(gitTimeout, exe, "", nil, "log", at, "--pretty=format:"+prettyFormat, fmt.Sprintf("-%d", count), path)
if err != nil {
return nil, err
}
return parseLog(string(out)), nil
}
// Parent returns the parent ChangeList for cl.
func Parent(cl ChangeList) (ChangeList, error) {
out, err := shell.Exec(gitTimeout, exe, "", nil, "log", "--pretty=format:"+prettyFormat, fmt.Sprintf("%v^", cl.Hash))
if err != nil {
return ChangeList{}, err
}
cls := parseLog(string(out))
if len(cls) == 0 {
return ChangeList{}, fmt.Errorf("Unexpected output")
}
return cls[0], nil
}
// HeadCL returns the HEAD ChangeList at the given commit/tag/branch.
func HeadCL(path string) (ChangeList, error) {
cls, err := LogFrom(path, "HEAD", 1)
if err != nil {
return ChangeList{}, err
}
if len(cls) == 0 {
return ChangeList{}, fmt.Errorf("No commits found")
}
return cls[0], nil
}
// Show content of the file at path for the given commit/tag/branch.
func Show(path, at string) ([]byte, error) {
return shell.Exec(gitTimeout, exe, "", nil, "show", at+":"+path)
}
const prettyFormat = "ǁ%Hǀ%cIǀ%an <%ae>ǀ%sǀ%b"
func parseLog(str string) []ChangeList {
msgs := strings.Split(str, "ǁ")
cls := make([]ChangeList, 0, len(msgs))
for _, s := range msgs {
if parts := strings.Split(s, "ǀ"); len(parts) == 5 {
cl := ChangeList{
Hash: ParseHash(parts[0]),
Author: strings.TrimSpace(parts[2]),
Subject: strings.TrimSpace(parts[3]),
Description: strings.TrimSpace(parts[4]),
}
date, err := time.Parse(time.RFC3339, parts[1])
if err != nil {
panic(err)
}
cl.Date = date
cls = append(cls, cl)
}
}
return cls
}