blob: efcfb200e532d8dd83fdedb30322c1cc191b6ede [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// 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
import (
"bytes"
"compress/gzip"
"encoding/hex"
"fmt"
"io/ioutil"
"regexp"
"golang.org/x/net/context"
"github.com/golang/protobuf/proto"
"github.com/luci/gae/service/memcache"
"github.com/luci/luci-go/common/api/gitiles"
"github.com/luci/luci-go/common/errors"
"github.com/luci/luci-go/common/logging"
"github.com/luci/luci-go/common/proto/google"
milo "github.com/luci/luci-go/milo/api/proto"
)
var gitHash = regexp.MustCompile("[0-9a-fA-F]{40}")
// Resolve resolves a commitish to a git commit hash.
//
// This operation will assumed to be either fully local (in the case that
// commitish is already a git-hash-looking-thing), or local to the datastore (in
// the case that commitish is a fully-qualified-ref, ref tables are populated by
// a backend cron).
//
// If commitish is some other pattern (e.g. "HEAD~"), this will return the
// commitish as-is.
//
// `resolved` will be true if `commit` is, in fact, a git hash; if it's false,
// then higher layers should be careful about using it as part of a cache key.
func Resolve(c context.Context, url, commitish string) (commit string, resolved bool, err error) {
if gitHash.MatchString(commitish) {
return commitish, true, nil
}
// TODO(iannucci): actually do lookup and cache it. Maybe have a backend cron
// which refreshes the entire refs space?
return commitish, false, nil
}
// protoCache maintains a single memcache entry containing a gzipped
// proto.Message.
//
// Args:
// - enabled: if false, bypass the cache and return get() directly.
// - key: the memcache key to read/write
// - out: the proto.Message which should be populated from the cache. This is
// always a pointer-to-a-proto-struct.
// - get: populates `out` 'the slow way' returning an error if encountered.
//
// Example:
// useCache := cachingEnabled()
// obj := &mypb.Object{} // some protobuf object
// return obj, protoCache(c, useCache, "memcache_key", func() error {
// return PopulateFromNetwork(obj)
// })
func protoCache(c context.Context, enabled bool, key string, out proto.Message, get func() error) error {
if !enabled {
return get()
}
cacheEntry := memcache.NewItem(c, key+"|gz")
// try reading from cache
ok := func() bool {
switch err := memcache.Get(c, cacheEntry); err {
case nil:
r, err := gzip.NewReader(bytes.NewReader(cacheEntry.Value()))
if err != nil {
logging.WithError(err).Warningf(c, "making ungzip reader for memcache entry")
return false
}
data, err := ioutil.ReadAll(r)
if err != nil {
logging.WithError(err).Warningf(c, "ungzipping memcache entry")
return false
}
if err := proto.Unmarshal(data, out); err != nil {
logging.WithError(err).Warningf(c, "unmarshalling cache entry")
return false
}
return true
case memcache.ErrCacheMiss:
default:
logging.WithError(err).Warningf(c, "memcache lookup")
}
return false
}()
if ok {
return nil
}
if err := get(); err != nil {
return err
}
data, err := proto.Marshal(out)
if err != nil {
logging.WithError(err).Warningf(c, "marshaling proto")
return nil
}
var buf bytes.Buffer
wr := gzip.NewWriter(&buf)
wr.Write(data) // err is buffered on the writer till Close
if err := wr.Close(); err != nil {
logging.WithError(err).Warningf(c, "gzipping proto")
return nil
}
cacheEntry.SetValue(buf.Bytes())
if err := memcache.Set(c, cacheEntry); err != nil {
logging.WithError(err).Warningf(c, "memcache set")
}
return nil
}
// GetHistory makes a (cached) call to gitiles to obtain the ConsoleGitInfo for
// the given url, commitish and limit.
func GetHistory(c context.Context, url, commitish string, limit int) (*milo.ConsoleGitInfo, error) {
commitish, useCache, err := Resolve(c, url, commitish)
if err != nil {
return nil, errors.Annotate(err, "resolving %q", commitish).Err()
}
ret := &milo.ConsoleGitInfo{}
cacheKey := fmt.Sprintf("GetHistory|%s|%s|%d", url, commitish, limit)
err = protoCache(c, useCache, cacheKey, ret, func() error {
rawEntries, err := gitiles.Log(c, url, commitish, limit)
if err != nil {
return errors.Annotate(err, "GetHistory").Err()
}
ret.Commits = make([]*milo.ConsoleGitInfo_Commit, len(rawEntries))
for i, e := range rawEntries {
commit := &milo.ConsoleGitInfo_Commit{}
if commit.Hash, err = hex.DecodeString(e.Commit); err != nil {
return errors.Annotate(err, "commit is not hex (%q)", e.Commit).Err()
}
commit.AuthorName = e.Author.Name
commit.AuthorEmail = e.Author.Email
ts, err := e.Committer.GetTime()
if err != nil {
return errors.Annotate(err, "commit time unparsible (%q)", e.Committer.Time).Err()
}
commit.CommitTime = google.NewTimestamp(ts)
commit.Msg = e.Message
ret.Commits[i] = commit
}
return nil
})
return ret, err
}