// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package repo

import (
	"context"
	"errors"
	"fmt"
	"html/template"
	"maps"
	"net"
	"net/url"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"sync"

	"code.gitea.io/gitea/models/db"
	"code.gitea.io/gitea/models/unit"
	user_model "code.gitea.io/gitea/models/user"
	"code.gitea.io/gitea/modules/base"
	"code.gitea.io/gitea/modules/git"
	giturl "code.gitea.io/gitea/modules/git/url"
	"code.gitea.io/gitea/modules/httplib"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/markup"
	"code.gitea.io/gitea/modules/optional"
	"code.gitea.io/gitea/modules/setting"
	api "code.gitea.io/gitea/modules/structs"
	"code.gitea.io/gitea/modules/timeutil"
	"code.gitea.io/gitea/modules/util"

	"xorm.io/builder"
)

// ErrUserDoesNotHaveAccessToRepo represents an error where the user doesn't has access to a given repo.
type ErrUserDoesNotHaveAccessToRepo struct {
	UserID   int64
	RepoName string
}

// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrUserDoesNotHaveAccessToRepo.
func IsErrUserDoesNotHaveAccessToRepo(err error) bool {
	_, ok := err.(ErrUserDoesNotHaveAccessToRepo)
	return ok
}

func (err ErrUserDoesNotHaveAccessToRepo) Error() string {
	return fmt.Sprintf("user doesn't have access to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName)
}

func (err ErrUserDoesNotHaveAccessToRepo) Unwrap() error {
	return util.ErrPermissionDenied
}

type ErrRepoIsArchived struct {
	Repo *Repository
}

func (err ErrRepoIsArchived) Error() string {
	return err.Repo.LogString() + " is archived"
}

type globalVarsStruct struct {
	validRepoNamePattern     *regexp.Regexp
	invalidRepoNamePattern   *regexp.Regexp
	reservedRepoNames        []string
	reservedRepoNamePatterns []string
}

var globalVars = sync.OnceValue(func() *globalVarsStruct {
	return &globalVarsStruct{
		validRepoNamePattern:     regexp.MustCompile(`^[-.\w]+$`),
		invalidRepoNamePattern:   regexp.MustCompile(`[.]{2,}`),
		reservedRepoNames:        []string{".", "..", "-"},
		reservedRepoNamePatterns: []string{"*.wiki", "*.git", "*.rss", "*.atom"},
	}
})

// IsUsableRepoName returns true when name is usable
func IsUsableRepoName(name string) error {
	vars := globalVars()
	if !vars.validRepoNamePattern.MatchString(name) || vars.invalidRepoNamePattern.MatchString(name) {
		// Note: usually this error is normally caught up earlier in the UI
		return db.ErrNameCharsNotAllowed{Name: name}
	}
	return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoNamePatterns, name)
}

// IsValidSSHAccessRepoName is like IsUsableRepoName, but it allows "*.wiki" because wiki repo needs to be accessed in SSH code
func IsValidSSHAccessRepoName(name string) bool {
	vars := globalVars()
	if !vars.validRepoNamePattern.MatchString(name) || vars.invalidRepoNamePattern.MatchString(name) {
		return false
	}
	return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoNamePatterns[1:], name) == nil
}

// TrustModelType defines the types of trust model for this repository
type TrustModelType int

// kinds of TrustModel
const (
	DefaultTrustModel TrustModelType = iota // default trust model
	CommitterTrustModel
	CollaboratorTrustModel
	CollaboratorCommitterTrustModel
)

// String converts a TrustModelType to a string
func (t TrustModelType) String() string {
	switch t {
	case DefaultTrustModel:
		return "default"
	case CommitterTrustModel:
		return "committer"
	case CollaboratorTrustModel:
		return "collaborator"
	case CollaboratorCommitterTrustModel:
		return "collaboratorcommitter"
	}
	return "default"
}

// ToTrustModel converts a string to a TrustModelType
func ToTrustModel(model string) TrustModelType {
	switch strings.ToLower(strings.TrimSpace(model)) {
	case "default":
		return DefaultTrustModel
	case "collaborator":
		return CollaboratorTrustModel
	case "committer":
		return CommitterTrustModel
	case "collaboratorcommitter":
		return CollaboratorCommitterTrustModel
	}
	return DefaultTrustModel
}

// RepositoryStatus defines the status of repository
type RepositoryStatus int

