code.gitea.io/gitea@v1.22.3/models/repo/repo.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package repo
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"html/template"
    10  	"net"
    11  	"net/url"
    12  	"path/filepath"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"code.gitea.io/gitea/models/db"
    17  	"code.gitea.io/gitea/models/unit"
    18  	user_model "code.gitea.io/gitea/models/user"
    19  	"code.gitea.io/gitea/modules/base"
    20  	"code.gitea.io/gitea/modules/git"
    21  	"code.gitea.io/gitea/modules/log"
    22  	"code.gitea.io/gitea/modules/markup"
    23  	"code.gitea.io/gitea/modules/optional"
    24  	"code.gitea.io/gitea/modules/setting"
    25  	api "code.gitea.io/gitea/modules/structs"
    26  	"code.gitea.io/gitea/modules/timeutil"
    27  	"code.gitea.io/gitea/modules/util"
    28  
    29  	"xorm.io/builder"
    30  )
    31  
    32  // ErrUserDoesNotHaveAccessToRepo represents an error where the user doesn't has access to a given repo.
    33  type ErrUserDoesNotHaveAccessToRepo struct {
    34  	UserID   int64
    35  	RepoName string
    36  }
    37  
    38  // IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExists.
    39  func IsErrUserDoesNotHaveAccessToRepo(err error) bool {
    40  	_, ok := err.(ErrUserDoesNotHaveAccessToRepo)
    41  	return ok
    42  }
    43  
    44  func (err ErrUserDoesNotHaveAccessToRepo) Error() string {
    45  	return fmt.Sprintf("user doesn't have access to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName)
    46  }
    47  
    48  func (err ErrUserDoesNotHaveAccessToRepo) Unwrap() error {
    49  	return util.ErrPermissionDenied
    50  }
    51  
    52  type ErrRepoIsArchived struct {
    53  	Repo *Repository
    54  }
    55  
    56  func (err ErrRepoIsArchived) Error() string {
    57  	return fmt.Sprintf("%s is archived", err.Repo.LogString())
    58  }
    59  
    60  var (
    61  	reservedRepoNames    = []string{".", "..", "-"}
    62  	reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"}
    63  )
    64  
    65  // IsUsableRepoName returns true when repository is usable
    66  func IsUsableRepoName(name string) error {
    67  	if db.AlphaDashDotPattern.MatchString(name) {
    68  		// Note: usually this error is normally caught up earlier in the UI
    69  		return db.ErrNameCharsNotAllowed{Name: name}
    70  	}
    71  	return db.IsUsableName(reservedRepoNames, reservedRepoPatterns, name)
    72  }
    73  
    74  // TrustModelType defines the types of trust model for this repository
    75  type TrustModelType int
    76  
    77  // kinds of TrustModel
    78  const (
    79  	DefaultTrustModel TrustModelType = iota // default trust model
    80  	CommitterTrustModel
    81  	CollaboratorTrustModel
    82  	CollaboratorCommitterTrustModel
    83  )
    84  
    85  // String converts a TrustModelType to a string
    86  func (t TrustModelType) String() string {
    87  	switch t {
    88  	case DefaultTrustModel:
    89  		return "default"
    90  	case CommitterTrustModel:
    91  		return "committer"
    92  	case CollaboratorTrustModel:
    93  		return "collaborator"
    94  	case CollaboratorCommitterTrustModel:
    95  		return "collaboratorcommitter"
    96  	}
    97  	return "default"
    98  }
    99  
   100  // ToTrustModel converts a string to a TrustModelType
   101  func ToTrustModel(model string) TrustModelType {
   102  	switch strings.ToLower(strings.TrimSpace(model)) {
   103  	case "default":
   104  		return DefaultTrustModel
   105  	case "collaborator":
   106  		return CollaboratorTrustModel
   107  	case "committer":
   108  		return CommitterTrustModel
   109  	case "collaboratorcommitter":
   110  		return CollaboratorCommitterTrustModel
   111  	}
   112  	return DefaultTrustModel
   113  }
   114  
   115  // RepositoryStatus defines the status of repository
   116  type RepositoryStatus int
   117  
   118  // all kinds of RepositoryStatus
   119  const (
   120  	RepositoryReady           RepositoryStatus = iota // a normal repository
   121  	RepositoryBeingMigrated                           // repository is migrating
   122  	RepositoryPendingTransfer                         // repository pending in ownership transfer state
   123  	RepositoryBroken                                  // repository is in a permanently broken state
   124  )
   125  
   126  // Repository represents a git repository.
   127  type Repository struct {
   128  	ID                  int64 `xorm:"pk autoincr"`
   129  	OwnerID             int64 `xorm:"UNIQUE(s) index"`
   130  	OwnerName           string
   131  	Owner               *user_model.User   `xorm:"-"`
   132  	LowerName           string             `xorm:"UNIQUE(s) INDEX NOT NULL"`
   133  	Name                string             `xorm:"INDEX NOT NULL"`
   134  	Description         string             `xorm:"TEXT"`
   135  	Website             string             `xorm:"VARCHAR(2048)"`
   136  	OriginalServiceType api.GitServiceType `xorm:"index"`
   137  	OriginalURL         string             `xorm:"VARCHAR(2048)"`
   138  	DefaultBranch       string
   139  	DefaultWikiBranch   string
   140  
   141  	NumWatches          int
   142  	NumStars            int
   143  	NumForks            int
   144  	NumIssues           int
   145  	NumClosedIssues     int
   146  	NumOpenIssues       int `xorm:"-"`
   147  	NumPulls            int
   148  	NumClosedPulls      int
   149  	NumOpenPulls        int `xorm:"-"`
   150  	NumMilestones       int `xorm:"NOT NULL DEFAULT 0"`
   151  	NumClosedMilestones int `xorm:"NOT NULL DEFAULT 0"`
   152  	NumOpenMilestones   int `xorm:"-"`
   153  	NumProjects         int `xorm:"NOT NULL DEFAULT 0"`
   154  	NumClosedProjects   int `xorm:"NOT NULL DEFAULT 0"`
   155  	NumOpenProjects     int `xorm:"-"`
   156  	NumActionRuns       int `xorm:"NOT NULL DEFAULT 0"`
   157  	NumClosedActionRuns int `xorm:"NOT NULL DEFAULT 0"`
   158  	NumOpenActionRuns   int `xorm:"-"`
   159  
   160  	IsPrivate  bool `xorm:"INDEX"`
   161  	IsEmpty    bool `xorm:"INDEX"`
   162  	IsArchived bool `xorm:"INDEX"`
   163  	IsMirror   bool `xorm:"INDEX"`
   164  
   165  	Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"`
   166  
   167  	RenderingMetas         map[string]string `xorm:"-"`
   168  	DocumentRenderingMetas map[string]string `xorm:"-"`
   169  	Units                  []*RepoUnit       `xorm:"-"`
   170  	PrimaryLanguage        *LanguageStat     `xorm:"-"`
   171  
   172  	IsFork                          bool               `xorm:"INDEX NOT NULL DEFAULT false"`
   173  	ForkID                          int64              `xorm:"INDEX"`
   174  	BaseRepo                        *Repository        `xorm:"-"`
   175  	IsTemplate                      bool               `xorm:"INDEX NOT NULL DEFAULT false"`
   176  	TemplateID                      int64              `xorm:"INDEX"`
   177  	Size                            int64              `xorm:"NOT NULL DEFAULT 0"`
   178  	GitSize                         int64              `xorm:"NOT NULL DEFAULT 0"`
   179  	LFSSize                         int64              `xorm:"NOT NULL DEFAULT 0"`
   180  	CodeIndexerStatus               *RepoIndexerStatus `xorm:"-"`
   181  	StatsIndexerStatus              *RepoIndexerStatus `xorm:"-"`
   182  	IsFsckEnabled                   bool               `xorm:"NOT NULL DEFAULT true"`
   183  	CloseIssuesViaCommitInAnyBranch bool               `xorm:"NOT NULL DEFAULT false"`
   184  	Topics                          []string           `xorm:"TEXT JSON"`
   185  	ObjectFormatName                string             `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"`
   186  
   187  	TrustModel TrustModelType
   188  
   189  	// Avatar: ID(10-20)-md5(32) - must fit into 64 symbols
   190  	Avatar string `xorm:"VARCHAR(64)"`
   191  
   192  	CreatedUnix  timeutil.TimeStamp `xorm:"INDEX created"`
   193  	UpdatedUnix  timeutil.TimeStamp `xorm:"INDEX updated"`
   194  	ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT 0"`
   195  }
   196  
   197  func init() {
   198  	db.RegisterModel(new(Repository))
   199  }
   200  
   201  func (repo *Repository) GetName() string {
   202  	return repo.Name
   203  }
   204  
   205  func (repo *Repository) GetOwnerName() string {
   206  	return repo.OwnerName
   207  }
   208  
   209  // SanitizedOriginalURL returns a sanitized OriginalURL
   210  func (repo *Repository) SanitizedOriginalURL() string {
   211  	if repo.OriginalURL == "" {
   212  		return ""
   213  	}
   214  	u, _ := util.SanitizeURL(repo.OriginalURL)
   215  	return u
   216  }
   217  
   218  // text representations to be returned in SizeDetail.Name
   219  const (
   220  	SizeDetailNameGit = "git"
   221  	SizeDetailNameLFS = "lfs"
   222  )
   223  
   224  type SizeDetail struct {
   225  	Name string
   226  	Size int64
   227  }
   228  
   229  // SizeDetails forms a struct with various size details about repository
   230  func (repo *Repository) SizeDetails() []SizeDetail {
   231  	sizeDetails := []SizeDetail{
   232  		{
   233  			Name: SizeDetailNameGit,
   234  			Size: repo.GitSize,
   235  		},
   236  		{
   237  			Name: SizeDetailNameLFS,
   238  			Size: repo.LFSSize,
   239  		},
   240  	}
   241  	return sizeDetails
   242  }
   243  
   244  // SizeDetailsString returns a concatenation of all repository size details as a string
   245  func (repo *Repository) SizeDetailsString() string {
   246  	var str strings.Builder
   247  	sizeDetails := repo.SizeDetails()
   248  	for _, detail := range sizeDetails {
   249  		str.WriteString(fmt.Sprintf("%s: %s, ", detail.Name, base.FileSize(detail.Size)))
   250  	}
   251  	return strings.TrimSuffix(str.String(), ", ")
   252  }
   253  
   254  func (repo *Repository) LogString() string {
   255  	if repo == nil {
   256  		return "<Repository nil>"
   257  	}
   258  	return fmt.Sprintf("<Repository %d:%s/%s>", repo.ID, repo.OwnerName, repo.Name)
   259  }
   260  
   261  // IsBeingMigrated indicates that repository is being migrated
   262  func (repo *Repository) IsBeingMigrated() bool {
   263  	return repo.Status == RepositoryBeingMigrated
   264  }
   265  
   266  // IsBeingCreated indicates that repository is being migrated or forked
   267  func (repo *Repository) IsBeingCreated() bool {
   268  	return repo.IsBeingMigrated()
   269  }
   270  
   271  // IsBroken indicates that repository is broken
   272  func (repo *Repository) IsBroken() bool {
   273  	return repo.Status == RepositoryBroken
   274  }
   275  
   276  // MarkAsBrokenEmpty marks the repo as broken and empty
   277  func (repo *Repository) MarkAsBrokenEmpty() {
   278  	repo.Status = RepositoryBroken
   279  	repo.IsEmpty = true
   280  }
   281  
   282  // AfterLoad is invoked from XORM after setting the values of all fields of this object.
   283  func (repo *Repository) AfterLoad() {
   284  	repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues
   285  	repo.NumOpenPulls = repo.NumPulls - repo.NumClosedPulls
   286  	repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones
   287  	repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects
   288  	repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns
   289  	if repo.DefaultWikiBranch == "" {
   290  		repo.DefaultWikiBranch = setting.Repository.DefaultBranch
   291  	}
   292  }
   293  
   294  // LoadAttributes loads attributes of the repository.
   295  func (repo *Repository) LoadAttributes(ctx context.Context) error {
   296  	// Load owner
   297  	if err := repo.LoadOwner(ctx); err != nil {
   298  		return fmt.Errorf("load owner: %w", err)
   299  	}
   300  
   301  	// Load primary language
   302  	stats := make(LanguageStatList, 0, 1)
   303  	if err := db.GetEngine(ctx).
   304  		Where("`repo_id` = ? AND `is_primary` = ? AND `language` != ?", repo.ID, true, "other").
   305  		Find(&stats); err != nil {
   306  		return fmt.Errorf("find primary languages: %w", err)
   307  	}
   308  	stats.LoadAttributes()
   309  	for _, st := range stats {
   310  		if st.RepoID == repo.ID {
   311  			repo.PrimaryLanguage = st
   312  			break
   313  		}
   314  	}
   315  	return nil
   316  }
   317  
   318  // FullName returns the repository full name
   319  func (repo *Repository) FullName() string {
   320  	return repo.OwnerName + "/" + repo.Name
   321  }
   322  
   323  // HTMLURL returns the repository HTML URL
   324  func (repo *Repository) HTMLURL() string {
   325  	return setting.AppURL + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
   326  }
   327  
   328  // CommitLink make link to by commit full ID
   329  // note: won't check whether it's an right id
   330  func (repo *Repository) CommitLink(commitID string) (result string) {
   331  	if git.IsEmptyCommitID(commitID) {
   332  		result = ""
   333  	} else {
   334  		result = repo.Link() + "/commit/" + url.PathEscape(commitID)
   335  	}
   336  	return result
   337  }
   338  
   339  // APIURL returns the repository API URL
   340  func (repo *Repository) APIURL() string {
   341  	return setting.AppURL + "api/v1/repos/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
   342  }
   343  
   344  // GetCommitsCountCacheKey returns cache key used for commits count caching.
   345  func (repo *Repository) GetCommitsCountCacheKey(contextName string, isRef bool) string {
   346  	var prefix string
   347  	if isRef {
   348  		prefix = "ref"
   349  	} else {
   350  		prefix = "commit"
   351  	}
   352  	return fmt.Sprintf("commits-count-%d-%s-%s", repo.ID, prefix, contextName)
   353  }
   354  
   355  // LoadUnits loads repo units into repo.Units
   356  func (repo *Repository) LoadUnits(ctx context.Context) (err error) {
   357  	if repo.Units != nil {
   358  		return nil
   359  	}
   360  
   361  	repo.Units, err = getUnitsByRepoID(ctx, repo.ID)
   362  	if log.IsTrace() {
   363  		unitTypeStrings := make([]string, len(repo.Units))
   364  		for i, unit := range repo.Units {
   365  			unitTypeStrings[i] = unit.Type.String()
   366  		}
   367  		log.Trace("repo.Units, ID=%d, Types: [%s]", repo.ID, strings.Join(unitTypeStrings, ", "))
   368  	}
   369  
   370  	return err
   371  }
   372  
   373  // UnitEnabled if this repository has the given unit enabled
   374  func (repo *Repository) UnitEnabled(ctx context.Context, tp unit.Type) bool {
   375  	if err := repo.LoadUnits(ctx); err != nil {
   376  		log.Warn("Error loading repository (ID: %d) units: %s", repo.ID, err.Error())
   377  	}
   378  	for _, unit := range repo.Units {
   379  		if unit.Type == tp {
   380  			return true
   381  		}
   382  	}
   383  	return false
   384  }
   385  
   386  // MustGetUnit always returns a RepoUnit object
   387  func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit {
   388  	ru, err := repo.GetUnit(ctx, tp)
   389  	if err == nil {
   390  		return ru
   391  	}
   392  
   393  	if tp == unit.TypeExternalWiki {
   394  		return &RepoUnit{
   395  			Type:   tp,
   396  			Config: new(ExternalWikiConfig),
   397  		}
   398  	} else if tp == unit.TypeExternalTracker {
   399  		return &RepoUnit{
   400  			Type:   tp,
   401  			Config: new(ExternalTrackerConfig),
   402  		}
   403  	} else if tp == unit.TypePullRequests {
   404  		return &RepoUnit{
   405  			Type:   tp,
   406  			Config: new(PullRequestsConfig),
   407  		}
   408  	} else if tp == unit.TypeIssues {
   409  		return &RepoUnit{
   410  			Type:   tp,
   411  			Config: new(IssuesConfig),
   412  		}
   413  	} else if tp == unit.TypeActions {
   414  		return &RepoUnit{
   415  			Type:   tp,
   416  			Config: new(ActionsConfig),
   417  		}
   418  	} else if tp == unit.TypeProjects {
   419  		cfg := new(ProjectsConfig)
   420  		cfg.ProjectsMode = ProjectsModeNone
   421  		return &RepoUnit{
   422  			Type:   tp,
   423  			Config: cfg,
   424  		}
   425  	}
   426  
   427  	return &RepoUnit{
   428  		Type:   tp,
   429  		Config: new(UnitConfig),
   430  	}
   431  }
   432  
   433  // GetUnit returns a RepoUnit object
   434  func (repo *Repository) GetUnit(ctx context.Context, tp unit.Type) (*RepoUnit, error) {
   435  	if err := repo.LoadUnits(ctx); err != nil {
   436  		return nil, err
   437  	}
   438  	for _, unit := range repo.Units {
   439  		if unit.Type == tp {
   440  			return unit, nil
   441  		}
   442  	}
   443  	return nil, ErrUnitTypeNotExist{tp}
   444  }
   445  
   446  // LoadOwner loads owner user
   447  func (repo *Repository) LoadOwner(ctx context.Context) (err error) {
   448  	if repo.Owner != nil {
   449  		return nil
   450  	}
   451  
   452  	repo.Owner, err = user_model.GetUserByID(ctx, repo.OwnerID)
   453  	return err
   454  }
   455  
   456  // MustOwner always returns a valid *user_model.User object to avoid
   457  // conceptually impossible error handling.
   458  // It creates a fake object that contains error details
   459  // when error occurs.
   460  func (repo *Repository) MustOwner(ctx context.Context) *user_model.User {
   461  	if err := repo.LoadOwner(ctx); err != nil {
   462  		return &user_model.User{
   463  			Name:     "error",
   464  			FullName: err.Error(),
   465  		}
   466  	}
   467  
   468  	return repo.Owner
   469  }
   470  
   471  // ComposeMetas composes a map of metas for properly rendering issue links and external issue trackers.
   472  func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string {
   473  	if len(repo.RenderingMetas) == 0 {
   474  		metas := map[string]string{
   475  			"user":     repo.OwnerName,
   476  			"repo":     repo.Name,
   477  			"repoPath": repo.RepoPath(),
   478  			"mode":     "comment",
   479  		}
   480  
   481  		unit, err := repo.GetUnit(ctx, unit.TypeExternalTracker)
   482  		if err == nil {
   483  			metas["format"] = unit.ExternalTrackerConfig().ExternalTrackerFormat
   484  			switch unit.ExternalTrackerConfig().ExternalTrackerStyle {
   485  			case markup.IssueNameStyleAlphanumeric:
   486  				metas["style"] = markup.IssueNameStyleAlphanumeric
   487  			case markup.IssueNameStyleRegexp:
   488  				metas["style"] = markup.IssueNameStyleRegexp
   489  				metas["regexp"] = unit.ExternalTrackerConfig().ExternalTrackerRegexpPattern
   490  			default:
   491  				metas["style"] = markup.IssueNameStyleNumeric
   492  			}
   493  		}
   494  
   495  		repo.MustOwner(ctx)
   496  		if repo.Owner.IsOrganization() {
   497  			teams := make([]string, 0, 5)
   498  			_ = db.GetEngine(ctx).Table("team_repo").
   499  				Join("INNER", "team", "team.id = team_repo.team_id").
   500  				Where("team_repo.repo_id = ?", repo.ID).
   501  				Select("team.lower_name").
   502  				OrderBy("team.lower_name").
   503  				Find(&teams)
   504  			metas["teams"] = "," + strings.Join(teams, ",") + ","
   505  			metas["org"] = strings.ToLower(repo.OwnerName)
   506  		}
   507  
   508  		repo.RenderingMetas = metas
   509  	}
   510  	return repo.RenderingMetas
   511  }
   512  
   513  // ComposeDocumentMetas composes a map of metas for properly rendering documents
   514  func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]string {
   515  	if len(repo.DocumentRenderingMetas) == 0 {
   516  		metas := map[string]string{}
   517  		for k, v := range repo.ComposeMetas(ctx) {
   518  			metas[k] = v
   519  		}
   520  		metas["mode"] = "document"
   521  		repo.DocumentRenderingMetas = metas
   522  	}
   523  	return repo.DocumentRenderingMetas
   524  }
   525  
   526  // GetBaseRepo populates repo.BaseRepo for a fork repository and
   527  // returns an error on failure (NOTE: no error is returned for
   528  // non-fork repositories, and BaseRepo will be left untouched)
   529  func (repo *Repository) GetBaseRepo(ctx context.Context) (err error) {
   530  	if !repo.IsFork {
   531  		return nil
   532  	}
   533  
   534  	if repo.BaseRepo != nil {
   535  		return nil
   536  	}
   537  	repo.BaseRepo, err = GetRepositoryByID(ctx, repo.ForkID)
   538  	return err
   539  }
   540  
   541  // IsGenerated returns whether _this_ repository was generated from a template
   542  func (repo *Repository) IsGenerated() bool {
   543  	return repo.TemplateID != 0
   544  }
   545  
   546  // RepoPath returns repository path by given user and repository name.
   547  func RepoPath(userName, repoName string) string { //revive:disable-line:exported
   548  	return filepath.Join(user_model.UserPath(userName), strings.ToLower(repoName)+".git")
   549  }
   550  
   551  // RepoPath returns the repository path
   552  func (repo *Repository) RepoPath() string {
   553  	return RepoPath(repo.OwnerName, repo.Name)
   554  }
   555  
   556  // Link returns the repository relative url
   557  func (repo *Repository) Link() string {
   558  	return setting.AppSubURL + "/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
   559  }
   560  
   561  // ComposeCompareURL returns the repository comparison URL
   562  func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) string {
   563  	return fmt.Sprintf("%s/%s/compare/%s...%s", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), util.PathEscapeSegments(oldCommitID), util.PathEscapeSegments(newCommitID))
   564  }
   565  
   566  func (repo *Repository) ComposeBranchCompareURL(baseRepo *Repository, branchName string) string {
   567  	if baseRepo == nil {
   568  		baseRepo = repo
   569  	}
   570  	var cmpBranchEscaped string
   571  	if repo.ID != baseRepo.ID {
   572  		cmpBranchEscaped = fmt.Sprintf("%s/%s:", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name))
   573  	}
   574  	cmpBranchEscaped = fmt.Sprintf("%s%s", cmpBranchEscaped, util.PathEscapeSegments(branchName))
   575  	return fmt.Sprintf("%s/compare/%s...%s", baseRepo.Link(), util.PathEscapeSegments(baseRepo.DefaultBranch), cmpBranchEscaped)
   576  }
   577  
   578  // IsOwnedBy returns true when user owns this repository
   579  func (repo *Repository) IsOwnedBy(userID int64) bool {
   580  	return repo.OwnerID == userID
   581  }
   582  
   583  // CanCreateBranch returns true if repository meets the requirements for creating new branches.
   584  func (repo *Repository) CanCreateBranch() bool {
   585  	return !repo.IsMirror
   586  }
   587  
   588  // CanEnablePulls returns true if repository meets the requirements of accepting pulls.
   589  func (repo *Repository) CanEnablePulls() bool {
   590  	return !repo.IsMirror && !repo.IsEmpty
   591  }
   592  
   593  // AllowsPulls returns true if repository meets the requirements of accepting pulls and has them enabled.
   594  func (repo *Repository) AllowsPulls(ctx context.Context) bool {
   595  	return repo.CanEnablePulls() && repo.UnitEnabled(ctx, unit.TypePullRequests)
   596  }
   597  
   598  // CanEnableEditor returns true if repository meets the requirements of web editor.
   599  func (repo *Repository) CanEnableEditor() bool {
   600  	return !repo.IsMirror
   601  }
   602  
   603  // DescriptionHTML does special handles to description and return HTML string.
   604  func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
   605  	desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{
   606  		Ctx: ctx,
   607  		// Don't use Metas to speedup requests
   608  	}, repo.Description)
   609  	if err != nil {
   610  		log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err)
   611  		return template.HTML(markup.SanitizeDescription(repo.Description))
   612  	}
   613  	return template.HTML(markup.SanitizeDescription(desc))
   614  }
   615  
   616  // CloneLink represents different types of clone URLs of repository.
   617  type CloneLink struct {
   618  	SSH   string
   619  	HTTPS string
   620  }
   621  
   622  // ComposeHTTPSCloneURL returns HTTPS clone URL based on given owner and repository name.
   623  func ComposeHTTPSCloneURL(owner, repo string) string {
   624  	return fmt.Sprintf("%s%s/%s.git", setting.AppURL, url.PathEscape(owner), url.PathEscape(repo))
   625  }
   626  
   627  func ComposeSSHCloneURL(ownerName, repoName string) string {
   628  	sshUser := setting.SSH.User
   629  	sshDomain := setting.SSH.Domain
   630  
   631  	// non-standard port, it must use full URI
   632  	if setting.SSH.Port != 22 {
   633  		sshHost := net.JoinHostPort(sshDomain, strconv.Itoa(setting.SSH.Port))
   634  		return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
   635  	}
   636  
   637  	// for standard port, it can use a shorter URI (without the port)
   638  	sshHost := sshDomain
   639  	if ip := net.ParseIP(sshHost); ip != nil && ip.To4() == nil {
   640  		sshHost = "[" + sshHost + "]" // for IPv6 address, wrap it with brackets
   641  	}
   642  	if setting.Repository.UseCompatSSHURI {
   643  		return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
   644  	}
   645  	return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
   646  }
   647  
   648  func (repo *Repository) cloneLink(isWiki bool) *CloneLink {
   649  	repoName := repo.Name
   650  	if isWiki {
   651  		repoName += ".wiki"
   652  	}
   653  
   654  	cl := new(CloneLink)
   655  	cl.SSH = ComposeSSHCloneURL(repo.OwnerName, repoName)
   656  	cl.HTTPS = ComposeHTTPSCloneURL(repo.OwnerName, repoName)
   657  	return cl
   658  }
   659  
   660  // CloneLink returns clone URLs of repository.
   661  func (repo *Repository) CloneLink() (cl *CloneLink) {
   662  	return repo.cloneLink(false)
   663  }
   664  
   665  // GetOriginalURLHostname returns the hostname of a URL or the URL
   666  func (repo *Repository) GetOriginalURLHostname() string {
   667  	u, err := url.Parse(repo.OriginalURL)
   668  	if err != nil {
   669  		return repo.OriginalURL
   670  	}
   671  
   672  	return u.Host
   673  }
   674  
   675  // GetTrustModel will get the TrustModel for the repo or the default trust model
   676  func (repo *Repository) GetTrustModel() TrustModelType {
   677  	trustModel := repo.TrustModel
   678  	if trustModel == DefaultTrustModel {
   679  		trustModel = ToTrustModel(setting.Repository.Signing.DefaultTrustModel)
   680  		if trustModel == DefaultTrustModel {
   681  			return CollaboratorTrustModel
   682  		}
   683  	}
   684  	return trustModel
   685  }
   686  
   687  // MustNotBeArchived returns ErrRepoIsArchived if the repo is archived
   688  func (repo *Repository) MustNotBeArchived() error {
   689  	if repo.IsArchived {
   690  		return ErrRepoIsArchived{Repo: repo}
   691  	}
   692  	return nil
   693  }
   694  
   695  // __________                           .__  __
   696  // \______   \ ____ ______   ____  _____|__|/  |_  ___________ ___.__.
   697  //  |       _// __ \\____ \ /  _ \/  ___/  \   __\/  _ \_  __ <   |  |
   698  //  |    |   \  ___/|  |_> >  <_> )___ \|  ||  | (  <_> )  | \/\___  |
   699  //  |____|_  /\___  >   __/ \____/____  >__||__|  \____/|__|   / ____|
   700  //         \/     \/|__|              \/                       \/
   701  
   702  // ErrRepoNotExist represents a "RepoNotExist" kind of error.
   703  type ErrRepoNotExist struct {
   704  	ID        int64
   705  	UID       int64
   706  	OwnerName string
   707  	Name      string
   708  }
   709  
   710  // IsErrRepoNotExist checks if an error is a ErrRepoNotExist.
   711  func IsErrRepoNotExist(err error) bool {
   712  	_, ok := err.(ErrRepoNotExist)
   713  	return ok
   714  }
   715  
   716  func (err ErrRepoNotExist) Error() string {
   717  	return fmt.Sprintf("repository does not exist [id: %d, uid: %d, owner_name: %s, name: %s]",
   718  		err.ID, err.UID, err.OwnerName, err.Name)
   719  }
   720  
   721  // Unwrap unwraps this error as a ErrNotExist error
   722  func (err ErrRepoNotExist) Unwrap() error {
   723  	return util.ErrNotExist
   724  }
   725  
   726  // GetRepositoryByOwnerAndName returns the repository by given owner name and repo name
   727  func GetRepositoryByOwnerAndName(ctx context.Context, ownerName, repoName string) (*Repository, error) {
   728  	var repo Repository
   729  	has, err := db.GetEngine(ctx).Table("repository").Select("repository.*").
   730  		Join("INNER", "`user`", "`user`.id = repository.owner_id").
   731  		Where("repository.lower_name = ?", strings.ToLower(repoName)).
   732  		And("`user`.lower_name = ?", strings.ToLower(ownerName)).
   733  		Get(&repo)
   734  	if err != nil {
   735  		return nil, err
   736  	} else if !has {
   737  		return nil, ErrRepoNotExist{0, 0, ownerName, repoName}
   738  	}
   739  	return &repo, nil
   740  }
   741  
   742  // GetRepositoryByName returns the repository by given name under user if exists.
   743  func GetRepositoryByName(ctx context.Context, ownerID int64, name string) (*Repository, error) {
   744  	var repo Repository
   745  	has, err := db.GetEngine(ctx).
   746  		Where("`owner_id`=?", ownerID).
   747  		And("`lower_name`=?", strings.ToLower(name)).
   748  		NoAutoCondition().
   749  		Get(&repo)
   750  	if err != nil {
   751  		return nil, err
   752  	} else if !has {
   753  		return nil, ErrRepoNotExist{0, ownerID, "", name}
   754  	}
   755  	return &repo, err
   756  }
   757  
   758  // getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url
   759  func getRepositoryURLPathSegments(repoURL string) []string {
   760  	if strings.HasPrefix(repoURL, setting.AppURL) {
   761  		return strings.Split(strings.TrimPrefix(repoURL, setting.AppURL), "/")
   762  	}
   763  
   764  	sshURLVariants := [4]string{
   765  		setting.SSH.Domain + ":",
   766  		setting.SSH.User + "@" + setting.SSH.Domain + ":",
   767  		"git+ssh://" + setting.SSH.Domain + "/",
   768  		"git+ssh://" + setting.SSH.User + "@" + setting.SSH.Domain + "/",
   769  	}
   770  
   771  	for _, sshURL := range sshURLVariants {
   772  		if strings.HasPrefix(repoURL, sshURL) {
   773  			return strings.Split(strings.TrimPrefix(repoURL, sshURL), "/")
   774  		}
   775  	}
   776  
   777  	return nil
   778  }
   779  
   780  // GetRepositoryByURL returns the repository by given url
   781  func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error) {
   782  	// possible urls for git:
   783  	//  https://my.domain/sub-path/<owner>/<repo>.git
   784  	//  https://my.domain/sub-path/<owner>/<repo>
   785  	//  git+ssh://user@my.domain/<owner>/<repo>.git
   786  	//  git+ssh://user@my.domain/<owner>/<repo>
   787  	//  user@my.domain:<owner>/<repo>.git
   788  	//  user@my.domain:<owner>/<repo>
   789  
   790  	pathSegments := getRepositoryURLPathSegments(repoURL)
   791  
   792  	if len(pathSegments) != 2 {
   793  		return nil, fmt.Errorf("unknown or malformed repository URL")
   794  	}
   795  
   796  	ownerName := pathSegments[0]
   797  	repoName := strings.TrimSuffix(pathSegments[1], ".git")
   798  	return GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
   799  }
   800  
   801  // GetRepositoryByID returns the repository by given id if exists.
   802  func GetRepositoryByID(ctx context.Context, id int64) (*Repository, error) {
   803  	repo := new(Repository)
   804  	has, err := db.GetEngine(ctx).ID(id).Get(repo)
   805  	if err != nil {
   806  		return nil, err
   807  	} else if !has {
   808  		return nil, ErrRepoNotExist{id, 0, "", ""}
   809  	}
   810  	return repo, nil
   811  }
   812  
   813  // GetRepositoriesMapByIDs returns the repositories by given id slice.
   814  func GetRepositoriesMapByIDs(ctx context.Context, ids []int64) (map[int64]*Repository, error) {
   815  	repos := make(map[int64]*Repository, len(ids))
   816  	return repos, db.GetEngine(ctx).In("id", ids).Find(&repos)
   817  }
   818  
   819  // IsRepositoryModelOrDirExist returns true if the repository with given name under user has already existed.
   820  func IsRepositoryModelOrDirExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
   821  	has, err := IsRepositoryModelExist(ctx, u, repoName)
   822  	if err != nil {
   823  		return false, err
   824  	}
   825  	isDir, err := util.IsDir(RepoPath(u.Name, repoName))
   826  	return has || isDir, err
   827  }
   828  
   829  func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
   830  	return db.GetEngine(ctx).Get(&Repository{
   831  		OwnerID:   u.ID,
   832  		LowerName: strings.ToLower(repoName),
   833  	})
   834  }
   835  
   836  // GetTemplateRepo populates repo.TemplateRepo for a generated repository and
   837  // returns an error on failure (NOTE: no error is returned for
   838  // non-generated repositories, and TemplateRepo will be left untouched)
   839  func GetTemplateRepo(ctx context.Context, repo *Repository) (*Repository, error) {
   840  	if !repo.IsGenerated() {
   841  		return nil, nil
   842  	}
   843  
   844  	return GetRepositoryByID(ctx, repo.TemplateID)
   845  }
   846  
   847  // TemplateRepo returns the repository, which is template of this repository
   848  func (repo *Repository) TemplateRepo(ctx context.Context) *Repository {
   849  	repo, err := GetTemplateRepo(ctx, repo)
   850  	if err != nil {
   851  		log.Error("TemplateRepo: %v", err)
   852  		return nil
   853  	}
   854  	return repo
   855  }
   856  
   857  type CountRepositoryOptions struct {
   858  	OwnerID int64
   859  	Private optional.Option[bool]
   860  }
   861  
   862  // CountRepositories returns number of repositories.
   863  // Argument private only takes effect when it is false,
   864  // set it true to count all repositories.
   865  func CountRepositories(ctx context.Context, opts CountRepositoryOptions) (int64, error) {
   866  	sess := db.GetEngine(ctx).Where("id > 0")
   867  
   868  	if opts.OwnerID > 0 {
   869  		sess.And("owner_id = ?", opts.OwnerID)
   870  	}
   871  	if opts.Private.Has() {
   872  		sess.And("is_private=?", opts.Private.Value())
   873  	}
   874  
   875  	count, err := sess.Count(new(Repository))
   876  	if err != nil {
   877  		return 0, fmt.Errorf("countRepositories: %w", err)
   878  	}
   879  	return count, nil
   880  }
   881  
   882  // UpdateRepoIssueNumbers updates one of a repositories amount of (open|closed) (issues|PRs) with the current count
   883  func UpdateRepoIssueNumbers(ctx context.Context, repoID int64, isPull, isClosed bool) error {
   884  	field := "num_"
   885  	if isClosed {
   886  		field += "closed_"
   887  	}
   888  	if isPull {
   889  		field += "pulls"
   890  	} else {
   891  		field += "issues"
   892  	}
   893  
   894  	subQuery := builder.Select("count(*)").
   895  		From("issue").Where(builder.Eq{
   896  		"repo_id": repoID,
   897  		"is_pull": isPull,
   898  	}.And(builder.If(isClosed, builder.Eq{"is_closed": isClosed})))
   899  
   900  	// builder.Update(cond) will generate SQL like UPDATE ... SET cond
   901  	query := builder.Update(builder.Eq{field: subQuery}).
   902  		From("repository").
   903  		Where(builder.Eq{"id": repoID})
   904  	_, err := db.Exec(ctx, query)
   905  	return err
   906  }
   907  
   908  // CountNullArchivedRepository counts the number of repositories with is_archived is null
   909  func CountNullArchivedRepository(ctx context.Context) (int64, error) {
   910  	return db.GetEngine(ctx).Where(builder.IsNull{"is_archived"}).Count(new(Repository))
   911  }
   912  
   913  // FixNullArchivedRepository sets is_archived to false where it is null
   914  func FixNullArchivedRepository(ctx context.Context) (int64, error) {
   915  	return db.GetEngine(ctx).Where(builder.IsNull{"is_archived"}).Cols("is_archived").NoAutoTime().Update(&Repository{
   916  		IsArchived: false,
   917  	})
   918  }
   919  
   920  // UpdateRepositoryOwnerName updates the owner name of all repositories owned by the user
   921  func UpdateRepositoryOwnerName(ctx context.Context, oldUserName, newUserName string) error {
   922  	if _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET owner_name=? WHERE owner_name=?", newUserName, oldUserName); err != nil {
   923  		return fmt.Errorf("change repo owner name: %w", err)
   924  	}
   925  	return nil
   926  }