actions.go 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936
  1. package database
  2. import (
  3. "context"
  4. "fmt"
  5. "path"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "unicode"
  10. "github.com/cockroachdb/errors"
  11. "github.com/gogs/git-module"
  12. api "github.com/gogs/go-gogs-client"
  13. jsoniter "github.com/json-iterator/go"
  14. "gorm.io/gorm"
  15. log "unknwon.dev/clog/v2"
  16. "gogs.io/gogs/internal/conf"
  17. "gogs.io/gogs/internal/lazyregexp"
  18. "gogs.io/gogs/internal/repoutil"
  19. "gogs.io/gogs/internal/strutil"
  20. "gogs.io/gogs/internal/testutil"
  21. "gogs.io/gogs/internal/tool"
  22. )
  23. // ActionsStore is the storage layer for actions.
  24. type ActionsStore struct {
  25. db *gorm.DB
  26. }
  27. func newActionsStore(db *gorm.DB) *ActionsStore {
  28. return &ActionsStore{db: db}
  29. }
  30. func (s *ActionsStore) listByOrganization(ctx context.Context, orgID, actorID, afterID int64) *gorm.DB {
  31. /*
  32. Equivalent SQL for PostgreSQL:
  33. SELECT * FROM "action"
  34. WHERE
  35. user_id = @userID
  36. AND (@skipAfter OR id < @afterID)
  37. AND repo_id IN (
  38. SELECT repository.id FROM "repository"
  39. JOIN team_repo ON repository.id = team_repo.repo_id
  40. WHERE team_repo.team_id IN (
  41. SELECT team_id FROM "team_user"
  42. WHERE
  43. team_user.org_id = @orgID AND uid = @actorID)
  44. OR (repository.is_private = FALSE AND repository.is_unlisted = FALSE)
  45. )
  46. ORDER BY id DESC
  47. LIMIT @limit
  48. */
  49. return s.db.WithContext(ctx).
  50. Where("user_id = ?", orgID).
  51. Where(s.db.
  52. // Not apply when afterID is not given
  53. Where("?", afterID <= 0).
  54. Or("id < ?", afterID),
  55. ).
  56. Where("repo_id IN (?)", s.db.
  57. Select("repository.id").
  58. Table("repository").
  59. Joins("JOIN team_repo ON repository.id = team_repo.repo_id").
  60. Where("team_repo.team_id IN (?)", s.db.
  61. Select("team_id").
  62. Table("team_user").
  63. Where("team_user.org_id = ? AND uid = ?", orgID, actorID),
  64. ).
  65. Or("repository.is_private = ? AND repository.is_unlisted = ?", false, false),
  66. ).
  67. Limit(conf.UI.User.NewsFeedPagingNum).
  68. Order("id DESC")
  69. }
  70. // ListByOrganization returns actions of the organization viewable by the actor.
  71. // Results are paginated if `afterID` is given.
  72. func (s *ActionsStore) ListByOrganization(ctx context.Context, orgID, actorID, afterID int64) ([]*Action, error) {
  73. actions := make([]*Action, 0, conf.UI.User.NewsFeedPagingNum)
  74. return actions, s.listByOrganization(ctx, orgID, actorID, afterID).Find(&actions).Error
  75. }
  76. func (s *ActionsStore) listByUser(ctx context.Context, userID, actorID, afterID int64, isProfile bool) *gorm.DB {
  77. /*
  78. Equivalent SQL for PostgreSQL:
  79. SELECT * FROM "action"
  80. WHERE
  81. user_id = @userID
  82. AND (@skipAfter OR id < @afterID)
  83. AND (@includePrivate OR (is_private = FALSE AND act_user_id = @actorID))
  84. ORDER BY id DESC
  85. LIMIT @limit
  86. */
  87. return s.db.WithContext(ctx).
  88. Where("user_id = ?", userID).
  89. Where(s.db.
  90. // Not apply when afterID is not given
  91. Where("?", afterID <= 0).
  92. Or("id < ?", afterID),
  93. ).
  94. Where(s.db.
  95. // Not apply when in not profile page or the user is viewing own profile
  96. Where("?", !isProfile || actorID == userID).
  97. Or("is_private = ? AND act_user_id = ?", false, userID),
  98. ).
  99. Limit(conf.UI.User.NewsFeedPagingNum).
  100. Order("id DESC")
  101. }
  102. // ListByUser returns actions of the user viewable by the actor. Results are
  103. // paginated if `afterID` is given. The `isProfile` indicates whether repository
  104. // permissions should be considered.
  105. func (s *ActionsStore) ListByUser(ctx context.Context, userID, actorID, afterID int64, isProfile bool) ([]*Action, error) {
  106. actions := make([]*Action, 0, conf.UI.User.NewsFeedPagingNum)
  107. return actions, s.listByUser(ctx, userID, actorID, afterID, isProfile).Find(&actions).Error
  108. }
  109. // notifyWatchers creates rows in action table for watchers who are able to see the action.
  110. func (s *ActionsStore) notifyWatchers(ctx context.Context, act *Action) error {
  111. watches, err := newReposStore(s.db).ListWatches(ctx, act.RepoID)
  112. if err != nil {
  113. return errors.Wrap(err, "list watches")
  114. }
  115. // Clone returns a deep copy of the action with UserID assigned
  116. clone := func(userID int64) *Action {
  117. tmp := *act
  118. tmp.UserID = userID
  119. return &tmp
  120. }
  121. // Plus one for the actor
  122. actions := make([]*Action, 0, len(watches)+1)
  123. actions = append(actions, clone(act.ActUserID))
  124. for _, watch := range watches {
  125. if act.ActUserID == watch.UserID {
  126. continue
  127. }
  128. actions = append(actions, clone(watch.UserID))
  129. }
  130. return s.db.Create(actions).Error
  131. }
  132. // NewRepo creates an action for creating a new repository. The action type
  133. // could be ActionCreateRepo or ActionForkRepo based on whether the repository
  134. // is a fork.
  135. func (s *ActionsStore) NewRepo(ctx context.Context, doer, owner *User, repo *Repository) error {
  136. opType := ActionCreateRepo
  137. if repo.IsFork {
  138. opType = ActionForkRepo
  139. }
  140. return s.notifyWatchers(ctx,
  141. &Action{
  142. ActUserID: doer.ID,
  143. ActUserName: doer.Name,
  144. OpType: opType,
  145. RepoID: repo.ID,
  146. RepoUserName: owner.Name,
  147. RepoName: repo.Name,
  148. IsPrivate: repo.IsPrivate || repo.IsUnlisted,
  149. },
  150. )
  151. }
  152. // RenameRepo creates an action for renaming a repository.
  153. func (s *ActionsStore) RenameRepo(ctx context.Context, doer, owner *User, oldRepoName string, repo *Repository) error {
  154. return s.notifyWatchers(ctx,
  155. &Action{
  156. ActUserID: doer.ID,
  157. ActUserName: doer.Name,
  158. OpType: ActionRenameRepo,
  159. RepoID: repo.ID,
  160. RepoUserName: owner.Name,
  161. RepoName: repo.Name,
  162. IsPrivate: repo.IsPrivate || repo.IsUnlisted,
  163. Content: oldRepoName,
  164. },
  165. )
  166. }
  167. func (s *ActionsStore) mirrorSyncAction(ctx context.Context, opType ActionType, owner *User, repo *Repository, refName string, content []byte) error {
  168. return s.notifyWatchers(ctx,
  169. &Action{
  170. ActUserID: owner.ID,
  171. ActUserName: owner.Name,
  172. OpType: opType,
  173. Content: string(content),
  174. RepoID: repo.ID,
  175. RepoUserName: owner.Name,
  176. RepoName: repo.Name,
  177. RefName: refName,
  178. IsPrivate: repo.IsPrivate || repo.IsUnlisted,
  179. },
  180. )
  181. }
  182. type MirrorSyncPushOptions struct {
  183. Owner *User
  184. Repo *Repository
  185. RefName string
  186. OldCommitID string
  187. NewCommitID string
  188. Commits *PushCommits
  189. }
  190. // MirrorSyncPush creates an action for mirror synchronization of pushed
  191. // commits.
  192. func (s *ActionsStore) MirrorSyncPush(ctx context.Context, opts MirrorSyncPushOptions) error {
  193. if conf.UI.FeedMaxCommitNum > 0 && len(opts.Commits.Commits) > conf.UI.FeedMaxCommitNum {
  194. opts.Commits.Commits = opts.Commits.Commits[:conf.UI.FeedMaxCommitNum]
  195. }
  196. apiCommits, err := opts.Commits.APIFormat(ctx,
  197. newUsersStore(s.db),
  198. repoutil.RepositoryPath(opts.Owner.Name, opts.Repo.Name),
  199. repoutil.HTMLURL(opts.Owner.Name, opts.Repo.Name),
  200. )
  201. if err != nil {
  202. return errors.Wrap(err, "convert commits to API format")
  203. }
  204. opts.Commits.CompareURL = repoutil.CompareCommitsPath(opts.Owner.Name, opts.Repo.Name, opts.OldCommitID, opts.NewCommitID)
  205. apiPusher := opts.Owner.APIFormat()
  206. err = PrepareWebhooks(
  207. opts.Repo,
  208. HookEventTypePush,
  209. &api.PushPayload{
  210. Ref: opts.RefName,
  211. Before: opts.OldCommitID,
  212. After: opts.NewCommitID,
  213. CompareURL: conf.Server.ExternalURL + opts.Commits.CompareURL,
  214. Commits: apiCommits,
  215. Repo: opts.Repo.APIFormat(opts.Owner),
  216. Pusher: apiPusher,
  217. Sender: apiPusher,
  218. },
  219. )
  220. if err != nil {
  221. return errors.Wrap(err, "prepare webhooks")
  222. }
  223. data, err := jsoniter.Marshal(opts.Commits)
  224. if err != nil {
  225. return errors.Wrap(err, "marshal JSON")
  226. }
  227. return s.mirrorSyncAction(ctx, ActionMirrorSyncPush, opts.Owner, opts.Repo, opts.RefName, data)
  228. }
  229. // MirrorSyncCreate creates an action for mirror synchronization of a new
  230. // reference.
  231. func (s *ActionsStore) MirrorSyncCreate(ctx context.Context, owner *User, repo *Repository, refName string) error {
  232. return s.mirrorSyncAction(ctx, ActionMirrorSyncCreate, owner, repo, refName, nil)
  233. }
  234. // MirrorSyncDelete creates an action for mirror synchronization of a reference
  235. // deletion.
  236. func (s *ActionsStore) MirrorSyncDelete(ctx context.Context, owner *User, repo *Repository, refName string) error {
  237. return s.mirrorSyncAction(ctx, ActionMirrorSyncDelete, owner, repo, refName, nil)
  238. }
  239. // MergePullRequest creates an action for merging a pull request.
  240. func (s *ActionsStore) MergePullRequest(ctx context.Context, doer, owner *User, repo *Repository, pull *Issue) error {
  241. return s.notifyWatchers(ctx,
  242. &Action{
  243. ActUserID: doer.ID,
  244. ActUserName: doer.Name,
  245. OpType: ActionMergePullRequest,
  246. Content: fmt.Sprintf("%d|%s", pull.Index, pull.Title),
  247. RepoID: repo.ID,
  248. RepoUserName: owner.Name,
  249. RepoName: repo.Name,
  250. IsPrivate: repo.IsPrivate || repo.IsUnlisted,
  251. },
  252. )
  253. }
  254. // TransferRepo creates an action for transferring a repository to a new owner.
  255. func (s *ActionsStore) TransferRepo(ctx context.Context, doer, oldOwner, newOwner *User, repo *Repository) error {
  256. return s.notifyWatchers(ctx,
  257. &Action{
  258. ActUserID: doer.ID,
  259. ActUserName: doer.Name,
  260. OpType: ActionTransferRepo,
  261. RepoID: repo.ID,
  262. RepoUserName: newOwner.Name,
  263. RepoName: repo.Name,
  264. IsPrivate: repo.IsPrivate || repo.IsUnlisted,
  265. Content: oldOwner.Name + "/" + repo.Name,
  266. },
  267. )
  268. }
  269. var (
  270. // Same as GitHub, see https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue
  271. issueCloseKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"}
  272. issueReopenKeywords = []string{"reopen", "reopens", "reopened"}
  273. issueCloseKeywordsPattern = lazyregexp.New(assembleKeywordsPattern(issueCloseKeywords))
  274. issueReopenKeywordsPattern = lazyregexp.New(assembleKeywordsPattern(issueReopenKeywords))
  275. issueReferencePattern = lazyregexp.New(`(?i)(?:)(^| )\S*#\d+`)
  276. )
  277. func assembleKeywordsPattern(words []string) string {
  278. return fmt.Sprintf(`(?i)(?:%s) \S+`, strings.Join(words, "|"))
  279. }
  280. // updateCommitReferencesToIssues checks if issues are manipulated by commit message.
  281. func updateCommitReferencesToIssues(doer *User, repo *Repository, commits []*PushCommit) error {
  282. trimRightNonDigits := func(c rune) bool {
  283. return !unicode.IsDigit(c)
  284. }
  285. // Commits are appended in the reverse order.
  286. for i := len(commits) - 1; i >= 0; i-- {
  287. c := commits[i]
  288. refMarked := make(map[int64]bool)
  289. for _, ref := range issueReferencePattern.FindAllString(c.Message, -1) {
  290. ref = strings.TrimSpace(ref)
  291. ref = strings.TrimRightFunc(ref, trimRightNonDigits)
  292. if ref == "" {
  293. continue
  294. }
  295. // Add repo name if missing
  296. if ref[0] == '#' {
  297. ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
  298. } else if !strings.Contains(ref, "/") {
  299. // FIXME: We don't support User#ID syntax yet
  300. continue
  301. }
  302. issue, err := GetIssueByRef(ref)
  303. if err != nil {
  304. if IsErrIssueNotExist(err) {
  305. continue
  306. }
  307. return err
  308. }
  309. if refMarked[issue.ID] {
  310. continue
  311. }
  312. refMarked[issue.ID] = true
  313. msgLines := strings.Split(c.Message, "\n")
  314. shortMsg := msgLines[0]
  315. if len(msgLines) > 2 {
  316. shortMsg += "..."
  317. }
  318. message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, repo.Link(), c.Sha1, shortMsg)
  319. if err = CreateRefComment(doer, repo, issue, message, c.Sha1); err != nil {
  320. return err
  321. }
  322. }
  323. refMarked = make(map[int64]bool)
  324. // FIXME: Can merge this and the next for loop to a common function.
  325. for _, ref := range issueCloseKeywordsPattern.FindAllString(c.Message, -1) {
  326. ref = ref[strings.IndexByte(ref, byte(' '))+1:]
  327. ref = strings.TrimRightFunc(ref, trimRightNonDigits)
  328. if ref == "" {
  329. continue
  330. }
  331. // Add repo name if missing
  332. if ref[0] == '#' {
  333. ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
  334. } else if !strings.Contains(ref, "/") {
  335. // FIXME: We don't support User#ID syntax yet
  336. continue
  337. }
  338. issue, err := GetIssueByRef(ref)
  339. if err != nil {
  340. if IsErrIssueNotExist(err) {
  341. continue
  342. }
  343. return err
  344. }
  345. if refMarked[issue.ID] {
  346. continue
  347. }
  348. refMarked[issue.ID] = true
  349. if issue.RepoID != repo.ID || issue.IsClosed {
  350. continue
  351. }
  352. if err = issue.ChangeStatus(doer, repo, true); err != nil {
  353. return err
  354. }
  355. }
  356. // It is conflict to have close and reopen at same time, so refsMarkd doesn't need to reinit here.
  357. for _, ref := range issueReopenKeywordsPattern.FindAllString(c.Message, -1) {
  358. ref = ref[strings.IndexByte(ref, byte(' '))+1:]
  359. ref = strings.TrimRightFunc(ref, trimRightNonDigits)
  360. if ref == "" {
  361. continue
  362. }
  363. // Add repo name if missing
  364. if ref[0] == '#' {
  365. ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
  366. } else if !strings.Contains(ref, "/") {
  367. // We don't support User#ID syntax yet
  368. // return ErrNotImplemented
  369. continue
  370. }
  371. issue, err := GetIssueByRef(ref)
  372. if err != nil {
  373. if IsErrIssueNotExist(err) {
  374. continue
  375. }
  376. return err
  377. }
  378. if refMarked[issue.ID] {
  379. continue
  380. }
  381. refMarked[issue.ID] = true
  382. if issue.RepoID != repo.ID || !issue.IsClosed {
  383. continue
  384. }
  385. if err = issue.ChangeStatus(doer, repo, false); err != nil {
  386. return err
  387. }
  388. }
  389. }
  390. return nil
  391. }
  392. type CommitRepoOptions struct {
  393. Owner *User
  394. Repo *Repository
  395. PusherName string
  396. RefFullName string
  397. OldCommitID string
  398. NewCommitID string
  399. Commits *PushCommits
  400. }
  401. // CommitRepo creates actions for pushing commits to the repository. An action
  402. // with the type ActionDeleteBranch is created if the push deletes a branch; an
  403. // action with the type ActionCommitRepo is created for a regular push. If the
  404. // regular push also creates a new branch, then another action with type
  405. // ActionCreateBranch is created.
  406. func (s *ActionsStore) CommitRepo(ctx context.Context, opts CommitRepoOptions) error {
  407. err := newReposStore(s.db).Touch(ctx, opts.Repo.ID)
  408. if err != nil {
  409. return errors.Wrap(err, "touch repository")
  410. }
  411. pusher, err := newUsersStore(s.db).GetByUsername(ctx, opts.PusherName)
  412. if err != nil {
  413. return errors.Wrapf(err, "get pusher [name: %s]", opts.PusherName)
  414. }
  415. isNewRef := opts.OldCommitID == git.EmptyID
  416. isDelRef := opts.NewCommitID == git.EmptyID
  417. // If not the first commit, set the compare URL.
  418. if !isNewRef && !isDelRef {
  419. opts.Commits.CompareURL = repoutil.CompareCommitsPath(opts.Owner.Name, opts.Repo.Name, opts.OldCommitID, opts.NewCommitID)
  420. }
  421. refName := git.RefShortName(opts.RefFullName)
  422. action := &Action{
  423. ActUserID: pusher.ID,
  424. ActUserName: pusher.Name,
  425. RepoID: opts.Repo.ID,
  426. RepoUserName: opts.Owner.Name,
  427. RepoName: opts.Repo.Name,
  428. RefName: refName,
  429. IsPrivate: opts.Repo.IsPrivate || opts.Repo.IsUnlisted,
  430. }
  431. apiRepo := opts.Repo.APIFormat(opts.Owner)
  432. apiPusher := pusher.APIFormat()
  433. if isDelRef {
  434. err = PrepareWebhooks(
  435. opts.Repo,
  436. HookEventTypeDelete,
  437. &api.DeletePayload{
  438. Ref: refName,
  439. RefType: "branch",
  440. PusherType: api.PUSHER_TYPE_USER,
  441. Repo: apiRepo,
  442. Sender: apiPusher,
  443. },
  444. )
  445. if err != nil {
  446. return errors.Wrap(err, "prepare webhooks for delete branch")
  447. }
  448. action.OpType = ActionDeleteBranch
  449. err = s.notifyWatchers(ctx, action)
  450. if err != nil {
  451. return errors.Wrap(err, "notify watchers")
  452. }
  453. // Delete branch doesn't have anything to push or compare
  454. return nil
  455. }
  456. // Only update issues via commits when internal issue tracker is enabled
  457. if opts.Repo.EnableIssues && !opts.Repo.EnableExternalTracker {
  458. if err = updateCommitReferencesToIssues(pusher, opts.Repo, opts.Commits.Commits); err != nil {
  459. log.Error("update commit references to issues: %v", err)
  460. }
  461. }
  462. if conf.UI.FeedMaxCommitNum > 0 && len(opts.Commits.Commits) > conf.UI.FeedMaxCommitNum {
  463. opts.Commits.Commits = opts.Commits.Commits[:conf.UI.FeedMaxCommitNum]
  464. }
  465. data, err := jsoniter.Marshal(opts.Commits)
  466. if err != nil {
  467. return errors.Wrap(err, "marshal JSON")
  468. }
  469. action.Content = string(data)
  470. var compareURL string
  471. if isNewRef {
  472. err = PrepareWebhooks(
  473. opts.Repo,
  474. HookEventTypeCreate,
  475. &api.CreatePayload{
  476. Ref: refName,
  477. RefType: "branch",
  478. DefaultBranch: opts.Repo.DefaultBranch,
  479. Repo: apiRepo,
  480. Sender: apiPusher,
  481. },
  482. )
  483. if err != nil {
  484. return errors.Wrap(err, "prepare webhooks for new branch")
  485. }
  486. action.OpType = ActionCreateBranch
  487. err = s.notifyWatchers(ctx, action)
  488. if err != nil {
  489. return errors.Wrap(err, "notify watchers")
  490. }
  491. } else {
  492. compareURL = conf.Server.ExternalURL + opts.Commits.CompareURL
  493. }
  494. commits, err := opts.Commits.APIFormat(ctx,
  495. newUsersStore(s.db),
  496. repoutil.RepositoryPath(opts.Owner.Name, opts.Repo.Name),
  497. repoutil.HTMLURL(opts.Owner.Name, opts.Repo.Name),
  498. )
  499. if err != nil {
  500. return errors.Wrap(err, "convert commits to API format")
  501. }
  502. err = PrepareWebhooks(
  503. opts.Repo,
  504. HookEventTypePush,
  505. &api.PushPayload{
  506. Ref: opts.RefFullName,
  507. Before: opts.OldCommitID,
  508. After: opts.NewCommitID,
  509. CompareURL: compareURL,
  510. Commits: commits,
  511. Repo: apiRepo,
  512. Pusher: apiPusher,
  513. Sender: apiPusher,
  514. },
  515. )
  516. if err != nil {
  517. return errors.Wrap(err, "prepare webhooks for new commit")
  518. }
  519. action.OpType = ActionCommitRepo
  520. err = s.notifyWatchers(ctx, action)
  521. if err != nil {
  522. return errors.Wrap(err, "notify watchers")
  523. }
  524. return nil
  525. }
  526. type PushTagOptions struct {
  527. Owner *User
  528. Repo *Repository
  529. PusherName string
  530. RefFullName string
  531. NewCommitID string
  532. }
  533. // PushTag creates an action for pushing tags to the repository. An action with
  534. // the type ActionDeleteTag is created if the push deletes a tag. Otherwise, an
  535. // action with the type ActionPushTag is created for a regular push.
  536. func (s *ActionsStore) PushTag(ctx context.Context, opts PushTagOptions) error {
  537. err := newReposStore(s.db).Touch(ctx, opts.Repo.ID)
  538. if err != nil {
  539. return errors.Wrap(err, "touch repository")
  540. }
  541. pusher, err := newUsersStore(s.db).GetByUsername(ctx, opts.PusherName)
  542. if err != nil {
  543. return errors.Wrapf(err, "get pusher [name: %s]", opts.PusherName)
  544. }
  545. refName := git.RefShortName(opts.RefFullName)
  546. action := &Action{
  547. ActUserID: pusher.ID,
  548. ActUserName: pusher.Name,
  549. RepoID: opts.Repo.ID,
  550. RepoUserName: opts.Owner.Name,
  551. RepoName: opts.Repo.Name,
  552. RefName: refName,
  553. IsPrivate: opts.Repo.IsPrivate || opts.Repo.IsUnlisted,
  554. }
  555. apiRepo := opts.Repo.APIFormat(opts.Owner)
  556. apiPusher := pusher.APIFormat()
  557. if opts.NewCommitID == git.EmptyID {
  558. err = PrepareWebhooks(
  559. opts.Repo,
  560. HookEventTypeDelete,
  561. &api.DeletePayload{
  562. Ref: refName,
  563. RefType: "tag",
  564. PusherType: api.PUSHER_TYPE_USER,
  565. Repo: apiRepo,
  566. Sender: apiPusher,
  567. },
  568. )
  569. if err != nil {
  570. return errors.Wrap(err, "prepare webhooks for delete tag")
  571. }
  572. action.OpType = ActionDeleteTag
  573. err = s.notifyWatchers(ctx, action)
  574. if err != nil {
  575. return errors.Wrap(err, "notify watchers")
  576. }
  577. return nil
  578. }
  579. err = PrepareWebhooks(
  580. opts.Repo,
  581. HookEventTypeCreate,
  582. &api.CreatePayload{
  583. Ref: refName,
  584. RefType: "tag",
  585. Sha: opts.NewCommitID,
  586. DefaultBranch: opts.Repo.DefaultBranch,
  587. Repo: apiRepo,
  588. Sender: apiPusher,
  589. },
  590. )
  591. if err != nil {
  592. return errors.Wrapf(err, "prepare webhooks for new tag")
  593. }
  594. action.OpType = ActionPushTag
  595. err = s.notifyWatchers(ctx, action)
  596. if err != nil {
  597. return errors.Wrap(err, "notify watchers")
  598. }
  599. return nil
  600. }
  601. // ActionType is the type of action.
  602. type ActionType int
  603. // ⚠️ WARNING: Only append to the end of list to maintain backward compatibility.
  604. const (
  605. ActionCreateRepo ActionType = iota + 1 // 1
  606. ActionRenameRepo // 2
  607. ActionStarRepo // 3
  608. ActionWatchRepo // 4
  609. ActionCommitRepo // 5
  610. ActionCreateIssue // 6
  611. ActionCreatePullRequest // 7
  612. ActionTransferRepo // 8
  613. ActionPushTag // 9
  614. ActionCommentIssue // 10
  615. ActionMergePullRequest // 11
  616. ActionCloseIssue // 12
  617. ActionReopenIssue // 13
  618. ActionClosePullRequest // 14
  619. ActionReopenPullRequest // 15
  620. ActionCreateBranch // 16
  621. ActionDeleteBranch // 17
  622. ActionDeleteTag // 18
  623. ActionForkRepo // 19
  624. ActionMirrorSyncPush // 20
  625. ActionMirrorSyncCreate // 21
  626. ActionMirrorSyncDelete // 22
  627. )
  628. // Action is a user operation to a repository. It implements template.Actioner
  629. // interface to be able to use it in template rendering.
  630. type Action struct {
  631. ID int64 `gorm:"primaryKey"`
  632. UserID int64 `gorm:"index"` // Receiver user ID
  633. OpType ActionType
  634. ActUserID int64 // Doer user ID
  635. ActUserName string // Doer user name
  636. ActAvatar string `xorm:"-" gorm:"-" json:"-"`
  637. RepoID int64 `xorm:"INDEX" gorm:"index"`
  638. RepoUserName string
  639. RepoName string
  640. RefName string
  641. IsPrivate bool `xorm:"NOT NULL DEFAULT false" gorm:"not null;default:FALSE"`
  642. Content string `xorm:"TEXT"`
  643. Created time.Time `xorm:"-" gorm:"-" json:"-"`
  644. CreatedUnix int64
  645. }
  646. // BeforeCreate implements the GORM create hook.
  647. func (a *Action) BeforeCreate(tx *gorm.DB) error {
  648. if a.CreatedUnix <= 0 {
  649. a.CreatedUnix = tx.NowFunc().Unix()
  650. }
  651. return nil
  652. }
  653. // AfterFind implements the GORM query hook.
  654. func (a *Action) AfterFind(_ *gorm.DB) error {
  655. a.Created = time.Unix(a.CreatedUnix, 0).Local()
  656. return nil
  657. }
  658. func (a *Action) GetOpType() int {
  659. return int(a.OpType)
  660. }
  661. func (a *Action) GetActUserName() string {
  662. return a.ActUserName
  663. }
  664. func (a *Action) ShortActUserName() string {
  665. return strutil.Ellipsis(a.ActUserName, 20)
  666. }
  667. func (a *Action) GetRepoUserName() string {
  668. return a.RepoUserName
  669. }
  670. func (a *Action) ShortRepoUserName() string {
  671. return strutil.Ellipsis(a.RepoUserName, 20)
  672. }
  673. func (a *Action) GetRepoName() string {
  674. return a.RepoName
  675. }
  676. func (a *Action) ShortRepoName() string {
  677. return strutil.Ellipsis(a.RepoName, 33)
  678. }
  679. func (a *Action) GetRepoPath() string {
  680. return path.Join(a.RepoUserName, a.RepoName)
  681. }
  682. func (a *Action) ShortRepoPath() string {
  683. return path.Join(a.ShortRepoUserName(), a.ShortRepoName())
  684. }
  685. func (a *Action) GetRepoLink() string {
  686. if conf.Server.Subpath != "" {
  687. return path.Join(conf.Server.Subpath, a.GetRepoPath())
  688. }
  689. return "/" + a.GetRepoPath()
  690. }
  691. func (a *Action) GetBranch() string {
  692. return a.RefName
  693. }
  694. func (a *Action) GetContent() string {
  695. return a.Content
  696. }
  697. func (a *Action) GetCreate() time.Time {
  698. return a.Created
  699. }
  700. func (a *Action) GetIssueInfos() []string {
  701. return strings.SplitN(a.Content, "|", 2)
  702. }
  703. func (a *Action) GetIssueTitle() string {
  704. index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
  705. issue, err := GetIssueByIndex(a.RepoID, index)
  706. if err != nil {
  707. log.Error("Failed to get issue title [repo_id: %d, index: %d]: %v", a.RepoID, index, err)
  708. return "error getting issue"
  709. }
  710. return issue.Title
  711. }
  712. func (a *Action) GetIssueContent() string {
  713. index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
  714. issue, err := GetIssueByIndex(a.RepoID, index)
  715. if err != nil {
  716. log.Error("Failed to get issue content [repo_id: %d, index: %d]: %v", a.RepoID, index, err)
  717. return "error getting issue"
  718. }
  719. return issue.Content
  720. }
  721. // PushCommit contains information of a pushed commit.
  722. type PushCommit struct {
  723. Sha1 string
  724. Message string
  725. AuthorEmail string
  726. AuthorName string
  727. CommitterEmail string
  728. CommitterName string
  729. Timestamp time.Time
  730. }
  731. // PushCommits is a list of pushed commits.
  732. type PushCommits struct {
  733. Len int
  734. Commits []*PushCommit
  735. CompareURL string
  736. avatars map[string]string
  737. }
  738. // NewPushCommits returns a new PushCommits.
  739. func NewPushCommits() *PushCommits {
  740. return &PushCommits{
  741. avatars: make(map[string]string),
  742. }
  743. }
  744. func (pcs *PushCommits) APIFormat(ctx context.Context, usersStore *UsersStore, repoPath, repoURL string) ([]*api.PayloadCommit, error) {
  745. // NOTE: We cache query results in case there are many commits in a single push.
  746. usernameByEmail := make(map[string]string)
  747. getUsernameByEmail := func(email string) (string, error) {
  748. username, ok := usernameByEmail[email]
  749. if ok {
  750. return username, nil
  751. }
  752. user, err := usersStore.GetByEmail(ctx, email)
  753. if err != nil {
  754. if IsErrUserNotExist(err) {
  755. usernameByEmail[email] = ""
  756. return "", nil
  757. }
  758. return "", err
  759. }
  760. usernameByEmail[email] = user.Name
  761. return user.Name, nil
  762. }
  763. commits := make([]*api.PayloadCommit, len(pcs.Commits))
  764. for i, commit := range pcs.Commits {
  765. authorUsername, err := getUsernameByEmail(commit.AuthorEmail)
  766. if err != nil {
  767. return nil, errors.Wrap(err, "get author username")
  768. }
  769. committerUsername, err := getUsernameByEmail(commit.CommitterEmail)
  770. if err != nil {
  771. return nil, errors.Wrap(err, "get committer username")
  772. }
  773. nameStatus := &git.NameStatus{}
  774. if !testutil.InTest {
  775. nameStatus, err = git.ShowNameStatus(repoPath, commit.Sha1)
  776. if err != nil {
  777. return nil, errors.Wrapf(err, "show name status [commit_sha1: %s]", commit.Sha1)
  778. }
  779. }
  780. commits[i] = &api.PayloadCommit{
  781. ID: commit.Sha1,
  782. Message: commit.Message,
  783. URL: fmt.Sprintf("%s/commit/%s", repoURL, commit.Sha1),
  784. Author: &api.PayloadUser{
  785. Name: commit.AuthorName,
  786. Email: commit.AuthorEmail,
  787. UserName: authorUsername,
  788. },
  789. Committer: &api.PayloadUser{
  790. Name: commit.CommitterName,
  791. Email: commit.CommitterEmail,
  792. UserName: committerUsername,
  793. },
  794. Added: nameStatus.Added,
  795. Removed: nameStatus.Removed,
  796. Modified: nameStatus.Modified,
  797. Timestamp: commit.Timestamp,
  798. }
  799. }
  800. return commits, nil
  801. }
  802. // AvatarLink tries to match user in database with email in order to show custom
  803. // avatars, and falls back to general avatar link.
  804. //
  805. // FIXME: This method does not belong to PushCommits, should be a pure template
  806. // function.
  807. func (pcs *PushCommits) AvatarLink(email string) string {
  808. _, ok := pcs.avatars[email]
  809. if !ok {
  810. u, err := Handle.Users().GetByEmail(context.Background(), email)
  811. if err != nil {
  812. pcs.avatars[email] = tool.AvatarLink(email)
  813. if !IsErrUserNotExist(err) {
  814. log.Error("Failed to get user [email: %s]: %v", email, err)
  815. }
  816. } else {
  817. pcs.avatars[email] = u.AvatarURLPath()
  818. }
  819. }
  820. return pcs.avatars[email]
  821. }