Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m32s
Build and Release / Lint (push) Failing after 1m53s
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 1m58s
- Update subdomain parser to use {repo}-{owner}.{domain} format
- Add middleware to intercept Pages subdomain requests
- Generate default config when Pages enabled but no .gitea/landing.yaml
- Pages are public landing pages (accessible even for private repos)
🤖 Generated with Claude Code
306 lines
8.8 KiB
Go
306 lines
8.8 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package pages
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
"code.gitea.io/gitea/modules/git"
|
|
"code.gitea.io/gitea/modules/json"
|
|
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
|
|
}
|
|
// If Pages is enabled but no config file, return a default config
|
|
if dbConfig != nil && dbConfig.Enabled {
|
|
return getDefaultConfig(repo, string(dbConfig.Template)), 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
|
|
|
|
|
|
// getDefaultConfig returns a default landing page configuration
|
|
func getDefaultConfig(repo *repo_model.Repository, template string) *pages_module.LandingConfig {
|
|
if template == "" {
|
|
template = "simple"
|
|
}
|
|
return &pages_module.LandingConfig{
|
|
Enabled: true,
|
|
Template: template,
|
|
Hero: pages_module.HeroConfig{
|
|
Title: repo.Name,
|
|
Tagline: repo.Description,
|
|
},
|
|
Branding: pages_module.BrandingConfig{
|
|
PrimaryColor: "#4183c4",
|
|
},
|
|
}
|
|
}
|
|
|
|
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, "", errors.New("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}.{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
|
|
// Extract domain from settings
|
|
domain := setting.Domain
|
|
return fmt.Sprintf("https://%s.%s", subdomain, domain)
|
|
}
|
|
|
|
// 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, sslExternal bool) (*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}
|
|
}
|
|
|
|
sslStatus := repo_model.SSLStatusPending
|
|
if sslExternal {
|
|
sslStatus = repo_model.SSLStatusActive
|
|
}
|
|
|
|
pagesDomain := &repo_model.PagesDomain{
|
|
RepoID: repoID,
|
|
Domain: domain,
|
|
SSLStatus: sslStatus,
|
|
}
|
|
|
|
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, 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, errors.New("domain not verified")
|
|
}
|
|
|
|
return repo_model.GetRepositoryByID(ctx, domain.RepoID)
|
|
}
|
|
|
|
// HasPublicLanding checks if a repository has public landing enabled
|
|
// This allows private repos to have a public-facing landing page
|
|
func HasPublicLanding(ctx context.Context, repo *repo_model.Repository) bool {
|
|
config, err := GetPagesConfig(ctx, repo)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return config.Enabled && config.PublicLanding
|
|
}
|
|
|
|
// HasPublicReleases checks if a repository has public releases enabled
|
|
// This allows private repos to have publicly accessible releases
|
|
func HasPublicReleases(ctx context.Context, repo *repo_model.Repository) bool {
|
|
config, err := GetPagesConfig(ctx, repo)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return config.Enabled && config.Advanced.PublicReleases
|
|
}
|