// all kinds of RepositoryStatus
const (
	RepositoryReady           RepositoryStatus = iota // a normal repository
	RepositoryBeingMigrated                           // repository is migrating
	RepositoryPendingTransfer                         // repository pending in ownership transfer state
	RepositoryBroken                                  // repository is in a permanently broken state
)

// Repository represents a git repository.
type Repository struct {
	ID                  int64 `xorm:"pk autoincr"`
	OwnerID             int64 `xorm:"UNIQUE(s) index"`
	OwnerName           string
	Owner               *user_model.User   `xorm:"-"`
	LowerName           string             `xorm:"UNIQUE(s) INDEX NOT NULL"`
	Name                string             `xorm:"INDEX NOT NULL"`
	Description         string             `xorm:"TEXT"`
	Website             string             `xorm:"VARCHAR(2048)"`
	OriginalServiceType api.GitServiceType `xorm:"index"`
	OriginalURL         string             `xorm:"VARCHAR(2048)"`
	DefaultBranch       string
	DefaultWikiBranch   string

	NumWatches          int
	NumStars            int
	NumForks            int
	NumIssues           int
	NumClosedIssues     int
	NumOpenIssues       int `xorm:"-"`
	NumPulls            int
	NumClosedPulls      int
	NumOpenPulls        int `xorm:"-"`
	NumMilestones       int `xorm:"NOT NULL DEFAULT 0"`
	NumClosedMilestones int `xorm:"NOT NULL DEFAULT 0"`
	NumOpenMilestones   int `xorm:"-"`
	NumProjects         int `xorm:"NOT NULL DEFAULT 0"`
	NumClosedProjects   int `xorm:"NOT NULL DEFAULT 0"`
	NumOpenProjects     int `xorm:"-"`
	NumActionRuns       int `xorm:"NOT NULL DEFAULT 0"`
	NumClosedActionRuns int `xorm:"NOT NULL DEFAULT 0"`
	NumOpenActionRuns   int `xorm:"-"`

	IsPrivate  bool `xorm:"INDEX"`
	IsEmpty    bool `xorm:"INDEX"`
	IsArchived bool `xorm:"INDEX"`
	IsMirror   bool `xorm:"INDEX"`

	Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"`

	commonRenderingMetas map[string]string `xorm:"-"`

	Units           []*RepoUnit   `xorm:"-"`
	PrimaryLanguage *LanguageStat `xorm:"-"`

	IsFork                          bool               `xorm:"INDEX NOT NULL DEFAULT false"`
	ForkID                          int64              `xorm:"INDEX"`
	BaseRepo                        *Repository        `xorm:"-"`
	IsTemplate                      bool               `xorm:"INDEX NOT NULL DEFAULT false"`
	TemplateID                      int64              `xorm:"INDEX"`
	Size                            int64              `xorm:"NOT NULL DEFAULT 0"`
	GitSize                         int64              `xorm:"NOT NULL DEFAULT 0"`
	LFSSize                         int64              `xorm:"NOT NULL DEFAULT 0"`
	CodeIndexerStatus               *RepoIndexerStatus `xorm:"-"`
	StatsIndexerStatus              *RepoIndexerStatus `xorm:"-"`
	IsFsckEnabled                   bool               `xorm:"NOT NULL DEFAULT true"`
	CloseIssuesViaCommitInAnyBranch bool               `xorm:"NOT NULL DEFAULT false"`
	Topics                          []string           `xorm:"TEXT JSON"`
	ObjectFormatName                string             `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"`

	TrustModel TrustModelType

	// Avatar: ID(10-20)-md5(32) - must fit into 64 symbols
	Avatar string `xorm:"VARCHAR(64)"`

	CreatedUnix  timeutil.TimeStamp `xorm:"INDEX created"`
	UpdatedUnix  timeutil.TimeStamp `xorm:"INDEX updated"`
	ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT 0"`
}

func init() {
	db.RegisterModel(new(Repository))
}

func RelativePath(ownerName, repoName string) string {
	return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".git"
}

func RelativeWikiPath(ownerName, repoName string) string {
	return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".wiki.git"
}

// RelativePath should be an unix style path like username/reponame.git
func (repo *Repository) RelativePath() string {
	return RelativePath(repo.OwnerName, repo.Name)
}

type StorageRepo string

// RelativePath should be an unix style path like username/reponame.git
func (sr StorageRepo) RelativePath() string {
	return string(sr)
}

