gitea源码

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