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 }