// WikiStorageRepo returns the storage repo for the wiki
// The wiki repository should have the same object format as the code repository
func (repo *Repository) WikiStorageRepo() StorageRepo {
	return StorageRepo(RelativeWikiPath(repo.OwnerName, repo.Name))
}

// SanitizedOriginalURL returns a sanitized OriginalURL
func (repo *Repository) SanitizedOriginalURL() string {
	if repo.OriginalURL == "" {
		return ""
	}
	u, _ := util.SanitizeURL(repo.OriginalURL)
	return u
}

// text representations to be returned in SizeDetail.Name
const (
	SizeDetailNameGit = "git"
	SizeDetailNameLFS = "lfs"
)

type SizeDetail struct {
	Name string
	Size int64
}

// SizeDetails forms a struct with various size details about repository
func (repo *Repository) SizeDetails() []SizeDetail {
	sizeDetails := []SizeDetail{
		{
			Name: SizeDetailNameGit,
			Size: repo.GitSize,
		},
		{
			Name: SizeDetailNameLFS,
			Size: repo.LFSSize,
		},
	}
	return sizeDetails
}

// SizeDetailsString returns a concatenation of all repository size details as a string
func (repo *Repository) SizeDetailsString() string {
	var str strings.Builder
	sizeDetails := repo.SizeDetails()
	for _, detail := range sizeDetails {
		str.WriteString(fmt.Sprintf("%s: %s, ", detail.Name, base.FileSize(detail.Size)))
	}
	return strings.TrimSuffix(str.String(), ", ")
}

func (repo *Repository) LogString() string {
	if repo == nil {
		return "<Repository nil>"
	}
	return fmt.Sprintf("<Repository %d:%s/%s>", repo.ID, repo.OwnerName, repo.Name)
}

// IsBeingMigrated indicates that repository is being migrated
func (repo *Repository) IsBeingMigrated() bool {
	return repo.Status == RepositoryBeingMigrated
}

// IsBeingCreated indicates that repository is being migrated or forked
func (repo *Repository) IsBeingCreated() bool {
	return repo.IsBeingMigrated()
}

// IsBroken indicates that repository is broken
func (repo *Repository) IsBroken() bool {
	return repo.Status == RepositoryBroken
}

// MarkAsBrokenEmpty marks the repo as broken and empty
// FIXME: the status "broken" and "is_empty" were abused,
// The code always set them together, no way to distinguish whether a repo is really "empty" or "broken"
func (repo *Repository) MarkAsBrokenEmpty() {
	repo.Status = RepositoryBroken
	repo.IsEmpty = true
}

// AfterLoad is invoked from XORM after setting the values of all fields of this object.
func (repo *Repository) AfterLoad() {
	repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues
	repo.NumOpenPulls = repo.NumPulls - repo.NumClosedPulls
	repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones
	repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects
	repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns
	if repo.DefaultWikiBranch == "" {
		repo.DefaultWikiBranch = setting.Repository.DefaultBranch
	}
}

// LoadAttributes loads attributes of the repository.
func (repo *Repository) LoadAttributes(ctx context.Context) error {
	// Load owner
	if err := repo.LoadOwner(ctx); err != nil {
		return fmt.Errorf("load owner: %w", err)
	}

	// Load primary language
	stats := make(LanguageStatList, 0, 1)
	if err := db.GetEngine(ctx).
		Where("`repo_id` = ? AND `is_primary` = ? AND `language` != ?", repo.ID, true, "other").
		Find(&stats); err != nil {
		return fmt.Errorf("find primary languages: %w", err)
	}
	stats.LoadAttributes()
	for _, st := range stats {
		if st.RepoID == repo.ID {
			repo.PrimaryLanguage = st
			break
		}
	}
	return nil
}

// FullName returns the repository full name
func (repo *Repository) FullName() string {
	return repo.OwnerName + "/" + repo.Name
}

// HTMLURL returns the repository HTML URL
func (repo *Repository) HTMLURL(ctxs ...context.Context) string {
	// FIXME: this HTMLURL is still used in mail templates, so the "ctx" is not provided.
	ctx := util.OptionalArg(ctxs, context.TODO())
	return httplib.MakeAbsoluteURL(ctx, repo.Link())
}

// CommitLink make link to by commit full ID
// note: won't check whether it's an right id
func (repo *Repository) CommitLink(commitID string) (result string) {
	if git.IsEmptyCommitID(commitID) {
		result = ""
	} else {
		result = repo.Link() + "/commit/" + url.PathEscape(commitID)
	}
	return result
}

