// 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 '. 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 --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 }