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

     1  // Copyright 2021 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  
    12  	"code.gitea.io/gitea/models"
    13  	actions_model "code.gitea.io/gitea/models/actions"
    14  	"code.gitea.io/gitea/models/db"
    15  	git_model "code.gitea.io/gitea/models/git"
    16  	issues_model "code.gitea.io/gitea/models/issues"
    17  	repo_model "code.gitea.io/gitea/models/repo"
    18  	user_model "code.gitea.io/gitea/models/user"
    19  	"code.gitea.io/gitea/modules/git"
    20  	"code.gitea.io/gitea/modules/graceful"
    21  	"code.gitea.io/gitea/modules/log"
    22  	"code.gitea.io/gitea/modules/queue"
    23  	repo_module "code.gitea.io/gitea/modules/repository"
    24  	"code.gitea.io/gitea/modules/timeutil"
    25  	"code.gitea.io/gitea/modules/util"
    26  	webhook_module "code.gitea.io/gitea/modules/webhook"
    27  	notify_service "code.gitea.io/gitea/services/notify"
    28  	files_service "code.gitea.io/gitea/services/repository/files"
    29  
    30  	"xorm.io/builder"
    31  )
    32  
    33  // CreateNewBranch creates a new repository branch
    34  func CreateNewBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, oldBranchName, branchName string) (err error) {
    35  	branch, err := git_model.GetBranch(ctx, repo.ID, oldBranchName)
    36  	if err != nil {
    37  		return err
    38  	}
    39  
    40  	return CreateNewBranchFromCommit(ctx, doer, repo, gitRepo, branch.CommitID, branchName)
    41  }
    42  
    43  // Branch contains the branch information
    44  type Branch struct {
    45  	DBBranch          *git_model.Branch
    46  	IsProtected       bool
    47  	IsIncluded        bool
    48  	CommitsAhead      int
    49  	CommitsBehind     int
    50  	LatestPullRequest *issues_model.PullRequest
    51  	MergeMovedOn      bool
    52  }
    53  
    54  // LoadBranches loads branches from the repository limited by page & pageSize.
    55  func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, isDeletedBranch util.OptionalBool, keyword string, page, pageSize int) (*Branch, []*Branch, int64, error) {
    56  	defaultDBBranch, err := git_model.GetBranch(ctx, repo.ID, repo.DefaultBranch)
    57  	if err != nil {
    58  		return nil, nil, 0, err
    59  	}
    60  
    61  	branchOpts := git_model.FindBranchOptions{
    62  		RepoID:          repo.ID,
    63  		IsDeletedBranch: isDeletedBranch,
    64  		ListOptions: db.ListOptions{
    65  			Page:     page,
    66  			PageSize: pageSize,
    67  		},
    68  		Keyword: keyword,
    69  	}
    70  
    71  	totalNumOfBranches, err := git_model.CountBranches(ctx, branchOpts)
    72  	if err != nil {
    73  		return nil, nil, 0, err
    74  	}
    75  
    76  	branchOpts.ExcludeBranchNames = []string{repo.DefaultBranch}
    77  
    78  	dbBranches, err := git_model.FindBranches(ctx, branchOpts)
    79  	if err != nil {
    80  		return nil, nil, 0, err
    81  	}
    82  
    83  	if err := dbBranches.LoadDeletedBy(ctx); err != nil {
    84  		return nil, nil, 0, err
    85  	}
    86  	if err := dbBranches.LoadPusher(ctx); err != nil {
    87  		return nil, nil, 0, err
    88  	}
    89  
    90  	rules, err := git_model.FindRepoProtectedBranchRules(ctx, repo.ID)
    91  	if err != nil {
    92  		return nil, nil, 0, err
    93  	}
    94  
    95  	repoIDToRepo := map[int64]*repo_model.Repository{}
    96  	repoIDToRepo[repo.ID] = repo
    97  
    98  	repoIDToGitRepo := map[int64]*git.Repository{}
    99  	repoIDToGitRepo[repo.ID] = gitRepo
   100  
   101  	branches := make([]*Branch, 0, len(dbBranches))
   102  	for i := range dbBranches {
   103  		branch, err := loadOneBranch(ctx, repo, dbBranches[i], &rules, repoIDToRepo, repoIDToGitRepo)
   104  		if err != nil {
   105  			return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err)
   106  		}
   107  
   108  		branches = append(branches, branch)
   109  	}
   110  
   111  	// Always add the default branch
   112  	log.Debug("loadOneBranch: load default: '%s'", defaultDBBranch.Name)
   113  	defaultBranch, err := loadOneBranch(ctx, repo, defaultDBBranch, &rules, repoIDToRepo, repoIDToGitRepo)
   114  	if err != nil {
   115  		return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err)
   116  	}
   117  
   118  	return defaultBranch, branches, totalNumOfBranches, nil
   119  }
   120  
   121  func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *git_model.Branch, protectedBranches *git_model.ProtectedBranchRules,
   122  	repoIDToRepo map[int64]*repo_model.Repository,
   123  	repoIDToGitRepo map[int64]*git.Repository,
   124  ) (*Branch, error) {
   125  	log.Trace("loadOneBranch: '%s'", dbBranch.Name)
   126  
   127  	branchName := dbBranch.Name
   128  	p := protectedBranches.GetFirstMatched(branchName)
   129  	isProtected := p != nil
   130  
   131  	divergence := &git.DivergeObject{
   132  		Ahead:  -1,
   133  		Behind: -1,
   134  	}
   135  
   136  	// it's not default branch
   137  	if repo.DefaultBranch != dbBranch.Name && !dbBranch.IsDeleted {
   138  		var err error
   139  		divergence, err = files_service.CountDivergingCommits(ctx, repo, git.BranchPrefix+branchName)
   140  		if err != nil {
   141  			log.Error("CountDivergingCommits: %v", err)
   142  		}
   143  	}
   144  
   145  	pr, err := issues_model.GetLatestPullRequestByHeadInfo(repo.ID, branchName)
   146  	if err != nil {
   147  		return nil, fmt.Errorf("GetLatestPullRequestByHeadInfo: %v", err)
   148  	}
   149  	headCommit := dbBranch.CommitID
   150  
   151  	mergeMovedOn := false
   152  	if pr != nil {
   153  		pr.HeadRepo = repo
   154  		if err := pr.LoadIssue(ctx); err != nil {
   155  			return nil, fmt.Errorf("LoadIssue: %v", err)
   156  		}
   157  		if repo, ok := repoIDToRepo[pr.BaseRepoID]; ok {
   158  			pr.BaseRepo = repo
   159  		} else if err := pr.LoadBaseRepo(ctx); err != nil {
   160  			return nil, fmt.Errorf("LoadBaseRepo: %v", err)
   161  		} else {
   162  			repoIDToRepo[pr.BaseRepoID] = pr.BaseRepo
   163  		}
   164  		pr.Issue.Repo = pr.BaseRepo
   165  
   166  		if pr.HasMerged {
   167  			baseGitRepo, ok := repoIDToGitRepo[pr.BaseRepoID]
   168  			if !ok {
   169  				baseGitRepo, err = git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
   170  				if err != nil {
   171  					return nil, fmt.Errorf("OpenRepository: %v", err)
   172  				}
   173  				defer baseGitRepo.Close()
   174  				repoIDToGitRepo[pr.BaseRepoID] = baseGitRepo
   175  			}
   176  			pullCommit, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
   177  			if err != nil && !git.IsErrNotExist(err) {
   178  				return nil, fmt.Errorf("GetBranchCommitID: %v", err)
   179  			}
   180  			if err == nil && headCommit != pullCommit {
   181  				// the head has moved on from the merge - we shouldn't delete
   182  				mergeMovedOn = true
   183  			}
   184  		}
   185  	}
   186  
   187  	isIncluded := divergence.Ahead == 0 && repo.DefaultBranch != branchName
   188  	return &Branch{
   189  		DBBranch:          dbBranch,
   190  		IsProtected:       isProtected,
   191  		IsIncluded:        isIncluded,
   192  		CommitsAhead:      divergence.Ahead,
   193  		CommitsBehind:     divergence.Behind,
   194  		LatestPullRequest: pr,
   195  		MergeMovedOn:      mergeMovedOn,
   196  	}, nil
   197  }
   198  
   199  func GetBranchCommitID(ctx context.Context, repo *repo_model.Repository, branch string) (string, error) {
   200  	return git.GetBranchCommitID(ctx, repo.RepoPath(), branch)
   201  }
   202  
   203  // checkBranchName validates branch name with existing repository branches
   204  func checkBranchName(ctx context.Context, repo *repo_model.Repository, name string) error {
   205  	_, err := git.WalkReferences(ctx, repo.RepoPath(), func(_, refName string) error {
   206  		branchRefName := strings.TrimPrefix(refName, git.BranchPrefix)
   207  		switch {
   208  		case branchRefName == name:
   209  			return git_model.ErrBranchAlreadyExists{
   210  				BranchName: name,
   211  			}
   212  		// If branchRefName like a/b but we want to create a branch named a then we have a conflict
   213  		case strings.HasPrefix(branchRefName, name+"/"):
   214  			return git_model.ErrBranchNameConflict{
   215  				BranchName: branchRefName,
   216  			}
   217  			// Conversely if branchRefName like a but we want to create a branch named a/b then we also have a conflict
   218  		case strings.HasPrefix(name, branchRefName+"/"):
   219  			return git_model.ErrBranchNameConflict{
   220  				BranchName: branchRefName,
   221  			}
   222  		case refName == git.TagPrefix+name:
   223  			return models.ErrTagAlreadyExists{
   224  				TagName: name,
   225  			}
   226  		}
   227  		return nil
   228  	})
   229  
   230  	return err
   231  }
   232  
   233  // syncBranchToDB sync the branch information in the database. It will try to update the branch first,
   234  // if updated success with affect records > 0, then all are done. Because that means the branch has been in the database.
   235  // If no record is affected, that means the branch does not exist in database. So there are two possibilities.
   236  // One is this is a new branch, then we just need to insert the record. Another is the branches haven't been synced,
   237  // then we need to sync all the branches into database.
   238  func syncBranchToDB(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) error {
   239  	cnt, err := git_model.UpdateBranch(ctx, repoID, pusherID, branchName, commit)
   240  	if err != nil {
   241  		return fmt.Errorf("git_model.UpdateBranch %d:%s failed: %v", repoID, branchName, err)
   242  	}
   243  	if cnt > 0 { // This means branch does exist, so it's a normal update. It also means the branch has been synced.
   244  		return nil
   245  	}
   246  
   247  	// if user haven't visit UI but directly push to a branch after upgrading from 1.20 -> 1.21,
   248  	// we cannot simply insert the branch but need to check we have branches or not
   249  	hasBranch, err := db.Exist[git_model.Branch](ctx, git_model.FindBranchOptions{
   250  		RepoID:          repoID,
   251  		IsDeletedBranch: util.OptionalBoolFalse,
   252  	}.ToConds())
   253  	if err != nil {
   254  		return err
   255  	}
   256  	if !hasBranch {
   257  		if _, err = repo_module.SyncRepoBranches(ctx, repoID, pusherID); err != nil {
   258  			return fmt.Errorf("repo_module.SyncRepoBranches %d:%s failed: %v", repoID, branchName, err)
   259  		}
   260  		return nil
   261  	}
   262  
   263  	// if database have branches but not this branch, it means this is a new branch
   264  	return db.Insert(ctx, &git_model.Branch{
   265  		RepoID:        repoID,
   266  		Name:          branchName,
   267  		CommitID:      commit.ID.String(),
   268  		CommitMessage: commit.Summary(),
   269  		PusherID:      pusherID,
   270  		CommitTime:    timeutil.TimeStamp(commit.Committer.When.Unix()),
   271  	})
   272  }
   273  
   274  // CreateNewBranchFromCommit creates a new repository branch
   275  func CreateNewBranchFromCommit(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, commitID, branchName string) (err error) {
   276  	err = repo.MustNotBeArchived()
   277  	if err != nil {
   278  		return err
   279  	}
   280  
   281  	// Check if branch name can be used
   282  	if err := checkBranchName(ctx, repo, branchName); err != nil {
   283  		return err
   284  	}
   285  
   286  	if err := git.Push(ctx, repo.RepoPath(), git.PushOptions{
   287  		Remote: repo.RepoPath(),
   288  		Branch: fmt.Sprintf("%s:%s%s", commitID, git.BranchPrefix, branchName),
   289  		Env:    repo_module.PushingEnvironment(doer, repo),
   290  	}); err != nil {
   291  		if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
   292  			return err
   293  		}
   294  		return fmt.Errorf("push: %w", err)
   295  	}
   296  	return nil
   297  }
   298  
   299  // RenameBranch rename a branch
   300  func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, gitRepo *git.Repository, from, to string) (string, error) {
   301  	if from == to {
   302  		return "target_exist", nil
   303  	}
   304  
   305  	if gitRepo.IsBranchExist(to) {
   306  		return "target_exist", nil
   307  	}
   308  
   309  	if !gitRepo.IsBranchExist(from) {
   310  		return "from_not_exist", nil
   311  	}
   312  
   313  	if err := git_model.RenameBranch(ctx, repo, from, to, func(ctx context.Context, isDefault bool) error {
   314  		err2 := gitRepo.RenameBranch(from, to)
   315  		if err2 != nil {
   316  			return err2
   317  		}
   318  
   319  		if isDefault {
   320  			// if default branch changed, we need to delete all schedules and cron jobs
   321  			if err := actions_model.DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil {
   322  				log.Error("DeleteCronTaskByRepo: %v", err)
   323  			}
   324  			// cancel running cron jobs of this repository and delete old schedules
   325  			if err := actions_model.CancelRunningJobs(
   326  				ctx,
   327  				repo.ID,
   328  				from,
   329  				"",
   330  				webhook_module.HookEventSchedule,
   331  			); err != nil {
   332  				log.Error("CancelRunningJobs: %v", err)
   333  			}
   334  
   335  			err2 = gitRepo.SetDefaultBranch(to)
   336  			if err2 != nil {
   337  				return err2
   338  			}
   339  		}
   340  
   341  		return nil
   342  	}); err != nil {
   343  		return "", err
   344  	}
   345  	refNameTo := git.RefNameFromBranch(to)
   346  	refID, err := gitRepo.GetRefCommitID(refNameTo.String())
   347  	if err != nil {
   348  		return "", err
   349  	}
   350  
   351  	notify_service.DeleteRef(ctx, doer, repo, git.RefNameFromBranch(from))
   352  	notify_service.CreateRef(ctx, doer, repo, refNameTo, refID)
   353  
   354  	return "", nil
   355  }
   356  
   357  // enmuerates all branch related errors
   358  var (
   359  	ErrBranchIsDefault = errors.New("branch is default")
   360  )
   361  
   362  // DeleteBranch delete branch
   363  func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, branchName string) error {
   364  	if branchName == repo.DefaultBranch {
   365  		return ErrBranchIsDefault
   366  	}
   367  
   368  	isProtected, err := git_model.IsBranchProtected(ctx, repo.ID, branchName)
   369  	if err != nil {
   370  		return err
   371  	}
   372  	if isProtected {
   373  		return git_model.ErrBranchIsProtected
   374  	}
   375  
   376  	rawBranch, err := git_model.GetBranch(ctx, repo.ID, branchName)
   377  	if err != nil {
   378  		return fmt.Errorf("GetBranch: %vc", err)
   379  	}
   380  
   381  	if rawBranch.IsDeleted {
   382  		return nil
   383  	}
   384  
   385  	commit, err := gitRepo.GetBranchCommit(branchName)
   386  	if err != nil {
   387  		return err
   388  	}
   389  
   390  	if err := db.WithTx(ctx, func(ctx context.Context) error {
   391  		if err := git_model.AddDeletedBranch(ctx, repo.ID, branchName, doer.ID); err != nil {
   392  			return err
   393  		}
   394  
   395  		return gitRepo.DeleteBranch(branchName, git.DeleteBranchOptions{
   396  			Force: true,
   397  		})
   398  	}); err != nil {
   399  		return err
   400  	}
   401  
   402  	// Don't return error below this
   403  	if err := PushUpdate(
   404  		&repo_module.PushUpdateOptions{
   405  			RefFullName:  git.RefNameFromBranch(branchName),
   406  			OldCommitID:  commit.ID.String(),
   407  			NewCommitID:  git.EmptySHA,
   408  			PusherID:     doer.ID,
   409  			PusherName:   doer.Name,
   410  			RepoUserName: repo.OwnerName,
   411  			RepoName:     repo.Name,
   412  		}); err != nil {
   413  		log.Error("Update: %v", err)
   414  	}
   415  
   416  	return nil
   417  }
   418  
   419  type BranchSyncOptions struct {
   420  	RepoID int64
   421  }
   422  
   423  // branchSyncQueue represents a queue to handle branch sync jobs.
   424  var branchSyncQueue *queue.WorkerPoolQueue[*BranchSyncOptions]
   425  
   426  func handlerBranchSync(items ...*BranchSyncOptions) []*BranchSyncOptions {
   427  	for _, opts := range items {
   428  		_, err := repo_module.SyncRepoBranches(graceful.GetManager().ShutdownContext(), opts.RepoID, 0)
   429  		if err != nil {
   430  			log.Error("syncRepoBranches [%d] failed: %v", opts.RepoID, err)
   431  		}
   432  	}
   433  	return nil
   434  }
   435  
   436  func addRepoToBranchSyncQueue(repoID, doerID int64) error {
   437  	return branchSyncQueue.Push(&BranchSyncOptions{
   438  		RepoID: repoID,
   439  	})
   440  }
   441  
   442  func initBranchSyncQueue(ctx context.Context) error {
   443  	branchSyncQueue = queue.CreateUniqueQueue(ctx, "branch_sync", handlerBranchSync)
   444  	if branchSyncQueue == nil {
   445  		return errors.New("unable to create branch_sync queue")
   446  	}
   447  	go graceful.GetManager().RunWithCancel(branchSyncQueue)
   448  
   449  	return nil
   450  }
   451  
   452  func AddAllRepoBranchesToSyncQueue(ctx context.Context, doerID int64) error {
   453  	if err := db.Iterate(ctx, builder.Eq{"is_empty": false}, func(ctx context.Context, repo *repo_model.Repository) error {
   454  		return addRepoToBranchSyncQueue(repo.ID, doerID)
   455  	}); err != nil {
   456  		return fmt.Errorf("run sync all branches failed: %v", err)
   457  	}
   458  	return nil
   459  }
   460  
   461  func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, newBranchName string) error {
   462  	if repo.DefaultBranch == newBranchName {
   463  		return nil
   464  	}
   465  
   466  	if !gitRepo.IsBranchExist(newBranchName) {
   467  		return git_model.ErrBranchNotExist{
   468  			BranchName: newBranchName,
   469  		}
   470  	}
   471  
   472  	oldDefaultBranchName := repo.DefaultBranch
   473  	repo.DefaultBranch = newBranchName
   474  	if err := db.WithTx(ctx, func(ctx context.Context) error {
   475  		if err := repo_model.UpdateDefaultBranch(ctx, repo); err != nil {
   476  			return err
   477  		}
   478  
   479  		if err := actions_model.DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil {
   480  			log.Error("DeleteCronTaskByRepo: %v", err)
   481  		}
   482  		// cancel running cron jobs of this repository and delete old schedules
   483  		if err := actions_model.CancelRunningJobs(
   484  			ctx,
   485  			repo.ID,
   486  			oldDefaultBranchName,
   487  			"",
   488  			webhook_module.HookEventSchedule,
   489  		); err != nil {
   490  			log.Error("CancelRunningJobs: %v", err)
   491  		}
   492  
   493  		if err := gitRepo.SetDefaultBranch(newBranchName); err != nil {
   494  			if !git.IsErrUnsupportedVersion(err) {
   495  				return err
   496  			}
   497  		}
   498  		return nil
   499  	}); err != nil {
   500  		return err
   501  	}
   502  
   503  	notify_service.ChangeDefaultBranch(ctx, repo)
   504  
   505  	return nil
   506  }