devtools/{depotview,hackdoc}: tie both together
Change-Id: I0a1ca3b4fa0e0a074eccbe0f8748839b926db9c1
diff --git a/devtools/depotview/service/service.go b/devtools/depotview/service/service.go
index 910bf36..fad2029 100644
--- a/devtools/depotview/service/service.go
+++ b/devtools/depotview/service/service.go
@@ -2,6 +2,7 @@
import (
"context"
+ "fmt"
"io"
"regexp"
"strings"
@@ -13,6 +14,7 @@
"google.golang.org/grpc/status"
git "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object"
@@ -30,9 +32,9 @@
remote string
storer storage.Storer
- mu sync.Mutex
- repo *git.Repository
- lastPull time.Time
+ mu sync.Mutex
+ repo *git.Repository
+ lastFetch time.Time
}
func New(remote string) *Service {
@@ -42,10 +44,10 @@
}
}
-func (s *Service) ensureRepo() error {
+func (s *Service) ensureRepo(ctx context.Context) error {
// Clone repository if necessary.
if s.repo == nil {
- repo, err := git.Clone(s.storer, nil, &git.CloneOptions{
+ repo, err := git.CloneContext(ctx, s.storer, nil, &git.CloneOptions{
URL: s.remote,
})
if err != nil {
@@ -53,17 +55,24 @@
return status.Error(codes.Unavailable, "could not clone repository")
}
s.repo = repo
- s.lastPull = time.Now()
}
// Fetch if necessary.
- if time.Since(s.lastPull) > time.Minute {
- err := s.repo.Fetch(&git.FetchOptions{})
+ if time.Since(s.lastFetch) > 10*time.Second {
+ glog.Infof("Fetching...")
+ err := s.repo.FetchContext(ctx, &git.FetchOptions{
+ RefSpecs: []config.RefSpec{
+ config.RefSpec("+refs/heads/*:refs/remotes/origin/*"),
+ config.RefSpec("+refs/changes/*:refs/changes/*"),
+ },
+ Force: true,
+ })
if err != nil && err != git.NoErrAlreadyUpToDate {
glog.Errorf("Fetch(): %v", err)
} else {
- s.lastPull = time.Now()
+ s.lastFetch = time.Now()
}
+
}
return nil
@@ -77,22 +86,69 @@
return nil, status.Error(codes.InvalidArgument, "ref must be set")
}
- if err := s.ensureRepo(); err != nil {
+ if err := s.ensureRepo(ctx); err != nil {
return nil, err
}
h, err := s.repo.ResolveRevision(plumbing.Revision(req.Ref))
switch {
case err == plumbing.ErrReferenceNotFound:
- return &pb.ResolveResponse{Hash: "", LastChecked: s.lastPull.UnixNano()}, nil
+ return &pb.ResolveResponse{Hash: "", LastChecked: s.lastFetch.UnixNano()}, nil
case err != nil:
return nil, status.Errorf(codes.Unavailable, "git resolve error: %v", err)
default:
- return &pb.ResolveResponse{Hash: h.String(), LastChecked: s.lastPull.UnixNano()}, nil
+ return &pb.ResolveResponse{Hash: h.String(), LastChecked: s.lastFetch.UnixNano()}, nil
}
}
-func (s *Service) getFile(hash, path string, notFoundOkay bool) (*object.File, error) {
+func (s *Service) ResolveGerritChange(ctx context.Context, req *pb.ResolveGerritChangeRequest) (*pb.ResolveGerritChangeResponse, error) {
+ if err := s.ensureRepo(ctx); err != nil {
+ return nil, err
+ }
+
+ // I'm totally guessing this, from these examples:
+ // refs/changes/03/3/meta
+ // refs/changes/77/77/meta
+ // refs/changes/47/247/meta
+ // etc...
+ shard := fmt.Sprintf("%02d", req.Change%100)
+ metaRef := fmt.Sprintf("refs/changes/%s/%d/meta", shard, req.Change)
+
+ h, err := s.repo.ResolveRevision(plumbing.Revision(metaRef))
+ switch {
+ case err == plumbing.ErrReferenceNotFound:
+ return &pb.ResolveGerritChangeResponse{Hash: "", LastChecked: s.lastFetch.UnixNano()}, nil
+ case err != nil:
+ return nil, status.Errorf(codes.Unavailable, "git metadata resolve error: %v", err)
+ }
+
+ c, err := s.repo.CommitObject(*h)
+ if err != nil {
+ return nil, status.Errorf(codes.Unavailable, "git error: %v", err)
+ }
+
+ var messages []string
+ for {
+ messages = append([]string{c.Message}, messages...)
+
+ if len(c.ParentHashes) != 1 {
+ break
+ }
+
+ c, err = s.repo.CommitObject(c.ParentHashes[0])
+ if err != nil {
+ return nil, status.Errorf(codes.Unavailable, "git error: %v", err)
+ }
+ }
+
+ meta := parseGerritMetadata(messages)
+ if meta == nil {
+ return nil, status.Errorf(codes.Internal, "could not parse gerrit metadata for ref %q", metaRef)
+ }
+ return &pb.ResolveGerritChangeResponse{Hash: meta.commit, LastChecked: s.lastFetch.UnixNano()}, nil
+}
+
+func (s *Service) getFile(ctx context.Context, hash, path string, notFoundOkay bool) (*object.File, error) {
if !reHash.MatchString(hash) {
return nil, status.Error(codes.InvalidArgument, "hash must be valid full git hash string")
}
@@ -105,7 +161,7 @@
return nil, status.Error(codes.InvalidArgument, "path must be a valid unix or depot-style path")
}
- if err := s.ensureRepo(); err != nil {
+ if err := s.ensureRepo(ctx); err != nil {
return nil, err
}
@@ -134,7 +190,7 @@
s.mu.Lock()
defer s.mu.Unlock()
- file, err := s.getFile(req.Hash, req.Path, true)
+ file, err := s.getFile(ctx, req.Hash, req.Path, true)
if err != nil {
return nil, err
}
@@ -157,7 +213,9 @@
s.mu.Lock()
defer s.mu.Unlock()
- file, err := s.getFile(req.Hash, req.Path, false)
+ ctx := srv.Context()
+
+ file, err := s.getFile(ctx, req.Hash, req.Path, false)
if err != nil {
return err
}
@@ -168,7 +226,6 @@
}
defer reader.Close()
- ctx := srv.Context()
for {
if ctx.Err() != nil {
return ctx.Err()