// APIURL returns the repository API URL
func (repo *Repository) APIURL() string {
	return setting.AppURL + "api/v1/repos/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
}

// GetCommitsCountCacheKey returns cache key used for commits count caching.
func (repo *Repository) GetCommitsCountCacheKey(contextName string, isRef bool) string {
	var prefix string
	if isRef {
		prefix = "ref"
	} else {
		prefix = "commit"
	}
	return fmt.Sprintf("commits-count-%d-%s-%s", repo.ID, prefix, contextName)
}

// LoadUnits loads repo units into repo.Units
func (repo *Repository) LoadUnits(ctx context.Context) (err error) {
	if repo.Units != nil {
		return nil
	}

	repo.Units, err = getUnitsByRepoID(ctx, repo.ID)
	if log.IsTrace() {
		unitTypeStrings := make([]string, len(repo.Units))
		for i, unit := range repo.Units {
			unitTypeStrings[i] = unit.Type.LogString()
		}
		log.Trace("repo.Units, ID=%d, Types: [%s]", repo.ID, strings.Join(unitTypeStrings, ", "))
	}

	return err
}

// UnitEnabled if this repository has the given unit enabled
func (repo *Repository) UnitEnabled(ctx context.Context, tp unit.Type) bool {
	if err := repo.LoadUnits(ctx); err != nil {
		log.Warn("Error loading repository (ID: %d) units: %s", repo.ID, err.Error())
	}
	for _, unit := range repo.Units {
		if unit.Type == tp {
			return true
		}
	}
	return false
}

// MustGetUnit always returns a RepoUnit object
func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit {
	ru, err := repo.GetUnit(ctx, tp)
	if err == nil {
		return ru
	}

	switch tp {
	case unit.TypeExternalWiki:
		return &RepoUnit{
			Type:   tp,
			Config: new(ExternalWikiConfig),
		}
	case unit.TypeExternalTracker:
		return &RepoUnit{
			Type:   tp,
			Config: new(ExternalTrackerConfig),
		}
	case unit.TypePullRequests:
		return &RepoUnit{
			Type:   tp,
			Config: new(PullRequestsConfig),
		}
	case unit.TypeIssues:
		return &RepoUnit{
			Type:   tp,
			Config: new(IssuesConfig),
		}
	case unit.TypeActions:
		return &RepoUnit{
			Type:   tp,
			Config: new(ActionsConfig),
		}
	case unit.TypeProjects:
		cfg := new(ProjectsConfig)
		cfg.ProjectsMode = ProjectsModeNone
		return &RepoUnit{
			Type:   tp,
			Config: cfg,
		}
	}

	return &RepoUnit{
		Type:   tp,
		Config: new(UnitConfig),
	}
}

// GetUnit returns a RepoUnit object
func (repo *Repository) GetUnit(ctx context.Context, tp unit.Type) (*RepoUnit, error) {
	if err := repo.LoadUnits(ctx); err != nil {
		return nil, err
	}
	for _, unit := range repo.Units {
		if unit.Type == tp {
			return unit, nil
		}
	}
	return nil, ErrUnitTypeNotExist{tp}
}

// LoadOwner loads owner user
func (repo *Repository) LoadOwner(ctx context.Context) (err error) {
	if repo.Owner != nil {
		return nil
	}

	repo.Owner, err = user_model.GetUserByID(ctx, repo.OwnerID)
	return err
}

// MustOwner always returns a valid *user_model.User object to avoid
// conceptually impossible error handling.
// It creates a fake object that contains error details
// when error occurs.
func (repo *Repository) MustOwner(ctx context.Context) *user_model.User {
	if err := repo.LoadOwner(ctx); err != nil {
		return &user_model.User{
			Name:     "error",
			FullName: err.Error(),
		}
	}

	return repo.Owner
}

