Skip to content

Commit

Permalink
Add status indicator on main home screen for each repo (#24638)
Browse files Browse the repository at this point in the history
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 <[email protected]>
Co-authored-by: delvh <[email protected]>
Co-authored-by: Giteabot <[email protected]>
  • Loading branch information
3 people authored May 13, 2023
1 parent 68081c4 commit 4810fe5
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 20 deletions.
50 changes: 50 additions & 0 deletions models/git/commit_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"

"xorm.io/builder"
"xorm.io/xorm"
)

Expand Down Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions modules/git/repo_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
54 changes: 41 additions & 13 deletions routers/web/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
})
Expand Down
4 changes: 4 additions & 0 deletions services/repository/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 14 additions & 0 deletions services/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down
21 changes: 20 additions & 1 deletion web_src/js/components/DashboardRepoList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
Expand All @@ -412,6 +423,14 @@ const sfc = {
return 'octicon-repo';
}
return 'octicon-repo';
},
statusIcon(status) {
return commitStatus[status].name;
},
statusColor(status) {
return commitStatus[status].color;
}
},
};
Expand Down
4 changes: 2 additions & 2 deletions web_src/js/features/org-team.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
});

Expand Down
4 changes: 2 additions & 2 deletions web_src/js/features/repo-issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions web_src/js/features/repo-template.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions web_src/js/svg.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down

0 comments on commit 4810fe5

Please sign in to comment.