actions.go 27 KB

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