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)} }