code.gitea.io/gitea@v1.21.7/models/git/commit_status.go (about)

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