code.gitea.io/gitea@v1.22.3/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/cache" 20 "code.gitea.io/gitea/modules/git" 21 "code.gitea.io/gitea/modules/gitrepo" 22 "code.gitea.io/gitea/modules/graceful" 23 "code.gitea.io/gitea/modules/json" 24 "code.gitea.io/gitea/modules/log" 25 "code.gitea.io/gitea/modules/optional" 26 "code.gitea.io/gitea/modules/queue" 27 repo_module "code.gitea.io/gitea/modules/repository" 28 "code.gitea.io/gitea/modules/timeutil" 29 "code.gitea.io/gitea/modules/util" 30 webhook_module "code.gitea.io/gitea/modules/webhook" 31 notify_service "code.gitea.io/gitea/services/notify" 32 files_service "code.gitea.io/gitea/services/repository/files" 33 34 "xorm.io/builder" 35 ) 36 37 // CreateNewBranch creates a new repository branch 38 func CreateNewBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, oldBranchName, branchName string) (err error) { 39 branch, err := git_model.GetBranch(ctx, repo.ID, oldBranchName) 40 if err != nil { 41 return err 42 } 43 44 return CreateNewBranchFromCommit(ctx, doer, repo, gitRepo, branch.CommitID, branchName) 45 } 46 47 // Branch contains the branch information 48 type Branch struct { 49 DBBranch *git_model.Branch 50 IsProtected bool 51 IsIncluded bool 52 CommitsAhead int 53 CommitsBehind int 54 LatestPullRequest *issues_model.PullRequest 55 MergeMovedOn bool 56 } 57 58 // LoadBranches loads branches from the repository limited by page & pageSize. 59 func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, isDeletedBranch optional.Option[bool], keyword string, page, pageSize int) (*Branch, []*Branch, int64, error) { 60 defaultDBBranch, err := git_model.GetBranch(ctx, repo.ID, repo.DefaultBranch) 61 if err != nil { 62 return nil, nil, 0, err 63 } 64 65 branchOpts := git_model.FindBranchOptions{ 66 RepoID: repo.ID, 67 IsDeletedBranch: isDeletedBranch, 68 ListOptions: db.ListOptions{ 69 Page: page, 70 PageSize: pageSize, 71 }, 72 Keyword: keyword, 73 ExcludeBranchNames: []string{repo.DefaultBranch}, 74 } 75 76 dbBranches, totalNumOfBranches, err := db.FindAndCount[git_model.Branch](ctx, branchOpts) 77 if err != nil { 78 return nil, nil, 0, err 79 } 80 81 if err := git_model.BranchList(dbBranches).LoadDeletedBy(ctx); err != nil { 82 return nil, nil, 0, err 83 } 84 if err := git_model.BranchList(dbBranches).LoadPusher(ctx); err != nil { 85 return nil, nil, 0, err 86 } 87 88 rules, err := git_model.FindRepoProtectedBranchRules(ctx, repo.ID) 89 if err != nil { 90 return nil, nil, 0, err 91 } 92 93 repoIDToRepo := map[int64]*repo_model.Repository{} 94 repoIDToRepo[repo.ID] = repo 95 96 repoIDToGitRepo := map[int64]*git.Repository{} 97 repoIDToGitRepo[repo.ID] = gitRepo 98 99 branches := make([]*Branch, 0, len(dbBranches)) 100 for i := range dbBranches { 101 branch, err := loadOneBranch(ctx, repo, dbBranches[i], &rules, repoIDToRepo, repoIDToGitRepo) 102 if err != nil { 103 return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err) 104 } 105 branches = append(branches, branch) 106 } 107 108 // Always add the default branch 109 log.Debug("loadOneBranch: load default: '%s'", defaultDBBranch.Name) 110 defaultBranch, err := loadOneBranch(ctx, repo, defaultDBBranch, &rules, repoIDToRepo, repoIDToGitRepo) 111 if err != nil { 112 return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err) 113 } 114 return defaultBranch, branches, totalNumOfBranches, nil 115 } 116 117 func getDivergenceCacheKey(repoID int64, branchName string) string { 118 return fmt.Sprintf("%d-%s", repoID, branchName) 119 } 120 121 // getDivergenceFromCache gets the divergence from cache 122 func getDivergenceFromCache(repoID int64, branchName string) (*git.DivergeObject, bool) { 123 data, ok := cache.GetCache().Get(getDivergenceCacheKey(repoID, branchName)) 124 res := git.DivergeObject{ 125 Ahead: -1, 126 Behind: -1, 127 } 128 if !ok || data == "" { 129 return &res, false 130 } 131 if err := json.Unmarshal(util.UnsafeStringToBytes(data), &res); err != nil { 132 log.Error("json.UnMarshal failed: %v", err) 133 return &res, false 134 } 135 return &res, true 136 } 137 138 func putDivergenceFromCache(repoID int64, branchName string, divergence *git.DivergeObject) error { 139 bs, err := json.Marshal(divergence) 140 if err != nil { 141 return err 142 } 143 return cache.GetCache().Put(getDivergenceCacheKey(repoID, branchName), util.UnsafeBytesToString(bs), 30*24*60*60) 144 } 145 146 func DelDivergenceFromCache(repoID int64, branchName string) error { 147 return cache.GetCache().Delete(getDivergenceCacheKey(repoID, branchName)) 148 } 149 150 // DelRepoDivergenceFromCache deletes all divergence caches of a repository 151 func DelRepoDivergenceFromCache(ctx context.Context, repoID int64) error { 152 dbBranches, err := db.Find[git_model.Branch](ctx, git_model.FindBranchOptions{ 153 RepoID: repoID, 154 ListOptions: db.ListOptionsAll, 155 }) 156 if err != nil { 157 return err 158 } 159 for i := range dbBranches { 160 if err := DelDivergenceFromCache(repoID, dbBranches[i].Name); err != nil { 161 log.Error("DelDivergenceFromCache: %v", err) 162 } 163 } 164 return nil 165 } 166 167 func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *git_model.Branch, protectedBranches *git_model.ProtectedBranchRules, 168 repoIDToRepo map[int64]*repo_model.Repository, 169 repoIDToGitRepo map[int64]*git.Repository, 170 ) (*Branch, error) { 171 log.Trace("loadOneBranch: '%s'", dbBranch.Name) 172 173 branchName := dbBranch.Name 174 p := protectedBranches.GetFirstMatched(branchName) 175 isProtected := p != nil 176 177 var divergence *git.DivergeObject 178 179 // it's not default branch 180 if repo.DefaultBranch != dbBranch.Name && !dbBranch.IsDeleted { 181 var cached bool 182 divergence, cached = getDivergenceFromCache(repo.ID, dbBranch.Name) 183 if !cached { 184 var err error 185 divergence, err = files_service.CountDivergingCommits(ctx, repo, git.BranchPrefix+branchName) 186 if err != nil { 187 log.Error("CountDivergingCommits: %v", err) 188 } else { 189 if err = putDivergenceFromCache(repo.ID, dbBranch.Name, divergence); err != nil { 190 log.Error("putDivergenceFromCache: %v", err) 191 } 192 } 193 } 194 } 195 196 if divergence == nil { 197 // tolerate the error that we cannot get divergence 198 divergence = &git.DivergeObject{Ahead: -1, Behind: -1} 199 } 200 201 pr, err := issues_model.GetLatestPullRequestByHeadInfo(ctx, repo.ID, branchName) 202 if err != nil { 203 return nil, fmt.Errorf("GetLatestPullRequestByHeadInfo: %v", err) 204 } 205 headCommit := dbBranch.CommitID 206 207 mergeMovedOn := false 208 if pr != nil { 209 pr.HeadRepo = repo 210 if err := pr.LoadIssue(ctx); err != nil { 211 return nil, fmt.Errorf("LoadIssue: %v", err) 212 } 213 if repo, ok := repoIDToRepo[pr.BaseRepoID]; ok { 214 pr.BaseRepo = repo 215 } else if err := pr.LoadBaseRepo(ctx); err != nil { 216 return nil, fmt.Errorf("LoadBaseRepo: %v", err) 217 } else { 218 repoIDToRepo[pr.BaseRepoID] = pr.BaseRepo 219 } 220 pr.Issue.Repo = pr.BaseRepo 221 222 if pr.HasMerged { 223 baseGitRepo, ok := repoIDToGitRepo[pr.BaseRepoID] 224 if !ok { 225 baseGitRepo, err = gitrepo.OpenRepository(ctx, pr.BaseRepo) 226 if err != nil { 227 return nil, fmt.Errorf("OpenRepository: %v", err) 228 } 229 defer baseGitRepo.Close() 230 repoIDToGitRepo[pr.BaseRepoID] = baseGitRepo 231 } 232 pullCommit, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName()) 233 if err != nil && !git.IsErrNotExist(err) { 234 return nil, fmt.Errorf("GetBranchCommitID: %v", err) 235 } 236 if err == nil && headCommit != pullCommit { 237 // the head has moved on from the merge - we shouldn't delete 238 mergeMovedOn = true 239 } 240 } 241 } 242 243 isIncluded := divergence.Ahead == 0 && repo.DefaultBranch != branchName 244 return &Branch{ 245 DBBranch: dbBranch, 246 IsProtected: isProtected, 247 IsIncluded: isIncluded, 248 CommitsAhead: divergence.Ahead, 249 CommitsBehind: divergence.Behind, 250 LatestPullRequest: pr, 251 MergeMovedOn: mergeMovedOn, 252 }, nil 253 } 254 255 // checkBranchName validates branch name with existing repository branches 256 func checkBranchName(ctx context.Context, repo *repo_model.Repository, name string) error { 257 _, err := gitrepo.WalkReferences(ctx, repo, func(_, refName string) error { 258 branchRefName := strings.TrimPrefix(refName, git.BranchPrefix) 259 switch { 260 case branchRefName == name: 261 return git_model.ErrBranchAlreadyExists{ 262 BranchName: name, 263 } 264 // If branchRefName like a/b but we want to create a branch named a then we have a conflict 265 case strings.HasPrefix(branchRefName, name+"/"): 266 return git_model.ErrBranchNameConflict{ 267 BranchName: branchRefName, 268 } 269 // Conversely if branchRefName like a but we want to create a branch named a/b then we also have a conflict 270 case strings.HasPrefix(name, branchRefName+"/"): 271 return git_model.ErrBranchNameConflict{ 272 BranchName: branchRefName, 273 } 274 case refName == git.TagPrefix+name: 275 return models.ErrTagAlreadyExists{ 276 TagName: name, 277 } 278 } 279 return nil 280 }) 281 282 return err 283 } 284 285 // SyncBranchesToDB sync the branch information in the database. 286 // It will check whether the branches of the repository have never been synced before. 287 // If so, it will sync all branches of the repository. 288 // Otherwise, it will sync the branches that need to be updated. 289 func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames, commitIDs []string, getCommit func(commitID string) (*git.Commit, error)) error { 290 // Some designs that make the code look strange but are made for performance optimization purposes: 291 // 1. Sync branches in a batch to reduce the number of DB queries. 292 // 2. Lazy load commit information since it may be not necessary. 293 // 3. Exit early if synced all branches of git repo when there's no branch in DB. 294 // 4. Check the branches in DB if they are already synced. 295 // 296 // If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once. 297 // See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27 298 // For the first batch, it will hit optimization 3. 299 // For other batches, it will hit optimization 4. 300 301 if len(branchNames) != len(commitIDs) { 302 return fmt.Errorf("branchNames and commitIDs length not match") 303 } 304 305 return db.WithTx(ctx, func(ctx context.Context) error { 306 branches, err := git_model.GetBranches(ctx, repoID, branchNames) 307 if err != nil { 308 return fmt.Errorf("git_model.GetBranches: %v", err) 309 } 310 311 if len(branches) == 0 { 312 // if user haven't visit UI but directly push to a branch after upgrading from 1.20 -> 1.21, 313 // we cannot simply insert the branch but need to check we have branches or not 314 hasBranch, err := db.Exist[git_model.Branch](ctx, git_model.FindBranchOptions{ 315 RepoID: repoID, 316 IsDeletedBranch: optional.Some(false), 317 }.ToConds()) 318 if err != nil { 319 return err 320 } 321 if !hasBranch { 322 if _, err = repo_module.SyncRepoBranches(ctx, repoID, pusherID); err != nil { 323 return fmt.Errorf("repo_module.SyncRepoBranches %d failed: %v", repoID, err) 324 } 325 return nil 326 } 327 } 328 329 branchMap := make(map[string]*git_model.Branch, len(branches)) 330 for _, branch := range branches { 331 branchMap[branch.Name] = branch 332 } 333 334 newBranches := make([]*git_model.Branch, 0, len(branchNames)) 335 336 for i, branchName := range branchNames { 337 commitID := commitIDs[i] 338 branch, exist := branchMap[branchName] 339 if exist && branch.CommitID == commitID && !branch.IsDeleted { 340 continue 341 } 342 343 commit, err := getCommit(commitID) 344 if err != nil { 345 return fmt.Errorf("get commit of %s failed: %v", branchName, err) 346 } 347 348 if exist { 349 if _, err := git_model.UpdateBranch(ctx, repoID, pusherID, branchName, commit); err != nil { 350 return fmt.Errorf("git_model.UpdateBranch %d:%s failed: %v", repoID, branchName, err) 351 } 352 continue 353 } 354 355 // if database have branches but not this branch, it means this is a new branch 356 newBranches = append(newBranches, &git_model.Branch{ 357 RepoID: repoID, 358 Name: branchName, 359 CommitID: commit.ID.String(), 360 CommitMessage: commit.Summary(), 361 PusherID: pusherID, 362 CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()), 363 }) 364 } 365 366 if len(newBranches) > 0 { 367 return db.Insert(ctx, newBranches) 368 } 369 return nil 370 }) 371 } 372 373 // CreateNewBranchFromCommit creates a new repository branch 374 func CreateNewBranchFromCommit(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, commitID, branchName string) (err error) { 375 err = repo.MustNotBeArchived() 376 if err != nil { 377 return err 378 } 379 380 // Check if branch name can be used 381 if err := checkBranchName(ctx, repo, branchName); err != nil { 382 return err 383 } 384 385 if err := git.Push(ctx, repo.RepoPath(), git.PushOptions{ 386 Remote: repo.RepoPath(), 387 Branch: fmt.Sprintf("%s:%s%s", commitID, git.BranchPrefix, branchName), 388 Env: repo_module.PushingEnvironment(doer, repo), 389 }); err != nil { 390 if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) { 391 return err 392 } 393 return fmt.Errorf("push: %w", err) 394 } 395 return nil 396 } 397 398 // RenameBranch rename a branch 399 func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, gitRepo *git.Repository, from, to string) (string, error) { 400 err := repo.MustNotBeArchived() 401 if err != nil { 402 return "", err 403 } 404 405 if from == to { 406 return "target_exist", nil 407 } 408 409 if gitRepo.IsBranchExist(to) { 410 return "target_exist", nil 411 } 412 413 if !gitRepo.IsBranchExist(from) { 414 return "from_not_exist", nil 415 } 416 417 if err := git_model.RenameBranch(ctx, repo, from, to, func(ctx context.Context, isDefault bool) error { 418 err2 := gitRepo.RenameBranch(from, to) 419 if err2 != nil { 420 return err2 421 } 422 423 if isDefault { 424 // if default branch changed, we need to delete all schedules and cron jobs 425 if err := actions_model.DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil { 426 log.Error("DeleteCronTaskByRepo: %v", err) 427 } 428 // cancel running cron jobs of this repository and delete old schedules 429 if err := actions_model.CancelPreviousJobs( 430 ctx, 431 repo.ID, 432 from, 433 "", 434 webhook_module.HookEventSchedule, 435 ); err != nil { 436 log.Error("CancelPreviousJobs: %v", err) 437 } 438 439 err2 = gitrepo.SetDefaultBranch(ctx, repo, to) 440 if err2 != nil { 441 return err2 442 } 443 } 444 445 return nil 446 }); err != nil { 447 return "", err 448 } 449 refNameTo := git.RefNameFromBranch(to) 450 refID, err := gitRepo.GetRefCommitID(refNameTo.String()) 451 if err != nil { 452 return "", err 453 } 454 455 notify_service.DeleteRef(ctx, doer, repo, git.RefNameFromBranch(from)) 456 notify_service.CreateRef(ctx, doer, repo, refNameTo, refID) 457 458 return "", nil 459 } 460 461 // enmuerates all branch related errors 462 var ( 463 ErrBranchIsDefault = errors.New("branch is default") 464 ) 465 466 // DeleteBranch delete branch 467 func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, branchName string) error { 468 err := repo.MustNotBeArchived() 469 if err != nil { 470 return err 471 } 472 473 if branchName == repo.DefaultBranch { 474 return ErrBranchIsDefault 475 } 476 477 isProtected, err := git_model.IsBranchProtected(ctx, repo.ID, branchName) 478 if err != nil { 479 return err 480 } 481 if isProtected { 482 return git_model.ErrBranchIsProtected 483 } 484 485 rawBranch, err := git_model.GetBranch(ctx, repo.ID, branchName) 486 if err != nil && !git_model.IsErrBranchNotExist(err) { 487 return fmt.Errorf("GetBranch: %vc", err) 488 } 489 490 // database branch record not exist or it's a deleted branch 491 notExist := git_model.IsErrBranchNotExist(err) || rawBranch.IsDeleted 492 493 commit, err := gitRepo.GetBranchCommit(branchName) 494 if err != nil { 495 return err 496 } 497 498 if err := db.WithTx(ctx, func(ctx context.Context) error { 499 if !notExist { 500 if err := git_model.AddDeletedBranch(ctx, repo.ID, branchName, doer.ID); err != nil { 501 return err 502 } 503 } 504 505 return gitRepo.DeleteBranch(branchName, git.DeleteBranchOptions{ 506 Force: true, 507 }) 508 }); err != nil { 509 return err 510 } 511 512 objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) 513 514 // Don't return error below this 515 if err := PushUpdate( 516 &repo_module.PushUpdateOptions{ 517 RefFullName: git.RefNameFromBranch(branchName), 518 OldCommitID: commit.ID.String(), 519 NewCommitID: objectFormat.EmptyObjectID().String(), 520 PusherID: doer.ID, 521 PusherName: doer.Name, 522 RepoUserName: repo.OwnerName, 523 RepoName: repo.Name, 524 }); err != nil { 525 log.Error("Update: %v", err) 526 } 527 528 return nil 529 } 530 531 type BranchSyncOptions struct { 532 RepoID int64 533 } 534 535 // branchSyncQueue represents a queue to handle branch sync jobs. 536 var branchSyncQueue *queue.WorkerPoolQueue[*BranchSyncOptions] 537 538 func handlerBranchSync(items ...*BranchSyncOptions) []*BranchSyncOptions { 539 for _, opts := range items { 540 _, err := repo_module.SyncRepoBranches(graceful.GetManager().ShutdownContext(), opts.RepoID, 0) 541 if err != nil { 542 log.Error("syncRepoBranches [%d] failed: %v", opts.RepoID, err) 543 } 544 } 545 return nil 546 } 547 548 func addRepoToBranchSyncQueue(repoID, doerID int64) error { 549 return branchSyncQueue.Push(&BranchSyncOptions{ 550 RepoID: repoID, 551 }) 552 } 553 554 func initBranchSyncQueue(ctx context.Context) error { 555 branchSyncQueue = queue.CreateUniqueQueue(ctx, "branch_sync", handlerBranchSync) 556 if branchSyncQueue == nil { 557 return errors.New("unable to create branch_sync queue") 558 } 559 go graceful.GetManager().RunWithCancel(branchSyncQueue) 560 561 return nil 562 } 563 564 func AddAllRepoBranchesToSyncQueue(ctx context.Context, doerID int64) error { 565 if err := db.Iterate(ctx, builder.Eq{"is_empty": false}, func(ctx context.Context, repo *repo_model.Repository) error { 566 return addRepoToBranchSyncQueue(repo.ID, doerID) 567 }); err != nil { 568 return fmt.Errorf("run sync all branches failed: %v", err) 569 } 570 return nil 571 } 572 573 func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, newBranchName string) error { 574 if repo.DefaultBranch == newBranchName { 575 return nil 576 } 577 578 if !gitRepo.IsBranchExist(newBranchName) { 579 return git_model.ErrBranchNotExist{ 580 BranchName: newBranchName, 581 } 582 } 583 584 oldDefaultBranchName := repo.DefaultBranch 585 repo.DefaultBranch = newBranchName 586 if err := db.WithTx(ctx, func(ctx context.Context) error { 587 if err := repo_model.UpdateDefaultBranch(ctx, repo); err != nil { 588 return err 589 } 590 591 if err := actions_model.DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil { 592 log.Error("DeleteCronTaskByRepo: %v", err) 593 } 594 // cancel running cron jobs of this repository and delete old schedules 595 if err := actions_model.CancelPreviousJobs( 596 ctx, 597 repo.ID, 598 oldDefaultBranchName, 599 "", 600 webhook_module.HookEventSchedule, 601 ); err != nil { 602 log.Error("CancelPreviousJobs: %v", err) 603 } 604 605 if err := gitrepo.SetDefaultBranch(ctx, repo, newBranchName); err != nil { 606 if !git.IsErrUnsupportedVersion(err) { 607 return err 608 } 609 } 610 return nil 611 }); err != nil { 612 return err 613 } 614 615 notify_service.ChangeDefaultBranch(ctx, repo) 616 617 return nil 618 }