Add status indicator on main home screen for each repo (#24638)
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>
This commit is contained in:
parent
68081c4721
commit
4810fe55e3
10 changed files with 152 additions and 20 deletions
|
@ -23,6 +23,7 @@ import (
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
"xorm.io/xorm"
|
"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)
|
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
|
// FindRepoRecentCommitStatusContexts returns repository's recent commit status contexts
|
||||||
func FindRepoRecentCommitStatusContexts(ctx context.Context, repoID int64, before time.Duration) ([]string, error) {
|
func FindRepoRecentCommitStatusContexts(ctx context.Context, repoID int64, before time.Duration) ([]string, error) {
|
||||||
start := timeutil.TimeStampNow().AddDuration(-before)
|
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)
|
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
|
// GetBranches returns a slice of *git.Branch
|
||||||
func (repo *Repository) GetBranches(skip, limit int) ([]*Branch, int, error) {
|
func (repo *Repository) GetBranches(skip, limit int) ([]*Branch, int, error) {
|
||||||
brs, countAll, err := repo.GetBranchNames(skip, limit)
|
brs, countAll, err := repo.GetBranchNames(skip, limit)
|
||||||
|
|
|
@ -9,9 +9,11 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
"code.gitea.io/gitea/models/organization"
|
"code.gitea.io/gitea/models/organization"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
@ -576,9 +578,33 @@ func SearchRepo(ctx *context.Context) {
|
||||||
return
|
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 {
|
for i, repo := range repos {
|
||||||
results[i] = &api.Repository{
|
results[i] = &repo_service.WebSearchRepository{
|
||||||
|
Repository: &api.Repository{
|
||||||
ID: repo.ID,
|
ID: repo.ID,
|
||||||
FullName: repo.FullName(),
|
FullName: repo.FullName(),
|
||||||
Fork: repo.IsFork,
|
Fork: repo.IsFork,
|
||||||
|
@ -589,10 +615,12 @@ func SearchRepo(ctx *context.Context) {
|
||||||
HTMLURL: repo.HTMLURL(),
|
HTMLURL: repo.HTMLURL(),
|
||||||
Link: repo.Link(),
|
Link: repo.Link(),
|
||||||
Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
|
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,
|
OK: true,
|
||||||
Data: results,
|
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)
|
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
|
// checkBranchName validates branch name with existing repository branches
|
||||||
func checkBranchName(ctx context.Context, repo *repo_model.Repository, name string) error {
|
func checkBranchName(ctx context.Context, repo *repo_model.Repository, name string) error {
|
||||||
_, err := git.WalkReferences(ctx, repo.RepoPath(), func(_, refName 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"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/models/git"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
"code.gitea.io/gitea/models/organization"
|
"code.gitea.io/gitea/models/organization"
|
||||||
packages_model "code.gitea.io/gitea/models/packages"
|
packages_model "code.gitea.io/gitea/models/packages"
|
||||||
|
@ -20,9 +21,22 @@ import (
|
||||||
"code.gitea.io/gitea/modules/notification"
|
"code.gitea.io/gitea/modules/notification"
|
||||||
repo_module "code.gitea.io/gitea/modules/repository"
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/structs"
|
||||||
pull_service "code.gitea.io/gitea/services/pull"
|
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.
|
// 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) {
|
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)
|
repo, err := repo_module.CreateRepository(doer, owner, opts)
|
||||||
|
|
|
@ -79,6 +79,8 @@
|
||||||
<svg-icon name="octicon-archive" :size="16" class-name="gt-ml-2"/>
|
<svg-icon name="octicon-archive" :size="16" class-name="gt-ml-2"/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -154,6 +156,15 @@ import {SvgIcon} from '../svg.js';
|
||||||
|
|
||||||
const {appSubUrl, assetUrlPrefix, pageData} = window.config;
|
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 = {
|
const sfc = {
|
||||||
components: {SvgIcon},
|
components: {SvgIcon},
|
||||||
data() {
|
data() {
|
||||||
|
@ -387,7 +398,7 @@ const sfc = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchedURL === this.searchURL) {
|
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');
|
const count = response.headers.get('X-Total-Count');
|
||||||
if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') {
|
if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') {
|
||||||
this.reposTotalCount = count;
|
this.reposTotalCount = count;
|
||||||
|
@ -412,6 +423,14 @@ const sfc = {
|
||||||
return 'octicon-repo';
|
return 'octicon-repo';
|
||||||
}
|
}
|
||||||
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 = [];
|
const items = [];
|
||||||
$.each(response.data, (_i, item) => {
|
$.each(response.data, (_i, item) => {
|
||||||
items.push({
|
items.push({
|
||||||
title: item.full_name.split('/')[1],
|
title: item.repository.full_name.split('/')[1],
|
||||||
description: item.full_name
|
description: item.repository.full_name
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -291,8 +291,8 @@ export function initRepoIssueReferenceRepositorySearch() {
|
||||||
const filteredResponse = {success: true, results: []};
|
const filteredResponse = {success: true, results: []};
|
||||||
$.each(response.data, (_r, repo) => {
|
$.each(response.data, (_r, repo) => {
|
||||||
filteredResponse.results.push({
|
filteredResponse.results.push({
|
||||||
name: htmlEscape(repo.full_name),
|
name: htmlEscape(repo.repository.full_name),
|
||||||
value: repo.full_name
|
value: repo.repository.full_name
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return filteredResponse;
|
return filteredResponse;
|
||||||
|
|
|
@ -34,8 +34,8 @@ export function initRepoTemplateSearch() {
|
||||||
// Parse the response from the api to work with our dropdown
|
// Parse the response from the api to work with our dropdown
|
||||||
$.each(response.data, (_r, repo) => {
|
$.each(response.data, (_r, repo) => {
|
||||||
filteredResponse.results.push({
|
filteredResponse.results.push({
|
||||||
name: htmlEscape(repo.full_name),
|
name: htmlEscape(repo.repository.full_name),
|
||||||
value: repo.id
|
value: repo.repository.id
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return filteredResponse;
|
return filteredResponse;
|
||||||
|
|
|
@ -2,10 +2,12 @@ import {h} from 'vue';
|
||||||
import giteaDoubleChevronLeft from '../../public/img/svg/gitea-double-chevron-left.svg';
|
import giteaDoubleChevronLeft from '../../public/img/svg/gitea-double-chevron-left.svg';
|
||||||
import giteaDoubleChevronRight from '../../public/img/svg/gitea-double-chevron-right.svg';
|
import giteaDoubleChevronRight from '../../public/img/svg/gitea-double-chevron-right.svg';
|
||||||
import giteaEmptyCheckbox from '../../public/img/svg/gitea-empty-checkbox.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 octiconArchive from '../../public/img/svg/octicon-archive.svg';
|
||||||
import octiconArrowSwitch from '../../public/img/svg/octicon-arrow-switch.svg';
|
import octiconArrowSwitch from '../../public/img/svg/octicon-arrow-switch.svg';
|
||||||
import octiconBlocked from '../../public/img/svg/octicon-blocked.svg';
|
import octiconBlocked from '../../public/img/svg/octicon-blocked.svg';
|
||||||
import octiconBold from '../../public/img/svg/octicon-bold.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 octiconCheckbox from '../../public/img/svg/octicon-checkbox.svg';
|
||||||
import octiconCheckCircleFill from '../../public/img/svg/octicon-check-circle-fill.svg';
|
import octiconCheckCircleFill from '../../public/img/svg/octicon-check-circle-fill.svg';
|
||||||
import octiconChevronDown from '../../public/img/svg/octicon-chevron-down.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 octiconDiffModified from '../../public/img/svg/octicon-diff-modified.svg';
|
||||||
import octiconDiffRemoved from '../../public/img/svg/octicon-diff-removed.svg';
|
import octiconDiffRemoved from '../../public/img/svg/octicon-diff-removed.svg';
|
||||||
import octiconDiffRenamed from '../../public/img/svg/octicon-diff-renamed.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 octiconEye from '../../public/img/svg/octicon-eye.svg';
|
||||||
import octiconFile from '../../public/img/svg/octicon-file.svg';
|
import octiconFile from '../../public/img/svg/octicon-file.svg';
|
||||||
import octiconFileDirectoryFill from '../../public/img/svg/octicon-file-directory-fill.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-left': giteaDoubleChevronLeft,
|
||||||
'gitea-double-chevron-right': giteaDoubleChevronRight,
|
'gitea-double-chevron-right': giteaDoubleChevronRight,
|
||||||
'gitea-empty-checkbox': giteaEmptyCheckbox,
|
'gitea-empty-checkbox': giteaEmptyCheckbox,
|
||||||
|
'gitea-exclamation': giteaExclamation,
|
||||||
'octicon-archive': octiconArchive,
|
'octicon-archive': octiconArchive,
|
||||||
'octicon-arrow-switch': octiconArrowSwitch,
|
'octicon-arrow-switch': octiconArrowSwitch,
|
||||||
'octicon-blocked': octiconBlocked,
|
'octicon-blocked': octiconBlocked,
|
||||||
'octicon-bold': octiconBold,
|
'octicon-bold': octiconBold,
|
||||||
|
'octicon-check': octiconCheck,
|
||||||
'octicon-check-circle-fill': octiconCheckCircleFill,
|
'octicon-check-circle-fill': octiconCheckCircleFill,
|
||||||
'octicon-checkbox': octiconCheckbox,
|
'octicon-checkbox': octiconCheckbox,
|
||||||
'octicon-chevron-down': octiconChevronDown,
|
'octicon-chevron-down': octiconChevronDown,
|
||||||
|
@ -84,6 +89,7 @@ const svgs = {
|
||||||
'octicon-diff-modified': octiconDiffModified,
|
'octicon-diff-modified': octiconDiffModified,
|
||||||
'octicon-diff-removed': octiconDiffRemoved,
|
'octicon-diff-removed': octiconDiffRemoved,
|
||||||
'octicon-diff-renamed': octiconDiffRenamed,
|
'octicon-diff-renamed': octiconDiffRenamed,
|
||||||
|
'octicon-dot-fill': octiconDotFill,
|
||||||
'octicon-eye': octiconEye,
|
'octicon-eye': octiconEye,
|
||||||
'octicon-file': octiconFile,
|
'octicon-file': octiconFile,
|
||||||
'octicon-file-directory-fill': octiconFileDirectoryFill,
|
'octicon-file-directory-fill': octiconFileDirectoryFill,
|
||||||
|
|
Loading…
Reference in a new issue