feat: allow any README for .profile (#8798)

closes #1624

Co-authored-by: Maxim Slipenko <maks1ms@altlinux.org>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8798
Reviewed-by: Otto <otto@codeberg.org>
Co-authored-by: Maxim Slipenko <maks1ms@noreply.codeberg.org>
Co-committed-by: Maxim Slipenko <maks1ms@noreply.codeberg.org>
This commit is contained in:
Maxim Slipenko 2025-09-01 13:58:00 +02:00 committed by Otto
commit ed3b70cbb9
7 changed files with 102 additions and 34 deletions

View file

@ -5,6 +5,8 @@ package org
import ( import (
"fmt" "fmt"
gotemplate "html/template"
"io"
"net/http" "net/http"
"path" "path"
"strings" "strings"
@ -180,20 +182,30 @@ func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repositor
} else { } else {
defer rc.Close() defer rc.Close()
if profileContent, err := markdown.RenderReader(&markup.RenderContext{ if markupType := markup.Type(profileReadme.Name()); markupType != "" {
Ctx: ctx, if profileContent, err := markdown.RenderReader(&markup.RenderContext{
GitRepo: profileGitRepo, Ctx: ctx,
Links: markup.Links{ Type: markupType,
// Pass repo link to markdown render for the full link of media elements. GitRepo: profileGitRepo,
// The profile of default branch would be shown. Links: markup.Links{
Base: profileDbRepo.Link(), // Pass repo link to markdown render for the full link of media elements.
BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), // The profile of default branch would be shown.
}, Base: profileDbRepo.Link(),
Metas: map[string]string{"mode": "document"}, BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
}, rc); err != nil { },
log.Error("failed to RenderString: %v", err) Metas: map[string]string{"mode": "document"},
}, rc); err != nil {
log.Error("failed to RenderString: %v", err)
} else {
ctx.Data["ProfileReadme"] = profileContent
}
} else { } else {
ctx.Data["ProfileReadme"] = profileContent content, err := io.ReadAll(rc)
if err != nil {
log.Error("Read readme content failed: %v", err)
}
ctx.Data["ProfileReadme"] = gotemplate.HTMLEscapeString(util.UnsafeBytesToString(content))
ctx.Data["IsProfileReadmePlain"] = true
} }
} }
} }

View file

@ -22,6 +22,7 @@ import (
"forgejo.org/modules/markup/markdown" "forgejo.org/modules/markup/markdown"
"forgejo.org/modules/optional" "forgejo.org/modules/optional"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/routers/web/repo"
"forgejo.org/services/context" "forgejo.org/services/context"
) )
@ -104,7 +105,22 @@ func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profile
if commit, err := profileGitRepo.GetBranchCommit(profileDbRepo.DefaultBranch); err != nil { if commit, err := profileGitRepo.GetBranchCommit(profileDbRepo.DefaultBranch); err != nil {
log.Error("FindUserProfileReadme failed to GetBranchCommit: %v", err) log.Error("FindUserProfileReadme failed to GetBranchCommit: %v", err)
} else { } else {
profileReadmeBlob, _ = commit.GetBlobByFoldedPath("README.md") tree, err := commit.SubTree("")
if err != nil {
log.Error("FindUserProfileReadme failed to get SubTree: %v", err)
} else {
entries, err := tree.ListEntries()
if err != nil {
log.Error("FindUserProfileReadme failed to list entries: %v", err)
} else {
_, readmeEntry, err := repo.FindReadmeFileInEntries(ctx, entries, true)
if err != nil {
log.Error("FindUserProfileReadme failed to find readme in entries: %v", err)
} else if readmeEntry != nil {
profileReadmeBlob = readmeEntry.Blob()
}
}
}
} }
} }
} }

View file

@ -8,6 +8,8 @@ package user
import ( import (
"errors" "errors"
"fmt" "fmt"
gotemplate "html/template"
"io"
"net/http" "net/http"
"path" "path"
"strings" "strings"
@ -269,23 +271,33 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
} else { } else {
defer rc.Close() defer rc.Close()
if profileContent, err := markdown.RenderReader(&markup.RenderContext{ if markupType := markup.Type(profileReadme.Name()); markupType != "" {
Ctx: ctx, if profileContent, err := markdown.RenderReader(&markup.RenderContext{
GitRepo: profileGitRepo, Ctx: ctx,
Links: markup.Links{ Type: markupType,
// Give the repo link to the markdown render for the full link of media element. GitRepo: profileGitRepo,
// the media link usually be like /[user]/[repoName]/media/branch/[branchName], Links: markup.Links{
// Eg. /Tom/.profile/media/branch/main // Give the repo link to the markdown render for the full link of media element.
// The branch shown on the profile page is the default branch, this need to be in sync with doc, see: // the media link usually be like /[user]/[repoName]/media/branch/[branchName],
// https://docs.gitea.com/usage/profile-readme // Eg. /Tom/.profile/media/branch/main
Base: profileDbRepo.Link(), // The branch shown on the profile page is the default branch, this need to be in sync with doc, see:
BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), // https://docs.gitea.com/usage/profile-readme
}, Base: profileDbRepo.Link(),
Metas: map[string]string{"mode": "document"}, BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
}, rc); err != nil { },
log.Error("failed to RenderString: %v", err) Metas: map[string]string{"mode": "document"},
}, rc); err != nil {
log.Error("failed to RenderString: %v", err)
} else {
ctx.Data["ProfileReadme"] = profileContent
}
} else { } else {
ctx.Data["ProfileReadme"] = profileContent content, err := io.ReadAll(rc)
if err != nil {
log.Error("Read readme content failed: %v", err)
}
ctx.Data["ProfileReadme"] = gotemplate.HTMLEscapeString(util.UnsafeBytesToString(content))
ctx.Data["IsProfileReadmePlain"] = true
} }
} }
default: // default to "repositories" default: // default to "repositories"

