repositories.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. // Copyright 2020 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package database
  5. import (
  6. "context"
  7. "fmt"
  8. "strings"
  9. "time"
  10. api "github.com/gogs/go-gogs-client"
  11. "github.com/pkg/errors"
  12. "gorm.io/gorm"
  13. "gogs.io/gogs/internal/errutil"
  14. "gogs.io/gogs/internal/repoutil"
  15. )
  16. // BeforeCreate implements the GORM create hook.
  17. func (r *Repository) BeforeCreate(tx *gorm.DB) error {
  18. if r.CreatedUnix == 0 {
  19. r.CreatedUnix = tx.NowFunc().Unix()
  20. }
  21. return nil
  22. }
  23. // BeforeUpdate implements the GORM update hook.
  24. func (r *Repository) BeforeUpdate(tx *gorm.DB) error {
  25. r.UpdatedUnix = tx.NowFunc().Unix()
  26. return nil
  27. }
  28. // AfterFind implements the GORM query hook.
  29. func (r *Repository) AfterFind(_ *gorm.DB) error {
  30. r.Created = time.Unix(r.CreatedUnix, 0).Local()
  31. r.Updated = time.Unix(r.UpdatedUnix, 0).Local()
  32. return nil
  33. }
  34. type RepositoryAPIFormatOptions struct {
  35. Permission *api.Permission
  36. Parent *api.Repository
  37. }
  38. // APIFormat returns the API format of a repository.
  39. func (r *Repository) APIFormat(owner *User, opts ...RepositoryAPIFormatOptions) *api.Repository {
  40. var opt RepositoryAPIFormatOptions
  41. if len(opts) > 0 {
  42. opt = opts[0]
  43. }
  44. cloneLink := repoutil.NewCloneLink(owner.Name, r.Name, false)
  45. return &api.Repository{
  46. ID: r.ID,
  47. Owner: owner.APIFormat(),
  48. Name: r.Name,
  49. FullName: owner.Name + "/" + r.Name,
  50. Description: r.Description,
  51. Private: r.IsPrivate,
  52. Fork: r.IsFork,
  53. Parent: opt.Parent,
  54. Empty: r.IsBare,
  55. Mirror: r.IsMirror,
  56. Size: r.Size,
  57. HTMLURL: repoutil.HTMLURL(owner.Name, r.Name),
  58. SSHURL: cloneLink.SSH,
  59. CloneURL: cloneLink.HTTPS,
  60. Website: r.Website,
  61. Stars: r.NumStars,
  62. Forks: r.NumForks,
  63. Watchers: r.NumWatches,
  64. OpenIssues: r.NumOpenIssues,
  65. DefaultBranch: r.DefaultBranch,
  66. Created: r.Created,
  67. Updated: r.Updated,
  68. Permissions: opt.Permission,
  69. }
  70. }
  71. // RepositoriesStore is the storage layer for repositories.
  72. type RepositoriesStore struct {
  73. db *gorm.DB
  74. }
  75. func newReposStore(db *gorm.DB) *RepositoriesStore {
  76. return &RepositoriesStore{db: db}
  77. }
  78. type ErrRepoAlreadyExist struct {
  79. args errutil.Args
  80. }
  81. func IsErrRepoAlreadyExist(err error) bool {
  82. _, ok := err.(ErrRepoAlreadyExist)
  83. return ok
  84. }
  85. func (err ErrRepoAlreadyExist) Error() string {
  86. return fmt.Sprintf("repository already exists: %v", err.args)
  87. }
  88. type CreateRepoOptions struct {
  89. Name string
  90. Description string
  91. DefaultBranch string
  92. Private bool
  93. Mirror bool
  94. EnableWiki bool
  95. EnableIssues bool
  96. EnablePulls bool
  97. Fork bool
  98. ForkID int64
  99. }
  100. // Create creates a new repository record in the database. It returns
  101. // ErrNameNotAllowed when the repository name is not allowed, or
  102. // ErrRepoAlreadyExist when a repository with same name already exists for the
  103. // owner.
  104. func (s *RepositoriesStore) Create(ctx context.Context, ownerID int64, opts CreateRepoOptions) (*Repository, error) {
  105. err := isRepoNameAllowed(opts.Name)
  106. if err != nil {
  107. return nil, err
  108. }
  109. _, err = s.GetByName(ctx, ownerID, opts.Name)
  110. if err == nil {
  111. return nil, ErrRepoAlreadyExist{
  112. args: errutil.Args{
  113. "ownerID": ownerID,
  114. "name": opts.Name,
  115. },
  116. }
  117. } else if !IsErrRepoNotExist(err) {
  118. return nil, err
  119. }
  120. repo := &Repository{
  121. OwnerID: ownerID,
  122. LowerName: strings.ToLower(opts.Name),
  123. Name: opts.Name,
  124. Description: opts.Description,
  125. DefaultBranch: opts.DefaultBranch,
  126. IsPrivate: opts.Private,
  127. IsMirror: opts.Mirror,
  128. EnableWiki: opts.EnableWiki,
  129. EnableIssues: opts.EnableIssues,
  130. EnablePulls: opts.EnablePulls,
  131. IsFork: opts.Fork,
  132. ForkID: opts.ForkID,
  133. }
  134. return repo, s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
  135. err = tx.Create(repo).Error
  136. if err != nil {
  137. return errors.Wrap(err, "create")
  138. }
  139. err = newReposStore(tx).Watch(ctx, ownerID, repo.ID)
  140. if err != nil {
  141. return errors.Wrap(err, "watch")
  142. }
  143. return nil
  144. })
  145. }
  146. // GetByCollaboratorID returns a list of repositories that the given
  147. // collaborator has access to. Results are limited to the given limit and sorted
  148. // by the given order (e.g. "updated_unix DESC"). Repositories that are owned
  149. // directly by the given collaborator are not included.
  150. func (s *RepositoriesStore) GetByCollaboratorID(ctx context.Context, collaboratorID int64, limit int, orderBy string) ([]*Repository, error) {
  151. /*
  152. Equivalent SQL for PostgreSQL:
  153. SELECT * FROM repository
  154. JOIN access ON access.repo_id = repository.id AND access.user_id = @collaboratorID
  155. WHERE access.mode >= @accessModeRead
  156. ORDER BY @orderBy
  157. LIMIT @limit
  158. */
  159. var repos []*Repository
  160. return repos, s.db.WithContext(ctx).
  161. Joins("JOIN access ON access.repo_id = repository.id AND access.user_id = ?", collaboratorID).
  162. Where("access.mode >= ?", AccessModeRead).
  163. Order(orderBy).
  164. Limit(limit).
  165. Find(&repos).
  166. Error
  167. }
  168. // GetByCollaboratorIDWithAccessMode returns a list of repositories and
  169. // corresponding access mode that the given collaborator has access to.
  170. // Repositories that are owned directly by the given collaborator are not
  171. // included.
  172. func (s *RepositoriesStore) GetByCollaboratorIDWithAccessMode(ctx context.Context, collaboratorID int64) (map[*Repository]AccessMode, error) {
  173. /*
  174. Equivalent SQL for PostgreSQL:
  175. SELECT
  176. repository.*,
  177. access.mode
  178. FROM repository
  179. JOIN access ON access.repo_id = repository.id AND access.user_id = @collaboratorID
  180. WHERE access.mode >= @accessModeRead
  181. */
  182. var reposWithAccessMode []*struct {
  183. *Repository
  184. Mode AccessMode
  185. }
  186. err := s.db.WithContext(ctx).
  187. Select("repository.*", "access.mode").
  188. Table("repository").
  189. Joins("JOIN access ON access.repo_id = repository.id AND access.user_id = ?", collaboratorID).
  190. Where("access.mode >= ?", AccessModeRead).
  191. Find(&reposWithAccessMode).
  192. Error
  193. if err != nil {
  194. return nil, err
  195. }
  196. repos := make(map[*Repository]AccessMode, len(reposWithAccessMode))
  197. for _, repoWithAccessMode := range reposWithAccessMode {
  198. repos[repoWithAccessMode.Repository] = repoWithAccessMode.Mode
  199. }
  200. return repos, nil
  201. }
  202. var _ errutil.NotFound = (*ErrRepoNotExist)(nil)
  203. type ErrRepoNotExist struct {
  204. args errutil.Args
  205. }
  206. func IsErrRepoNotExist(err error) bool {
  207. _, ok := err.(ErrRepoNotExist)
  208. return ok
  209. }
  210. func (err ErrRepoNotExist) Error() string {
  211. return fmt.Sprintf("repository does not exist: %v", err.args)
  212. }
  213. func (ErrRepoNotExist) NotFound() bool {
  214. return true
  215. }
  216. // GetByID returns the repository with given ID. It returns ErrRepoNotExist when
  217. // not found.
  218. func (s *RepositoriesStore) GetByID(ctx context.Context, id int64) (*Repository, error) {
  219. repo := new(Repository)
  220. err := s.db.WithContext(ctx).Where("id = ?", id).First(repo).Error
  221. if err != nil {
  222. if errors.Is(err, gorm.ErrRecordNotFound) {
  223. return nil, ErrRepoNotExist{errutil.Args{"repoID": id}}
  224. }
  225. return nil, err
  226. }
  227. return repo, nil
  228. }
  229. // GetByName returns the repository with given owner and name. It returns
  230. // ErrRepoNotExist when not found.
  231. func (s *RepositoriesStore) GetByName(ctx context.Context, ownerID int64, name string) (*Repository, error) {
  232. repo := new(Repository)
  233. err := s.db.WithContext(ctx).
  234. Where("owner_id = ? AND lower_name = ?", ownerID, strings.ToLower(name)).
  235. First(repo).
  236. Error
  237. if err != nil {
  238. if err == gorm.ErrRecordNotFound {
  239. return nil, ErrRepoNotExist{
  240. args: errutil.Args{
  241. "ownerID": ownerID,
  242. "name": name,
  243. },
  244. }
  245. }
  246. return nil, err
  247. }
  248. return repo, nil
  249. }
  250. func (s *RepositoriesStore) recountStars(tx *gorm.DB, userID, repoID int64) error {
  251. /*
  252. Equivalent SQL for PostgreSQL:
  253. UPDATE repository
  254. SET num_stars = (
  255. SELECT COUNT(*) FROM star WHERE repo_id = @repoID
  256. )
  257. WHERE id = @repoID
  258. */
  259. err := tx.Model(&Repository{}).
  260. Where("id = ?", repoID).
  261. Update(
  262. "num_stars",
  263. tx.Model(&Star{}).Select("COUNT(*)").Where("repo_id = ?", repoID),
  264. ).
  265. Error
  266. if err != nil {
  267. return errors.Wrap(err, `update "repository.num_stars"`)
  268. }
  269. /*
  270. Equivalent SQL for PostgreSQL:
  271. UPDATE "user"
  272. SET num_stars = (
  273. SELECT COUNT(*) FROM star WHERE uid = @userID
  274. )
  275. WHERE id = @userID
  276. */
  277. err = tx.Model(&User{}).
  278. Where("id = ?", userID).
  279. Update(
  280. "num_stars",
  281. tx.Model(&Star{}).Select("COUNT(*)").Where("uid = ?", userID),
  282. ).
  283. Error
  284. if err != nil {
  285. return errors.Wrap(err, `update "user.num_stars"`)
  286. }
  287. return nil
  288. }
  289. // Star marks the user to star the repository.
  290. func (s *RepositoriesStore) Star(ctx context.Context, userID, repoID int64) error {
  291. return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
  292. star := &Star{
  293. UserID: userID,
  294. RepoID: repoID,
  295. }
  296. result := tx.FirstOrCreate(star, star)
  297. if result.Error != nil {
  298. return errors.Wrap(result.Error, "upsert")
  299. } else if result.RowsAffected <= 0 {
  300. return nil // Relation already exists
  301. }
  302. return s.recountStars(tx, userID, repoID)
  303. })
  304. }
  305. // Touch updates the updated time to the current time and removes the bare state
  306. // of the given repository.
  307. func (s *RepositoriesStore) Touch(ctx context.Context, id int64) error {
  308. return s.db.WithContext(ctx).
  309. Model(new(Repository)).
  310. Where("id = ?", id).
  311. Updates(map[string]any{
  312. "is_bare": false,
  313. "updated_unix": s.db.NowFunc().Unix(),
  314. }).
  315. Error
  316. }
  317. // ListWatches returns all watches of the given repository.
  318. func (s *RepositoriesStore) ListWatches(ctx context.Context, repoID int64) ([]*Watch, error) {
  319. var watches []*Watch
  320. return watches, s.db.WithContext(ctx).Where("repo_id = ?", repoID).Find(&watches).Error
  321. }
  322. func (s *RepositoriesStore) recountWatches(tx *gorm.DB, repoID int64) error {
  323. /*
  324. Equivalent SQL for PostgreSQL:
  325. UPDATE repository
  326. SET num_watches = (
  327. SELECT COUNT(*) FROM watch WHERE repo_id = @repoID
  328. )
  329. WHERE id = @repoID
  330. */
  331. return tx.Model(&Repository{}).
  332. Where("id = ?", repoID).
  333. Update(
  334. "num_watches",
  335. tx.Model(&Watch{}).Select("COUNT(*)").Where("repo_id = ?", repoID),
  336. ).
  337. Error
  338. }
  339. // Watch marks the user to watch the repository.
  340. func (s *RepositoriesStore) Watch(ctx context.Context, userID, repoID int64) error {
  341. return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
  342. w := &Watch{
  343. UserID: userID,
  344. RepoID: repoID,
  345. }
  346. result := tx.FirstOrCreate(w, w)
  347. if result.Error != nil {
  348. return errors.Wrap(result.Error, "upsert")
  349. } else if result.RowsAffected <= 0 {
  350. return nil // Relation already exists
  351. }
  352. return s.recountWatches(tx, repoID)
  353. })
  354. }
  355. // HasForkedBy returns true if the given repository has forked by the given user.
  356. func (s *RepositoriesStore) HasForkedBy(ctx context.Context, repoID, userID int64) bool {
  357. var count int64
  358. s.db.WithContext(ctx).Model(new(Repository)).Where("owner_id = ? AND fork_id = ?", userID, repoID).Count(&count)
  359. return count > 0
  360. }