func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]string {
	if len(repo.commonRenderingMetas) == 0 {
		metas := map[string]string{
			"user": repo.OwnerName,
			"repo": repo.Name,
		}

		unitExternalTracker, err := repo.GetUnit(ctx, unit.TypeExternalTracker)
		if err == nil {
			metas["format"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerFormat
			switch unitExternalTracker.ExternalTrackerConfig().ExternalTrackerStyle {
			case markup.IssueNameStyleAlphanumeric:
				metas["style"] = markup.IssueNameStyleAlphanumeric
			case markup.IssueNameStyleRegexp:
				metas["style"] = markup.IssueNameStyleRegexp
				metas["regexp"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerRegexpPattern
			default:
				metas["style"] = markup.IssueNameStyleNumeric
			}
		}

		repo.MustOwner(ctx)
		if repo.Owner.IsOrganization() {
			teams := make([]string, 0, 5)
			_ = db.GetEngine(ctx).Table("team_repo").
				Join("INNER", "team", "team.id = team_repo.team_id").
				Where("team_repo.repo_id = ?", repo.ID).
				Select("team.lower_name").
				OrderBy("team.lower_name").
				Find(&teams)
			metas["teams"] = "," + strings.Join(teams, ",") + ","
			metas["org"] = strings.ToLower(repo.OwnerName)
		}

		repo.commonRenderingMetas = metas
	}
	return repo.commonRenderingMetas
}

// ComposeCommentMetas composes a map of metas for properly rendering comments or comment-like contents (commit message)
func (repo *Repository) ComposeCommentMetas(ctx context.Context) map[string]string {
	metas := maps.Clone(repo.composeCommonMetas(ctx))
	metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsComment.NewLineHardBreak)
	metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsComment.ShortIssuePattern)
	return metas
}

// ComposeWikiMetas composes a map of metas for properly rendering wikis
func (repo *Repository) ComposeWikiMetas(ctx context.Context) map[string]string {
	// does wiki need the "teams" and "org" from common metas?
	metas := maps.Clone(repo.composeCommonMetas(ctx))
	metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsWiki.NewLineHardBreak)
	metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsWiki.ShortIssuePattern)
	return metas
}

// ComposeRepoFileMetas composes a map of metas for properly rendering documents (repo files)
func (repo *Repository) ComposeRepoFileMetas(ctx context.Context) map[string]string {
	// does document(file) need the "teams" and "org" from common metas?
	metas := maps.Clone(repo.composeCommonMetas(ctx))
	metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsRepoFile.NewLineHardBreak)
	metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsRepoFile.ShortIssuePattern)
	return metas
}

// GetBaseRepo populates repo.BaseRepo for a fork repository and
// returns an error on failure (NOTE: no error is returned for
// non-fork repositories, and BaseRepo will be left untouched)
func (repo *Repository) GetBaseRepo(ctx context.Context) (err error) {
	if !repo.IsFork {
		return nil
	}

	if repo.BaseRepo != nil {
		return nil
	}
	repo.BaseRepo, err = GetRepositoryByID(ctx, repo.ForkID)
	return err
}

// IsGenerated returns whether _this_ repository was generated from a template
func (repo *Repository) IsGenerated() bool {
	return repo.TemplateID != 0
}

// RepoPath returns repository path by given user and repository name.
func RepoPath(userName, repoName string) string { //revive:disable-line:exported
	return filepath.Join(user_model.UserPath(userName), strings.ToLower(repoName)+".git")
}

// RepoPath returns the repository path
func (repo *Repository) RepoPath() string {
	return RepoPath(repo.OwnerName, repo.Name)
}

// Link returns the repository relative url
func (repo *Repository) Link() string {
	return setting.AppSubURL + "/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
}

// ComposeCompareURL returns the repository comparison URL
func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) string {
	return fmt.Sprintf("%s/%s/compare/%s...%s", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), util.PathEscapeSegments(oldCommitID), util.PathEscapeSegments(newCommitID))
}

func (repo *Repository) ComposeBranchCompareURL(baseRepo *Repository, branchName string) string {
	if baseRepo == nil {
		baseRepo = repo
	}
	var cmpBranchEscaped string
	if repo.ID != baseRepo.ID {
		cmpBranchEscaped = fmt.Sprintf("%s/%s:", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name))
	}
	cmpBranchEscaped = fmt.Sprintf("%s%s", cmpBranchEscaped, util.PathEscapeSegments(branchName))
	return fmt.Sprintf("%s/compare/%s...%s", baseRepo.Link(), util.PathEscapeSegments(baseRepo.DefaultBranch), cmpBranchEscaped)
}

// IsOwnedBy returns true when user owns this repository
func (repo *Repository) IsOwnedBy(userID int64) bool {
	return repo.OwnerID == userID
}

// CanCreateBranch returns true if repository meets the requirements for creating new branches.
func (repo *Repository) CanCreateBranch() bool {
	return !repo.IsMirror
}

