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