actions.go 27 KB

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