// CanEnablePulls returns true if repository meets the requirements of accepting pulls.
func (repo *Repository) CanEnablePulls() bool {
	return !repo.IsMirror && !repo.IsEmpty
}

// AllowsPulls returns true if repository meets the requirements of accepting pulls and has them enabled.
func (repo *Repository) AllowsPulls(ctx context.Context) bool {
	return repo.CanEnablePulls() && repo.UnitEnabled(ctx, unit.TypePullRequests)
}

// CanEnableEditor returns true if repository meets the requirements of web editor.
// FIXME: most CanEnableEditor calls should be replaced with CanContentChange
// And all other like CanCreateBranch / CanEnablePulls should also be updated
func (repo *Repository) CanEnableEditor() bool {
	return repo.CanContentChange()
}

func (repo *Repository) CanContentChange() bool {
	return !repo.IsMirror && !repo.IsArchived
}

// DescriptionHTML does special handles to description and return HTML string.
func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
	desc, err := markup.PostProcessDescriptionHTML(markup.NewRenderContext(ctx), repo.Description)
	if err != nil {
		log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err)
		return template.HTML(markup.SanitizeDescription(repo.Description))
	}
	return template.HTML(markup.SanitizeDescription(desc))
}

// CloneLink represents different types of clone URLs of repository.
type CloneLink struct {
	SSH   string
	HTTPS string
	Tea   string
}

// ComposeHTTPSCloneURL returns HTTPS clone URL based on the given owner and repository name.
func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string {
	return fmt.Sprintf("%s%s/%s.git", httplib.GuessCurrentAppURL(ctx), url.PathEscape(owner), url.PathEscape(repo))
}

// ComposeSSHCloneURL returns SSH clone URL based on the given owner and repository name.
func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string {
	sshUser := setting.SSH.User
	sshDomain := setting.SSH.Domain

	if sshUser == "(DOER_USERNAME)" {
		// Some users use SSH reverse-proxy and need to use the current signed-in username as the SSH user
		// to make the SSH reverse-proxy could prepare the user's public keys ahead.
		// For most cases we have the correct "doer", then use it as the SSH user.
		// If we can't get the doer, then use the built-in SSH user.
		if doer != nil {
			sshUser = doer.Name
		} else {
			sshUser = setting.SSH.BuiltinServerUser
		}
	}

	// non-standard port, it must use full URI
	if setting.SSH.Port != 22 {
		sshHost := net.JoinHostPort(sshDomain, strconv.Itoa(setting.SSH.Port))
		return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
	}

	// for standard port, it can use a shorter URI (without the port)
	sshHost := sshDomain
	if ip := net.ParseIP(sshHost); ip != nil && ip.To4() == nil {
		sshHost = "[" + sshHost + "]" // for IPv6 address, wrap it with brackets
	}
	if setting.Repository.UseCompatSSHURI {
		return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
	}
	return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
}

// ComposeTeaCloneCommand returns Tea CLI clone command based on the given owner and repository name.
func ComposeTeaCloneCommand(ctx context.Context, owner, repo string) string {
	return fmt.Sprintf("tea clone %s/%s", url.PathEscape(owner), url.PathEscape(repo))
}

func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string) *CloneLink {
	return &CloneLink{
		SSH:   ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName),
		HTTPS: ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName),
		Tea:   ComposeTeaCloneCommand(ctx, repo.OwnerName, repoPathName),
	}
}

// CloneLink returns clone URLs of repository.
func (repo *Repository) CloneLink(ctx context.Context, doer *user_model.User) (cl *CloneLink) {
	return repo.cloneLink(ctx, doer, repo.Name)
}

func (repo *Repository) CloneLinkGeneral(ctx context.Context) (cl *CloneLink) {
	return repo.cloneLink(ctx, nil /* no doer, use a general git user */, repo.Name)
}

// GetOriginalURLHostname returns the hostname of a URL or the URL
func (repo *Repository) GetOriginalURLHostname() string {
	u, err := url.Parse(repo.OriginalURL)
	if err != nil {
		return repo.OriginalURL
	}

	return u.Host
}

// GetTrustModel will get the TrustModel for the repo or the default trust model
func (repo *Repository) GetTrustModel() TrustModelType {
	trustModel := repo.TrustModel
	if trustModel == DefaultTrustModel {
		trustModel = ToTrustModel(setting.Repository.Signing.DefaultTrustModel)
		if trustModel == DefaultTrustModel {
			return CollaboratorTrustModel
		}
	}
	return trustModel
}

