Phase 3: Organization Public Profile Page - Pinned repositories with groups - Public members display with roles - API endpoints for pinned repos and groups Phase 4: Gitea Pages Foundation - Landing page templates (simple, docs, product, portfolio) - Custom domain support with verification - YAML configuration parser (.gitea/landing.yaml) - Repository settings UI for pages Phase 5: Enhanced Wiki System with V2 API - Full CRUD operations via v2 API - Full-text search with WikiIndex table - Link graph visualization - Wiki health metrics (orphaned, dead links, outdated) - Designed for external AI plugin integration - Developer guide for .NET integration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
254 lines
7.3 KiB
Go
254 lines
7.3 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package pages
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
"code.gitea.io/gitea/modules/git"
|
|
pages_module "code.gitea.io/gitea/modules/pages"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
)
|
|
|
|
const (
|
|
// LandingConfigPath is the path to the landing page configuration file
|
|
LandingConfigPath = ".gitea/landing.yaml"
|
|
// LandingConfigPathAlt is an alternative path for the configuration file
|
|
LandingConfigPathAlt = ".gitea/landing.yml"
|
|
)
|
|
|
|
// GetPagesConfig returns the pages configuration for a repository
|
|
// It first tries to load from the database cache, then falls back to parsing the file
|
|
func GetPagesConfig(ctx context.Context, repo *repo_model.Repository) (*pages_module.LandingConfig, error) {
|
|
// Check if pages is configured in DB
|
|
dbConfig, err := repo_model.GetPagesConfigByRepoID(ctx, repo.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Try to load and parse the config from the repository
|
|
fileConfig, fileHash, err := loadConfigFromRepo(ctx, repo)
|
|
if err != nil {
|
|
// If file doesn't exist, check if DB has config
|
|
if dbConfig != nil && dbConfig.ConfigJSON != "" {
|
|
// Use cached config
|
|
var config pages_module.LandingConfig
|
|
if err := json.Unmarshal([]byte(dbConfig.ConfigJSON), &config); err != nil {
|
|
return nil, err
|
|
}
|
|
return &config, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// If we have a file config, check if cache needs updating
|
|
if dbConfig != nil && dbConfig.ConfigHash == fileHash {
|
|
// Cache is up to date, use cached config
|
|
var config pages_module.LandingConfig
|
|
if err := json.Unmarshal([]byte(dbConfig.ConfigJSON), &config); err != nil {
|
|
// Cache is corrupted, use file config
|
|
return fileConfig, nil
|
|
}
|
|
return &config, nil
|
|
}
|
|
|
|
// Update cache with new config
|
|
configJSON, err := json.Marshal(fileConfig)
|
|
if err != nil {
|
|
return fileConfig, nil // Return config even if caching fails
|
|
}
|
|
|
|
if dbConfig == nil {
|
|
// Create new cache entry
|
|
dbConfig = &repo_model.PagesConfig{
|
|
RepoID: repo.ID,
|
|
Enabled: fileConfig.Enabled,
|
|
Template: repo_model.PagesTemplate(fileConfig.Template),
|
|
ConfigJSON: string(configJSON),
|
|
ConfigHash: fileHash,
|
|
}
|
|
if err := repo_model.CreatePagesConfig(ctx, dbConfig); err != nil {
|
|
// Log but don't fail
|
|
return fileConfig, nil
|
|
}
|
|
} else {
|
|
// Update existing cache
|
|
dbConfig.Enabled = fileConfig.Enabled
|
|
dbConfig.Template = repo_model.PagesTemplate(fileConfig.Template)
|
|
dbConfig.ConfigJSON = string(configJSON)
|
|
dbConfig.ConfigHash = fileHash
|
|
if err := repo_model.UpdatePagesConfig(ctx, dbConfig); err != nil {
|
|
// Log but don't fail
|
|
return fileConfig, nil
|
|
}
|
|
}
|
|
|
|
return fileConfig, nil
|
|
}
|
|
|
|
// loadConfigFromRepo loads the landing.yaml configuration from the repository
|
|
func loadConfigFromRepo(ctx context.Context, repo *repo_model.Repository) (*pages_module.LandingConfig, string, error) {
|
|
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer gitRepo.Close()
|
|
|
|
// Try to get the default branch
|
|
branch := repo.DefaultBranch
|
|
if branch == "" {
|
|
branch = "main"
|
|
}
|
|
|
|
// Try to get the commit
|
|
commit, err := gitRepo.GetBranchCommit(branch)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
// Try to get the config file
|
|
var content []byte
|
|
entry, err := commit.GetTreeEntryByPath(LandingConfigPath)
|
|
if err != nil {
|
|
// Try alternative path
|
|
entry, err = commit.GetTreeEntryByPath(LandingConfigPathAlt)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("landing config not found")
|
|
}
|
|
}
|
|
|
|
reader, err := entry.Blob().DataAsync()
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer reader.Close()
|
|
|
|
content = make([]byte, entry.Blob().Size())
|
|
_, err = reader.Read(content)
|
|
if err != nil && err.Error() != "EOF" {
|
|
return nil, "", err
|
|
}
|
|
|
|
// Parse the config
|
|
config, err := pages_module.ParseLandingConfig(content)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
// Calculate hash for cache invalidation
|
|
hash := pages_module.HashConfig(content)
|
|
|
|
return config, hash, nil
|
|
}
|
|
|
|
// IsPagesEnabled checks if pages is enabled for a repository
|
|
func IsPagesEnabled(ctx context.Context, repo *repo_model.Repository) (bool, error) {
|
|
config, err := GetPagesConfig(ctx, repo)
|
|
if err != nil {
|
|
return false, nil // Not enabled if config doesn't exist
|
|
}
|
|
return config.Enabled, nil
|
|
}
|
|
|
|
// EnablePages enables pages for a repository with default config
|
|
func EnablePages(ctx context.Context, repo *repo_model.Repository, template string) error {
|
|
if !pages_module.IsValidTemplate(template) {
|
|
template = "simple"
|
|
}
|
|
|
|
return repo_model.EnablePages(ctx, repo.ID, repo_model.PagesTemplate(template))
|
|
}
|
|
|
|
// DisablePages disables pages for a repository
|
|
func DisablePages(ctx context.Context, repo *repo_model.Repository) error {
|
|
return repo_model.DisablePages(ctx, repo.ID)
|
|
}
|
|
|
|
// GetPagesSubdomain returns the subdomain for a repository's pages
|
|
func GetPagesSubdomain(repo *repo_model.Repository) string {
|
|
// Format: {repo}.{owner}.pages.{domain}
|
|
return fmt.Sprintf("%s.%s", strings.ToLower(repo.Name), strings.ToLower(repo.OwnerName))
|
|
}
|
|
|
|
// GetPagesURL returns the full URL for a repository's pages
|
|
func GetPagesURL(repo *repo_model.Repository) string {
|
|
subdomain := GetPagesSubdomain(repo)
|
|
// This should be configurable
|
|
pagesDomain := setting.AppURL // TODO: Add proper pages domain setting
|
|
return fmt.Sprintf("https://%s.pages.%s", subdomain, pagesDomain)
|
|
}
|
|
|
|
// GetPagesDomains returns all custom domains for a repository's pages
|
|
func GetPagesDomains(ctx context.Context, repoID int64) ([]*repo_model.PagesDomain, error) {
|
|
return repo_model.GetPagesDomainsByRepoID(ctx, repoID)
|
|
}
|
|
|
|
// AddPagesDomain adds a custom domain for pages
|
|
func AddPagesDomain(ctx context.Context, repoID int64, domain string) (*repo_model.PagesDomain, error) {
|
|
// Normalize domain
|
|
domain = strings.ToLower(strings.TrimSpace(domain))
|
|
|
|
// Check if domain already exists
|
|
existing, err := repo_model.GetPagesDomainByDomain(ctx, domain)
|
|
if err == nil && existing != nil {
|
|
return nil, repo_model.ErrPagesDomainAlreadyExist{Domain: domain}
|
|
}
|
|
|
|
pagesDomain := &repo_model.PagesDomain{
|
|
RepoID: repoID,
|
|
Domain: domain,
|
|
}
|
|
|
|
if err := repo_model.CreatePagesDomain(ctx, pagesDomain); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return pagesDomain, nil
|
|
}
|
|
|
|
// RemovePagesDomain removes a custom domain
|
|
func RemovePagesDomain(ctx context.Context, repoID int64, domainID int64) error {
|
|
domain, err := repo_model.GetPagesDomainByID(ctx, domainID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify domain belongs to this repo
|
|
if domain.RepoID != repoID {
|
|
return repo_model.ErrPagesDomainNotExist{ID: domainID}
|
|
}
|
|
|
|
return repo_model.DeletePagesDomain(ctx, domainID)
|
|
}
|
|
|
|
// VerifyDomain verifies a custom domain by checking DNS records
|
|
func VerifyDomain(ctx context.Context, domainID int64) error {
|
|
domain, err := repo_model.GetPagesDomainByID(ctx, domainID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: Implement actual DNS verification
|
|
// For now, just mark as verified
|
|
return repo_model.VerifyPagesDomain(ctx, domain.ID)
|
|
}
|
|
|
|
// GetRepoByPagesDomain returns the repository for a pages domain
|
|
func GetRepoByPagesDomain(ctx context.Context, domainName string) (*repo_model.Repository, error) {
|
|
domain, err := repo_model.GetPagesDomainByDomain(ctx, domainName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !domain.Verified {
|
|
return nil, fmt.Errorf("domain not verified")
|
|
}
|
|
|
|
return repo_model.GetRepositoryByID(ctx, domain.RepoID)
|
|
}
|