code.gitea.io/gitea@v1.21.7/services/repository/push.go (about)

     1  // Copyright 2020 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package repository
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"strings"
    11  	"time"
    12  
    13  	"code.gitea.io/gitea/models/db"
    14  	git_model "code.gitea.io/gitea/models/git"
    15  	repo_model "code.gitea.io/gitea/models/repo"
    16  	user_model "code.gitea.io/gitea/models/user"
    17  	"code.gitea.io/gitea/modules/cache"
    18  	"code.gitea.io/gitea/modules/git"
    19  	"code.gitea.io/gitea/modules/graceful"
    20  	"code.gitea.io/gitea/modules/log"
    21  	"code.gitea.io/gitea/modules/process"
    22  	"code.gitea.io/gitea/modules/queue"
    23  	repo_module "code.gitea.io/gitea/modules/repository"
    24  	"code.gitea.io/gitea/modules/setting"
    25  	"code.gitea.io/gitea/modules/timeutil"
    26  	issue_service "code.gitea.io/gitea/services/issue"
    27  	notify_service "code.gitea.io/gitea/services/notify"
    28  	pull_service "code.gitea.io/gitea/services/pull"
    29  )
    30  
    31  // pushQueue represents a queue to handle update pull request tests
    32  var pushQueue *queue.WorkerPoolQueue[[]*repo_module.PushUpdateOptions]
    33  
    34  // handle passed PR IDs and test the PRs
    35  func handler(items ...[]*repo_module.PushUpdateOptions) [][]*repo_module.PushUpdateOptions {
    36  	for _, opts := range items {
    37  		if err := pushUpdates(opts); err != nil {
    38  			log.Error("pushUpdate failed: %v", err)
    39  		}
    40  	}
    41  	return nil
    42  }
    43  
    44  func initPushQueue() error {
    45  	pushQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "push_update", handler)
    46  	if pushQueue == nil {
    47  		return errors.New("unable to create push_update queue")
    48  	}
    49  	go graceful.GetManager().RunWithCancel(pushQueue)
    50  	return nil
    51  }
    52  
    53  // PushUpdate is an alias of PushUpdates for single push update options
    54  func PushUpdate(opts *repo_module.PushUpdateOptions) error {
    55  	return PushUpdates([]*repo_module.PushUpdateOptions{opts})
    56  }
    57  
    58  // PushUpdates adds a push update to push queue
    59  func PushUpdates(opts []*repo_module.PushUpdateOptions) error {
    60  	if len(opts) == 0 {
    61  		return nil
    62  	}
    63  
    64  	for _, opt := range opts {
    65  		if opt.IsNewRef() && opt.IsDelRef() {
    66  			return fmt.Errorf("Old and new revisions are both %s", git.EmptySHA)
    67  		}
    68  	}
    69  
    70  	return pushQueue.Push(opts)
    71  }
    72  
    73  // pushUpdates generates push action history feeds for push updating multiple refs
    74  func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
    75  	if len(optsList) == 0 {
    76  		return nil
    77  	}
    78  
    79  	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("PushUpdates: %s/%s", optsList[0].RepoUserName, optsList[0].RepoName))
    80  	defer finished()
    81  
    82  	repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, optsList[0].RepoUserName, optsList[0].RepoName)
    83  	if err != nil {
    84  		return fmt.Errorf("GetRepositoryByOwnerAndName failed: %w", err)
    85  	}
    86  
    87  	repoPath := repo.RepoPath()
    88  
    89  	gitRepo, err := git.OpenRepository(ctx, repoPath)
    90  	if err != nil {
    91  		return fmt.Errorf("OpenRepository[%s]: %w", repoPath, err)
    92  	}
    93  	defer gitRepo.Close()
    94  
    95  	if err = repo_module.UpdateRepoSize(ctx, repo); err != nil {
    96  		return fmt.Errorf("Failed to update size for repository: %v", err)
    97  	}
    98  
    99  	addTags := make([]string, 0, len(optsList))
   100  	delTags := make([]string, 0, len(optsList))
   101  	var pusher *user_model.User
   102  
   103  	for _, opts := range optsList {
   104  		log.Trace("pushUpdates: %-v %s %s %s", repo, opts.OldCommitID, opts.NewCommitID, opts.RefFullName)
   105  
   106  		if opts.IsNewRef() && opts.IsDelRef() {
   107  			return fmt.Errorf("old and new revisions are both %s", git.EmptySHA)
   108  		}
   109  		if opts.RefFullName.IsTag() {
   110  			if pusher == nil || pusher.ID != opts.PusherID {
   111  				if opts.PusherID == user_model.ActionsUserID {
   112  					pusher = user_model.NewActionsUser()
   113  				} else {
   114  					var err error
   115  					if pusher, err = user_model.GetUserByID(ctx, opts.PusherID); err != nil {
   116  						return err
   117  					}
   118  				}
   119  			}
   120  			tagName := opts.RefFullName.TagName()
   121  			if opts.IsDelRef() {
   122  				notify_service.PushCommits(
   123  					ctx, pusher, repo,
   124  					&repo_module.PushUpdateOptions{
   125  						RefFullName: git.RefNameFromTag(tagName),
   126  						OldCommitID: opts.OldCommitID,
   127  						NewCommitID: git.EmptySHA,
   128  					}, repo_module.NewPushCommits())
   129  
   130  				delTags = append(delTags, tagName)
   131  				notify_service.DeleteRef(ctx, pusher, repo, opts.RefFullName)
   132  			} else { // is new tag
   133  				newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
   134  				if err != nil {
   135  					return fmt.Errorf("gitRepo.GetCommit(%s) in %s/%s[%d]: %w", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err)
   136  				}
   137  
   138  				commits := repo_module.NewPushCommits()
   139  				commits.HeadCommit = repo_module.CommitToPushCommit(newCommit)
   140  				commits.CompareURL = repo.ComposeCompareURL(git.EmptySHA, opts.NewCommitID)
   141  
   142  				notify_service.PushCommits(
   143  					ctx, pusher, repo,
   144  					&repo_module.PushUpdateOptions{
   145  						RefFullName: opts.RefFullName,
   146  						OldCommitID: git.EmptySHA,
   147  						NewCommitID: opts.NewCommitID,
   148  					}, commits)
   149  
   150  				addTags = append(addTags, tagName)
   151  				notify_service.CreateRef(ctx, pusher, repo, opts.RefFullName, opts.NewCommitID)
   152  			}
   153  		} else if opts.RefFullName.IsBranch() {
   154  			if pusher == nil || pusher.ID != opts.PusherID {
   155  				if opts.PusherID == user_model.ActionsUserID {
   156  					pusher = user_model.NewActionsUser()
   157  				} else {
   158  					var err error
   159  					if pusher, err = user_model.GetUserByID(ctx, opts.PusherID); err != nil {
   160  						return err
   161  					}
   162  				}
   163  			}
   164  
   165  			branch := opts.RefFullName.BranchName()
   166  			if !opts.IsDelRef() {
   167  				log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name)
   168  				go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, opts.OldCommitID, opts.NewCommitID)
   169  
   170  				newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
   171  				if err != nil {
   172  					return fmt.Errorf("gitRepo.GetCommit(%s) in %s/%s[%d]: %w", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err)
   173  				}
   174  
   175  				refName := opts.RefName()
   176  
   177  				// Push new branch.
   178  				var l []*git.Commit
   179  				if opts.IsNewRef() {
   180  					if repo.IsEmpty { // Change default branch and empty status only if pushed ref is non-empty branch.
   181  						repo.DefaultBranch = refName
   182  						repo.IsEmpty = false
   183  						if repo.DefaultBranch != setting.Repository.DefaultBranch {
   184  							if err := gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
   185  								if !git.IsErrUnsupportedVersion(err) {
   186  									return err
   187  								}
   188  							}
   189  						}
   190  						// Update the is empty and default_branch columns
   191  						if err := repo_model.UpdateRepositoryCols(ctx, repo, "default_branch", "is_empty"); err != nil {
   192  							return fmt.Errorf("UpdateRepositoryCols: %w", err)
   193  						}
   194  					}
   195  
   196  					l, err = newCommit.CommitsBeforeLimit(10)
   197  					if err != nil {
   198  						return fmt.Errorf("newCommit.CommitsBeforeLimit: %w", err)
   199  					}
   200  					notify_service.CreateRef(ctx, pusher, repo, opts.RefFullName, opts.NewCommitID)
   201  				} else {
   202  					l, err = newCommit.CommitsBeforeUntil(opts.OldCommitID)
   203  					if err != nil {
   204  						return fmt.Errorf("newCommit.CommitsBeforeUntil: %w", err)
   205  					}
   206  
   207  					isForcePush, err := newCommit.IsForcePush(opts.OldCommitID)
   208  					if err != nil {
   209  						log.Error("IsForcePush %s:%s failed: %v", repo.FullName(), branch, err)
   210  					}
   211  
   212  					if isForcePush {
   213  						log.Trace("Push %s is a force push", opts.NewCommitID)
   214  
   215  						cache.Remove(repo.GetCommitsCountCacheKey(opts.RefName(), true))
   216  					} else {
   217  						// TODO: increment update the commit count cache but not remove
   218  						cache.Remove(repo.GetCommitsCountCacheKey(opts.RefName(), true))
   219  					}
   220  				}
   221  
   222  				commits := repo_module.GitToPushCommits(l)
   223  				commits.HeadCommit = repo_module.CommitToPushCommit(newCommit)
   224  
   225  				if err := issue_service.UpdateIssuesCommit(ctx, pusher, repo, commits.Commits, refName); err != nil {
   226  					log.Error("updateIssuesCommit: %v", err)
   227  				}
   228  
   229  				oldCommitID := opts.OldCommitID
   230  				if oldCommitID == git.EmptySHA && len(commits.Commits) > 0 {
   231  					oldCommit, err := gitRepo.GetCommit(commits.Commits[len(commits.Commits)-1].Sha1)
   232  					if err != nil && !git.IsErrNotExist(err) {
   233  						log.Error("unable to GetCommit %s from %-v: %v", oldCommitID, repo, err)
   234  					}
   235  					if oldCommit != nil {
   236  						for i := 0; i < oldCommit.ParentCount(); i++ {
   237  							commitID, _ := oldCommit.ParentID(i)
   238  							if !commitID.IsZero() {
   239  								oldCommitID = commitID.String()
   240  								break
   241  							}
   242  						}
   243  					}
   244  				}
   245  
   246  				if oldCommitID == git.EmptySHA && repo.DefaultBranch != branch {
   247  					oldCommitID = repo.DefaultBranch
   248  				}
   249  
   250  				if oldCommitID != git.EmptySHA {
   251  					commits.CompareURL = repo.ComposeCompareURL(oldCommitID, opts.NewCommitID)
   252  				} else {
   253  					commits.CompareURL = ""
   254  				}
   255  
   256  				if len(commits.Commits) > setting.UI.FeedMaxCommitNum {
   257  					commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum]
   258  				}
   259  
   260  				if err = syncBranchToDB(ctx, repo.ID, opts.PusherID, branch, newCommit); err != nil {
   261  					return fmt.Errorf("git_model.UpdateBranch %s:%s failed: %v", repo.FullName(), branch, err)
   262  				}
   263  
   264  				notify_service.PushCommits(ctx, pusher, repo, opts, commits)
   265  
   266  				// Cache for big repository
   267  				if err := CacheRef(graceful.GetManager().HammerContext(), repo, gitRepo, opts.RefFullName); err != nil {
   268  					log.Error("repo_module.CacheRef %s/%s failed: %v", repo.ID, branch, err)
   269  				}
   270  			} else {
   271  				notify_service.DeleteRef(ctx, pusher, repo, opts.RefFullName)
   272  				if err = pull_service.CloseBranchPulls(ctx, pusher, repo.ID, branch); err != nil {
   273  					// close all related pulls
   274  					log.Error("close related pull request failed: %v", err)
   275  				}
   276  
   277  				if err := git_model.AddDeletedBranch(ctx, repo.ID, branch, pusher.ID); err != nil {
   278  					return fmt.Errorf("AddDeletedBranch %s:%s failed: %v", repo.FullName(), branch, err)
   279  				}
   280  			}
   281  
   282  			// Even if user delete a branch on a repository which he didn't watch, he will be watch that.
   283  			if err = repo_model.WatchIfAuto(ctx, opts.PusherID, repo.ID, true); err != nil {
   284  				log.Warn("Fail to perform auto watch on user %v for repo %v: %v", opts.PusherID, repo.ID, err)
   285  			}
   286  		} else {
   287  			log.Trace("Non-tag and non-branch commits pushed.")
   288  		}
   289  	}
   290  	if err := PushUpdateAddDeleteTags(ctx, repo, gitRepo, addTags, delTags); err != nil {
   291  		return fmt.Errorf("PushUpdateAddDeleteTags: %w", err)
   292  	}
   293  
   294  	// Change repository last updated time.
   295  	if err := repo_model.UpdateRepositoryUpdatedTime(ctx, repo.ID, time.Now()); err != nil {
   296  		return fmt.Errorf("UpdateRepositoryUpdatedTime: %w", err)
   297  	}
   298  
   299  	return nil
   300  }
   301  
   302  // PushUpdateAddDeleteTags updates a number of added and delete tags
   303  func PushUpdateAddDeleteTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, addTags, delTags []string) error {
   304  	return db.WithTx(ctx, func(ctx context.Context) error {
   305  		if err := repo_model.PushUpdateDeleteTagsContext(ctx, repo, delTags); err != nil {
   306  			return err
   307  		}
   308  		return pushUpdateAddTags(ctx, repo, gitRepo, addTags)
   309  	})
   310  }
   311  
   312  // pushUpdateAddTags updates a number of add tags
   313  func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, tags []string) error {
   314  	if len(tags) == 0 {
   315  		return nil
   316  	}
   317  
   318  	releases, err := repo_model.GetReleasesByRepoIDAndNames(ctx, repo.ID, tags)
   319  	if err != nil {
   320  		return fmt.Errorf("GetReleasesByRepoIDAndNames: %w", err)
   321  	}
   322  	relMap := make(map[string]*repo_model.Release)
   323  	for _, rel := range releases {
   324  		relMap[rel.LowerTagName] = rel
   325  	}
   326  
   327  	lowerTags := make([]string, 0, len(tags))
   328  	for _, tag := range tags {
   329  		lowerTags = append(lowerTags, strings.ToLower(tag))
   330  	}
   331  
   332  	newReleases := make([]*repo_model.Release, 0, len(lowerTags)-len(relMap))
   333  
   334  	emailToUser := make(map[string]*user_model.User)
   335  
   336  	for i, lowerTag := range lowerTags {
   337  		tag, err := gitRepo.GetTag(tags[i])
   338  		if err != nil {
   339  			return fmt.Errorf("GetTag: %w", err)
   340  		}
   341  		commit, err := tag.Commit(gitRepo)
   342  		if err != nil {
   343  			return fmt.Errorf("Commit: %w", err)
   344  		}
   345  
   346  		sig := tag.Tagger
   347  		if sig == nil {
   348  			sig = commit.Author
   349  		}
   350  		if sig == nil {
   351  			sig = commit.Committer
   352  		}
   353  		var author *user_model.User
   354  		createdAt := time.Unix(1, 0)
   355  
   356  		if sig != nil {
   357  			var ok bool
   358  			author, ok = emailToUser[sig.Email]
   359  			if !ok {
   360  				author, err = user_model.GetUserByEmail(ctx, sig.Email)
   361  				if err != nil && !user_model.IsErrUserNotExist(err) {
   362  					return fmt.Errorf("GetUserByEmail: %w", err)
   363  				}
   364  				if author != nil {
   365  					emailToUser[sig.Email] = author
   366  				}
   367  			}
   368  			createdAt = sig.When
   369  		}
   370  
   371  		commitsCount, err := commit.CommitsCount()
   372  		if err != nil {
   373  			return fmt.Errorf("CommitsCount: %w", err)
   374  		}
   375  
   376  		rel, has := relMap[lowerTag]
   377  
   378  		if !has {
   379  			parts := strings.SplitN(tag.Message, "\n", 2)
   380  			note := ""
   381  			if len(parts) > 1 {
   382  				note = parts[1]
   383  			}
   384  			rel = &repo_model.Release{
   385  				RepoID:       repo.ID,
   386  				Title:        parts[0],
   387  				TagName:      tags[i],
   388  				LowerTagName: lowerTag,
   389  				Target:       "",
   390  				Sha1:         commit.ID.String(),
   391  				NumCommits:   commitsCount,
   392  				Note:         note,
   393  				IsDraft:      false,
   394  				IsPrerelease: false,
   395  				IsTag:        true,
   396  				CreatedUnix:  timeutil.TimeStamp(createdAt.Unix()),
   397  			}
   398  			if author != nil {
   399  				rel.PublisherID = author.ID
   400  			}
   401  
   402  			newReleases = append(newReleases, rel)
   403  		} else {
   404  			rel.Sha1 = commit.ID.String()
   405  			rel.CreatedUnix = timeutil.TimeStamp(createdAt.Unix())
   406  			rel.NumCommits = commitsCount
   407  			rel.IsDraft = false
   408  			if rel.IsTag && author != nil {
   409  				rel.PublisherID = author.ID
   410  			}
   411  			if err = repo_model.UpdateRelease(ctx, rel); err != nil {
   412  				return fmt.Errorf("Update: %w", err)
   413  			}
   414  		}
   415  	}
   416  
   417  	if len(newReleases) > 0 {
   418  		if err = db.Insert(ctx, newReleases); err != nil {
   419  			return fmt.Errorf("Insert: %w", err)
   420  		}
   421  	}
   422  
   423  	return nil
   424  }