* add global code search on explore * fix bug when no anyone public repos * change the icon * fix typo and add UnitTypeCode check for login non-admin user * fix ui description when no matchmaster
@@ -1945,6 +1945,12 @@ func GetRepositoryByID(id int64) (*Repository, error) { | |||||
return getRepositoryByID(x, id) | return getRepositoryByID(x, id) | ||||
} | } | ||||
// GetRepositoriesMapByIDs returns the repositories by given id slice. | |||||
func GetRepositoriesMapByIDs(ids []int64) (map[int64]*Repository, error) { | |||||
var repos = make(map[int64]*Repository, len(ids)) | |||||
return repos, x.In("id", ids).Find(&repos) | |||||
} | |||||
// GetUserRepositories returns a list of repositories of given user. | // GetUserRepositories returns a list of repositories of given user. | ||||
func GetUserRepositories(userID int64, private bool, page, pageSize int, orderBy string) ([]*Repository, error) { | func GetUserRepositories(userID int64, private bool, page, pageSize int, orderBy string) ([]*Repository, error) { | ||||
if len(orderBy) == 0 { | if len(orderBy) == 0 { | ||||
@@ -249,3 +249,28 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err | |||||
return repos, count, nil | return repos, count, nil | ||||
} | } | ||||
// FindUserAccessibleRepoIDs find all accessible repositories' ID by user's id | |||||
func FindUserAccessibleRepoIDs(userID int64) ([]int64, error) { | |||||
var accessCond builder.Cond = builder.Eq{"is_private": false} | |||||
if userID > 0 { | |||||
accessCond = accessCond.Or( | |||||
builder.Eq{"owner_id": userID}, | |||||
builder.And( | |||||
builder.Expr("id IN (SELECT repo_id FROM `access` WHERE access.user_id = ?)", userID), | |||||
builder.Neq{"owner_id": userID}, | |||||
), | |||||
) | |||||
} | |||||
repoIDs := make([]int64, 0, 10) | |||||
if err := x. | |||||
Table("repository"). | |||||
Cols("id"). | |||||
Where(accessCond). | |||||
Find(&repoIDs); err != nil { | |||||
return nil, fmt.Errorf("FindUserAccesibleRepoIDs: %v", err) | |||||
} | |||||
return repoIDs, nil | |||||
} |
@@ -16,6 +16,7 @@ import ( | |||||
"github.com/blevesearch/bleve/analysis/token/lowercase" | "github.com/blevesearch/bleve/analysis/token/lowercase" | ||||
"github.com/blevesearch/bleve/analysis/token/unique" | "github.com/blevesearch/bleve/analysis/token/unique" | ||||
"github.com/blevesearch/bleve/analysis/tokenizer/unicode" | "github.com/blevesearch/bleve/analysis/tokenizer/unicode" | ||||
"github.com/blevesearch/bleve/search/query" | |||||
"github.com/ethantkoenig/rupture" | "github.com/ethantkoenig/rupture" | ||||
) | ) | ||||
@@ -158,6 +159,7 @@ func DeleteRepoFromIndexer(repoID int64) error { | |||||
// RepoSearchResult result of performing a search in a repo | // RepoSearchResult result of performing a search in a repo | ||||
type RepoSearchResult struct { | type RepoSearchResult struct { | ||||
RepoID int64 | |||||
StartIndex int | StartIndex int | ||||
EndIndex int | EndIndex int | ||||
Filename string | Filename string | ||||
@@ -166,17 +168,29 @@ type RepoSearchResult struct { | |||||
// SearchRepoByKeyword searches for files in the specified repo. | // SearchRepoByKeyword searches for files in the specified repo. | ||||
// Returns the matching file-paths | // Returns the matching file-paths | ||||
func SearchRepoByKeyword(repoID int64, keyword string, page, pageSize int) (int64, []*RepoSearchResult, error) { | |||||
func SearchRepoByKeyword(repoIDs []int64, keyword string, page, pageSize int) (int64, []*RepoSearchResult, error) { | |||||
phraseQuery := bleve.NewMatchPhraseQuery(keyword) | phraseQuery := bleve.NewMatchPhraseQuery(keyword) | ||||
phraseQuery.FieldVal = "Content" | phraseQuery.FieldVal = "Content" | ||||
phraseQuery.Analyzer = repoIndexerAnalyzer | phraseQuery.Analyzer = repoIndexerAnalyzer | ||||
indexerQuery := bleve.NewConjunctionQuery( | |||||
numericEqualityQuery(repoID, "RepoID"), | |||||
phraseQuery, | |||||
) | |||||
var indexerQuery query.Query | |||||
if len(repoIDs) > 0 { | |||||
var repoQueries = make([]query.Query, 0, len(repoIDs)) | |||||
for _, repoID := range repoIDs { | |||||
repoQueries = append(repoQueries, numericEqualityQuery(repoID, "RepoID")) | |||||
} | |||||
indexerQuery = bleve.NewConjunctionQuery( | |||||
bleve.NewDisjunctionQuery(repoQueries...), | |||||
phraseQuery, | |||||
) | |||||
} else { | |||||
indexerQuery = phraseQuery | |||||
} | |||||
from := (page - 1) * pageSize | from := (page - 1) * pageSize | ||||
searchRequest := bleve.NewSearchRequestOptions(indexerQuery, pageSize, from, false) | searchRequest := bleve.NewSearchRequestOptions(indexerQuery, pageSize, from, false) | ||||
searchRequest.Fields = []string{"Content"} | |||||
searchRequest.Fields = []string{"Content", "RepoID"} | |||||
searchRequest.IncludeLocations = true | searchRequest.IncludeLocations = true | ||||
result, err := repoIndexer.Search(searchRequest) | result, err := repoIndexer.Search(searchRequest) | ||||
@@ -199,6 +213,7 @@ func SearchRepoByKeyword(repoID int64, keyword string, page, pageSize int) (int6 | |||||
} | } | ||||
} | } | ||||
searchResults[i] = &RepoSearchResult{ | searchResults[i] = &RepoSearchResult{ | ||||
RepoID: int64(hit.Fields["RepoID"].(float64)), | |||||
StartIndex: startIndex, | StartIndex: startIndex, | ||||
EndIndex: endIndex, | EndIndex: endIndex, | ||||
Filename: filenameOfIndexerID(hit.ID), | Filename: filenameOfIndexerID(hit.ID), | ||||
@@ -17,6 +17,7 @@ import ( | |||||
// Result a search result to display | // Result a search result to display | ||||
type Result struct { | type Result struct { | ||||
RepoID int64 | |||||
Filename string | Filename string | ||||
HighlightClass string | HighlightClass string | ||||
LineNumbers []int | LineNumbers []int | ||||
@@ -98,6 +99,7 @@ func searchResult(result *indexer.RepoSearchResult, startIndex, endIndex int) (* | |||||
index += len(line) | index += len(line) | ||||
} | } | ||||
return &Result{ | return &Result{ | ||||
RepoID: result.RepoID, | |||||
Filename: result.Filename, | Filename: result.Filename, | ||||
HighlightClass: highlight.FileNameToHighlightClass(result.Filename), | HighlightClass: highlight.FileNameToHighlightClass(result.Filename), | ||||
LineNumbers: lineNumbers, | LineNumbers: lineNumbers, | ||||
@@ -106,12 +108,12 @@ func searchResult(result *indexer.RepoSearchResult, startIndex, endIndex int) (* | |||||
} | } | ||||
// PerformSearch perform a search on a repository | // PerformSearch perform a search on a repository | ||||
func PerformSearch(repoID int64, keyword string, page, pageSize int) (int, []*Result, error) { | |||||
func PerformSearch(repoIDs []int64, keyword string, page, pageSize int) (int, []*Result, error) { | |||||
if len(keyword) == 0 { | if len(keyword) == 0 { | ||||
return 0, nil, nil | return 0, nil, nil | ||||
} | } | ||||
total, results, err := indexer.SearchRepoByKeyword(repoID, keyword, page, pageSize) | |||||
total, results, err := indexer.SearchRepoByKeyword(repoIDs, keyword, page, pageSize) | |||||
if err != nil { | if err != nil { | ||||
return 0, nil, err | return 0, nil, err | ||||
} | } | ||||
@@ -169,9 +169,12 @@ repos = Repositories | |||||
users = Users | users = Users | ||||
organizations = Organizations | organizations = Organizations | ||||
search = Search | search = Search | ||||
code = Code | |||||
repo_no_results = No matching repositories have been found. | repo_no_results = No matching repositories have been found. | ||||
user_no_results = No matching users have been found. | user_no_results = No matching users have been found. | ||||
org_no_results = No matching organizations have been found. | org_no_results = No matching organizations have been found. | ||||
code_no_results = No matching codes have been found. | |||||
code_search_results = Search results for "%s" | |||||
[auth] | [auth] | ||||
create_new_account = Create Account | create_new_account = Create Account | ||||
@@ -11,6 +11,7 @@ import ( | |||||
"code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
"code.gitea.io/gitea/modules/base" | "code.gitea.io/gitea/modules/base" | ||||
"code.gitea.io/gitea/modules/context" | "code.gitea.io/gitea/modules/context" | ||||
"code.gitea.io/gitea/modules/search" | |||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
"code.gitea.io/gitea/modules/util" | "code.gitea.io/gitea/modules/util" | ||||
"code.gitea.io/gitea/routers/user" | "code.gitea.io/gitea/routers/user" | ||||
@@ -27,6 +28,8 @@ const ( | |||||
tplExploreUsers base.TplName = "explore/users" | tplExploreUsers base.TplName = "explore/users" | ||||
// tplExploreOrganizations explore organizations page template | // tplExploreOrganizations explore organizations page template | ||||
tplExploreOrganizations base.TplName = "explore/organizations" | tplExploreOrganizations base.TplName = "explore/organizations" | ||||
// tplExploreCode explore code page template | |||||
tplExploreCode base.TplName = "explore/code" | |||||
) | ) | ||||
// Home render home page | // Home render home page | ||||
@@ -49,6 +52,7 @@ func Home(ctx *context.Context) { | |||||
} | } | ||||
ctx.Data["PageIsHome"] = true | ctx.Data["PageIsHome"] = true | ||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled | |||||
ctx.HTML(200, tplHome) | ctx.HTML(200, tplHome) | ||||
} | } | ||||
@@ -124,6 +128,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { | |||||
ctx.Data["Total"] = count | ctx.Data["Total"] = count | ||||
ctx.Data["Page"] = paginater.New(int(count), opts.PageSize, page, 5) | ctx.Data["Page"] = paginater.New(int(count), opts.PageSize, page, 5) | ||||
ctx.Data["Repos"] = repos | ctx.Data["Repos"] = repos | ||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled | |||||
ctx.HTML(200, opts.TplName) | ctx.HTML(200, opts.TplName) | ||||
} | } | ||||
@@ -133,6 +138,7 @@ func ExploreRepos(ctx *context.Context) { | |||||
ctx.Data["Title"] = ctx.Tr("explore") | ctx.Data["Title"] = ctx.Tr("explore") | ||||
ctx.Data["PageIsExplore"] = true | ctx.Data["PageIsExplore"] = true | ||||
ctx.Data["PageIsExploreRepositories"] = true | ctx.Data["PageIsExploreRepositories"] = true | ||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled | |||||
var ownerID int64 | var ownerID int64 | ||||
if ctx.User != nil && !ctx.User.IsAdmin { | if ctx.User != nil && !ctx.User.IsAdmin { | ||||
@@ -194,6 +200,7 @@ func RenderUserSearch(ctx *context.Context, opts *models.SearchUserOptions, tplN | |||||
ctx.Data["Page"] = paginater.New(int(count), opts.PageSize, opts.Page, 5) | ctx.Data["Page"] = paginater.New(int(count), opts.PageSize, opts.Page, 5) | ||||
ctx.Data["Users"] = users | ctx.Data["Users"] = users | ||||
ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail | ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail | ||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled | |||||
ctx.HTML(200, tplName) | ctx.HTML(200, tplName) | ||||
} | } | ||||
@@ -203,6 +210,7 @@ func ExploreUsers(ctx *context.Context) { | |||||
ctx.Data["Title"] = ctx.Tr("explore") | ctx.Data["Title"] = ctx.Tr("explore") | ||||
ctx.Data["PageIsExplore"] = true | ctx.Data["PageIsExplore"] = true | ||||
ctx.Data["PageIsExploreUsers"] = true | ctx.Data["PageIsExploreUsers"] = true | ||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled | |||||
RenderUserSearch(ctx, &models.SearchUserOptions{ | RenderUserSearch(ctx, &models.SearchUserOptions{ | ||||
Type: models.UserTypeIndividual, | Type: models.UserTypeIndividual, | ||||
@@ -216,6 +224,7 @@ func ExploreOrganizations(ctx *context.Context) { | |||||
ctx.Data["Title"] = ctx.Tr("explore") | ctx.Data["Title"] = ctx.Tr("explore") | ||||
ctx.Data["PageIsExplore"] = true | ctx.Data["PageIsExplore"] = true | ||||
ctx.Data["PageIsExploreOrganizations"] = true | ctx.Data["PageIsExploreOrganizations"] = true | ||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled | |||||
RenderUserSearch(ctx, &models.SearchUserOptions{ | RenderUserSearch(ctx, &models.SearchUserOptions{ | ||||
Type: models.UserTypeOrganization, | Type: models.UserTypeOrganization, | ||||
@@ -223,6 +232,113 @@ func ExploreOrganizations(ctx *context.Context) { | |||||
}, tplExploreOrganizations) | }, tplExploreOrganizations) | ||||
} | } | ||||
// ExploreCode render explore code page | |||||
func ExploreCode(ctx *context.Context) { | |||||
if !setting.Indexer.RepoIndexerEnabled { | |||||
ctx.Redirect("/explore", 302) | |||||
return | |||||
} | |||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled | |||||
ctx.Data["Title"] = ctx.Tr("explore") | |||||
ctx.Data["PageIsExplore"] = true | |||||
ctx.Data["PageIsExploreCode"] = true | |||||
keyword := strings.TrimSpace(ctx.Query("q")) | |||||
page := ctx.QueryInt("page") | |||||
if page <= 0 { | |||||
page = 1 | |||||
} | |||||
var ( | |||||
repoIDs []int64 | |||||
err error | |||||
isAdmin bool | |||||
userID int64 | |||||
) | |||||
if ctx.User != nil { | |||||
userID = ctx.User.ID | |||||
isAdmin = ctx.User.IsAdmin | |||||
} | |||||
// guest user or non-admin user | |||||
if ctx.User == nil || !isAdmin { | |||||
repoIDs, err = models.FindUserAccessibleRepoIDs(userID) | |||||
if err != nil { | |||||
ctx.ServerError("SearchResults", err) | |||||
return | |||||
} | |||||
} | |||||
var ( | |||||
total int | |||||
searchResults []*search.Result | |||||
) | |||||
// if non-admin login user, we need check UnitTypeCode at first | |||||
if ctx.User != nil && len(repoIDs) > 0 { | |||||
repoMaps, err := models.GetRepositoriesMapByIDs(repoIDs) | |||||
if err != nil { | |||||
ctx.ServerError("SearchResults", err) | |||||
return | |||||
} | |||||
var rightRepoMap = make(map[int64]*models.Repository, len(repoMaps)) | |||||
repoIDs = make([]int64, 0, len(repoMaps)) | |||||
for id, repo := range repoMaps { | |||||
if repo.CheckUnitUser(userID, isAdmin, models.UnitTypeCode) { | |||||
rightRepoMap[id] = repo | |||||
repoIDs = append(repoIDs, id) | |||||
} | |||||
} | |||||
ctx.Data["RepoMaps"] = rightRepoMap | |||||
total, searchResults, err = search.PerformSearch(repoIDs, keyword, page, setting.UI.RepoSearchPagingNum) | |||||
if err != nil { | |||||
ctx.ServerError("SearchResults", err) | |||||
return | |||||
} | |||||
// if non-login user or isAdmin, no need to check UnitTypeCode | |||||
} else if (ctx.User == nil && len(repoIDs) > 0) || isAdmin { | |||||
total, searchResults, err = search.PerformSearch(repoIDs, keyword, page, setting.UI.RepoSearchPagingNum) | |||||
if err != nil { | |||||
ctx.ServerError("SearchResults", err) | |||||
return | |||||
} | |||||
var loadRepoIDs = make([]int64, 0, len(searchResults)) | |||||
for _, result := range searchResults { | |||||
var find bool | |||||
for _, id := range loadRepoIDs { | |||||
if id == result.RepoID { | |||||
find = true | |||||
break | |||||
} | |||||
} | |||||
if !find { | |||||
loadRepoIDs = append(loadRepoIDs, result.RepoID) | |||||
} | |||||
} | |||||
repoMaps, err := models.GetRepositoriesMapByIDs(loadRepoIDs) | |||||
if err != nil { | |||||
ctx.ServerError("SearchResults", err) | |||||
return | |||||
} | |||||
ctx.Data["RepoMaps"] = repoMaps | |||||
} | |||||
ctx.Data["Keyword"] = keyword | |||||
pager := paginater.New(total, setting.UI.RepoSearchPagingNum, page, 5) | |||||
ctx.Data["Page"] = pager | |||||
ctx.Data["SearchResults"] = searchResults | |||||
ctx.Data["RequireHighlightJS"] = true | |||||
ctx.Data["PageIsViewCode"] = true | |||||
ctx.HTML(200, tplExploreCode) | |||||
} | |||||
// NotFound render 404 page | // NotFound render 404 page | ||||
func NotFound(ctx *context.Context) { | func NotFound(ctx *context.Context) { | ||||
ctx.Data["Title"] = "Page Not Found" | ctx.Data["Title"] = "Page Not Found" | ||||
@@ -29,7 +29,8 @@ func Search(ctx *context.Context) { | |||||
if page <= 0 { | if page <= 0 { | ||||
page = 1 | page = 1 | ||||
} | } | ||||
total, searchResults, err := search.PerformSearch(ctx.Repo.Repository.ID, keyword, page, setting.UI.RepoSearchPagingNum) | |||||
total, searchResults, err := search.PerformSearch([]int64{ctx.Repo.Repository.ID}, | |||||
keyword, page, setting.UI.RepoSearchPagingNum) | |||||
if err != nil { | if err != nil { | ||||
ctx.ServerError("SearchResults", err) | ctx.ServerError("SearchResults", err) | ||||
return | return | ||||
@@ -170,6 +170,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
m.Get("/repos", routers.ExploreRepos) | m.Get("/repos", routers.ExploreRepos) | ||||
m.Get("/users", routers.ExploreUsers) | m.Get("/users", routers.ExploreUsers) | ||||
m.Get("/organizations", routers.ExploreOrganizations) | m.Get("/organizations", routers.ExploreOrganizations) | ||||
m.Get("/code", routers.ExploreCode) | |||||
}, ignSignIn) | }, ignSignIn) | ||||
m.Combo("/install", routers.InstallInit).Get(routers.Install). | m.Combo("/install", routers.InstallInit).Get(routers.Install). | ||||
Post(bindIgnErr(auth.InstallForm{}), routers.InstallPost) | Post(bindIgnErr(auth.InstallForm{}), routers.InstallPost) | ||||
@@ -0,0 +1,55 @@ | |||||
{{template "base/head" .}} | |||||
<div class="explore users"> | |||||
{{template "explore/navbar" .}} | |||||
<div class="ui container"> | |||||
<form class="ui form" style="max-width: 100%"> | |||||
<div class="ui fluid action input"> | |||||
<input name="q" value="{{.Keyword}}" placeholder="{{.i18n.Tr "explore.search"}}..." autofocus> | |||||
<input type="hidden" name="tab" value="{{$.TabName}}"> | |||||
<button class="ui blue button">{{.i18n.Tr "explore.search"}}</button> | |||||
</div> | |||||
</form> | |||||
<div class="ui divider"></div> | |||||
<div class="ui user list"> | |||||
{{if .SearchResults}} | |||||
<h3> | |||||
{{.i18n.Tr "explore.code_search_results" (.Keyword|Escape) | Str2html }} | |||||
</h3> | |||||
<div class="repository search"> | |||||
{{range $result := .SearchResults}} | |||||
{{$repo := (index $.RepoMaps .RepoID)}} | |||||
<div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result"> | |||||
<h4 class="ui top attached normal header"> | |||||
<span class="file"><a rel="nofollow" href="{{EscapePound $repo.HTMLURL}}">{{$repo.FullName}}</a> - {{.Filename}}</span> | |||||
<a class="ui basic grey tiny button" rel="nofollow" href="{{EscapePound $repo.HTMLURL}}/src/branch/{{$repo.DefaultBranch}}/{{EscapePound .Filename}}">{{$.i18n.Tr "repo.diff.view_file"}}</a> | |||||
</h4> | |||||
<div class="ui attached table segment"> | |||||
<div class="file-body file-code code-view"> | |||||
<table> | |||||
<tbody> | |||||
<tr> | |||||
<td class="lines-num"> | |||||
{{range .LineNumbers}} | |||||
<a href="{{EscapePound $repo.HTMLURL}}/src/branch/{{$repo.DefaultBranch}}/{{EscapePound $result.Filename}}#L{{.}}"><span>{{.}}</span></a> | |||||
{{end}} | |||||
</td> | |||||
<td class="lines-code"><pre><code class="{{.HighlightClass}}"><ol class="linenums">{{.FormattedLines}}</ol></code></pre></td> | |||||
</tr> | |||||
</tbody> | |||||
</table> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
{{end}} | |||||
</div> | |||||
{{else}} | |||||
<div>{{$.i18n.Tr "explore.code_no_results"}}</div> | |||||
{{end}} | |||||
</div> | |||||
{{template "base/paginate" .}} | |||||
</div> | |||||
</div> | |||||
{{template "base/footer" .}} | |||||
@@ -8,4 +8,9 @@ | |||||
<a class="{{if .PageIsExploreOrganizations}}active{{end}} item" href="{{AppSubUrl}}/explore/organizations"> | <a class="{{if .PageIsExploreOrganizations}}active{{end}} item" href="{{AppSubUrl}}/explore/organizations"> | ||||
<span class="octicon octicon-organization"></span> {{.i18n.Tr "explore.organizations"}} | <span class="octicon octicon-organization"></span> {{.i18n.Tr "explore.organizations"}} | ||||
</a> | </a> | ||||
{{if .IsRepoIndexerEnabled}} | |||||
<a class="{{if .PageIsExploreCode}}active{{end}} item" href="{{AppSubUrl}}/explore/code"> | |||||
<span class="octicon octicon-code"></span> {{.i18n.Tr "explore.code"}} | |||||
</a> | |||||
{{end}} | |||||
</div> | </div> |