code.gitea.io/gitea@v1.22.3/models/git/branch.go (about) 1 // Copyright 2016 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package git 5 6 import ( 7 "context" 8 "fmt" 9 "time" 10 11 "code.gitea.io/gitea/models/db" 12 repo_model "code.gitea.io/gitea/models/repo" 13 "code.gitea.io/gitea/models/unit" 14 user_model "code.gitea.io/gitea/models/user" 15 "code.gitea.io/gitea/modules/git" 16 "code.gitea.io/gitea/modules/log" 17 "code.gitea.io/gitea/modules/optional" 18 "code.gitea.io/gitea/modules/timeutil" 19 "code.gitea.io/gitea/modules/util" 20 21 "xorm.io/builder" 22 ) 23 24 // ErrBranchNotExist represents an error that branch with such name does not exist. 25 type ErrBranchNotExist struct { 26 RepoID int64 27 BranchName string 28 } 29 30 // IsErrBranchNotExist checks if an error is an ErrBranchDoesNotExist. 31 func IsErrBranchNotExist(err error) bool { 32 _, ok := err.(ErrBranchNotExist) 33 return ok 34 } 35 36 func (err ErrBranchNotExist) Error() string { 37 return fmt.Sprintf("branch does not exist [repo_id: %d name: %s]", err.RepoID, err.BranchName) 38 } 39 40 func (err ErrBranchNotExist) Unwrap() error { 41 return util.ErrNotExist 42 } 43 44 // ErrBranchAlreadyExists represents an error that branch with such name already exists. 45 type ErrBranchAlreadyExists struct { 46 BranchName string 47 } 48 49 // IsErrBranchAlreadyExists checks if an error is an ErrBranchAlreadyExists. 50 func IsErrBranchAlreadyExists(err error) bool { 51 _, ok := err.(ErrBranchAlreadyExists) 52 return ok 53 } 54 55 func (err ErrBranchAlreadyExists) Error() string { 56 return fmt.Sprintf("branch already exists [name: %s]", err.BranchName) 57 } 58 59 func (err ErrBranchAlreadyExists) Unwrap() error { 60 return util.ErrAlreadyExist 61 } 62 63 // ErrBranchNameConflict represents an error that branch name conflicts with other branch. 64 type ErrBranchNameConflict struct { 65 BranchName string 66 } 67 68 // IsErrBranchNameConflict checks if an error is an ErrBranchNameConflict. 69 func IsErrBranchNameConflict(err error) bool { 70 _, ok := err.(ErrBranchNameConflict) 71 return ok 72 } 73 74 func (err ErrBranchNameConflict) Error() string { 75 return fmt.Sprintf("branch conflicts with existing branch [name: %s]", err.BranchName) 76 } 77 78 func (err ErrBranchNameConflict) Unwrap() error { 79 return util.ErrAlreadyExist 80 } 81 82 // ErrBranchesEqual represents an error that base branch is equal to the head branch. 83 type ErrBranchesEqual struct { 84 BaseBranchName string 85 HeadBranchName string 86 } 87 88 // IsErrBranchesEqual checks if an error is an ErrBranchesEqual. 89 func IsErrBranchesEqual(err error) bool { 90 _, ok := err.(ErrBranchesEqual) 91 return ok 92 } 93 94 func (err ErrBranchesEqual) Error() string { 95 return fmt.Sprintf("branches are equal [head: %sm base: %s]", err.HeadBranchName, err.BaseBranchName) 96 } 97 98 func (err ErrBranchesEqual) Unwrap() error { 99 return util.ErrInvalidArgument 100 } 101 102 // Branch represents a branch of a repository 103 // For those repository who have many branches, stored into database is a good choice 104 // for pagination, keyword search and filtering 105 type Branch struct { 106 ID int64 107 RepoID int64 `xorm:"UNIQUE(s)"` 108 Repo *repo_model.Repository `xorm:"-"` 109 Name string `xorm:"UNIQUE(s) NOT NULL"` // git's ref-name is case-sensitive internally, however, in some databases (mssql, mysql, by default), it's case-insensitive at the moment 110 CommitID string 111 CommitMessage string `xorm:"TEXT"` // it only stores the message summary (the first line) 112 PusherID int64 113 Pusher *user_model.User `xorm:"-"` 114 IsDeleted bool `xorm:"index"` 115 DeletedByID int64 116 DeletedBy *user_model.User `xorm:"-"` 117 DeletedUnix timeutil.TimeStamp `xorm:"index"` 118 CommitTime timeutil.TimeStamp // The commit 119 CreatedUnix timeutil.TimeStamp `xorm:"created"` 120 UpdatedUnix timeutil.TimeStamp `xorm:"updated"` 121 } 122 123 func (b *Branch) LoadDeletedBy(ctx context.Context) (err error) { 124 if b.DeletedBy == nil { 125 b.DeletedBy, err = user_model.GetUserByID(ctx, b.DeletedByID) 126 if user_model.IsErrUserNotExist(err) { 127 b.DeletedBy = user_model.NewGhostUser() 128 err = nil 129 } 130 } 131 return err 132 } 133 134 func (b *Branch) LoadPusher(ctx context.Context) (err error) { 135 if b.Pusher == nil && b.PusherID > 0 { 136 b.Pusher, err = user_model.GetUserByID(ctx, b.PusherID) 137 if user_model.IsErrUserNotExist(err) { 138 b.Pusher = user_model.NewGhostUser() 139 err = nil 140 } 141 } 142 return err 143 } 144 145 func (b *Branch) LoadRepo(ctx context.Context) (err error) { 146 if b.Repo != nil || b.RepoID == 0 { 147 return nil 148 } 149 b.Repo, err = repo_model.GetRepositoryByID(ctx, b.RepoID) 150 return err 151 } 152 153 func init() { 154 db.RegisterModel(new(Branch)) 155 db.RegisterModel(new(RenamedBranch)) 156 } 157 158 func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, error) { 159 var branch Branch 160 has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("name=?", branchName).Get(&branch) 161 if err != nil { 162 return nil, err 163 } else if !has { 164 return nil, ErrBranchNotExist{ 165 RepoID: repoID, 166 BranchName: branchName, 167 } 168 } 169 return &branch, nil 170 } 171 172 func GetBranches(ctx context.Context, repoID int64, branchNames []string) ([]*Branch, error) { 173 branches := make([]*Branch, 0, len(branchNames)) 174 return branches, db.GetEngine(ctx).Where("repo_id=?", repoID).In("name", branchNames).Find(&branches) 175 } 176 177 func AddBranches(ctx context.Context, branches []*Branch) error { 178 for _, branch := range branches { 179 if _, err := db.GetEngine(ctx).Insert(branch); err != nil { 180 return err 181 } 182 } 183 return nil 184 } 185 186 func GetDeletedBranchByID(ctx context.Context, repoID, branchID int64) (*Branch, error) { 187 var branch Branch 188 has, err := db.GetEngine(ctx).ID(branchID).Get(&branch) 189 if err != nil { 190 return nil, err 191 } else if !has { 192 return nil, ErrBranchNotExist{ 193 RepoID: repoID, 194 } 195 } 196 if branch.RepoID != repoID { 197 return nil, ErrBranchNotExist{ 198 RepoID: repoID, 199 } 200 } 201 if !branch.IsDeleted { 202 return nil, ErrBranchNotExist{ 203 RepoID: repoID, 204 } 205 } 206 return &branch, nil 207 } 208 209 func DeleteBranches(ctx context.Context, repoID, doerID int64, branchIDs []int64) error { 210 return db.WithTx(ctx, func(ctx context.Context) error { 211 branches := make([]*Branch, 0, len(branchIDs)) 212 if err := db.GetEngine(ctx).In("id", branchIDs).Find(&branches); err != nil { 213 return err 214 } 215 for _, branch := range branches { 216 if err := AddDeletedBranch(ctx, repoID, branch.Name, doerID); err != nil { 217 return err 218 } 219 } 220 return nil 221 }) 222 } 223 224 // UpdateBranch updates the branch information in the database. 225 func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) (int64, error) { 226 return db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, branchName). 227 Cols("commit_id, commit_message, pusher_id, commit_time, is_deleted, updated_unix"). 228 Update(&Branch{ 229 CommitID: commit.ID.String(), 230 CommitMessage: commit.Summary(), 231 PusherID: pusherID, 232 CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()), 233 IsDeleted: false, 234 }) 235 } 236 237 // AddDeletedBranch adds a deleted branch to the database 238 func AddDeletedBranch(ctx context.Context, repoID int64, branchName string, deletedByID int64) error { 239 branch, err := GetBranch(ctx, repoID, branchName) 240 if err != nil { 241 return err 242 } 243 if branch.IsDeleted { 244 return nil 245 } 246 247 cnt, err := db.GetEngine(ctx).Where("repo_id=? AND name=? AND is_deleted=?", repoID, branchName, false). 248 Cols("is_deleted, deleted_by_id, deleted_unix"). 249 Update(&Branch{ 250 IsDeleted: true, 251 DeletedByID: deletedByID, 252 DeletedUnix: timeutil.TimeStampNow(), 253 }) 254 if err != nil { 255 return err 256 } 257 if cnt == 0 { 258 return fmt.Errorf("branch %s not found or has been deleted", branchName) 259 } 260 return err 261 } 262 263 func RemoveDeletedBranchByID(ctx context.Context, repoID, branchID int64) error { 264 _, err := db.GetEngine(ctx).Where("repo_id=? AND id=? AND is_deleted = ?", repoID, branchID, true).Delete(new(Branch)) 265 return err 266 } 267 268 // RemoveOldDeletedBranches removes old deleted branches 269 func RemoveOldDeletedBranches(ctx context.Context, olderThan time.Duration) { 270 // Nothing to do for shutdown or terminate 271 log.Trace("Doing: DeletedBranchesCleanup") 272 273 deleteBefore := time.Now().Add(-olderThan) 274 _, err := db.GetEngine(ctx).Where("is_deleted=? AND deleted_unix < ?", true, deleteBefore.Unix()).Delete(new(Branch)) 275 if err != nil { 276 log.Error("DeletedBranchesCleanup: %v", err) 277 } 278 } 279 280 // RenamedBranch provide renamed branch log 281 // will check it when a branch can't be found 282 type RenamedBranch struct { 283 ID int64 `xorm:"pk autoincr"` 284 RepoID int64 `xorm:"INDEX NOT NULL"` 285 From string 286 To string 287 CreatedUnix timeutil.TimeStamp `xorm:"created"` 288 } 289 290 // FindRenamedBranch check if a branch was renamed 291 func FindRenamedBranch(ctx context.Context, repoID int64, from string) (branch *RenamedBranch, exist bool, err error) { 292 branch = &RenamedBranch{ 293 RepoID: repoID, 294 From: from, 295 } 296 exist, err = db.GetEngine(ctx).Get(branch) 297 298 return branch, exist, err 299 } 300 301 // RenameBranch rename a branch 302 func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(ctx context.Context, isDefault bool) error) (err error) { 303 ctx, committer, err := db.TxContext(ctx) 304 if err != nil { 305 return err 306 } 307 defer committer.Close() 308 309 sess := db.GetEngine(ctx) 310 311 // check whether from branch exist 312 var branch Branch 313 exist, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repo.ID, from).Get(&branch) 314 if err != nil { 315 return err 316 } else if !exist || branch.IsDeleted { 317 return ErrBranchNotExist{ 318 RepoID: repo.ID, 319 BranchName: from, 320 } 321 } 322 323 // check whether to branch exist or is_deleted 324 var dstBranch Branch 325 exist, err = db.GetEngine(ctx).Where("repo_id=? AND name=?", repo.ID, to).Get(&dstBranch) 326 if err != nil { 327 return err 328 } 329 if exist { 330 if !dstBranch.IsDeleted { 331 return ErrBranchAlreadyExists{ 332 BranchName: to, 333 } 334 } 335 336 if _, err := db.GetEngine(ctx).ID(dstBranch.ID).NoAutoCondition().Delete(&dstBranch); err != nil { 337 return err 338 } 339 } 340 341 // 1. update branch in database 342 if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{ 343 Name: to, 344 }); err != nil { 345 return err 346 } else if n <= 0 { 347 return ErrBranchNotExist{ 348 RepoID: repo.ID, 349 BranchName: from, 350 } 351 } 352 353 // 2. update default branch if needed 354 isDefault := repo.DefaultBranch == from 355 if isDefault { 356 repo.DefaultBranch = to 357 _, err = sess.ID(repo.ID).Cols("default_branch").Update(repo) 358 if err != nil { 359 return err 360 } 361 } 362 363 // 3. Update protected branch if needed 364 protectedBranch, err := GetProtectedBranchRuleByName(ctx, repo.ID, from) 365 if err != nil { 366 return err 367 } 368 369 if protectedBranch != nil { 370 // there is a protect rule for this branch 371 protectedBranch.RuleName = to 372 _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch) 373 if err != nil { 374 return err 375 } 376 } else { 377 // some glob protect rules may match this branch 378 protected, err := IsBranchProtected(ctx, repo.ID, from) 379 if err != nil { 380 return err 381 } 382 if protected { 383 return ErrBranchIsProtected 384 } 385 } 386 387 // 4. Update all not merged pull request base branch name 388 _, err = sess.Table("pull_request").Where("base_repo_id=? AND base_branch=? AND has_merged=?", 389 repo.ID, from, false). 390 Update(map[string]any{"base_branch": to}) 391 if err != nil { 392 return err 393 } 394 395 // 4.1 Update all not merged pull request head branch name 396 if _, err = sess.Table("pull_request").Where("head_repo_id=? AND head_branch=? AND has_merged=?", 397 repo.ID, from, false). 398 Update(map[string]any{"head_branch": to}); err != nil { 399 return err 400 } 401 402 // 5. insert renamed branch record 403 renamedBranch := &RenamedBranch{ 404 RepoID: repo.ID, 405 From: from, 406 To: to, 407 } 408 err = db.Insert(ctx, renamedBranch) 409 if err != nil { 410 return err 411 } 412 413 // 6. do git action 414 if err = gitAction(ctx, isDefault); err != nil { 415 return err 416 } 417 418 return committer.Commit() 419 } 420 421 type FindRecentlyPushedNewBranchesOptions struct { 422 Repo *repo_model.Repository 423 BaseRepo *repo_model.Repository 424 CommitAfterUnix int64 425 MaxCount int 426 } 427 428 type RecentlyPushedNewBranch struct { 429 BranchDisplayName string 430 BranchLink string 431 BranchCompareURL string 432 CommitTime timeutil.TimeStamp 433 } 434 435 // FindRecentlyPushedNewBranches return at most 2 new branches pushed by the user in 2 hours which has no opened PRs created 436 // if opts.CommitAfterUnix is 0, we will find the branches that were committed to in the last 2 hours 437 // if opts.ListOptions is not set, we will only display top 2 latest branch 438 func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, opts *FindRecentlyPushedNewBranchesOptions) ([]*RecentlyPushedNewBranch, error) { 439 if doer == nil { 440 return []*RecentlyPushedNewBranch{}, nil 441 } 442 443 // find all related repo ids 444 repoOpts := repo_model.SearchRepoOptions{ 445 Actor: doer, 446 Private: true, 447 AllPublic: false, // Include also all public repositories of users and public organisations 448 AllLimited: false, // Include also all public repositories of limited organisations 449 Fork: optional.Some(true), 450 ForkFrom: opts.BaseRepo.ID, 451 Archived: optional.Some(false), 452 } 453 repoCond := repo_model.SearchRepositoryCondition(&repoOpts).And(repo_model.AccessibleRepositoryCondition(doer, unit.TypeCode)) 454 if opts.Repo.ID == opts.BaseRepo.ID { 455 // should also include the base repo's branches 456 repoCond = repoCond.Or(builder.Eq{"id": opts.BaseRepo.ID}) 457 } else { 458 // in fork repo, we only detect the fork repo's branch 459 repoCond = repoCond.And(builder.Eq{"id": opts.Repo.ID}) 460 } 461 repoIDs := builder.Select("id").From("repository").Where(repoCond) 462 463 if opts.CommitAfterUnix == 0 { 464 opts.CommitAfterUnix = time.Now().Add(-time.Hour * 2).Unix() 465 } 466 467 baseBranch, err := GetBranch(ctx, opts.BaseRepo.ID, opts.BaseRepo.DefaultBranch) 468 if err != nil { 469 return nil, err 470 } 471 472 // find all related branches, these branches may already created PRs, we will check later 473 var branches []*Branch 474 if err := db.GetEngine(ctx). 475 Where(builder.And( 476 builder.Eq{ 477 "pusher_id": doer.ID, 478 "is_deleted": false, 479 }, 480 builder.Gte{"commit_time": opts.CommitAfterUnix}, 481 builder.In("repo_id", repoIDs), 482 // newly created branch have no changes, so skip them 483 builder.Neq{"commit_id": baseBranch.CommitID}, 484 )). 485 OrderBy(db.SearchOrderByRecentUpdated.String()). 486 Find(&branches); err != nil { 487 return nil, err 488 } 489 490 newBranches := make([]*RecentlyPushedNewBranch, 0, len(branches)) 491 if opts.MaxCount == 0 { 492 // by default we display 2 recently pushed new branch 493 opts.MaxCount = 2 494 } 495 for _, branch := range branches { 496 // whether branch have already created PR 497 count, err := db.GetEngine(ctx).Table("pull_request"). 498 // we should not only use branch name here, because if there are branches with same name in other repos, 499 // we can not detect them correctly 500 Where(builder.Eq{"head_repo_id": branch.RepoID, "head_branch": branch.Name}).Count() 501 if err != nil { 502 return nil, err 503 } 504 505 // if no PR, we add to the result 506 if count == 0 { 507 if err := branch.LoadRepo(ctx); err != nil { 508 return nil, err 509 } 510 511 branchDisplayName := branch.Name 512 if branch.Repo.ID != opts.BaseRepo.ID && branch.Repo.ID != opts.Repo.ID { 513 branchDisplayName = fmt.Sprintf("%s:%s", branch.Repo.FullName(), branchDisplayName) 514 } 515 newBranches = append(newBranches, &RecentlyPushedNewBranch{ 516 BranchDisplayName: branchDisplayName, 517 BranchLink: fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)), 518 BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name), 519 CommitTime: branch.CommitTime, 520 }) 521 } 522 if len(newBranches) == opts.MaxCount { 523 break 524 } 525 } 526 527 return newBranches, nil 528 }