// MustNotBeArchived returns ErrRepoIsArchived if the repo is archived
func (repo *Repository) MustNotBeArchived() error {
	if repo.IsArchived {
		return ErrRepoIsArchived{Repo: repo}
	}
	return nil
}

// __________                           .__  __
// \______   \ ____ ______   ____  _____|__|/  |_  ___________ ___.__.
//  |       _// __ \\____ \ /  _ \/  ___/  \   __\/  _ \_  __ <   |  |
//  |    |   \  ___/|  |_> >  <_> )___ \|  ||  | (  <_> )  | \/\___  |
//  |____|_  /\___  >   __/ \____/____  >__||__|  \____/|__|   / ____|
//         \/     \/|__|              \/                       \/

// ErrRepoNotExist represents a "RepoNotExist" kind of error.
type ErrRepoNotExist struct {
	ID        int64
	UID       int64
	OwnerName string
	Name      string
}

// IsErrRepoNotExist checks if an error is a ErrRepoNotExist.
func IsErrRepoNotExist(err error) bool {
	_, ok := err.(ErrRepoNotExist)
	return ok
}

func (err ErrRepoNotExist) Error() string {
	return fmt.Sprintf("repository does not exist [id: %d, uid: %d, owner_name: %s, name: %s]",
		err.ID, err.UID, err.OwnerName, err.Name)
}

// Unwrap unwraps this error as a ErrNotExist error
func (err ErrRepoNotExist) Unwrap() error {
	return util.ErrNotExist
}

// GetRepositoryByOwnerAndName returns the repository by given owner name and repo name
func GetRepositoryByOwnerAndName(ctx context.Context, ownerName, repoName string) (*Repository, error) {
	var repo Repository
	has, err := db.GetEngine(ctx).Table("repository").Select("repository.*").
		Join("INNER", "`user`", "`user`.id = repository.owner_id").
		Where("repository.lower_name = ?", strings.ToLower(repoName)).
		And("`user`.lower_name = ?", strings.ToLower(ownerName)).
		Get(&repo)
	if err != nil {
		return nil, err
	} else if !has {
		return nil, ErrRepoNotExist{0, 0, ownerName, repoName}
	}
	return &repo, nil
}

// GetRepositoryByName returns the repository by given name under user if exists.
func GetRepositoryByName(ctx context.Context, ownerID int64, name string) (*Repository, error) {
	var repo Repository
	has, err := db.GetEngine(ctx).
		Where("`owner_id`=?", ownerID).
		And("`lower_name`=?", strings.ToLower(name)).
		NoAutoCondition().
		Get(&repo)
	if err != nil {
		return nil, err
	} else if !has {
		return nil, ErrRepoNotExist{0, ownerID, "", name}
	}
	return &repo, err
}

// GetRepositoryByURL returns the repository by given url
func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error) {
	ret, err := giturl.ParseRepositoryURL(ctx, repoURL)
	if err != nil || ret.OwnerName == "" {
		return nil, errors.New("unknown or malformed repository URL")
	}
	return GetRepositoryByOwnerAndName(ctx, ret.OwnerName, ret.RepoName)
}

// GetRepositoryByURLRelax also accepts an SSH clone URL without user part
func GetRepositoryByURLRelax(ctx context.Context, repoURL string) (*Repository, error) {
	if !strings.Contains(repoURL, "://") && !strings.Contains(repoURL, "@") {
		// convert "example.com:owner/repo" to "@example.com:owner/repo"
		p1, p2, p3 := strings.Index(repoURL, "."), strings.Index(repoURL, ":"), strings.Index(repoURL, "/")
		if 0 < p1 && p1 < p2 && p2 < p3 {
			repoURL = "@" + repoURL
		}
	}
	return GetRepositoryByURL(ctx, repoURL)
}

// GetRepositoryByID returns the repository by given id if exists.
func GetRepositoryByID(ctx context.Context, id int64) (*Repository, error) {
	repo := new(Repository)
	has, err := db.GetEngine(ctx).ID(id).Get(repo)
	if err != nil {
		return nil, err
	} else if !has {
		return nil, ErrRepoNotExist{id, 0, "", ""}
	}
	return repo, nil
}

// GetRepositoriesMapByIDs returns the repositories by given id slice.
func GetRepositoriesMapByIDs(ctx context.Context, ids []int64) (map[int64]*Repository, error) {
	repos := make(map[int64]*Repository, len(ids))
	if len(ids) == 0 {
		return repos, nil
	}
	return repos, db.GetEngine(ctx).In("id", ids).Find(&repos)
}

