| Index: go/src/infra/libs/git/repo/repo.go
|
| diff --git a/go/src/infra/libs/git/repo/repo.go b/go/src/infra/libs/git/repo/repo.go
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..ac70a6b70f356d6215c5f98590fb4486563951a9
|
| --- /dev/null
|
| +++ b/go/src/infra/libs/git/repo/repo.go
|
| @@ -0,0 +1,345 @@
|
| +// Copyright 2014 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +package repo
|
| +
|
| +import (
|
| + "bufio"
|
| + "bytes"
|
| + "fmt"
|
| + "io"
|
| + "os/exec"
|
| + "strconv"
|
| + "strings"
|
| +
|
| + "infra/libs/infra_util"
|
| +
|
| + . "infra/libs/git"
|
| +)
|
| +
|
| +// Types ///////////////////////////////////////////////////////////////////////
|
| +
|
| +// Repo represents a local git repository at the path |Path|.
|
| +type Repo struct {
|
| + // The path on disk to the location of the git repo.
|
| + path string
|
| +
|
| + catFile catFileService
|
| + catFileCheck catFileService
|
| +}
|
| +
|
| +// Constructors ///////////////////////////////////////////////////////////////
|
| +
|
| +func NewRepo(path string) *Repo {
|
| + return &Repo{path,
|
| + newCatFileService(path, false),
|
| + newCatFileService(path, true),
|
| + }
|
| +}
|
| +
|
| +// Member functions ////////////////////////////////////////////////////////////
|
| +
|
| +func (r *Repo) Path() string { return r.path }
|
| +
|
| +// RunInput runs a git command in the Repo, passing input on stdin, and returning
|
| +// the stdout and success (i.e. did the git command return 0?)
|
| +func (r *Repo) RunInput(input string, cmd ...string) (string, bool) {
|
| + ex := exec.Command("git", cmd...)
|
| + ex.Dir = r.Path()
|
| + ex.Stdin = bytes.NewBufferString(input)
|
| + out, err := ex.Output()
|
| + if err == nil {
|
| + return string(out), true
|
| + } else if _, ok := err.(*exec.ExitError); ok {
|
| + return string(out), false
|
| + } else {
|
| + panic(err)
|
| + }
|
| +}
|
| +
|
| +// Run runs a git command in the Repo, with no input, returning the stdout and
|
| +// success.
|
| +func (r *Repo) Run(cmd ...string) (string, bool) {
|
| + return r.RunInput("", cmd...)
|
| +}
|
| +
|
| +// RunOk runs a git command in the repo with no input, and returns its success
|
| +// (did it exit 0?)
|
| +func (r *Repo) RunOk(cmd ...string) bool {
|
| + _, ok := r.Run(cmd...)
|
| + return ok
|
| +}
|
| +
|
| +// MustRunOk is the same as RunOutput, but it panic's if the command fails
|
| +func (r *Repo) MustRunOk(cmd ...string) {
|
| + _, ok := r.Run(cmd...)
|
| + if !ok {
|
| + panic(fmt.Errorf("in (%s) faild to run %v", r.path, append([]string{"git"}, cmd...)))
|
| + }
|
| +}
|
| +
|
| +// RunOutput runs a git command in the repo with no input, and returns its
|
| +// stdout
|
| +func (r *Repo) RunOutput(cmd ...string) string {
|
| + out, _ := r.Run(cmd...)
|
| + return out
|
| +}
|
| +
|
| +// RunOutputS is the same as RunOutput, except it strips the trailing newline
|
| +func (r *Repo) RunOutputS(cmd ...string) string {
|
| + out, _ := r.Run(cmd...)
|
| + return strings.TrimRight(out, "\n")
|
| +}
|
| +
|
| +// MustRunOutput is the same as RunOutput, but it panic's if the command fails
|
| +func (r *Repo) MustRunOutput(cmd ...string) string {
|
| + out, ok := r.Run(cmd...)
|
| + if !ok {
|
| + panic(fmt.Errorf("in (%s) faild to run %v", r.path, append([]string{"git"}, cmd...)))
|
| + }
|
| + return out
|
| +}
|
| +
|
| +// GetObject retrieves a real representation of objectish.
|
| +//
|
| +// This may return MissingError if the object is not found.
|
| +func (r *Repo) GetObject(objectish string) (InternableObject, error) {
|
| + rsp := r.catFile.request(objectish)
|
| + if rsp == nil {
|
| + return nil, nil
|
| + }
|
| + return ObjectFromRawWithID(rsp.id, rsp.typ, rsp.data)
|
| +}
|
| +
|
| +func (r *Repo) GetObjectID(id Identifiable) (InternableObject, error) {
|
| + return r.GetObject(id.ID().String())
|
| +}
|
| +
|
| +func (r *Repo) ObjectInfo(objectish string) *ObjectInfo {
|
| + rslt := r.catFileCheck.request(objectish)
|
| + if rslt != nil {
|
| + return &ObjectInfo{rslt.typ, rslt.size, rslt.id}
|
| + } else {
|
| + return nil
|
| + }
|
| +}
|
| +
|
| +func (r *Repo) ObjectInfoID(id Identifiable) *ObjectInfo {
|
| + return r.ObjectInfo(id.ID().String())
|
| +}
|
| +
|
| +func (r *Repo) HasObject(objectish string) bool {
|
| + return r.ObjectInfo(objectish) != nil
|
| +}
|
| +
|
| +func (r *Repo) HasObjectID(id Identifiable) bool {
|
| + return r.ObjectInfoID(id) != nil
|
| +}
|
| +
|
| +func (r *Repo) GetTextDiff(left, right string) (string, error) {
|
| + rslt, ok := r.Run("diff", left, right)
|
| + if !ok {
|
| + return "", fmt.Errorf("cannot diff(%s, %s): %s", left, right, rslt)
|
| + }
|
| + return rslt, nil
|
| +}
|
| +
|
| +// DiffTree computes the 2-tree diff (with copy/rename detection) and returns
|
| +// a parsed TreeDiff of what it found.
|
| +//
|
| +// This diff-tree invocation is done with -t, which implies that it is recursive,
|
| +// and that the actual intermediate tree objects will also be contianed in the
|
| +// return value.
|
| +func (r *Repo) DiffTree(left, right string) (ret TreeDiff, err error) {
|
| + atoi := func(s string, base int) int {
|
| + ret, err := strconv.ParseInt(s, base, 0)
|
| + if err != nil {
|
| + panic(err)
|
| + }
|
| + return int(ret)
|
| + }
|
| +
|
| + lines := strings.Split(strings.TrimRight(
|
| + r.RunOutput("diff-tree", "-t", "-z", "-M", "-M", "-C", left, right), "\000"),
|
| + "\000")
|
| +
|
| + infoStream := make(chan string, len(lines))
|
| + for _, line := range lines {
|
| + infoStream <- line
|
| + }
|
| + close(infoStream)
|
| + for header := range infoStream {
|
| + if len(header) == 0 {
|
| + break
|
| + }
|
| + if header[0] != ':' {
|
| + return nil, fmt.Errorf("git.DiffTree: desynchronized parsing error")
|
| + }
|
| + info := strings.Fields(strings.TrimLeft(header, ":"))
|
| + // old_mode new_mode old_id new_id action
|
| + // oldPath (if action[0] in "RC")
|
| + // newPath
|
| + action := info[4]
|
| + similarity := 0
|
| + oldPath := <-infoStream
|
| + newPath := oldPath
|
| + if action[0] == 'R' || action[0] == 'C' {
|
| + newPath = <-infoStream
|
| + similarity = atoi(action[1:], 10)
|
| + }
|
| +
|
| + old_id, err := MakeObjectIDErr(info[2])
|
| + if err != nil {
|
| + return nil, err
|
| + }
|
| + old_c, err := NewEmptyChild(Mode(atoi(info[0], 8)), old_id)
|
| + if err != nil {
|
| + return nil, err
|
| + }
|
| +
|
| + new_id, err := MakeObjectIDErr(info[3])
|
| + if err != nil {
|
| + return nil, err
|
| + }
|
| + new_c, err := NewEmptyChild(Mode(atoi(info[1], 8)), new_id)
|
| + if err != nil {
|
| + return nil, err
|
| + }
|
| +
|
| + ret = append(ret, TreeDiffEntry{
|
| + Action: action,
|
| + Similarity: similarity,
|
| + Old: TreeDiffEntryHalf{
|
| + *old_c,
|
| + oldPath,
|
| + },
|
| + New: TreeDiffEntryHalf{
|
| + *new_c,
|
| + newPath,
|
| + },
|
| + })
|
| + }
|
| +
|
| + return
|
| +}
|
| +
|
| +// Intern takes an InternableObject (Blob, Tree, Commit), and writes it into
|
| +// the on-disk Repo.
|
| +func (r *Repo) Intern(obj InternableObject) (*ObjectID, error) {
|
| + return InternImpl(r, obj, func(data string) (string, error) {
|
| + cmd := []string{"hash-object", "-t", obj.Type().String(), "-w", "--stdin"}
|
| + out, ok := r.RunInput(data, cmd...)
|
| + if !ok {
|
| + return "", fmt.Errorf("error running %s <- %s: not ok", cmd, data)
|
| + }
|
| + return string(out), nil
|
| + })
|
| +}
|
| +
|
| +/// Private
|
| +
|
| +type catFileService struct {
|
| + path string
|
| + ch chan<- catFileRequest
|
| +}
|
| +
|
| +type catFileRequest struct {
|
| + objectish string
|
| + reply chan<- *catFileReply
|
| +}
|
| +
|
| +type catFileReply struct {
|
| + id *ObjectID
|
| + typ ObjectType
|
| + size int
|
| + data []byte
|
| +}
|
| +
|
| +func (c *catFileService) request(objectish string) *catFileReply {
|
| + if strings.ContainsAny(objectish, "\n") {
|
| + // These requests are generated internally from the library. The caller
|
| + // should check or recover if they think that objectish can contain
|
| + // a newline.
|
| + panic(fmt.Errorf("catFile request may not contain a newline"))
|
| + }
|
| +
|
| + rchan := make(chan *catFileReply, 1)
|
| + c.ch <- catFileRequest{objectish, rchan}
|
| + return <-rchan
|
| +}
|
| +
|
| +func newCatFileService(path string, checkOnly bool) catFileService {
|
| + ch := make(chan catFileRequest)
|
| + batchMode := "--batch"
|
| + if checkOnly {
|
| + batchMode = "--batch-check"
|
| + }
|
| + go func() {
|
| + var err error
|
| + var in io.WriteCloser
|
| + var nom func(byte) string
|
| + var yoink func(int) []byte
|
| + var out *bufio.Reader
|
| +
|
| + started := false
|
| + for req := range ch {
|
| + if !started {
|
| + started = true
|
| + catFile := exec.Command("git", "cat-file", batchMode)
|
| + catFile.Dir = path
|
| +
|
| + in, err = catFile.StdinPipe()
|
| + if err != nil {
|
| + panic(err)
|
| + }
|
| + defer in.Close()
|
| +
|
| + outRaw, err := catFile.StdoutPipe()
|
| + if err != nil {
|
| + panic(err)
|
| + }
|
| + defer outRaw.Close()
|
| +
|
| + if err = catFile.Start(); err != nil {
|
| + panic(err)
|
| + }
|
| +
|
| + out = bufio.NewReader(outRaw)
|
| + nom = infra_util.Nom(out)
|
| + yoink = infra_util.Yoink(out)
|
| + }
|
| +
|
| + func() {
|
| + defer close(req.reply)
|
| + in.Write([]byte(req.objectish + "\n"))
|
| + rsp := nom('\n')
|
| + if strings.HasSuffix(rsp, " missing") {
|
| + req.reply <- nil
|
| + return
|
| + }
|
| +
|
| + parts := strings.Split(rsp, " ")
|
| + objID, typ, sizeStr := parts[0], parts[1], parts[2]
|
| + size, err := strconv.ParseUint(sizeStr, 10, 64)
|
| + if err != nil {
|
| + panic(err) // git itself returned a non-integer size field!?
|
| + }
|
| +
|
| + data := []byte{}
|
| + if !checkOnly {
|
| + data = yoink(int(size))
|
| + out.ReadByte() // drop extra newline
|
| + }
|
| + req.reply <- &catFileReply{
|
| + id: MakeObjectID(objID),
|
| + typ: MakeObjectType(typ),
|
| + data: data,
|
| + size: int(size),
|
| + }
|
| + }()
|
| + }
|
| + }()
|
| +
|
| + return catFileService{path, ch}
|
| +}
|
|
|