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