View file

@ -11,7 +11,13 @@
<div class="ui mobile reversed stackable grid"> <div class="ui mobile reversed stackable grid">
<div class="ui {{if .ShowMemberAndTeamTab}}eleven wide{{end}} column"> <div class="ui {{if .ShowMemberAndTeamTab}}eleven wide{{end}} column">
{{if .ProfileReadme}} {{if .ProfileReadme}}
<div id="readme_profile" class="markup">{{.ProfileReadme}}</div> <div id="readme_profile" class="{{if not .IsProfileReadmePlain}}markup{{end}}">
{{if .IsProfileReadmePlain}}
<pre>{{.ProfileReadme}}</pre>
{{else}}
{{.ProfileReadme}}
{{end}}
</div>
{{end}} {{end}}
{{template "shared/repo_search" .}} {{template "shared/repo_search" .}}
{{template "explore/repo_list" .}} {{template "explore/repo_list" .}}

View file

@ -51,7 +51,13 @@
{{else if eq .TabName "followers"}} {{else if eq .TabName "followers"}}
{{template "repo/user_cards" .}} {{template "repo/user_cards" .}}
{{else if eq .TabName "overview"}} {{else if eq .TabName "overview"}}
<div id="readme_profile" class="markup">{{.ProfileReadme}}</div> <div id="readme_profile" class="{{if not .IsProfileReadmePlain}}markup{{end}}">
{{if .IsProfileReadmePlain}}
<pre>{{.ProfileReadme}}</pre>
{{else}}
{{.ProfileReadme}}
{{end}}
</div>
{{else}} {{else}}
{{template "shared/repo_search" .}} {{template "shared/repo_search" .}}
{{template "explore/repo_list" .}} {{template "explore/repo_list" .}}

View file

@ -64,7 +64,15 @@ func TestOrgProfile(t *testing.T) {
checkReadme(t, "README.md", "README.md", 1) checkReadme(t, "README.md", "README.md", 1)
checkReadme(t, "readme.md", "readme.md", 1) checkReadme(t, "readme.md", "readme.md", 1)
checkReadme(t, "ReadMe.mD", "ReadMe.mD", 1) checkReadme(t, "ReadMe.mD", "ReadMe.mD", 1)
checkReadme(t, "readme.org does not render", "README.org", 0) checkReadme(t, "readme.org", "README.org", 1)
checkReadme(t, "README.en-us.md", "README.en-us.md", 1)
checkReadme(t, "README.en.md", "README.en.md", 1)
checkReadme(t, "README.txt", "README.txt", 1)
checkReadme(t, "README", "README", 1)
checkReadme(t, "README.mdown", "README.mdown", 1)
checkReadme(t, "README.i18n.md", "README.i18n.md", 1)
checkReadme(t, "readmee", "readmee", 0)
checkReadme(t, "test.md", "test.md", 0)
t.Run("readme-size", func(t *testing.T) { t.Run("readme-size", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()

View file

@ -64,7 +64,15 @@ func TestUserProfile(t *testing.T) {
checkReadme(t, "README.md", "README.md", 1) checkReadme(t, "README.md", "README.md", 1)
checkReadme(t, "readme.md", "readme.md", 1) checkReadme(t, "readme.md", "readme.md", 1)
checkReadme(t, "ReadMe.mD", "ReadMe.mD", 1) checkReadme(t, "ReadMe.mD", "ReadMe.mD", 1)
checkReadme(t, "readme.org does not render", "README.org", 0) checkReadme(t, "readme.org", "README.org", 1)
checkReadme(t, "README.en-us.md", "README.en-us.md", 1)
checkReadme(t, "README.en.md", "README.en.md", 1)
checkReadme(t, "README.txt", "README.txt", 1)
checkReadme(t, "README", "README", 1)
checkReadme(t, "README.mdown", "README.mdown", 1)
checkReadme(t, "README.i18n.md", "README.i18n.md", 1)
checkReadme(t, "readmee", "readmee", 0)
checkReadme(t, "test.md", "test.md", 0)
t.Run("readme-size", func(t *testing.T) { t.Run("readme-size", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()