// 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) }