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