// IsRepositoryModelOrDirExist returns true if the repository with given name under user has already existed.
func IsRepositoryModelOrDirExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
	has, err := IsRepositoryModelExist(ctx, u, repoName)
	if err != nil {
		return false, err
	}
	isDir, err := util.IsDir(RepoPath(u.Name, repoName))
	return has || isDir, err
}

func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
	return db.GetEngine(ctx).Get(&Repository{
		OwnerID:   u.ID,
		LowerName: strings.ToLower(repoName),
	})
}

// GetTemplateRepo populates repo.TemplateRepo for a generated repository and
// returns an error on failure (NOTE: no error is returned for
// non-generated repositories, and TemplateRepo will be left untouched)
func GetTemplateRepo(ctx context.Context, repo *Repository) (*Repository, error) {
	if !repo.IsGenerated() {
		return nil, nil
	}

	return GetRepositoryByID(ctx, repo.TemplateID)
}

// TemplateRepo returns the repository, which is template of this repository
func (repo *Repository) TemplateRepo(ctx context.Context) *Repository {
	repo, err := GetTemplateRepo(ctx, repo)
	if err != nil {
		log.Error("TemplateRepo: %v", err)
		return nil
	}
	return repo
}

// ErrUserOwnRepos represents a "UserOwnRepos" kind of error.
type ErrUserOwnRepos struct {
	UID int64
}

// IsErrUserOwnRepos checks if an error is a ErrUserOwnRepos.
func IsErrUserOwnRepos(err error) bool {
	_, ok := err.(ErrUserOwnRepos)
	return ok
}

func (err ErrUserOwnRepos) Error() string {
	return fmt.Sprintf("user still has ownership of repositories [uid: %d]", err.UID)
}

type CountRepositoryOptions struct {
	OwnerID int64
	Private optional.Option[bool]
}

// CountRepositories returns number of repositories.
// Argument private only takes effect when it is false,
// set it true to count all repositories.
func CountRepositories(ctx context.Context, opts CountRepositoryOptions) (int64, error) {
	sess := db.GetEngine(ctx).Where("id > 0")

	if opts.OwnerID > 0 {
		sess.And("owner_id = ?", opts.OwnerID)
	}
	if opts.Private.Has() {
		sess.And("is_private=?", opts.Private.Value())
	}

	count, err := sess.Count(new(Repository))
	if err != nil {
		return 0, fmt.Errorf("countRepositories: %w", err)
	}
	return count, nil
}

// UpdateRepoIssueNumbers updates one of a repositories amount of (open|closed) (issues|PRs) with the current count
func UpdateRepoIssueNumbers(ctx context.Context, repoID int64, isPull, isClosed bool) error {
	field := "num_"
	if isClosed {
		field += "closed_"
	}
	if isPull {
		field += "pulls"
	} else {
		field += "issues"
	}

	subQuery := builder.Select("count(*)").
		From("issue").Where(builder.Eq{
		"repo_id": repoID,
		"is_pull": isPull,
	}.And(builder.If(isClosed, builder.Eq{"is_closed": isClosed})))

	// builder.Update(cond) will generate SQL like UPDATE ... SET cond
	query := builder.Update(builder.Eq{field: subQuery}).
		From("repository").
		Where(builder.Eq{"id": repoID})
	_, err := db.Exec(ctx, query)
	return err
}

// CountNullArchivedRepository counts the number of repositories with is_archived is null
func CountNullArchivedRepository(ctx context.Context) (int64, error) {
	return db.GetEngine(ctx).Where(builder.IsNull{"is_archived"}).Count(new(Repository))
}

// FixNullArchivedRepository sets is_archived to false where it is null
func FixNullArchivedRepository(ctx context.Context) (int64, error) {
	return db.GetEngine(ctx).Where(builder.IsNull{"is_archived"}).Cols("is_archived").NoAutoTime().Update(&Repository{
		IsArchived: false,
	})
}

// UpdateRepositoryOwnerName updates the owner name of all repositories owned by the user
func UpdateRepositoryOwnerName(ctx context.Context, oldUserName, newUserName string) error {
	if _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET owner_name=? WHERE owner_name=?", newUserName, oldUserName); err != nil {
		return fmt.Errorf("change repo owner name: %w", err)
	}
	return nil
}
