actions.go 26 KB

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