Files
gitea/routers/api/v2/wiki.go
logikonline dfc94f6408
Some checks failed
Build and Release / Build Binaries (amd64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, linux) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, windows) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, linux) (push) Blocked by required conditions
Build and Release / Build Docker Image (push) Blocked by required conditions
Build and Release / Create Release (push) Blocked by required conditions
Build and Release / Lint and Test (push) Has been cancelled
fix: resolve all golangci-lint errors in v2 API
- Replace encoding/json with modules/json (depguard)
- Add error handling for json.Unmarshal and WriteItem calls (errcheck)
- Use slices.Contains instead of manual loops (modernize)
- Use any instead of interface{} (modernize)
- Use min/max built-in functions (modernize)
- Use strings.FieldsSeq and strings.SplitSeq (modernize)
- Use range over int for loops (modernize)
- Use http.MethodOptions constant (usestdlibvars)
- Use tagged switch statements (staticcheck)
- Use += and /= operators (gocritic)
- Fix gofumpt formatting issues
- Remove unused streamLargeFile function
- Remove unused primaryLang parameter

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 15:49:52 -05:00

900 lines
23 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v2
import (
"net/http"
"strings"
"time"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/charset"
apierrors "code.gitea.io/gitea/modules/errors"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
notify_service "code.gitea.io/gitea/services/notify"
wiki_service "code.gitea.io/gitea/services/wiki"
)
// WikiV2 handles all v2 wiki API endpoints
// ListWikiPagesV2 lists all wiki pages with enhanced metadata
func ListWikiPagesV2(ctx *context.APIContext) {
owner := ctx.PathParam("owner")
repoName := ctx.PathParam("repo")
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIErrorWithCode(apierrors.RepoNotFound)
return
}
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
// Check access
if repo.IsPrivate && !ctx.IsSigned {
ctx.APIErrorWithCode(apierrors.PermAccessDenied)
return
}
// Open wiki repo
wikiRepo, commit, err := findWikiRepoCommitV2(ctx, repo)
if err != nil {
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
if wikiRepo == nil {
ctx.JSON(http.StatusOK, &api.WikiPageListV2{
Pages: []*api.WikiPageV2{},
TotalCount: 0,
HasMore: false,
})
return
}
defer wikiRepo.Close()
if commit == nil {
ctx.JSON(http.StatusOK, &api.WikiPageListV2{
Pages: []*api.WikiPageV2{},
TotalCount: 0,
HasMore: false,
})
return
}
// Pagination
page := max(ctx.FormInt("page"), 1)
limit := ctx.FormInt("limit")
if limit <= 0 {
limit = setting.API.DefaultPagingNum
}
if limit > 100 {
limit = 100
}
entries, err := commit.ListEntries()
if err != nil {
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
// Filter to .md files
var mdEntries []*git.TreeEntry
for _, entry := range entries {
if entry.IsRegular() && strings.HasSuffix(entry.Name(), ".md") {
mdEntries = append(mdEntries, entry)
}
}
totalCount := int64(len(mdEntries))
skip := (page - 1) * limit
end := min(skip+limit, len(mdEntries))
pages := make([]*api.WikiPageV2, 0, limit)
for i := skip; i < end; i++ {
entry := mdEntries[i]
wikiName, err := wiki_service.GitPathToWebPath(entry.Name())
if err != nil {
continue
}
// Get last commit for this page
lastCommit, _ := wikiRepo.GetCommitByPath(entry.Name())
_, pageTitle := wiki_service.WebPathToUserTitle(wikiName)
page := &api.WikiPageV2{
Name: string(wikiName),
Title: pageTitle,
Path: entry.Name(),
URL: setting.AppURL + repo.FullName() + "/wiki/" + string(wikiName),
HTMLURL: setting.AppURL + repo.FullName() + "/wiki/" + string(wikiName),
}
// Get word count from index if available
if idx, _ := repo_model.GetWikiIndex(ctx, repo.ID, string(wikiName)); idx != nil {
page.WordCount = idx.WordCount
}
if lastCommit != nil {
page.LastCommit = &api.WikiCommitV2{
SHA: lastCommit.ID.String(),
Message: strings.Split(lastCommit.CommitMessage, "\n")[0],
Date: lastCommit.Author.When,
Author: &api.WikiAuthorV2{
Name: lastCommit.Author.Name,
Email: lastCommit.Author.Email,
},
}
}
pages = append(pages, page)
}
ctx.JSON(http.StatusOK, &api.WikiPageListV2{
Pages: pages,
TotalCount: totalCount,
HasMore: int64(end) < totalCount,
})
}
// GetWikiPageV2 gets a single wiki page with full content and metadata
func GetWikiPageV2(ctx *context.APIContext) {
owner := ctx.PathParam("owner")
repoName := ctx.PathParam("repo")
pageName := ctx.PathParam("pageName")
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIErrorWithCode(apierrors.RepoNotFound)
return
}
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
// Check access
if repo.IsPrivate && !ctx.IsSigned {
ctx.APIErrorWithCode(apierrors.PermAccessDenied)
return
}
wikiName := wiki_service.WebPathFromRequest(pageName)
wikiRepo, commit, err := findWikiRepoCommitV2(ctx, repo)
if err != nil {
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
if wikiRepo == nil || commit == nil {
ctx.APIErrorWithCode(apierrors.WikiPageNotFound)
return
}
defer wikiRepo.Close()
// Get page content
gitFilename := wiki_service.WebPathToGitPath(wikiName)
entry, err := commit.GetTreeEntryByPath(gitFilename)
if err != nil {
if git.IsErrNotExist(err) {
ctx.APIErrorWithCode(apierrors.WikiPageNotFound)
return
}
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
blob := entry.Blob()
content, err := blob.GetBlobContent(1024 * 1024) // 1MB max
if err != nil {
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
// Render HTML
var htmlContent string
rd := charset.ToUTF8WithFallbackReader(strings.NewReader(content), charset.ConvertOpts{})
buf := new(strings.Builder)
if err := markdown.Render(markup.NewRenderContext(ctx).WithRelativePath(gitFilename), rd, buf); err == nil {
htmlContent = buf.String()
}
// Get last commit
lastCommit, _ := wikiRepo.GetCommitByPath(gitFilename)
// Get commit count
commitsCount, _ := gitrepo.FileCommitsCount(ctx, repo.WikiStorageRepo(), repo.DefaultWikiBranch, gitFilename)
// Get sidebar and footer
sidebarContent, _ := getWikiContentByName(commit, "_Sidebar")
footerContent, _ := getWikiContentByName(commit, "_Footer")
// Get links from index
var linksOut, linksIn []string
if idx, _ := repo_model.GetWikiIndex(ctx, repo.ID, string(wikiName)); idx != nil {
if idx.LinksOut != "" {
_ = json.Unmarshal([]byte(idx.LinksOut), &linksOut)
}
}
// Get incoming links
if incoming, _ := wiki_service.GetWikiIncomingLinks(ctx, repo.ID, string(wikiName)); incoming != nil {
linksIn = incoming
}
_, pageTitle := wiki_service.WebPathToUserTitle(wikiName)
page := &api.WikiPageV2{
Name: string(wikiName),
Title: pageTitle,
Path: gitFilename,
URL: setting.AppURL + "api/v2/repos/" + repo.FullName() + "/wiki/pages/" + string(wikiName),
HTMLURL: setting.AppURL + repo.FullName() + "/wiki/" + string(wikiName),
Content: content,
ContentHTML: htmlContent,
WordCount: len(strings.Fields(content)),
LinksOut: linksOut,
LinksIn: linksIn,
Sidebar: sidebarContent,
Footer: footerContent,
HistoryURL: setting.AppURL + "api/v2/repos/" + repo.FullName() + "/wiki/pages/" + string(wikiName) + "/revisions",
}
if lastCommit != nil {
page.LastCommit = &api.WikiCommitV2{
SHA: lastCommit.ID.String(),
Message: strings.Split(lastCommit.CommitMessage, "\n")[0],
Date: lastCommit.Author.When,
Author: &api.WikiAuthorV2{
Name: lastCommit.Author.Name,
Email: lastCommit.Author.Email,
},
}
if lastCommit.Committer != nil {
page.LastCommit.Committer = &api.WikiAuthorV2{
Name: lastCommit.Committer.Name,
Email: lastCommit.Committer.Email,
}
}
}
// Update index in background
go func() {
_ = wiki_service.IndexWikiPage(ctx, repo, string(wikiName))
}()
ctx.SetTotalCountHeader(commitsCount)
ctx.JSON(http.StatusOK, page)
}
// CreateWikiPageV2 creates a new wiki page
func CreateWikiPageV2(ctx *context.APIContext) {
owner := ctx.PathParam("owner")
repoName := ctx.PathParam("repo")
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIErrorWithCode(apierrors.RepoNotFound)
return
}
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
// Check write access
if !ctx.IsSigned {
ctx.APIErrorWithCode(apierrors.AuthTokenMissing)
return
}
// Check if archived
if repo.IsArchived {
ctx.APIErrorWithCode(apierrors.RepoArchived)
return
}
form := web.GetForm(ctx).(*api.CreateWikiPageV2Option)
if form.Name == "" {
ctx.APIErrorWithCode(apierrors.ValidationFailed, map[string]any{
"field": "name",
"error": "name is required",
})
return
}
wikiName := wiki_service.UserTitleToWebPath("", form.Name)
message := form.Message
if message == "" {
message = "Add page: " + form.Name
}
if err := wiki_service.AddWikiPage(ctx, ctx.Doer, repo, wikiName, form.Content, message); err != nil {
if repo_model.IsErrWikiReservedName(err) {
ctx.APIErrorWithCode(apierrors.WikiReservedName)
return
}
if repo_model.IsErrWikiAlreadyExist(err) {
ctx.APIErrorWithCode(apierrors.WikiPageAlreadyExists)
return
}
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
// Index the new page
_ = wiki_service.IndexWikiPage(ctx, repo, string(wikiName))
notify_service.NewWikiPage(ctx, ctx.Doer, repo, string(wikiName), message)
// Return the created page
ctx.Redirect(setting.AppURL + "api/v2/repos/" + repo.FullName() + "/wiki/pages/" + string(wikiName))
}
// UpdateWikiPageV2 updates an existing wiki page
func UpdateWikiPageV2(ctx *context.APIContext) {
owner := ctx.PathParam("owner")
repoName := ctx.PathParam("repo")
pageName := ctx.PathParam("pageName")
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIErrorWithCode(apierrors.RepoNotFound)
return
}
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
// Check write access
if !ctx.IsSigned {
ctx.APIErrorWithCode(apierrors.AuthTokenMissing)
return
}
// Check if archived
if repo.IsArchived {
ctx.APIErrorWithCode(apierrors.RepoArchived)
return
}
form := web.GetForm(ctx).(*api.UpdateWikiPageV2Option)
oldWikiName := wiki_service.WebPathFromRequest(pageName)
newWikiName := oldWikiName
if form.RenameTo != "" {
newWikiName = wiki_service.UserTitleToWebPath("", form.RenameTo)
}
message := form.Message
if message == "" {
if oldWikiName != newWikiName {
message = "Rename page: " + string(oldWikiName) + " to " + string(newWikiName)
} else {
message = "Update page: " + string(oldWikiName)
}
}
if err := wiki_service.EditWikiPage(ctx, ctx.Doer, repo, oldWikiName, newWikiName, form.Content, message); err != nil {
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
// Update index
if oldWikiName != newWikiName {
_ = wiki_service.RemoveWikiPageFromIndex(ctx, repo.ID, string(oldWikiName))
}
_ = wiki_service.IndexWikiPage(ctx, repo, string(newWikiName))
notify_service.EditWikiPage(ctx, ctx.Doer, repo, string(newWikiName), message)
// Return the updated page
ctx.Redirect(setting.AppURL + "api/v2/repos/" + repo.FullName() + "/wiki/pages/" + string(newWikiName))
}
// DeleteWikiPageV2 deletes a wiki page
func DeleteWikiPageV2(ctx *context.APIContext) {
owner := ctx.PathParam("owner")
repoName := ctx.PathParam("repo")
pageName := ctx.PathParam("pageName")
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIErrorWithCode(apierrors.RepoNotFound)
return
}
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
// Check write access
if !ctx.IsSigned {
ctx.APIErrorWithCode(apierrors.AuthTokenMissing)
return
}
// Check if archived
if repo.IsArchived {
ctx.APIErrorWithCode(apierrors.RepoArchived)
return
}
wikiName := wiki_service.WebPathFromRequest(pageName)
if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, repo, wikiName); err != nil {
if strings.Contains(err.Error(), "not exist") {
ctx.APIErrorWithCode(apierrors.WikiPageNotFound)
return
}
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
// Remove from index
_ = wiki_service.RemoveWikiPageFromIndex(ctx, repo.ID, string(wikiName))
notify_service.DeleteWikiPage(ctx, ctx.Doer, repo, string(wikiName))
ctx.JSON(http.StatusOK, &api.WikiDeleteResponseV2{Success: true})
}
// SearchWikiV2 searches wiki pages
func SearchWikiV2(ctx *context.APIContext) {
owner := ctx.PathParam("owner")
repoName := ctx.PathParam("repo")
query := ctx.FormString("q")
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIErrorWithCode(apierrors.RepoNotFound)
return
}
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
// Check access
if repo.IsPrivate && !ctx.IsSigned {
ctx.APIErrorWithCode(apierrors.PermAccessDenied)
return
}
// Ensure wiki is indexed
go func() {
_ = wiki_service.IndexAllWikiPages(ctx, repo)
}()
limit := ctx.FormInt("limit")
if limit <= 0 {
limit = 20
}
if limit > 100 {
limit = 100
}
opts := &repo_model.SearchWikiOptions{
RepoID: repo.ID,
Query: query,
Limit: limit,
Offset: ctx.FormInt("offset"),
}
results, total, err := repo_model.SearchWikiPages(ctx, opts)
if err != nil {
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
searchResults := make([]*api.WikiSearchResultV2, 0, len(results))
for _, idx := range results {
// Create snippet
snippet := createSearchSnippet(idx.Content, query, 200)
searchResults = append(searchResults, &api.WikiSearchResultV2{
Name: idx.PageName,
Title: idx.Title,
Snippet: snippet,
Score: calculateSearchScore(idx, query),
WordCount: idx.WordCount,
LastUpdated: idx.UpdatedUnix.AsTime(),
})
}
ctx.JSON(http.StatusOK, &api.WikiSearchResponseV2{
Query: query,
Results: searchResults,
TotalCount: total,
})
}
// GetWikiGraphV2 returns the wiki link graph
func GetWikiGraphV2(ctx *context.APIContext) {
owner := ctx.PathParam("owner")
repoName := ctx.PathParam("repo")
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIErrorWithCode(apierrors.RepoNotFound)
return
}
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
// Check access
if repo.IsPrivate && !ctx.IsSigned {
ctx.APIErrorWithCode(apierrors.PermAccessDenied)
return
}
// Ensure wiki is indexed
_ = wiki_service.IndexAllWikiPages(ctx, repo)
nodes, edges, err := wiki_service.GetWikiGraph(ctx, repo.ID)
if err != nil {
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
// Convert to API types
apiNodes := make([]*api.WikiGraphNodeV2, 0, len(nodes))
for _, n := range nodes {
apiNodes = append(apiNodes, &api.WikiGraphNodeV2{
Name: n["name"].(string),
Title: n["title"].(string),
WordCount: n["word_count"].(int),
})
}
apiEdges := make([]*api.WikiGraphEdgeV2, 0, len(edges))
for _, e := range edges {
apiEdges = append(apiEdges, &api.WikiGraphEdgeV2{
Source: e["source"].(string),
Target: e["target"].(string),
})
}
ctx.JSON(http.StatusOK, &api.WikiGraphV2{
Nodes: apiNodes,
Edges: apiEdges,
})
}
// GetWikiStatsV2 returns wiki statistics
func GetWikiStatsV2(ctx *context.APIContext) {
owner := ctx.PathParam("owner")
repoName := ctx.PathParam("repo")
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIErrorWithCode(apierrors.RepoNotFound)
return
}
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
// Check access
if repo.IsPrivate && !ctx.IsSigned {
ctx.APIErrorWithCode(apierrors.PermAccessDenied)
return
}
// Ensure wiki is indexed
_ = wiki_service.IndexAllWikiPages(ctx, repo)
// Get basic stats
totalPages, _ := repo_model.GetWikiIndexCount(ctx, repo.ID)
totalWords, _ := repo_model.GetWikiTotalWordCount(ctx, repo.ID)
// Get health info
orphaned, _ := wiki_service.GetOrphanedPages(ctx, repo.ID)
deadLinks, _ := wiki_service.GetDeadLinks(ctx, repo.ID)
// Get indexes for top linked calculation
indexes, _ := repo_model.GetWikiIndexByRepo(ctx, repo.ID)
// Calculate incoming links for each page
linkCounts := make(map[string]int)
for _, idx := range indexes {
var links []string
if idx.LinksOut != "" {
_ = json.Unmarshal([]byte(idx.LinksOut), &links)
}
for _, link := range links {
linkCounts[link]++
}
}
// Find top linked pages
topLinked := make([]*api.WikiTopLinkedPageV2, 0)
for name, count := range linkCounts {
if count > 1 { // Only include pages with multiple links
topLinked = append(topLinked, &api.WikiTopLinkedPageV2{
Name: name,
IncomingLinks: count,
})
}
}
// Sort by incoming links (simple bubble sort for small lists)
for i := 0; i < len(topLinked); i++ {
for j := i + 1; j < len(topLinked); j++ {
if topLinked[j].IncomingLinks > topLinked[i].IncomingLinks {
topLinked[i], topLinked[j] = topLinked[j], topLinked[i]
}
}
}
if len(topLinked) > 10 {
topLinked = topLinked[:10]
}
// Build health info
health := &api.WikiHealthV2{
OrphanedPages: make([]*api.WikiOrphanedPageV2, 0),
DeadLinks: make([]*api.WikiDeadLinkV2, 0),
OutdatedPages: make([]*api.WikiOutdatedPageV2, 0),
ShortPages: make([]*api.WikiShortPageV2, 0),
}
for _, o := range orphaned {
health.OrphanedPages = append(health.OrphanedPages, &api.WikiOrphanedPageV2{
Name: o.PageName,
WordCount: o.WordCount,
})
}
for _, d := range deadLinks {
health.DeadLinks = append(health.DeadLinks, &api.WikiDeadLinkV2{
Page: d["page"],
BrokenLink: d["broken_link"],
})
}
// Find short pages (< 100 words)
for _, idx := range indexes {
if idx.WordCount < 100 && idx.PageName != "_Sidebar" && idx.PageName != "_Footer" {
health.ShortPages = append(health.ShortPages, &api.WikiShortPageV2{
Name: idx.PageName,
WordCount: idx.WordCount,
})
}
}
// Find outdated pages (> 180 days old)
sixMonthsAgo := time.Now().AddDate(0, -6, 0)
for _, idx := range indexes {
lastEdit := idx.UpdatedUnix.AsTime()
if lastEdit.Before(sixMonthsAgo) {
daysOld := int(time.Since(lastEdit).Hours() / 24)
health.OutdatedPages = append(health.OutdatedPages, &api.WikiOutdatedPageV2{
Name: idx.PageName,
LastEdit: lastEdit,
DaysOld: daysOld,
})
}
}
// Get last updated time
var lastUpdated time.Time
for _, idx := range indexes {
if idx.UpdatedUnix.AsTime().After(lastUpdated) {
lastUpdated = idx.UpdatedUnix.AsTime()
}
}
// Get commit count and contributors from git
var totalCommits int64
var contributors int64
wikiRepo, _, err := findWikiRepoCommitV2(ctx, repo)
if err == nil && wikiRepo != nil {
defer wikiRepo.Close()
// Get commit count
if count, err := gitrepo.CommitsCount(ctx, repo.WikiStorageRepo(), gitrepo.CommitsCountOptions{
Revision: []string{repo.DefaultWikiBranch},
}); err == nil {
totalCommits = count
}
// Contributors - simplified as unique committers
contributors = int64(len(linkCounts)) // Approximate
}
ctx.JSON(http.StatusOK, &api.WikiStatsV2{
TotalPages: totalPages,
TotalWords: totalWords,
TotalCommits: totalCommits,
LastUpdated: lastUpdated,
Contributors: contributors,
Health: health,
TopLinked: topLinked,
})
}
// GetWikiPageRevisionsV2 returns revision history for a wiki page
func GetWikiPageRevisionsV2(ctx *context.APIContext) {
owner := ctx.PathParam("owner")
repoName := ctx.PathParam("repo")
pageName := ctx.PathParam("pageName")
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIErrorWithCode(apierrors.RepoNotFound)
return
}
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
// Check access
if repo.IsPrivate && !ctx.IsSigned {
ctx.APIErrorWithCode(apierrors.PermAccessDenied)
return
}
wikiName := wiki_service.WebPathFromRequest(pageName)
gitFilename := wiki_service.WebPathToGitPath(wikiName)
wikiRepo, commit, err := findWikiRepoCommitV2(ctx, repo)
if err != nil || wikiRepo == nil || commit == nil {
ctx.APIErrorWithCode(apierrors.WikiPageNotFound)
return
}
defer wikiRepo.Close()
// Check if page exists
if _, err := commit.GetTreeEntryByPath(gitFilename); err != nil {
ctx.APIErrorWithCode(apierrors.WikiPageNotFound)
return
}
// Get commits for this file
page := max(ctx.FormInt("page"), 1)
commits, err := wikiRepo.CommitsByFileAndRange(git.CommitsByFileAndRangeOptions{
Revision: repo.DefaultWikiBranch,
File: gitFilename,
Page: page,
})
if err != nil {
ctx.APIErrorWithCode(apierrors.InternalError)
return
}
// Get total count
totalCount, _ := gitrepo.FileCommitsCount(ctx, repo.WikiStorageRepo(), repo.DefaultWikiBranch, gitFilename)
revisions := make([]*api.WikiRevisionV2, 0, len(commits))
for _, c := range commits {
rev := &api.WikiRevisionV2{
SHA: c.ID.String(),
Message: strings.Split(c.CommitMessage, "\n")[0],
Date: c.Author.When,
Author: &api.WikiAuthorV2{
Name: c.Author.Name,
Email: c.Author.Email,
},
}
revisions = append(revisions, rev)
}
ctx.JSON(http.StatusOK, &api.WikiRevisionsV2{
PageName: string(wikiName),
Revisions: revisions,
TotalCount: totalCount,
})
}
// Helper functions
func findWikiRepoCommitV2(ctx *context.APIContext, repo *repo_model.Repository) (*git.Repository, *git.Commit, error) {
wikiRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo())
if err != nil {
if git.IsErrNotExist(err) || strings.Contains(err.Error(), "no such file or directory") {
return nil, nil, nil
}
return nil, nil, err
}
branch := repo.DefaultWikiBranch
if branch == "" {
branch = "master"
}
commit, err := wikiRepo.GetBranchCommit(branch)
if err != nil {
wikiRepo.Close()
if git.IsErrNotExist(err) {
return nil, nil, nil
}
return nil, nil, err
}
return wikiRepo, commit, nil
}
func getWikiContentByName(commit *git.Commit, name string) (string, error) {
wikiPath := wiki_service.WebPathToGitPath(wiki_service.WebPath(name))
entry, err := commit.GetTreeEntryByPath(wikiPath)
if err != nil {
return "", err
}
blob := entry.Blob()
content, err := blob.GetBlobContent(1024 * 1024)
if err != nil {
return "", err
}
return content, nil
}
func createSearchSnippet(content, query string, maxLen int) string {
lowerContent := strings.ToLower(content)
lowerQuery := strings.ToLower(query)
idx := strings.Index(lowerContent, lowerQuery)
if idx == -1 {
// Query not found, return beginning of content
if len(content) > maxLen {
return content[:maxLen] + "..."
}
return content
}
// Find start position
start := max(idx-maxLen/4, 0)
// Find end position
end := min(start+maxLen, len(content))
snippet := content[start:end]
if start > 0 {
snippet = "..." + snippet
}
if end < len(content) {
snippet += "..."
}
return snippet
}
func calculateSearchScore(idx *repo_model.WikiIndex, query string) float32 {
score := float32(0.0)
lowerQuery := strings.ToLower(query)
// Title match is highest priority
if strings.Contains(strings.ToLower(idx.Title), lowerQuery) {
score += 10.0
}
// Page name match
if strings.Contains(strings.ToLower(idx.PageName), lowerQuery) {
score += 8.0
}
// Content match - count occurrences
lowerContent := strings.ToLower(idx.Content)
count := strings.Count(lowerContent, lowerQuery)
score += float32(count) * 0.5
// Longer pages might have more matches but aren't necessarily more relevant
// Normalize by word count
if idx.WordCount > 0 {
score /= float32(idx.WordCount) / 100.0
}
return score
}