This commit is contained in:
Jabberwocky238
2026-07-15 11:06:48 -04:00
commit c746f88bbe
69 changed files with 14975 additions and 0 deletions
+834
View File
@@ -0,0 +1,834 @@
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"
)
// Config carries the daemon's startup params. The server needs the git root to
// open repos and the rest to answer the Status RPC (which also hands gitctl the
// db_uri so it can connect directly for inspection reads).
type Config struct {
Root string
RPCAddr string
SSHAddr string
ManageAddr string
AuthURL string
DBUri string
SkipAuth bool
}
type Server struct {
gitRepoRoot string
cfg Config
startedAt time.Time
middleware *MiddlewareClient
mut state.IStateMut
}
func NewServer(cfg Config, middleware *MiddlewareClient, mut state.IStateMut) *Server {
return &Server{
gitRepoRoot: cfg.Root,
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.gitRepoRoot, 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{
RepoRoot: s.gitRepoRoot,
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)}
}