It will show the calculated commit status state of the latest commit on the default branch for each repository in the dashboard repo list - Closes #15620 # Before ![image](https://github.com/go-gitea/gitea/assets/20454870/aa1326c7-43c0-458a-a798-3102c766bcf9) # After ![image](https://github.com/go-gitea/gitea/assets/20454870/8658cc03-2224-442a-b1c8-bf64126e4575) --------- Signed-off-by: Yarden Shoham <git@yardenshoham.com> Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: Giteabot <teabot@gitea.io>tags/v1.20.0-rc0
@@ -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) |
@@ -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) |
@@ -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, | |||
}) |
@@ -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 { |
@@ -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) |
@@ -79,6 +79,8 @@ | |||
<svg-icon name="octicon-archive" :size="16" class-name="gt-ml-2"/> | |||
</span> | |||
</div> | |||
<!-- the commit status icon logic is taken from templates/repo/commit_status.tmpl --> | |||
<svg-icon v-if="repo.latest_commit_status_state" :name="statusIcon(repo.latest_commit_status_state)" :class-name="'commit-status icon text ' + statusColor(repo.latest_commit_status_state)" :size="16"/> | |||
</a> | |||
</li> | |||
</ul> | |||
@@ -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; | |||
} | |||
}, | |||
}; |
@@ -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 | |||
}); | |||
}); | |||
@@ -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; |
@@ -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; |
@@ -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, |