823 lines
24 KiB
Go
823 lines
24 KiB
Go
package gitrpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"connectrpc.com/connect"
|
|
"golang.org/x/crypto/ssh"
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
"simplegit/common"
|
|
"simplegit/gitcmd"
|
|
v1 "simplegit/gitrpc/v1"
|
|
"simplegit/gitrpc/v1/v1connect"
|
|
"simplegit/state"
|
|
)
|
|
|
|
type Server struct {
|
|
cfg *common.Config
|
|
startedAt time.Time
|
|
middleware *MiddlewareClient
|
|
mut state.IStateMut
|
|
}
|
|
|
|
func NewServer(cfg *common.Config, middleware *MiddlewareClient, mut state.IStateMut) *Server {
|
|
if err := cfg.Prepare(); err != nil {
|
|
return nil
|
|
}
|
|
return &Server{
|
|
cfg: cfg,
|
|
startedAt: time.Now(),
|
|
middleware: middleware,
|
|
mut: mut,
|
|
}
|
|
}
|
|
|
|
var (
|
|
_ v1connect.GitServiceHandler = (*Server)(nil)
|
|
_ v1connect.ManageServiceHandler = (*Server)(nil)
|
|
)
|
|
|
|
// ConnectHandler serves GitService (git kernel ops, per-repo authz).
|
|
func (s *Server) ConnectHandler() (string, http.Handler) {
|
|
return v1connect.NewGitServiceHandler(s, connect.WithInterceptors(s.authIntercept()))
|
|
}
|
|
|
|
// ManageConnectHandler serves ManageService (state.IStateMut: repo lifecycle +
|
|
// ACL) on a separate port (-manage). Unauthenticated -- the port is
|
|
// localhost-only and the caller (Updater/console) is trusted.
|
|
func (s *Server) ManageConnectHandler() (string, http.Handler) {
|
|
return v1connect.NewManageServiceHandler(s)
|
|
}
|
|
|
|
func (s *Server) authIntercept() connect.UnaryInterceptorFunc {
|
|
return func(next connect.UnaryFunc) connect.UnaryFunc {
|
|
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
|
|
if s.middleware != nil {
|
|
proc := req.Spec().Procedure
|
|
perm := permForProcedure(proc)
|
|
repo := repoFromRequest(req)
|
|
if repo == "" {
|
|
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("missing repo param"))
|
|
}
|
|
claims, status, msg := s.authorizeRequest(ctx, req.Header().Get("Authorization"), repo, perm)
|
|
if status != 0 {
|
|
return nil, statusToConnectError(status, msg)
|
|
}
|
|
if claims != nil {
|
|
ctx = WithClaims(ctx, claims)
|
|
}
|
|
}
|
|
return next(ctx, req)
|
|
}
|
|
}
|
|
}
|
|
|
|
func permForProcedure(proc string) common.Perm {
|
|
if i := strings.LastIndexByte(proc, '/'); i >= 0 {
|
|
proc = proc[i+1:]
|
|
}
|
|
switch proc {
|
|
case "CreateFile", "DeleteFile", "Merge":
|
|
return common.PermWrite
|
|
default:
|
|
return common.PermRead
|
|
}
|
|
}
|
|
|
|
func repoFromRequest(req connect.AnyRequest) string {
|
|
type repoGetter interface{ GetRepo() string }
|
|
if rg, ok := req.Any().(repoGetter); ok {
|
|
return rg.GetRepo()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *Server) authorizeRequest(ctx context.Context, authzHeader, repo string, perm common.Perm) (*Claims, int, string) {
|
|
if s.middleware == nil {
|
|
return nil, 0, ""
|
|
}
|
|
if !strings.HasPrefix(authzHeader, "Bearer ") {
|
|
return nil, http.StatusUnauthorized, "missing bearer token"
|
|
}
|
|
claims, err := s.middleware.verifier.Parse(strings.TrimPrefix(authzHeader, "Bearer "))
|
|
if err != nil {
|
|
return nil, http.StatusUnauthorized, "invalid or expired token"
|
|
}
|
|
if err := s.middleware.Authz(ctx, claims.UserID(), repo, perm); err != nil {
|
|
if errors.Is(err, ErrDenied) {
|
|
return nil, http.StatusForbidden, "access denied"
|
|
}
|
|
return nil, http.StatusBadGateway, "authz: " + err.Error()
|
|
}
|
|
return claims, 0, ""
|
|
}
|
|
|
|
func (s *Server) authorize(r *http.Request, repo string, perm common.Perm) (*Claims, int, string) {
|
|
return s.authorizeRequest(r.Context(), r.Header.Get("Authorization"), repo, perm)
|
|
}
|
|
|
|
func statusToConnectError(status int, msg string) error {
|
|
switch status {
|
|
case http.StatusUnauthorized:
|
|
return connect.NewError(connect.CodeUnauthenticated, errors.New(msg))
|
|
case http.StatusForbidden:
|
|
return connect.NewError(connect.CodePermissionDenied, errors.New(msg))
|
|
case http.StatusBadGateway:
|
|
return connect.NewError(connect.CodeUnavailable, errors.New(msg))
|
|
default:
|
|
return connect.NewError(connect.CodeInternal, errors.New(msg))
|
|
}
|
|
}
|
|
|
|
func toConnectError(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
var re *rpcError
|
|
if errors.As(err, &re) {
|
|
switch re.Code {
|
|
case codeRepoNotFound:
|
|
return connect.NewError(connect.CodeNotFound, errors.New(re.Message))
|
|
case codeInvalidParams:
|
|
return connect.NewError(connect.CodeInvalidArgument, errors.New(re.Message))
|
|
default:
|
|
return connect.NewError(connect.CodeInternal, errors.New(re.Message))
|
|
}
|
|
}
|
|
return connect.NewError(connect.CodeInternal, err)
|
|
}
|
|
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodGet && r.URL.Path == "/raw":
|
|
s.serveRaw(w, r)
|
|
case r.Method == http.MethodGet && r.URL.Path == "/archive":
|
|
s.serveArchive(w, r)
|
|
case r.Method == http.MethodGet && r.URL.Path == "/patch":
|
|
s.servePatch(w, r)
|
|
default:
|
|
w.Header().Set("Allow", "GET")
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (s *Server) open(ctx context.Context, repo string) (*gitcmd.Repository, error) {
|
|
p, err := common.Resolve(s.cfg.RepoRoot(), repo)
|
|
if err != nil {
|
|
return nil, newError(codeInvalidParams, "invalid repo name: "+err.Error())
|
|
}
|
|
r, err := gitcmd.OpenRepository(ctx, p)
|
|
if err != nil {
|
|
return nil, newError(codeRepoNotFound, "repository not on disk: "+repo)
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (s *Server) ListBranches(ctx context.Context, req *connect.Request[v1.RepoRequest]) (*connect.Response[v1.ListBranchesResponse], error) {
|
|
repo, err := s.open(ctx, req.Msg.Repo)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
branches, err := repo.ListBranches()
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
return connect.NewResponse(&v1.ListBranchesResponse{Branches: branchesToV1(branches)}), nil
|
|
}
|
|
|
|
func (s *Server) ListTags(ctx context.Context, req *connect.Request[v1.RepoRequest]) (*connect.Response[v1.ListTagsResponse], error) {
|
|
repo, err := s.open(ctx, req.Msg.Repo)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
tags, err := repo.ListTags()
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
return connect.NewResponse(&v1.ListTagsResponse{Tags: tagsToV1(tags)}), nil
|
|
}
|
|
|
|
func (s *Server) ListCommits(ctx context.Context, req *connect.Request[v1.ListCommitsRequest]) (*connect.Response[v1.ListCommitsResponse], error) {
|
|
repo, err := s.open(ctx, req.Msg.Repo)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
ref := req.Msg.Ref
|
|
if ref == "" {
|
|
ref = "HEAD"
|
|
}
|
|
commits, err := repo.ListCommits(ref, int(req.Msg.Limit))
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
return connect.NewResponse(&v1.ListCommitsResponse{Commits: commitsToV1(commits)}), nil
|
|
}
|
|
|
|
func (s *Server) ListCommitsByPath(ctx context.Context, req *connect.Request[v1.ListCommitsByPathRequest]) (*connect.Response[v1.ListCommitsResponse], error) {
|
|
repo, err := s.open(ctx, req.Msg.Repo)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
commits, err := repo.ListCommitsByPath(req.Msg.Ref, req.Msg.Path, int(req.Msg.Limit))
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
return connect.NewResponse(&v1.ListCommitsResponse{Commits: commitsToV1(commits)}), nil
|
|
}
|
|
|
|
func (s *Server) GetCommit(ctx context.Context, req *connect.Request[v1.GetCommitRequest]) (*connect.Response[v1.CommitDetail], error) {
|
|
repo, err := s.open(ctx, req.Msg.Repo)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
detail, err := repo.GetCommitDetail(req.Msg.Sha)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
return connect.NewResponse(commitDetailToV1(detail)), nil
|
|
}
|
|
|
|
func (s *Server) GetCommitDiff(ctx context.Context, req *connect.Request[v1.GetCommitDiffRequest]) (*connect.Response[v1.DiffResult], error) {
|
|
repo, err := s.open(ctx, req.Msg.Repo)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
diff, err := repo.CommitDiff(req.Msg.Sha)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
return connect.NewResponse(diffResultToV1(diff)), nil
|
|
}
|
|
|
|
func (s *Server) GetTree(ctx context.Context, req *connect.Request[v1.GetTreeRequest]) (*connect.Response[v1.Tree], error) {
|
|
repo, err := s.open(ctx, req.Msg.Repo)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
tree, err := repo.GetTree(req.Msg.Ref, req.Msg.Path)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
return connect.NewResponse(treeToV1(tree)), nil
|
|
}
|
|
|
|
func (s *Server) GetBlob(ctx context.Context, req *connect.Request[v1.GetBlobRequest]) (*connect.Response[v1.Blob], error) {
|
|
repo, err := s.open(ctx, req.Msg.Repo)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
blob, err := repo.GetBlob(req.Msg.Ref, req.Msg.Path)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
return connect.NewResponse(blobToV1(blob)), nil
|
|
}
|
|
|
|
func (s *Server) Compare(ctx context.Context, req *connect.Request[v1.CompareRequest]) (*connect.Response[v1.CompareResult], error) {
|
|
repo, err := s.open(ctx, req.Msg.Repo)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
res, err := repo.Compare(req.Msg.Base, req.Msg.Head)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
return connect.NewResponse(compareResultToV1(res)), nil
|
|
}
|
|
|
|
func (s *Server) Blame(ctx context.Context, req *connect.Request[v1.BlameRequest]) (*connect.Response[v1.BlameResult], error) {
|
|
repo, err := s.open(ctx, req.Msg.Repo)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
res, err := repo.Blame(req.Msg.Ref, req.Msg.Path)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
return connect.NewResponse(blameResultToV1(res)), nil
|
|
}
|
|
|
|
func (s *Server) GetContributors(ctx context.Context, req *connect.Request[v1.GetContributorsRequest]) (*connect.Response[v1.GetContributorsResponse], error) {
|
|
repo, err := s.open(ctx, req.Msg.Repo)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
contribs, err := repo.GetContributors(req.Msg.Ref)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
return connect.NewResponse(&v1.GetContributorsResponse{Contributors: contributorsToV1(contribs)}), nil
|
|
}
|
|
|
|
func (s *Server) GetStats(ctx context.Context, req *connect.Request[v1.RepoRequest]) (*connect.Response[v1.RepoStats], error) {
|
|
repo, err := s.open(ctx, req.Msg.Repo)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
stats, err := repo.GetStats()
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
return connect.NewResponse(repoStatsToV1(stats)), nil
|
|
}
|
|
|
|
func (s *Server) CountObjects(ctx context.Context, req *connect.Request[v1.RepoRequest]) (*connect.Response[v1.CountObjectsResult], error) {
|
|
repo, err := s.open(ctx, req.Msg.Repo)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
res, err := repo.CountObjects()
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
return connect.NewResponse(countObjectsToV1(res)), nil
|
|
}
|
|
|
|
const (
|
|
strategyPlumbing = "plumbing"
|
|
strategyTempRepo = "temprepo"
|
|
)
|
|
|
|
func (s *Server) CreateFile(ctx context.Context, req *connect.Request[v1.CreateFileRequest]) (*connect.Response[v1.FileCommitResult], error) {
|
|
m := req.Msg
|
|
repo, err := s.open(ctx, m.Repo)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
name, email := author(m.AuthorName, m.AuthorEmail)
|
|
var res *gitcmd.FileCommitResult
|
|
if m.Strategy == strategyTempRepo {
|
|
res, err = repo.CreateOrUpdateFileViaTempRepo(m.Branch, m.Path, m.Content, m.Message, name, email, m.ExpectedSha)
|
|
} else {
|
|
res, err = repo.CreateOrUpdateFile(m.Branch, m.Path, m.Content, m.Message, name, email, m.ExpectedSha)
|
|
}
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
return connect.NewResponse(fileCommitResultToV1(res)), nil
|
|
}
|
|
|
|
func (s *Server) DeleteFile(ctx context.Context, req *connect.Request[v1.DeleteFileRequest]) (*connect.Response[v1.FileCommitResult], error) {
|
|
m := req.Msg
|
|
repo, err := s.open(ctx, m.Repo)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
name, email := author(m.AuthorName, m.AuthorEmail)
|
|
var res *gitcmd.FileCommitResult
|
|
if m.Strategy == strategyTempRepo {
|
|
res, err = repo.DeleteFileViaTempRepo(m.Branch, m.Path, m.Message, name, email, m.ExpectedSha)
|
|
} else {
|
|
res, err = repo.DeleteFile(m.Branch, m.Path, m.Message, name, email, m.ExpectedSha)
|
|
}
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
return connect.NewResponse(fileCommitResultToV1(res)), nil
|
|
}
|
|
|
|
func (s *Server) Merge(ctx context.Context, req *connect.Request[v1.MergeRequest]) (*connect.Response[v1.MergeResult], error) {
|
|
m := req.Msg
|
|
repo, err := s.open(ctx, m.Repo)
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
name, email := author(m.AuthorName, m.AuthorEmail)
|
|
var res *gitcmd.MergeResult
|
|
if m.Strategy == strategyTempRepo {
|
|
res, err = repo.MergeViaTempRepo(m.Base, m.Head, m.Message, name, email, m.NoFf)
|
|
} else {
|
|
res, err = repo.Merge(m.Base, m.Head, m.Message, name, email, m.NoFf)
|
|
}
|
|
if err != nil {
|
|
return nil, toConnectError(err)
|
|
}
|
|
return connect.NewResponse(mergeResultToV1(res)), nil
|
|
}
|
|
|
|
// ---- ManageService handlers (system token; backed by state.IStateMut) ----
|
|
//
|
|
// These delegate to the self-contained state DB (s.mut) for repo lifecycle and
|
|
// ACL grants. Unauthenticated (localhost-only -manage port); the state layer
|
|
// does the real work.
|
|
|
|
func (s *Server) CreateRepo(ctx context.Context, req *connect.Request[v1.CreateRepoRequest]) (*connect.Response[emptypb.Empty], error) {
|
|
if err := s.mut.CreateRepo(req.Msg.Ns, req.Msg.Name); err != nil {
|
|
return nil, toManageError(err)
|
|
}
|
|
return connect.NewResponse(&emptypb.Empty{}), nil
|
|
}
|
|
|
|
func (s *Server) ExistRepo(ctx context.Context, req *connect.Request[v1.ExistRepoRequest]) (*connect.Response[emptypb.Empty], error) {
|
|
if err := s.mut.ExistRepo(req.Msg.Ns, req.Msg.Name); err != nil {
|
|
return nil, toManageError(err)
|
|
}
|
|
return connect.NewResponse(&emptypb.Empty{}), nil
|
|
}
|
|
|
|
func (s *Server) DeleteRepo(ctx context.Context, req *connect.Request[v1.DeleteRepoRequest]) (*connect.Response[emptypb.Empty], error) {
|
|
if err := s.mut.DeleteRepo(req.Msg.Ns, req.Msg.Name); err != nil {
|
|
return nil, toManageError(err)
|
|
}
|
|
return connect.NewResponse(&emptypb.Empty{}), nil
|
|
}
|
|
|
|
func (s *Server) MoveRepo(ctx context.Context, req *connect.Request[v1.MoveRepoRequest]) (*connect.Response[emptypb.Empty], error) {
|
|
if err := s.mut.MoveRepo(req.Msg.Src, req.Msg.Dst); err != nil {
|
|
return nil, toManageError(err)
|
|
}
|
|
return connect.NewResponse(&emptypb.Empty{}), nil
|
|
}
|
|
|
|
func (s *Server) MoveNS(ctx context.Context, req *connect.Request[v1.MoveNSRequest]) (*connect.Response[emptypb.Empty], error) {
|
|
if err := s.mut.MoveNS(req.Msg.Src, req.Msg.Dst); err != nil {
|
|
return nil, toManageError(err)
|
|
}
|
|
return connect.NewResponse(&emptypb.Empty{}), nil
|
|
}
|
|
|
|
func (s *Server) ACLUpsertSSHKeyOnNS(ctx context.Context, req *connect.Request[v1.ACLUpsertSSHKeyOnNSRequest]) (*connect.Response[v1.ACLIDResponse], error) {
|
|
key, err := parsePublicKey(req.Msg.Key)
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInvalidArgument, err)
|
|
}
|
|
id, err := s.mut.ACLUpsertSSHKeyOnNS(req.Msg.Ns, key, common.Perm(req.Msg.Perm))
|
|
if err != nil {
|
|
return nil, toManageError(err)
|
|
}
|
|
return connect.NewResponse(&v1.ACLIDResponse{AclId: id}), nil
|
|
}
|
|
|
|
func (s *Server) ACLUpsertPATOnNS(ctx context.Context, req *connect.Request[v1.ACLUpsertPATOnNSRequest]) (*connect.Response[v1.ACLIDResponse], error) {
|
|
id, err := s.mut.ACLUpsertPATOnNS(req.Msg.Ns, req.Msg.Pat, common.Perm(req.Msg.Perm))
|
|
if err != nil {
|
|
return nil, toManageError(err)
|
|
}
|
|
return connect.NewResponse(&v1.ACLIDResponse{AclId: id}), nil
|
|
}
|
|
|
|
func (s *Server) ACLUpsertSSHKeyOnRepo(ctx context.Context, req *connect.Request[v1.ACLUpsertSSHKeyOnRepoRequest]) (*connect.Response[v1.ACLIDResponse], error) {
|
|
key, err := parsePublicKey(req.Msg.Key)
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInvalidArgument, err)
|
|
}
|
|
id, err := s.mut.ACLUpsertSSHKeyOnRepo(req.Msg.Reponame, key, common.Perm(req.Msg.Perm))
|
|
if err != nil {
|
|
return nil, toManageError(err)
|
|
}
|
|
return connect.NewResponse(&v1.ACLIDResponse{AclId: id}), nil
|
|
}
|
|
|
|
func (s *Server) ACLUpsertPATOnRepo(ctx context.Context, req *connect.Request[v1.ACLUpsertPATOnRepoRequest]) (*connect.Response[v1.ACLIDResponse], error) {
|
|
id, err := s.mut.ACLUpsertPATOnRepo(req.Msg.Reponame, req.Msg.Pat, common.Perm(req.Msg.Perm))
|
|
if err != nil {
|
|
return nil, toManageError(err)
|
|
}
|
|
return connect.NewResponse(&v1.ACLIDResponse{AclId: id}), nil
|
|
}
|
|
|
|
func (s *Server) ACLDelete(ctx context.Context, req *connect.Request[v1.ACLDeleteRequest]) (*connect.Response[emptypb.Empty], error) {
|
|
if err := s.mut.ACLDelete(req.Msg.AclId); err != nil {
|
|
return nil, toManageError(err)
|
|
}
|
|
return connect.NewResponse(&emptypb.Empty{}), nil
|
|
}
|
|
|
|
// ---- ManageService Status (diagnostics) ----
|
|
//
|
|
// Returns the daemon's startup params, listen locations, start time / uptime,
|
|
// and the state DB URI. gitctl uses db_uri to connect to the DB directly for
|
|
// inspection reads (there is no list RPC on this service); the state package
|
|
// stays free of List*/management-read methods.
|
|
|
|
func (s *Server) Status(ctx context.Context, req *connect.Request[emptypb.Empty]) (*connect.Response[v1.StatusResponse], error) {
|
|
return connect.NewResponse(&v1.StatusResponse{
|
|
Root: s.cfg.Root,
|
|
RpcAddr: s.cfg.RPCAddr,
|
|
SshAddr: s.cfg.SSHAddr,
|
|
ManageAddr: s.cfg.ManageAddr,
|
|
AuthUrl: s.cfg.AuthURL,
|
|
DbUri: s.cfg.DBUri,
|
|
SkipAuth: s.cfg.SkipAuth,
|
|
StartedAt: timeToTs(s.startedAt),
|
|
UptimeSeconds: int64(time.Since(s.startedAt).Seconds()),
|
|
}), nil
|
|
}
|
|
|
|
// parsePublicKey parses authorized-keys text into an ssh.PublicKey (the first
|
|
// key if multiple are present).
|
|
func parsePublicKey(text string) (ssh.PublicKey, error) {
|
|
key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(text))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid public key: %w", err)
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
// toManageError maps state-layer errors to connect codes.
|
|
func toManageError(err error) error {
|
|
switch {
|
|
case errors.Is(err, state.ErrRepoNotFound), errors.Is(err, state.ErrNamespaceNotFound):
|
|
return connect.NewError(connect.CodeNotFound, err)
|
|
default:
|
|
return connect.NewError(connect.CodeInternal, err)
|
|
}
|
|
}
|
|
|
|
func author(name, email string) (string, string) {
|
|
if name == "" {
|
|
name = "unknown"
|
|
}
|
|
if email == "" {
|
|
email = name
|
|
}
|
|
return name, email
|
|
}
|
|
|
|
func timeToTs(t time.Time) *timestamppb.Timestamp {
|
|
if t.IsZero() {
|
|
return nil
|
|
}
|
|
return timestamppb.New(t)
|
|
}
|
|
|
|
func commitToV1(c gitcmd.Commit) *v1.Commit {
|
|
return &v1.Commit{
|
|
Id: c.ID,
|
|
ShortSha: c.ShortSHA,
|
|
Message: c.Message,
|
|
Author: c.Author,
|
|
AuthorEmail: c.AuthorEmail,
|
|
When: timeToTs(c.When),
|
|
Parents: c.Parents,
|
|
}
|
|
}
|
|
|
|
func commitsToV1(cs []gitcmd.Commit) []*v1.Commit {
|
|
out := make([]*v1.Commit, len(cs))
|
|
for i, c := range cs {
|
|
out[i] = commitToV1(c)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func diffStatsToV1(s gitcmd.DiffStats) *v1.DiffStats {
|
|
return &v1.DiffStats{
|
|
Additions: int32(s.Additions),
|
|
Deletions: int32(s.Deletions),
|
|
Total: int32(s.Total),
|
|
Files: int32(s.Files),
|
|
}
|
|
}
|
|
|
|
func fileDiffToV1(f gitcmd.FileDiff) *v1.FileDiff {
|
|
return &v1.FileDiff{
|
|
Path: f.Path,
|
|
Additions: int32(f.Additions),
|
|
Deletions: int32(f.Deletions),
|
|
Status: f.Status,
|
|
}
|
|
}
|
|
|
|
func commitDetailToV1(d *gitcmd.CommitDetail) *v1.CommitDetail {
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
files := make([]*v1.FileDiff, len(d.Files))
|
|
for i, f := range d.Files {
|
|
files[i] = fileDiffToV1(f)
|
|
}
|
|
return &v1.CommitDetail{
|
|
Commit: commitToV1(d.Commit),
|
|
Stats: diffStatsToV1(d.Stats),
|
|
Files: files,
|
|
}
|
|
}
|
|
|
|
func branchesToV1(bs []gitcmd.Branch) []*v1.Branch {
|
|
out := make([]*v1.Branch, len(bs))
|
|
for i, b := range bs {
|
|
out[i] = &v1.Branch{
|
|
Name: b.Name,
|
|
Sha: b.SHA,
|
|
ShortSha: b.ShortSHA,
|
|
Message: b.Message,
|
|
Author: b.Author,
|
|
When: timeToTs(b.When),
|
|
IsDefault: b.IsDefault,
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func tagsToV1(ts []gitcmd.Tag) []*v1.Tag {
|
|
out := make([]*v1.Tag, len(ts))
|
|
for i, t := range ts {
|
|
out[i] = &v1.Tag{
|
|
Name: t.Name,
|
|
Sha: t.SHA,
|
|
ShortSha: t.ShortSHA,
|
|
Message: t.Message,
|
|
Tagger: t.Tagger,
|
|
TagDate: timeToTs(t.TagDate),
|
|
IsAnnotated: t.IsAnnotated,
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func treeEntryToV1(e gitcmd.TreeEntry) *v1.TreeEntry {
|
|
return &v1.TreeEntry{
|
|
Name: e.Name,
|
|
Path: e.Path,
|
|
Type: string(e.Type),
|
|
Mode: e.Mode,
|
|
Size: e.Size,
|
|
Sha: e.SHA,
|
|
}
|
|
}
|
|
|
|
func treeToV1(t *gitcmd.Tree) *v1.Tree {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
entries := make([]*v1.TreeEntry, len(t.Entries))
|
|
for i, e := range t.Entries {
|
|
entries[i] = treeEntryToV1(e)
|
|
}
|
|
return &v1.Tree{Ref: t.Ref, Path: t.Path, Entries: entries}
|
|
}
|
|
|
|
func blobToV1(b *gitcmd.Blob) *v1.Blob {
|
|
if b == nil {
|
|
return nil
|
|
}
|
|
return &v1.Blob{
|
|
Ref: b.Ref,
|
|
Path: b.Path,
|
|
Content: b.Content,
|
|
Encoding: b.Encoding,
|
|
Size: b.Size,
|
|
Sha: b.SHA,
|
|
IsBinary: b.IsBinary,
|
|
}
|
|
}
|
|
|
|
func diffLineToV1(l gitcmd.DiffLine) *v1.DiffLine {
|
|
return &v1.DiffLine{Type: string(l.Type), Text: l.Text}
|
|
}
|
|
|
|
func hunkToV1(h gitcmd.Hunk) *v1.Hunk {
|
|
lines := make([]*v1.DiffLine, len(h.Lines))
|
|
for i, l := range h.Lines {
|
|
lines[i] = diffLineToV1(l)
|
|
}
|
|
return &v1.Hunk{
|
|
OldStart: int32(h.OldStart),
|
|
OldCount: int32(h.OldCount),
|
|
NewStart: int32(h.NewStart),
|
|
NewCount: int32(h.NewCount),
|
|
Lines: lines,
|
|
}
|
|
}
|
|
|
|
func fileChangeToV1(f gitcmd.FileChange) *v1.FileChange {
|
|
hunks := make([]*v1.Hunk, len(f.Hunks))
|
|
for i, h := range f.Hunks {
|
|
hunks[i] = hunkToV1(h)
|
|
}
|
|
return &v1.FileChange{
|
|
Path: f.Path,
|
|
OldPath: f.OldPath,
|
|
Status: f.Status,
|
|
IsBinary: f.IsBinary,
|
|
Additions: int32(f.Additions),
|
|
Deletions: int32(f.Deletions),
|
|
Hunks: hunks,
|
|
}
|
|
}
|
|
|
|
func diffResultToV1(d *gitcmd.DiffResult) *v1.DiffResult {
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
files := make([]*v1.FileChange, len(d.Files))
|
|
for i, f := range d.Files {
|
|
files[i] = fileChangeToV1(f)
|
|
}
|
|
return &v1.DiffResult{Files: files}
|
|
}
|
|
|
|
func compareResultToV1(c *gitcmd.CompareResult) *v1.CompareResult {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
return &v1.CompareResult{
|
|
Base: c.Base,
|
|
Head: c.Head,
|
|
Ahead: int32(c.Ahead),
|
|
Behind: int32(c.Behind),
|
|
Commits: commitsToV1(c.Commits),
|
|
Diff: diffResultToV1(&c.Diff),
|
|
}
|
|
}
|
|
|
|
func blameLineToV1(l gitcmd.BlameLine) *v1.BlameLine {
|
|
return &v1.BlameLine{
|
|
Sha: l.Sha,
|
|
ShortSha: l.ShortSHA,
|
|
Author: l.Author,
|
|
Email: l.Email,
|
|
When: timeToTs(l.When),
|
|
LineNo: int32(l.LineNo),
|
|
Content: l.Content,
|
|
Summary: l.Summary,
|
|
}
|
|
}
|
|
|
|
func blameResultToV1(b *gitcmd.BlameResult) *v1.BlameResult {
|
|
if b == nil {
|
|
return nil
|
|
}
|
|
lines := make([]*v1.BlameLine, len(b.Lines))
|
|
for i, l := range b.Lines {
|
|
lines[i] = blameLineToV1(l)
|
|
}
|
|
return &v1.BlameResult{Ref: b.Ref, Path: b.Path, Lines: lines}
|
|
}
|
|
|
|
func contributorsToV1(cs []gitcmd.Contributor) []*v1.Contributor {
|
|
out := make([]*v1.Contributor, len(cs))
|
|
for i, c := range cs {
|
|
out[i] = &v1.Contributor{
|
|
Author: c.Author,
|
|
Email: c.Email,
|
|
Commits: int32(c.Commits),
|
|
Additions: int32(c.Additions),
|
|
Deletions: int32(c.Deletions),
|
|
TotalLines: int32(c.TotalLines),
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func repoStatsToV1(s *gitcmd.RepoStats) *v1.RepoStats {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
return &v1.RepoStats{
|
|
TotalCommits: int32(s.TotalCommits),
|
|
TotalBranches: int32(s.TotalBranches),
|
|
TotalTags: int32(s.TotalTags),
|
|
TotalSize: s.TotalSize,
|
|
Contributors: contributorsToV1(s.Contributors),
|
|
FirstCommit: timeToTs(s.FirstCommit),
|
|
LastCommit: timeToTs(s.LastCommit),
|
|
}
|
|
}
|
|
|
|
func countObjectsToV1(c *gitcmd.CountObjectsResult) *v1.CountObjectsResult {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
return &v1.CountObjectsResult{
|
|
Count: c.Count,
|
|
Size: c.Size,
|
|
InPack: c.InPack,
|
|
Packs: c.Packs,
|
|
SizePack: c.SizePack,
|
|
Garbage: c.Garbage,
|
|
SizeGarbage: c.SizeGarbage,
|
|
}
|
|
}
|
|
|
|
func mergeResultToV1(m *gitcmd.MergeResult) *v1.MergeResult {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
out := &v1.MergeResult{
|
|
Base: m.Base,
|
|
Head: m.Head,
|
|
FastForward: m.FastForward,
|
|
Conflicts: m.Conflicts,
|
|
}
|
|
if m.MergeCommit != nil {
|
|
out.MergeCommit = commitToV1(*m.MergeCommit)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func fileCommitResultToV1(r *gitcmd.FileCommitResult) *v1.FileCommitResult {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
return &v1.FileCommitResult{Commit: commitToV1(r.Commit)}
|
|
}
|