Files
forge/routers/web/shared/user/header.go
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

197 lines
7.0 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"errors"
"net/url"
"github.com/hanzoai/git/models/db"
"github.com/hanzoai/git/models/organization"
access_model "github.com/hanzoai/git/models/perm/access"
project_model "github.com/hanzoai/git/models/project"
repo_model "github.com/hanzoai/git/models/repo"
"github.com/hanzoai/git/models/unit"
user_model "github.com/hanzoai/git/models/user"
"github.com/hanzoai/git/modules/git"
"github.com/hanzoai/git/modules/gitrepo"
"github.com/hanzoai/git/modules/log"
"github.com/hanzoai/git/modules/markup"
"github.com/hanzoai/git/modules/markup/markdown"
"github.com/hanzoai/git/modules/optional"
"github.com/hanzoai/git/modules/setting"
"github.com/hanzoai/git/modules/util"
"github.com/hanzoai/git/services/context"
)
// prepareContextForProfileBigAvatar set the context for big avatar view on the profile page
func prepareContextForProfileBigAvatar(ctx *context.Context) {
ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate
if setting.Service.UserLocationMapURL != "" {
ctx.Data["ContextUserLocationMapURL"] = setting.Service.UserLocationMapURL + url.QueryEscape(ctx.ContextUser.Location)
}
// Show OpenID URIs
openIDs, err := user_model.GetUserOpenIDs(ctx, ctx.ContextUser.ID)
if err != nil {
ctx.ServerError("GetUserOpenIDs", err)
return
}
ctx.Data["OpenIDs"] = openIDs
if len(ctx.ContextUser.Description) != 0 {
content, err := markdown.RenderString(markup.NewRenderContext(ctx).WithMetas(markup.ComposeSimpleDocumentMetas()), ctx.ContextUser.Description)
if err != nil {
ctx.ServerError("RenderString", err)
return
}
ctx.Data["RenderedDescription"] = content
}
orgs, err := db.Find[organization.Organization](ctx, organization.FindOrgOptions{
UserID: ctx.ContextUser.ID,
IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, ctx.ContextUser),
ListOptions: db.ListOptions{
Page: 1,
// query one more result (without a separate counting) to see whether we need to add the "show more orgs" link
PageSize: setting.UI.User.OrgPagingNum + 1,
},
})
if err != nil {
ctx.ServerError("FindOrgs", err)
return
}
if len(orgs) > setting.UI.User.OrgPagingNum {
orgs = orgs[:setting.UI.User.OrgPagingNum]
ctx.Data["ShowMoreOrgs"] = true
}
ctx.Data["Orgs"] = orgs
ctx.Data["HasOrgsVisible"] = organization.HasOrgsVisible(ctx, orgs, ctx.Doer)
badges, _, err := user_model.GetUserBadges(ctx, ctx.ContextUser)
if err != nil {
ctx.ServerError("GetUserBadges", err)
return
}
ctx.Data["Badges"] = badges
// in case the numbers are already provided by other functions, no need to query again (which is slow)
if _, ok := ctx.Data["NumFollowers"]; !ok {
_, ctx.Data["NumFollowers"], _ = user_model.GetUserFollowers(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{PageSize: 1, Page: 1})
}
if _, ok := ctx.Data["NumFollowing"]; !ok {
_, ctx.Data["NumFollowing"], _ = user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{PageSize: 1, Page: 1})
}
if ctx.Doer != nil {
ctx.Data["UserBlocking"], err = user_model.GetBlocking(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
if err != nil && !errors.Is(err, util.ErrNotExist) {
ctx.ServerError("GetBlocking", err)
}
}
}
func FindOwnerProfileReadme(ctx *context.Context, doer *user_model.User, optProfileRepoName ...string) (profileDbRepo *repo_model.Repository, profileReadmeBlob *git.Blob) {
profileRepoName := util.OptionalArg(optProfileRepoName, RepoNameProfile)
profileDbRepo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, profileRepoName)
if err != nil {
if !repo_model.IsErrRepoNotExist(err) {
log.Error("FindOwnerProfileReadme failed to GetRepositoryByName: %v", err)
}
return nil, nil
}
perm, err := access_model.GetDoerRepoPermission(ctx, profileDbRepo, doer)
if err != nil {
log.Error("FindOwnerProfileReadme failed to GetRepositoryByName: %v", err)
return nil, nil
}
if profileDbRepo.IsEmpty || !perm.CanRead(unit.TypeCode) {
return nil, nil
}
profileGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, profileDbRepo)
if err != nil {
log.Error("FindOwnerProfileReadme failed to OpenRepository: %v", err)
return nil, nil
}
commit, err := profileGitRepo.GetBranchCommit(profileDbRepo.DefaultBranch)
if err != nil {
log.Error("FindOwnerProfileReadme failed to GetBranchCommit: %v", err)
return nil, nil
}
profileReadmeBlob, _ = commit.GetBlobByPath(ctx, profileGitRepo, "README.md") // no need to handle this error
return profileDbRepo, profileReadmeBlob
}
type PrepareOwnerHeaderResult struct {
ProfilePublicRepo *repo_model.Repository
ProfilePublicReadmeBlob *git.Blob
ProfilePrivateRepo *repo_model.Repository
ProfilePrivateReadmeBlob *git.Blob
HasOrgProfileReadme bool
}
const (
RepoNameProfilePrivate = ".profile-private"
RepoNameProfile = ".profile"
)
func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) {
ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
ctx.Data["EnableFeed"] = setting.Other.EnableFeed
ctx.Data["FeedURL"] = ctx.ContextUser.HomeLink()
if err := loadHeaderCount(ctx); err != nil {
return nil, err
}
result = &PrepareOwnerHeaderResult{}
if ctx.ContextUser.IsOrganization() {
result.ProfilePublicRepo, result.ProfilePublicReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer)
result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate)
result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil
ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab
} else {
_, profileReadmeBlob := FindOwnerProfileReadme(ctx, ctx.Doer)
ctx.Data["HasUserProfileReadme"] = profileReadmeBlob != nil
prepareContextForProfileBigAvatar(ctx)
}
return result, nil
}
func loadHeaderCount(ctx *context.Context) error {
repoCount, err := repo_model.CountRepository(ctx, repo_model.SearchRepoOptions{
Actor: ctx.Doer,
OwnerID: ctx.ContextUser.ID,
Private: ctx.IsSigned,
Collaborate: optional.Some(false),
IncludeDescription: setting.UI.SearchRepoDescription,
})
if err != nil {
return err
}
ctx.Data["RepoCount"] = repoCount
var projectType project_model.Type
if ctx.ContextUser.IsOrganization() {
projectType = project_model.TypeOrganization
} else {
projectType = project_model.TypeIndividual
}
projectCount, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
OwnerID: ctx.ContextUser.ID,
IsClosed: optional.Some(false),
Type: projectType,
})
if err != nil {
return err
}
ctx.Data["ProjectCount"] = projectCount
return nil
}