diff --git a/models/git/commit_status.go b/models/git/commit_status.go
index 82cbb23637..6028e46649 100644
--- a/models/git/commit_status.go
+++ b/models/git/commit_status.go
@@ -23,6 +23,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
+ "xorm.io/builder"
"xorm.io/xorm"
)
@@ -240,6 +241,55 @@ func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOp
return statuses, count, db.GetEngine(ctx).In("id", ids).Find(&statuses)
}
+// GetLatestCommitStatusForPairs returns all statuses with a unique context for a given list of repo-sha pairs
+func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHAs map[int64]string, listOptions db.ListOptions) (map[int64][]*CommitStatus, error) {
+ type result struct {
+ ID int64
+ RepoID int64
+ }
+
+ results := make([]result, 0, len(repoIDsToLatestCommitSHAs))
+
+ sess := db.GetEngine(ctx).Table(&CommitStatus{})
+
+ // Create a disjunction of conditions for each repoID and SHA pair
+ conds := make([]builder.Cond, 0, len(repoIDsToLatestCommitSHAs))
+ for repoID, sha := range repoIDsToLatestCommitSHAs {
+ conds = append(conds, builder.Eq{"repo_id": repoID, "sha": sha})
+ }
+ sess = sess.Where(builder.Or(conds...)).
+ Select("max( id ) as id, repo_id").
+ GroupBy("context_hash, repo_id").OrderBy("max( id ) desc")
+
+ sess = db.SetSessionPagination(sess, &listOptions)
+
+ err := sess.Find(&results)
+ if err != nil {
+ return nil, err
+ }
+
+ ids := make([]int64, 0, len(results))
+ repoStatuses := make(map[int64][]*CommitStatus)
+ for _, result := range results {
+ ids = append(ids, result.ID)
+ }
+
+ statuses := make([]*CommitStatus, 0, len(ids))
+ if len(ids) > 0 {
+ err = db.GetEngine(ctx).In("id", ids).Find(&statuses)
+ if err != nil {
+ return nil, err
+ }
+
+ // Group the statuses by repo ID
+ for _, status := range statuses {
+ repoStatuses[status.RepoID] = append(repoStatuses[status.RepoID], status)
+ }
+ }
+
+ return repoStatuses, nil
+}
+
// FindRepoRecentCommitStatusContexts returns repository's recent commit status contexts
func FindRepoRecentCommitStatusContexts(ctx context.Context, repoID int64, before time.Duration) ([]string, error) {
start := timeutil.TimeStampNow().AddDuration(-before)
diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go
index 14dcf14d8a..3bb6ef5223 100644
--- a/modules/git/repo_branch.go
+++ b/modules/git/repo_branch.go
@@ -106,6 +106,17 @@ func GetBranchesByPath(ctx context.Context, path string, skip, limit int) ([]*Br
return gitRepo.GetBranches(skip, limit)
}
+// GetBranchCommitID returns a branch commit ID by its name
+func GetBranchCommitID(ctx context.Context, path, branch string) (string, error) {
+ gitRepo, err := OpenRepository(ctx, path)
+ if err != nil {
+ return "", err
+ }
+ defer gitRepo.Close()
+
+ return gitRepo.GetBranchCommitID(branch)
+}
+
// GetBranches returns a slice of *git.Branch
func (repo *Repository) GetBranches(skip, limit int) ([]*Branch, int, error) {
brs, countAll, err := repo.GetBranchNames(skip, limit)
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 2f87e19022..f697d9433e 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -9,9 +9,11 @@ import (
"fmt"
"net/http"
"strings"
+ "sync"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
@@ -576,23 +578,49 @@ func SearchRepo(ctx *context.Context) {
return
}
- results := make([]*api.Repository, len(repos))
+ // collect the latest commit of each repo
+ repoIDsToLatestCommitSHAs := make(map[int64]string)
+ wg := sync.WaitGroup{}
+ wg.Add(len(repos))
+ for _, repo := range repos {
+ go func(repo *repo_model.Repository) {
+ defer wg.Done()
+ commitID, err := repo_service.GetBranchCommitID(ctx, repo, repo.DefaultBranch)
+ if err != nil {
+ return
+ }
+ repoIDsToLatestCommitSHAs[repo.ID] = commitID
+ }(repo)
+ }
+ wg.Wait()
+
+ // call the database O(1) times to get the commit statuses for all repos
+ repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoIDsToLatestCommitSHAs, db.ListOptions{})
+ if err != nil {
+ log.Error("GetLatestCommitStatusForPairs: %v", err)
+ return
+ }
+
+ results := make([]*repo_service.WebSearchRepository, len(repos))
for i, repo := range repos {
- results[i] = &api.Repository{
- ID: repo.ID,
- FullName: repo.FullName(),
- Fork: repo.IsFork,
- Private: repo.IsPrivate,
- Template: repo.IsTemplate,
- Mirror: repo.IsMirror,
- Stars: repo.NumStars,
- HTMLURL: repo.HTMLURL(),
- Link: repo.Link(),
- Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
+ results[i] = &repo_service.WebSearchRepository{
+ Repository: &api.Repository{
+ ID: repo.ID,
+ FullName: repo.FullName(),
+ Fork: repo.IsFork,
+ Private: repo.IsPrivate,
+ Template: repo.IsTemplate,
+ Mirror: repo.IsMirror,
+ Stars: repo.NumStars,
+ HTMLURL: repo.HTMLURL(),
+ Link: repo.Link(),
+ Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
+ },
+ LatestCommitStatus: git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID]),
}
}
- ctx.JSON(http.StatusOK, api.SearchResults{
+ ctx.JSON(http.StatusOK, repo_service.WebSearchResults{
OK: true,
Data: results,
})
diff --git a/services/repository/branch.go b/services/repository/branch.go
index a085026ae1..cafad34cef 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -53,6 +53,10 @@ func GetBranches(ctx context.Context, repo *repo_model.Repository, skip, limit i
return git.GetBranchesByPath(ctx, repo.RepoPath(), skip, limit)
}
+func GetBranchCommitID(ctx context.Context, repo *repo_model.Repository, branch string) (string, error) {
+ return git.GetBranchCommitID(ctx, repo.RepoPath(), branch)
+}
+
// checkBranchName validates branch name with existing repository branches
func checkBranchName(ctx context.Context, repo *repo_model.Repository, name string) error {
_, err := git.WalkReferences(ctx, repo.RepoPath(), func(_, refName string) error {
diff --git a/services/repository/repository.go b/services/repository/repository.go
index 0d6529383c..0914a8f6ec 100644
--- a/services/repository/repository.go
+++ b/services/repository/repository.go
@@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
packages_model "code.gitea.io/gitea/models/packages"
@@ -20,9 +21,22 @@ import (
"code.gitea.io/gitea/modules/notification"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/structs"
pull_service "code.gitea.io/gitea/services/pull"
)
+// WebSearchRepository represents a repository returned by web search
+type WebSearchRepository struct {
+ Repository *structs.Repository `json:"repository"`
+ LatestCommitStatus *git.CommitStatus `json:"latest_commit_status"`
+}
+
+// WebSearchResults results of a successful web search
+type WebSearchResults struct {
+ OK bool `json:"ok"`
+ Data []*WebSearchRepository `json:"data"`
+}
+
// CreateRepository creates a repository for the user/organization.
func CreateRepository(ctx context.Context, doer, owner *user_model.User, opts repo_module.CreateRepoOptions) (*repo_model.Repository, error) {
repo, err := repo_module.CreateRepository(doer, owner, opts)
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index 161fca9414..84ee886618 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -79,6 +79,8 @@
+
+
@@ -154,6 +156,15 @@ import {SvgIcon} from '../svg.js';
const {appSubUrl, assetUrlPrefix, pageData} = window.config;
+const commitStatus = {
+ pending: {name: 'octicon-dot-fill', color: 'grey'},
+ running: {name: 'octicon-dot-fill', color: 'yellow'},
+ success: {name: 'octicon-check', color: 'green'},
+ error: {name: 'gitea-exclamation', color: 'red'},
+ failure: {name: 'octicon-x', color: 'red'},
+ warning: {name: 'gitea-exclamation', color: 'yellow'},
+};
+
const sfc = {
components: {SvgIcon},
data() {
@@ -387,7 +398,7 @@ const sfc = {
}
if (searchedURL === this.searchURL) {
- this.repos = json.data;
+ this.repos = json.data.map((webSearchRepo) => {return {...webSearchRepo.repository, latest_commit_status_state: webSearchRepo.latest_commit_status.State}});
const count = response.headers.get('X-Total-Count');
if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') {
this.reposTotalCount = count;
@@ -412,6 +423,14 @@ const sfc = {
return 'octicon-repo';
}
return 'octicon-repo';
+ },
+
+ statusIcon(status) {
+ return commitStatus[status].name;
+ },
+
+ statusColor(status) {
+ return commitStatus[status].color;
}
},
};
diff --git a/web_src/js/features/org-team.js b/web_src/js/features/org-team.js
index 3640bb96f7..957dce02d8 100644
--- a/web_src/js/features/org-team.js
+++ b/web_src/js/features/org-team.js
@@ -26,8 +26,8 @@ export function initOrgTeamSearchRepoBox() {
const items = [];
$.each(response.data, (_i, item) => {
items.push({
- title: item.full_name.split('/')[1],
- description: item.full_name
+ title: item.repository.full_name.split('/')[1],
+ description: item.repository.full_name
});
});
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index d2942cd933..3723e0f627 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -291,8 +291,8 @@ export function initRepoIssueReferenceRepositorySearch() {
const filteredResponse = {success: true, results: []};
$.each(response.data, (_r, repo) => {
filteredResponse.results.push({
- name: htmlEscape(repo.full_name),
- value: repo.full_name
+ name: htmlEscape(repo.repository.full_name),
+ value: repo.repository.full_name
});
});
return filteredResponse;
diff --git a/web_src/js/features/repo-template.js b/web_src/js/features/repo-template.js
index 0c5ea5233a..1e83e74780 100644
--- a/web_src/js/features/repo-template.js
+++ b/web_src/js/features/repo-template.js
@@ -34,8 +34,8 @@ export function initRepoTemplateSearch() {
// Parse the response from the api to work with our dropdown
$.each(response.data, (_r, repo) => {
filteredResponse.results.push({
- name: htmlEscape(repo.full_name),
- value: repo.id
+ name: htmlEscape(repo.repository.full_name),
+ value: repo.repository.id
});
});
return filteredResponse;
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 0894bbb169..49376c1643 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -2,10 +2,12 @@ import {h} from 'vue';
import giteaDoubleChevronLeft from '../../public/img/svg/gitea-double-chevron-left.svg';
import giteaDoubleChevronRight from '../../public/img/svg/gitea-double-chevron-right.svg';
import giteaEmptyCheckbox from '../../public/img/svg/gitea-empty-checkbox.svg';
+import giteaExclamation from '../../public/img/svg/gitea-exclamation.svg';
import octiconArchive from '../../public/img/svg/octicon-archive.svg';
import octiconArrowSwitch from '../../public/img/svg/octicon-arrow-switch.svg';
import octiconBlocked from '../../public/img/svg/octicon-blocked.svg';
import octiconBold from '../../public/img/svg/octicon-bold.svg';
+import octiconCheck from '../../public/img/svg/octicon-check.svg';
import octiconCheckbox from '../../public/img/svg/octicon-checkbox.svg';
import octiconCheckCircleFill from '../../public/img/svg/octicon-check-circle-fill.svg';
import octiconChevronDown from '../../public/img/svg/octicon-chevron-down.svg';
@@ -19,6 +21,7 @@ import octiconDiffAdded from '../../public/img/svg/octicon-diff-added.svg';
import octiconDiffModified from '../../public/img/svg/octicon-diff-modified.svg';
import octiconDiffRemoved from '../../public/img/svg/octicon-diff-removed.svg';
import octiconDiffRenamed from '../../public/img/svg/octicon-diff-renamed.svg';
+import octiconDotFill from '../../public/img/svg/octicon-dot-fill.svg';
import octiconEye from '../../public/img/svg/octicon-eye.svg';
import octiconFile from '../../public/img/svg/octicon-file.svg';
import octiconFileDirectoryFill from '../../public/img/svg/octicon-file-directory-fill.svg';
@@ -67,10 +70,12 @@ const svgs = {
'gitea-double-chevron-left': giteaDoubleChevronLeft,
'gitea-double-chevron-right': giteaDoubleChevronRight,
'gitea-empty-checkbox': giteaEmptyCheckbox,
+ 'gitea-exclamation': giteaExclamation,
'octicon-archive': octiconArchive,
'octicon-arrow-switch': octiconArrowSwitch,
'octicon-blocked': octiconBlocked,
'octicon-bold': octiconBold,
+ 'octicon-check': octiconCheck,
'octicon-check-circle-fill': octiconCheckCircleFill,
'octicon-checkbox': octiconCheckbox,
'octicon-chevron-down': octiconChevronDown,
@@ -84,6 +89,7 @@ const svgs = {
'octicon-diff-modified': octiconDiffModified,
'octicon-diff-removed': octiconDiffRemoved,
'octicon-diff-renamed': octiconDiffRenamed,
+ 'octicon-dot-fill': octiconDotFill,
'octicon-eye': octiconEye,
'octicon-file': octiconFile,
'octicon-file-directory-fill': octiconFileDirectoryFill,