diff options
Diffstat (limited to '')
-rw-r--r-- | models/repo/repo.go | 951 |
1 files changed, 951 insertions, 0 deletions
diff --git a/models/repo/repo.go b/models/repo/repo.go new file mode 100644 index 0000000..cd6be48 --- /dev/null +++ b/models/repo/repo.go @@ -0,0 +1,951 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "context" + "fmt" + "html/template" + "net" + "net/url" + "path/filepath" + "strconv" + "strings" + + "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/git" + "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/translation" + "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 ErrRepoFileAlreadyExists. +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 fmt.Sprintf("%s is archived", err.Repo.LogString()) +} + +var ( + reservedRepoNames = []string{".", "..", "-"} + reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"} +) + +// IsUsableRepoName returns true when repository is usable +func IsUsableRepoName(name string) error { + if db.AlphaDashDotPattern.MatchString(name) { + // Note: usually this error is normally caught up earlier in the UI + return db.ErrNameCharsNotAllowed{Name: name} + } + return db.IsUsableName(reservedRepoNames, reservedRepoPatterns, name) +} + +// 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 + WikiBranch 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"` + + RenderingMetas map[string]string `xorm:"-"` + DocumentRenderingMetas 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 (repo *Repository) GetName() string { + return repo.Name +} + +func (repo *Repository) GetOwnerName() string { + return repo.OwnerName +} + +func (repo *Repository) GetWikiBranchName() string { + if repo.WikiBranch == "" { + return setting.Repository.DefaultBranch + } + return repo.WikiBranch +} + +// 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 +// Note: SizeDetailsString below expects it to have 2 entries +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(locale translation.Locale) string { + sizeDetails := repo.SizeDetails() + return locale.TrString("repo.size_format", sizeDetails[0].Name, locale.TrSize(sizeDetails[0].Size), sizeDetails[1].Name, locale.TrSize(sizeDetails[1].Size)) +} + +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 +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 +} + +// 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() string { + return setting.AppURL + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name) +} + +// 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, nil) { + 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) +} + +// APActorID returns the activitypub repository API URL +func (repo *Repository) APActorID() string { + return fmt.Sprintf("%vapi/v1/activitypub/repository-id/%v", setting.AppURL, url.PathEscape(fmt.Sprint(repo.ID))) +} + +// 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.String() + } + 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 + } + + if tp == unit.TypeExternalWiki { + return &RepoUnit{ + Type: tp, + Config: new(ExternalWikiConfig), + } + } else if tp == unit.TypeExternalTracker { + return &RepoUnit{ + Type: tp, + Config: new(ExternalTrackerConfig), + } + } else if tp == unit.TypePullRequests { + return &RepoUnit{ + Type: tp, + Config: new(PullRequestsConfig), + } + } else if tp == unit.TypeIssues { + return &RepoUnit{ + Type: tp, + Config: new(IssuesConfig), + } + } else if tp == unit.TypeActions { + return &RepoUnit{ + Type: tp, + Config: new(ActionsConfig), + } + } + + 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} +} + +// AllUnitsEnabled returns true if all units are enabled for the repo. +func (repo *Repository) AllUnitsEnabled(ctx context.Context) bool { + hasAnyUnitEnabled := func(unitGroup []unit.Type) bool { + // Loop over the group of units + for _, unit := range unitGroup { + // If *any* of them is enabled, return true. + if repo.UnitEnabled(ctx, unit) { + return true + } + } + + // If none are enabled, return false. + return false + } + + for _, unitGroup := range unit.AllowedRepoUnitGroups { + // If any disabled unit is found, return false immediately. + if !hasAnyUnitEnabled(unitGroup) { + return false + } + } + + return true +} + +// 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 +} + +// ComposeMetas composes a map of metas for properly rendering issue links and external issue trackers. +func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string { + if len(repo.RenderingMetas) == 0 { + metas := map[string]string{ + "user": repo.OwnerName, + "repo": repo.Name, + "repoPath": repo.RepoPath(), + "mode": "comment", + } + + unit, err := repo.GetUnit(ctx, unit.TypeExternalTracker) + if err == nil { + metas["format"] = unit.ExternalTrackerConfig().ExternalTrackerFormat + switch unit.ExternalTrackerConfig().ExternalTrackerStyle { + case markup.IssueNameStyleAlphanumeric: + metas["style"] = markup.IssueNameStyleAlphanumeric + case markup.IssueNameStyleRegexp: + metas["style"] = markup.IssueNameStyleRegexp + metas["regexp"] = unit.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.RenderingMetas = metas + } + return repo.RenderingMetas +} + +// ComposeDocumentMetas composes a map of metas for properly rendering documents +func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]string { + if len(repo.DocumentRenderingMetas) == 0 { + metas := map[string]string{} + for k, v := range repo.ComposeMetas(ctx) { + metas[k] = v + } + metas["mode"] = "document" + repo.DocumentRenderingMetas = metas + } + return repo.DocumentRenderingMetas +} + +// 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. +func (repo *Repository) CanEnableEditor() bool { + return !repo.IsMirror +} + +// DescriptionHTML does special handles to description and return HTML string. +func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML { + desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{ + Ctx: ctx, + // Don't use Metas to speedup requests + }, 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 +} + +// ComposeHTTPSCloneURL returns HTTPS clone URL based on given owner and repository name. +func ComposeHTTPSCloneURL(owner, repo string) string { + return fmt.Sprintf("%s%s/%s.git", setting.AppURL, url.PathEscape(owner), url.PathEscape(repo)) +} + +func ComposeSSHCloneURL(ownerName, repoName string) string { + sshUser := setting.SSH.User + sshDomain := setting.SSH.Domain + + // 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)) +} + +func (repo *Repository) cloneLink(isWiki bool) *CloneLink { + repoName := repo.Name + if isWiki { + repoName += ".wiki" + } + + cl := new(CloneLink) + cl.SSH = ComposeSSHCloneURL(repo.OwnerName, repoName) + cl.HTTPS = ComposeHTTPSCloneURL(repo.OwnerName, repoName) + return cl +} + +// CloneLink returns clone URLs of repository. +func (repo *Repository) CloneLink() (cl *CloneLink) { + return repo.cloneLink(false) +} + +// 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 +} + +// getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url +func getRepositoryURLPathSegments(repoURL string) []string { + if strings.HasPrefix(repoURL, setting.AppURL) { + return strings.Split(strings.TrimPrefix(repoURL, setting.AppURL), "/") + } + + sshURLVariants := [4]string{ + setting.SSH.Domain + ":", + setting.SSH.User + "@" + setting.SSH.Domain + ":", + "git+ssh://" + setting.SSH.Domain + "/", + "git+ssh://" + setting.SSH.User + "@" + setting.SSH.Domain + "/", + } + + for _, sshURL := range sshURLVariants { + if strings.HasPrefix(repoURL, sshURL) { + return strings.Split(strings.TrimPrefix(repoURL, sshURL), "/") + } + } + + return nil +} + +// GetRepositoryByURL returns the repository by given url +func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error) { + // possible urls for git: + // https://my.domain/sub-path/<owner>/<repo>.git + // https://my.domain/sub-path/<owner>/<repo> + // git+ssh://user@my.domain/<owner>/<repo>.git + // git+ssh://user@my.domain/<owner>/<repo> + // user@my.domain:<owner>/<repo>.git + // user@my.domain:<owner>/<repo> + + pathSegments := getRepositoryURLPathSegments(repoURL) + + if len(pathSegments) != 2 { + return nil, fmt.Errorf("unknown or malformed repository URL") + } + + ownerName := pathSegments[0] + repoName := strings.TrimSuffix(pathSegments[1], ".git") + return GetRepositoryByOwnerAndName(ctx, ownerName, repoName) +} + +// 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)) + 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 +} + +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 +} |