repositories.go 10 KB

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