diff --git a/app/api/controller/repo/summary.go b/app/api/controller/repo/summary.go new file mode 100644 index 000000000..3131db3f8 --- /dev/null +++ b/app/api/controller/repo/summary.go @@ -0,0 +1,53 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/git" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// Summary returns commit, branch, tag and pull req count for a repo. +func (c *Controller) Summary( + ctx context.Context, + session *auth.Session, + repoRef string, +) (*types.RepositorySummary, error) { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView) + if err != nil { + return nil, fmt.Errorf("access check failed: %w", err) + } + + summary, err := c.git.Summary(ctx, &git.ReadParams{RepoUID: repo.GitUID}) + if err != nil { + return nil, fmt.Errorf("failed to get repo summary: %w", err) + } + + return &types.RepositorySummary{ + DefaultBranchCommitCount: summary.CommitCount, + BranchCount: summary.BranchCount, + TagCount: summary.TagCount, + PullReqSummary: types.RepositoryPullReqSummary{ + OpenCount: repo.NumOpenPulls, + ClosedCount: repo.NumClosedPulls, + MergedCount: repo.NumMergedPulls, + }, + }, nil +} diff --git a/app/api/handler/repo/summary.go b/app/api/handler/repo/summary.go new file mode 100644 index 000000000..779123fc6 --- /dev/null +++ b/app/api/handler/repo/summary.go @@ -0,0 +1,44 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/repo" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +// HandleSummary writes json-encoded repository summary information to the http response body. +func HandleSummary(repoCtrl *repo.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + repoRef, err := request.GetRepoRefFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + summary, err := repoCtrl.Summary(ctx, session, repoRef) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, summary) + } +} diff --git a/app/api/openapi/repo.go b/app/api/openapi/repo.go index 03f61a2de..e379c9e02 100644 --- a/app/api/openapi/repo.go +++ b/app/api/openapi/repo.go @@ -1154,4 +1154,17 @@ func repoOperations(reflector *openapi3.Reflector) { _ = reflector.SetJSONResponse(&opArchive, new(usererror.Error), http.StatusForbidden) _ = reflector.SetJSONResponse(&opArchive, new(usererror.Error), http.StatusNotFound) _ = reflector.Spec.AddOperation(http.MethodGet, "/repos/{repo_ref}/archive/{git_ref}.{format}", opArchive) + + opSummary := openapi3.Operation{} + opSummary.WithTags("repository") + opSummary.WithMapOfAnything( + map[string]interface{}{"operationId": "summary"}) + _ = reflector.SetRequest(&opSummary, new(repoRequest), http.MethodGet) + _ = reflector.SetJSONResponse(&opSummary, new(types.RepositorySummary), http.StatusOK) + _ = reflector.SetJSONResponse(&opSummary, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opSummary, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opSummary, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opSummary, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opSummary, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, "/repos/{repo_ref}/summary", opSummary) } diff --git a/app/router/api.go b/app/router/api.go index 7237cfc90..f6ba0993c 100644 --- a/app/router/api.go +++ b/app/router/api.go @@ -287,6 +287,8 @@ func setupRepos(r chi.Router, r.Patch("/general", handlerreposettings.HandleGeneralUpdate(repoSettingsCtrl)) }) + r.Get("/summary", handlerrepo.HandleSummary(repoCtrl)) + r.Post("/move", handlerrepo.HandleMove(repoCtrl)) r.Get("/service-accounts", handlerrepo.HandleListServiceAccounts(repoCtrl)) diff --git a/git/api/branch.go b/git/api/branch.go index 65ae0c672..0825936f8 100644 --- a/git/api/branch.go +++ b/git/api/branch.go @@ -15,9 +15,11 @@ package api import ( + "bufio" "bytes" "context" "fmt" + "io" "strings" "github.com/harness/gitness/git/command" @@ -104,3 +106,42 @@ func (g *Git) IsBranchExist(ctx context.Context, repoPath, name string) (bool, e } return true, nil } + +func (g *Git) GetBranchCount( + ctx context.Context, + repoPath string, +) (int, error) { + if repoPath == "" { + return 0, ErrRepositoryPathEmpty + } + + pipeOut, pipeIn := io.Pipe() + defer pipeOut.Close() + + cmd := command.New("branch", + command.WithFlag("--list"), + command.WithFlag("--format=%(refname:short)"), + ) + + var err error + go func() { + defer pipeIn.Close() + err = cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(pipeIn)) + }() + if err != nil { + return 0, processGitErrorf(err, "failed to trigger branch command") + } + + return countLines(pipeOut), nil +} + +func countLines(pipe io.Reader) int { + scanner := bufio.NewScanner(pipe) + count := 0 + + for scanner.Scan() { + count++ + } + + return count +} diff --git a/git/api/tag.go b/git/api/tag.go index df1b9849c..c752eb464 100644 --- a/git/api/tag.go +++ b/git/api/tag.go @@ -398,3 +398,28 @@ l: } return tag, nil } + +func (g *Git) GetTagCount( + ctx context.Context, + repoPath string, +) (int, error) { + if repoPath == "" { + return 0, ErrRepositoryPathEmpty + } + + pipeOut, pipeIn := io.Pipe() + defer pipeOut.Close() + + cmd := command.New("tag") + + var err error + go func() { + defer pipeIn.Close() + err = cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(pipeIn)) + }() + if err != nil { + return 0, processGitErrorf(err, "failed to trigger branch command") + } + + return countLines(pipeOut), nil +} diff --git a/git/command/builder.go b/git/command/builder.go index 2da838357..9eb0315ab 100644 --- a/git/command/builder.go +++ b/git/command/builder.go @@ -67,6 +67,7 @@ var descriptions = map[string]builder{ // git-blame(1) does not support disambiguating options from paths from revisions. flags: NoRefUpdates | NoEndOfOptions, }, + "branch": {}, "bundle": { flags: NoRefUpdates, }, diff --git a/git/interface.go b/git/interface.go index 226d85b7c..d80fb928f 100644 --- a/git/interface.go +++ b/git/interface.go @@ -102,4 +102,9 @@ type Interface interface { */ ScanSecrets(ctx context.Context, param *ScanSecretsParams) (*ScanSecretsOutput, error) Archive(ctx context.Context, params ArchiveParams, w io.Writer) error + + /* + * Repo Summary service + */ + Summary(ctx context.Context, params *ReadParams) (*SummaryOutput, error) } diff --git a/git/merge/check.go b/git/merge/check.go index cf71008a3..3d14e24d9 100644 --- a/git/merge/check.go +++ b/git/merge/check.go @@ -89,7 +89,11 @@ func CommitCount( repoPath string, start, end string, ) (int, error) { - cmd := command.New("rev-list", command.WithFlag("--count"), command.WithArg(start+".."+end)) + arg := command.WithArg(end) + if len(start) > 0 { + arg = command.WithArg(start + ".." + end) + } + cmd := command.New("rev-list", command.WithFlag("--count"), arg) stdout := bytes.NewBuffer(nil) diff --git a/git/summary.go b/git/summary.go new file mode 100644 index 000000000..cadd6b539 --- /dev/null +++ b/git/summary.go @@ -0,0 +1,76 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package git + +import ( + "context" + "fmt" + "strings" + + "github.com/harness/gitness/git/merge" + + "golang.org/x/sync/errgroup" +) + +type SummaryOutput struct { + CommitCount int + BranchCount int + TagCount int +} + +func (s *Service) Summary( + ctx context.Context, + params *ReadParams, +) (*SummaryOutput, error) { + repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) + + defaultBranch, err := s.git.GetDefaultBranch(ctx, repoPath) + if err != nil { + return nil, err + } + defaultBranch = strings.TrimSpace(defaultBranch) + + g, ctx := errgroup.WithContext(ctx) + + var commitCount, branchCount, tagCount int + + g.Go(func() error { + var err error + commitCount, err = merge.CommitCount(ctx, repoPath, "", defaultBranch) + return err + }) + + g.Go(func() error { + var err error + branchCount, err = s.git.GetBranchCount(ctx, repoPath) + return err + }) + + g.Go(func() error { + var err error + tagCount, err = s.git.GetTagCount(ctx, repoPath) + return err + }) + + if err := g.Wait(); err != nil { + return nil, fmt.Errorf("failed to get repo summary: %w", err) + } + + return &SummaryOutput{ + CommitCount: commitCount, + BranchCount: branchCount, + TagCount: tagCount, + }, nil +} diff --git a/types/repo.go b/types/repo.go index 002db84eb..6dfac5061 100644 --- a/types/repo.go +++ b/types/repo.go @@ -98,3 +98,16 @@ type RepositoryGitInfo struct { ParentID int64 GitUID string } + +type RepositoryPullReqSummary struct { + OpenCount int `json:"open_count"` + ClosedCount int `json:"closed_count"` + MergedCount int `json:"merged_count"` +} + +type RepositorySummary struct { + DefaultBranchCommitCount int `json:"default_branch_commit_count"` + BranchCount int `json:"branch_count"` + TagCount int `json:"tag_count"` + PullReqSummary RepositoryPullReqSummary `json:"pull_req_summary"` +}