devtools/{depotview,hackdoc}: tie both together
Change-Id: I0a1ca3b4fa0e0a074eccbe0f8748839b926db9c1
diff --git a/devtools/depotview/proto/depotview.proto b/devtools/depotview/proto/depotview.proto
index 2584961..b948fae 100644
--- a/devtools/depotview/proto/depotview.proto
+++ b/devtools/depotview/proto/depotview.proto
@@ -2,6 +2,18 @@
package depotview;
option go_package = "code.hackerspace.pl/hscloud/devtools/depotview/proto";
+service DepotView {
+ // Resolve a git branch/tag/ref... into a commit hash.
+ rpc Resolve(ResolveRequest) returns (ResolveResponse);
+
+ // Resolve a gerrit change number into a git commit hash.
+ rpc ResolveGerritChange(ResolveGerritChangeRequest) returns (ResolveGerritChangeResponse);
+
+ // Minimal file access API. It kinda stinks.
+ rpc Stat(StatRequest) returns (StatResponse);
+ rpc Read(ReadRequest) returns (stream ReadResponse);
+}
+
message ResolveRequest {
string ref = 1;
}
@@ -11,6 +23,15 @@
int64 last_checked = 2;
}
+message ResolveGerritChangeRequest {
+ int64 change = 1;
+}
+
+message ResolveGerritChangeResponse {
+ string hash = 1;
+ int64 last_checked = 2;
+}
+
message StatRequest {
string hash = 1;
string path = 2;
@@ -35,12 +56,3 @@
// Chunk of data. Empty once everything has been sent over.
bytes data = 1;
}
-
-service DepotView {
- // Resolve a git branch/tag/ref... into a commit hash.
- rpc Resolve(ResolveRequest) returns (ResolveResponse);
-
- // Minimal file access API. It kinda stinks.
- rpc Stat(StatRequest) returns (StatResponse);
- rpc Read(ReadRequest) returns (stream ReadResponse);
-}
diff --git a/devtools/depotview/service/BUILD.bazel b/devtools/depotview/service/BUILD.bazel
index 6f7337c..056ec30 100644
--- a/devtools/depotview/service/BUILD.bazel
+++ b/devtools/depotview/service/BUILD.bazel
@@ -2,12 +2,16 @@
go_library(
name = "go_default_library",
- srcs = ["service.go"],
+ srcs = [
+ "gerrit.go",
+ "service.go",
+ ],
importpath = "code.hackerspace.pl/hscloud/devtools/depotview/service",
visibility = ["//visibility:public"],
deps = [
"//devtools/depotview/proto:go_default_library",
"@com_github_go_git_go_git_v5//:go_default_library",
+ "@com_github_go_git_go_git_v5//config:go_default_library",
"@com_github_go_git_go_git_v5//plumbing:go_default_library",
"@com_github_go_git_go_git_v5//plumbing/filemode:go_default_library",
"@com_github_go_git_go_git_v5//plumbing/object:go_default_library",
diff --git a/devtools/depotview/service/gerrit.go b/devtools/depotview/service/gerrit.go
new file mode 100644
index 0000000..7dab9e6
--- /dev/null
+++ b/devtools/depotview/service/gerrit.go
@@ -0,0 +1,56 @@
+package service
+
+import (
+ "strconv"
+ "strings"
+)
+
+type gerritMeta struct {
+ patchSet int64
+ changeId string
+ commit string
+}
+
+// parseGerritMetadata takes a NoteDB metadata entry and extracts info from it.
+func parseGerritMetadata(messages []string) *gerritMeta {
+ meta := &gerritMeta{}
+
+ for _, message := range messages {
+ for _, line := range strings.Split(message, "\n") {
+ line = strings.TrimSpace(line)
+ if len(line) == 0 {
+ continue
+ }
+
+ parts := strings.SplitN(line, ":", 2)
+ if len(parts) < 2 {
+ continue
+ }
+ k, v := parts[0], strings.TrimSpace(parts[1])
+
+ switch k {
+ case "Patch-set":
+ n, err := strconv.ParseInt(v, 10, 64)
+ if err != nil {
+ continue
+ }
+ meta.patchSet = n
+ case "Change-id":
+ meta.changeId = v
+ case "Commit":
+ meta.commit = v
+ }
+ }
+ }
+
+ if meta.patchSet == 0 {
+ return nil
+ }
+ if meta.changeId == "" {
+ return nil
+ }
+ if meta.commit == "" {
+ return nil
+ }
+ return meta
+}
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()