You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

commit_status.go 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. // Copyright 2017 Gitea. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package git
  4. import (
  5. "context"
  6. "crypto/sha1"
  7. "errors"
  8. "fmt"
  9. "net/url"
  10. "strconv"
  11. "strings"
  12. "time"
  13. asymkey_model "code.gitea.io/gitea/models/asymkey"
  14. "code.gitea.io/gitea/models/db"
  15. repo_model "code.gitea.io/gitea/models/repo"
  16. user_model "code.gitea.io/gitea/models/user"
  17. "code.gitea.io/gitea/modules/git"
  18. "code.gitea.io/gitea/modules/log"
  19. "code.gitea.io/gitea/modules/setting"
  20. api "code.gitea.io/gitea/modules/structs"
  21. "code.gitea.io/gitea/modules/timeutil"
  22. "code.gitea.io/gitea/modules/translation"
  23. "xorm.io/builder"
  24. )
  25. // CommitStatus holds a single Status of a single Commit
  26. type CommitStatus struct {
  27. ID int64 `xorm:"pk autoincr"`
  28. Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
  29. RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
  30. Repo *repo_model.Repository `xorm:"-"`
  31. State api.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"`
  32. SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"`
  33. TargetURL string `xorm:"TEXT"`
  34. Description string `xorm:"TEXT"`
  35. ContextHash string `xorm:"char(40) index"`
  36. Context string `xorm:"TEXT"`
  37. Creator *user_model.User `xorm:"-"`
  38. CreatorID int64
  39. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  40. UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
  41. }
  42. func init() {
  43. db.RegisterModel(new(CommitStatus))
  44. db.RegisterModel(new(CommitStatusIndex))
  45. }
  46. func postgresGetCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) {
  47. res, err := db.GetEngine(ctx).Query("INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+
  48. "VALUES (?,?,1) ON CONFLICT (repo_id, sha) DO UPDATE SET max_index = `commit_status_index`.max_index+1 RETURNING max_index",
  49. repoID, sha)
  50. if err != nil {
  51. return 0, err
  52. }
  53. if len(res) == 0 {
  54. return 0, db.ErrGetResourceIndexFailed
  55. }
  56. return strconv.ParseInt(string(res[0]["max_index"]), 10, 64)
  57. }
  58. func mysqlGetCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) {
  59. if _, err := db.GetEngine(ctx).Exec("INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+
  60. "VALUES (?,?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1",
  61. repoID, sha); err != nil {
  62. return 0, err
  63. }
  64. var idx int64
  65. _, err := db.GetEngine(ctx).SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ?",
  66. repoID, sha).Get(&idx)
  67. if err != nil {
  68. return 0, err
  69. }
  70. if idx == 0 {
  71. return 0, errors.New("cannot get the correct index")
  72. }
  73. return idx, nil
  74. }
  75. func mssqlGetCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) {
  76. if _, err := db.GetEngine(ctx).Exec(`
  77. MERGE INTO commit_status_index WITH (HOLDLOCK) AS target
  78. USING (SELECT ? AS repo_id, ? AS sha) AS source
  79. (repo_id, sha)
  80. ON target.repo_id = source.repo_id AND target.sha = source.sha
  81. WHEN MATCHED
  82. THEN UPDATE
  83. SET max_index = max_index + 1
  84. WHEN NOT MATCHED
  85. THEN INSERT (repo_id, sha, max_index)
  86. VALUES (?, ?, 1);
  87. `, repoID, sha, repoID, sha); err != nil {
  88. return 0, err
  89. }
  90. var idx int64
  91. _, err := db.GetEngine(ctx).SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ?",
  92. repoID, sha).Get(&idx)
  93. if err != nil {
  94. return 0, err
  95. }
  96. if idx == 0 {
  97. return 0, errors.New("cannot get the correct index")
  98. }
  99. return idx, nil
  100. }
  101. // GetNextCommitStatusIndex retried 3 times to generate a resource index
  102. func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) {
  103. if !git.IsValidSHAPattern(sha) {
  104. return 0, git.ErrInvalidSHA{SHA: sha}
  105. }
  106. switch {
  107. case setting.Database.Type.IsPostgreSQL():
  108. return postgresGetCommitStatusIndex(ctx, repoID, sha)
  109. case setting.Database.Type.IsMySQL():
  110. return mysqlGetCommitStatusIndex(ctx, repoID, sha)
  111. case setting.Database.Type.IsMSSQL():
  112. return mssqlGetCommitStatusIndex(ctx, repoID, sha)
  113. }
  114. e := db.GetEngine(ctx)
  115. // try to update the max_index to next value, and acquire the write-lock for the record
  116. res, err := e.Exec("UPDATE `commit_status_index` SET max_index=max_index+1 WHERE repo_id=? AND sha=?", repoID, sha)
  117. if err != nil {
  118. return 0, fmt.Errorf("update failed: %w", err)
  119. }
  120. affected, err := res.RowsAffected()
  121. if err != nil {
  122. return 0, err
  123. }
  124. if affected == 0 {
  125. // this slow path is only for the first time of creating a resource index
  126. _, errIns := e.Exec("INSERT INTO `commit_status_index` (repo_id, sha, max_index) VALUES (?, ?, 0)", repoID, sha)
  127. res, err = e.Exec("UPDATE `commit_status_index` SET max_index=max_index+1 WHERE repo_id=? AND sha=?", repoID, sha)
  128. if err != nil {
  129. return 0, fmt.Errorf("update2 failed: %w", err)
  130. }
  131. affected, err = res.RowsAffected()
  132. if err != nil {
  133. return 0, fmt.Errorf("RowsAffected failed: %w", err)
  134. }
  135. // if the update still can not update any records, the record must not exist and there must be some errors (insert error)
  136. if affected == 0 {
  137. if errIns == nil {
  138. return 0, errors.New("impossible error when GetNextCommitStatusIndex, insert and update both succeeded but no record is updated")
  139. }
  140. return 0, fmt.Errorf("insert failed: %w", errIns)
  141. }
  142. }
  143. // now, the new index is in database (protected by the transaction and write-lock)
  144. var newIdx int64
  145. has, err := e.SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id=? AND sha=?", repoID, sha).Get(&newIdx)
  146. if err != nil {
  147. return 0, fmt.Errorf("select failed: %w", err)
  148. }
  149. if !has {
  150. return 0, errors.New("impossible error when GetNextCommitStatusIndex, upsert succeeded but no record can be selected")
  151. }
  152. return newIdx, nil
  153. }
  154. func (status *CommitStatus) loadAttributes(ctx context.Context) (err error) {
  155. if status.Repo == nil {
  156. status.Repo, err = repo_model.GetRepositoryByID(ctx, status.RepoID)
  157. if err != nil {
  158. return fmt.Errorf("getRepositoryByID [%d]: %w", status.RepoID, err)
  159. }
  160. }
  161. if status.Creator == nil && status.CreatorID > 0 {
  162. status.Creator, err = user_model.GetUserByID(ctx, status.CreatorID)
  163. if err != nil {
  164. return fmt.Errorf("getUserByID [%d]: %w", status.CreatorID, err)
  165. }
  166. }
  167. return nil
  168. }
  169. // APIURL returns the absolute APIURL to this commit-status.
  170. func (status *CommitStatus) APIURL(ctx context.Context) string {
  171. _ = status.loadAttributes(ctx)
  172. return status.Repo.APIURL() + "/statuses/" + url.PathEscape(status.SHA)
  173. }
  174. // LocaleString returns the locale string name of the Status
  175. func (status *CommitStatus) LocaleString(lang translation.Locale) string {
  176. return lang.Tr("repo.commitstatus." + status.State.String())
  177. }
  178. // CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc
  179. func CalcCommitStatus(statuses []*CommitStatus) *CommitStatus {
  180. var lastStatus *CommitStatus
  181. state := api.CommitStatusSuccess
  182. for _, status := range statuses {
  183. if status.State.NoBetterThan(state) {
  184. state = status.State
  185. lastStatus = status
  186. }
  187. }
  188. if lastStatus == nil {
  189. if len(statuses) > 0 {
  190. lastStatus = statuses[0]
  191. } else {
  192. lastStatus = &CommitStatus{}
  193. }
  194. }
  195. return lastStatus
  196. }
  197. // CommitStatusOptions holds the options for query commit statuses
  198. type CommitStatusOptions struct {
  199. db.ListOptions
  200. RepoID int64
  201. SHA string
  202. State string
  203. SortType string
  204. }
  205. func (opts *CommitStatusOptions) ToConds() builder.Cond {
  206. var cond builder.Cond = builder.Eq{
  207. "repo_id": opts.RepoID,
  208. "sha": opts.SHA,
  209. }
  210. switch opts.State {
  211. case "pending", "success", "error", "failure", "warning":
  212. cond = cond.And(builder.Eq{
  213. "state": opts.State,
  214. })
  215. }
  216. return cond
  217. }
  218. func (opts *CommitStatusOptions) ToOrders() string {
  219. switch opts.SortType {
  220. case "oldest":
  221. return "created_unix ASC"
  222. case "recentupdate":
  223. return "updated_unix DESC"
  224. case "leastupdate":
  225. return "updated_unix ASC"
  226. case "leastindex":
  227. return "`index` DESC"
  228. case "highestindex":
  229. return "`index` ASC"
  230. default:
  231. return "created_unix DESC"
  232. }
  233. }
  234. // GetCommitStatuses returns all statuses for a given commit.
  235. func GetCommitStatuses(ctx context.Context, opts *CommitStatusOptions) ([]*CommitStatus, int64, error) {
  236. sess := db.GetEngine(ctx).
  237. Where(opts.ToConds()).
  238. OrderBy(opts.ToOrders())
  239. db.SetSessionPagination(sess, opts)
  240. statuses := make([]*CommitStatus, 0, opts.PageSize)
  241. count, err := sess.FindAndCount(&statuses)
  242. return statuses, count, err
  243. }
  244. // CommitStatusIndex represents a table for commit status index
  245. type CommitStatusIndex struct {
  246. ID int64
  247. RepoID int64 `xorm:"unique(repo_sha)"`
  248. SHA string `xorm:"unique(repo_sha)"`
  249. MaxIndex int64 `xorm:"index"`
  250. }
  251. // GetLatestCommitStatus returns all statuses with a unique context for a given commit.
  252. func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOptions db.ListOptions) ([]*CommitStatus, int64, error) {
  253. ids := make([]int64, 0, 10)
  254. sess := db.GetEngine(ctx).Table(&CommitStatus{}).
  255. Where("repo_id = ?", repoID).And("sha = ?", sha).
  256. Select("max( id ) as id").
  257. GroupBy("context_hash").OrderBy("max( id ) desc")
  258. if !listOptions.IsListAll() {
  259. sess = db.SetSessionPagination(sess, &listOptions)
  260. }
  261. count, err := sess.FindAndCount(&ids)
  262. if err != nil {
  263. return nil, count, err
  264. }
  265. statuses := make([]*CommitStatus, 0, len(ids))
  266. if len(ids) == 0 {
  267. return statuses, count, nil
  268. }
  269. return statuses, count, db.GetEngine(ctx).In("id", ids).Find(&statuses)
  270. }
  271. // GetLatestCommitStatusForPairs returns all statuses with a unique context for a given list of repo-sha pairs
  272. func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHAs map[int64]string, listOptions db.ListOptions) (map[int64][]*CommitStatus, error) {
  273. type result struct {
  274. ID int64
  275. RepoID int64
  276. }
  277. results := make([]result, 0, len(repoIDsToLatestCommitSHAs))
  278. sess := db.GetEngine(ctx).Table(&CommitStatus{})
  279. // Create a disjunction of conditions for each repoID and SHA pair
  280. conds := make([]builder.Cond, 0, len(repoIDsToLatestCommitSHAs))
  281. for repoID, sha := range repoIDsToLatestCommitSHAs {
  282. conds = append(conds, builder.Eq{"repo_id": repoID, "sha": sha})
  283. }
  284. sess = sess.Where(builder.Or(conds...)).
  285. Select("max( id ) as id, repo_id").
  286. GroupBy("context_hash, repo_id").OrderBy("max( id ) desc")
  287. sess = db.SetSessionPagination(sess, &listOptions)
  288. err := sess.Find(&results)
  289. if err != nil {
  290. return nil, err
  291. }
  292. ids := make([]int64, 0, len(results))
  293. repoStatuses := make(map[int64][]*CommitStatus)
  294. for _, result := range results {
  295. ids = append(ids, result.ID)
  296. }
  297. statuses := make([]*CommitStatus, 0, len(ids))
  298. if len(ids) > 0 {
  299. err = db.GetEngine(ctx).In("id", ids).Find(&statuses)
  300. if err != nil {
  301. return nil, err
  302. }
  303. // Group the statuses by repo ID
  304. for _, status := range statuses {
  305. repoStatuses[status.RepoID] = append(repoStatuses[status.RepoID], status)
  306. }
  307. }
  308. return repoStatuses, nil
  309. }
  310. // GetLatestCommitStatusForRepoCommitIDs returns all statuses with a unique context for a given list of repo-sha pairs
  311. func GetLatestCommitStatusForRepoCommitIDs(ctx context.Context, repoID int64, commitIDs []string) (map[string][]*CommitStatus, error) {
  312. type result struct {
  313. ID int64
  314. Sha string
  315. }
  316. results := make([]result, 0, len(commitIDs))
  317. sess := db.GetEngine(ctx).Table(&CommitStatus{})
  318. // Create a disjunction of conditions for each repoID and SHA pair
  319. conds := make([]builder.Cond, 0, len(commitIDs))
  320. for _, sha := range commitIDs {
  321. conds = append(conds, builder.Eq{"sha": sha})
  322. }
  323. sess = sess.Where(builder.Eq{"repo_id": repoID}.And(builder.Or(conds...))).
  324. Select("max( id ) as id, sha").
  325. GroupBy("context_hash, sha").OrderBy("max( id ) desc")
  326. err := sess.Find(&results)
  327. if err != nil {
  328. return nil, err
  329. }
  330. ids := make([]int64, 0, len(results))
  331. repoStatuses := make(map[string][]*CommitStatus)
  332. for _, result := range results {
  333. ids = append(ids, result.ID)
  334. }
  335. statuses := make([]*CommitStatus, 0, len(ids))
  336. if len(ids) > 0 {
  337. err = db.GetEngine(ctx).In("id", ids).Find(&statuses)
  338. if err != nil {
  339. return nil, err
  340. }
  341. // Group the statuses by repo ID
  342. for _, status := range statuses {
  343. repoStatuses[status.SHA] = append(repoStatuses[status.SHA], status)
  344. }
  345. }
  346. return repoStatuses, nil
  347. }
  348. // FindRepoRecentCommitStatusContexts returns repository's recent commit status contexts
  349. func FindRepoRecentCommitStatusContexts(ctx context.Context, repoID int64, before time.Duration) ([]string, error) {
  350. start := timeutil.TimeStampNow().AddDuration(-before)
  351. ids := make([]int64, 0, 10)
  352. if err := db.GetEngine(ctx).Table("commit_status").
  353. Where("repo_id = ?", repoID).
  354. And("updated_unix >= ?", start).
  355. Select("max( id ) as id").
  356. GroupBy("context_hash").OrderBy("max( id ) desc").
  357. Find(&ids); err != nil {
  358. return nil, err
  359. }
  360. contexts := make([]string, 0, len(ids))
  361. if len(ids) == 0 {
  362. return contexts, nil
  363. }
  364. return contexts, db.GetEngine(ctx).Select("context").Table("commit_status").In("id", ids).Find(&contexts)
  365. }
  366. // NewCommitStatusOptions holds options for creating a CommitStatus
  367. type NewCommitStatusOptions struct {
  368. Repo *repo_model.Repository
  369. Creator *user_model.User
  370. SHA string
  371. CommitStatus *CommitStatus
  372. }
  373. // NewCommitStatus save commit statuses into database
  374. func NewCommitStatus(ctx context.Context, opts NewCommitStatusOptions) error {
  375. if opts.Repo == nil {
  376. return fmt.Errorf("NewCommitStatus[nil, %s]: no repository specified", opts.SHA)
  377. }
  378. repoPath := opts.Repo.RepoPath()
  379. if opts.Creator == nil {
  380. return fmt.Errorf("NewCommitStatus[%s, %s]: no user specified", repoPath, opts.SHA)
  381. }
  382. if _, err := git.NewIDFromString(opts.SHA); err != nil {
  383. return fmt.Errorf("NewCommitStatus[%s, %s]: invalid sha: %w", repoPath, opts.SHA, err)
  384. }
  385. ctx, committer, err := db.TxContext(ctx)
  386. if err != nil {
  387. return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", opts.Repo.ID, opts.Creator.ID, opts.SHA, err)
  388. }
  389. defer committer.Close()
  390. // Get the next Status Index
  391. idx, err := GetNextCommitStatusIndex(ctx, opts.Repo.ID, opts.SHA)
  392. if err != nil {
  393. return fmt.Errorf("generate commit status index failed: %w", err)
  394. }
  395. opts.CommitStatus.Description = strings.TrimSpace(opts.CommitStatus.Description)
  396. opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context)
  397. opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL)
  398. opts.CommitStatus.SHA = opts.SHA
  399. opts.CommitStatus.CreatorID = opts.Creator.ID
  400. opts.CommitStatus.RepoID = opts.Repo.ID
  401. opts.CommitStatus.Index = idx
  402. log.Debug("NewCommitStatus[%s, %s]: %d", repoPath, opts.SHA, opts.CommitStatus.Index)
  403. opts.CommitStatus.ContextHash = hashCommitStatusContext(opts.CommitStatus.Context)
  404. // Insert new CommitStatus
  405. if _, err = db.GetEngine(ctx).Insert(opts.CommitStatus); err != nil {
  406. return fmt.Errorf("insert CommitStatus[%s, %s]: %w", repoPath, opts.SHA, err)
  407. }
  408. return committer.Commit()
  409. }
  410. // SignCommitWithStatuses represents a commit with validation of signature and status state.
  411. type SignCommitWithStatuses struct {
  412. Status *CommitStatus
  413. Statuses []*CommitStatus
  414. *asymkey_model.SignCommit
  415. }
  416. // ParseCommitsWithStatus checks commits latest statuses and calculates its worst status state
  417. func ParseCommitsWithStatus(ctx context.Context, oldCommits []*asymkey_model.SignCommit, repo *repo_model.Repository) []*SignCommitWithStatuses {
  418. newCommits := make([]*SignCommitWithStatuses, 0, len(oldCommits))
  419. for _, c := range oldCommits {
  420. commit := &SignCommitWithStatuses{
  421. SignCommit: c,
  422. }
  423. statuses, _, err := GetLatestCommitStatus(ctx, repo.ID, commit.ID.String(), db.ListOptions{})
  424. if err != nil {
  425. log.Error("GetLatestCommitStatus: %v", err)
  426. } else {
  427. commit.Statuses = statuses
  428. commit.Status = CalcCommitStatus(statuses)
  429. }
  430. newCommits = append(newCommits, commit)
  431. }
  432. return newCommits
  433. }
  434. // hashCommitStatusContext hash context
  435. func hashCommitStatusContext(context string) string {
  436. return fmt.Sprintf("%x", sha1.Sum([]byte(context)))
  437. }
  438. // ConvertFromGitCommit converts git commits into SignCommitWithStatuses
  439. func ConvertFromGitCommit(ctx context.Context, commits []*git.Commit, repo *repo_model.Repository) []*SignCommitWithStatuses {
  440. return ParseCommitsWithStatus(ctx,
  441. asymkey_model.ParseCommitsWithSignature(
  442. ctx,
  443. user_model.ValidateCommitsWithEmails(ctx, commits),
  444. repo.GetTrustModel(),
  445. func(user *user_model.User) (bool, error) {
  446. return repo_model.IsOwnerMemberCollaborator(ctx, repo, user.ID)
  447. },
  448. ),
  449. repo,
  450. )
  451. }