// Copyright 2016 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"fmt"
"time"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
// ErrBranchNotExist represents an error that branch with such name does not exist.
type ErrBranchNotExist struct {
RepoID int64
BranchName string
}
// IsErrBranchNotExist checks if an error is an ErrBranchDoesNotExist.
func IsErrBranchNotExist ( err error ) bool {
_ , ok := err . ( ErrBranchNotExist )
return ok
}
func ( err ErrBranchNotExist ) Error ( ) string {
return fmt . Sprintf ( "branch does not exist [repo_id: %d name: %s]" , err . RepoID , err . BranchName )
}
func ( err ErrBranchNotExist ) Unwrap ( ) error {
return util . ErrNotExist
}
// ErrBranchAlreadyExists represents an error that branch with such name already exists.
type ErrBranchAlreadyExists struct {
BranchName string
}
// IsErrBranchAlreadyExists checks if an error is an ErrBranchAlreadyExists.
func IsErrBranchAlreadyExists ( err error ) bool {
_ , ok := err . ( ErrBranchAlreadyExists )
return ok
}
func ( err ErrBranchAlreadyExists ) Error ( ) string {
return fmt . Sprintf ( "branch already exists [name: %s]" , err . BranchName )
}
func ( err ErrBranchAlreadyExists ) Unwrap ( ) error {
return util . ErrAlreadyExist
}
// ErrBranchNameConflict represents an error that branch name conflicts with other branch.
type ErrBranchNameConflict struct {
BranchName string
}
// IsErrBranchNameConflict checks if an error is an ErrBranchNameConflict.
func IsErrBranchNameConflict ( err error ) bool {
_ , ok := err . ( ErrBranchNameConflict )
return ok
}
func ( err ErrBranchNameConflict ) Error ( ) string {
return fmt . Sprintf ( "branch conflicts with existing branch [name: %s]" , err . BranchName )
}
func ( err ErrBranchNameConflict ) Unwrap ( ) error {
return util . ErrAlreadyExist
}
// ErrBranchesEqual represents an error that base branch is equal to the head branch.
type ErrBranchesEqual struct {
BaseBranchName string
HeadBranchName string
}
// IsErrBranchesEqual checks if an error is an ErrBranchesEqual.
func IsErrBranchesEqual ( err error ) bool {
_ , ok := err . ( ErrBranchesEqual )
return ok
}
func ( err ErrBranchesEqual ) Error ( ) string {
return fmt . Sprintf ( "branches are equal [head: %sm base: %s]" , err . HeadBranchName , err . BaseBranchName )
}
func ( err ErrBranchesEqual ) Unwrap ( ) error {
return util . ErrInvalidArgument
}
// Branch represents a branch of a repository
// For those repository who have many branches, stored into database is a good choice
// for pagination, keyword search and filtering
type Branch struct {
ID int64
RepoID int64 ` xorm:"UNIQUE(s)" `
Repo * repo_model . Repository ` xorm:"-" `
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
CommitID string
CommitMessage string ` xorm:"TEXT" ` // it only stores the message summary (the first line)
PusherID int64
Pusher * user_model . User ` xorm:"-" `
IsDeleted bool ` xorm:"index" `
DeletedByID int64
DeletedBy * user_model . User ` xorm:"-" `
DeletedUnix timeutil . TimeStamp ` xorm:"index" `
CommitTime timeutil . TimeStamp // The commit
CreatedUnix timeutil . TimeStamp ` xorm:"created" `
UpdatedUnix timeutil . TimeStamp ` xorm:"updated" `
}
func ( b * Branch ) LoadDeletedBy ( ctx context . Context ) ( err error ) {
if b . DeletedBy == nil {
b . DeletedBy , err = user_model . GetUserByID ( ctx , b . DeletedByID )
if user_model . IsErrUserNotExist ( err ) {
b . DeletedBy = user_model . NewGhostUser ( )
err = nil
}
}
return err
}
func ( b * Branch ) LoadPusher ( ctx context . Context ) ( err error ) {
if b . Pusher == nil && b . PusherID > 0 {
b . Pusher , err = user_model . GetUserByID ( ctx , b . PusherID )
if user_model . IsErrUserNotExist ( err ) {
b . Pusher = user_model . NewGhostUser ( )
err = nil
}
}
return err
}
func ( b * Branch ) LoadRepo ( ctx context . Context ) ( err error ) {
if b . Repo != nil || b . RepoID == 0 {
return nil
}
b . Repo , err = repo_model . GetRepositoryByID ( ctx , b . RepoID )
return err
}
func init ( ) {
db . RegisterModel ( new ( Branch ) )
db . RegisterModel ( new ( RenamedBranch ) )
}
func GetBranch ( ctx context . Context , repoID int64 , branchName string ) ( * Branch , error ) {
var branch Branch
has , err := db . GetEngine ( ctx ) . Where ( "repo_id=?" , repoID ) . And ( "name=?" , branchName ) . Get ( & branch )
if err != nil {
return nil , err
} else if ! has {
return nil , ErrBranchNotExist {
RepoID : repoID ,
BranchName : branchName ,
}
}
return & branch , nil
}
func GetBranches ( ctx context . Context , repoID int64 , branchNames [ ] string ) ( [ ] * Branch , error ) {
branches := make ( [ ] * Branch , 0 , len ( branchNames ) )
return branches , db . GetEngine ( ctx ) . Where ( "repo_id=?" , repoID ) . In ( "name" , branchNames ) . Find ( & branches )
}
func AddBranches ( ctx context . Context , branches [ ] * Branch ) error {
for _ , branch := range branches {
if _ , err := db . GetEngine ( ctx ) . Insert ( branch ) ; err != nil {
return err
}
}
return nil
}
func GetDeletedBranchByID ( ctx context . Context , repoID , branchID int64 ) ( * Branch , error ) {
var branch Branch
has , err := db . GetEngine ( ctx ) . ID ( branchID ) . Get ( & branch )
if err != nil {
return nil , err
} else if ! has {
return nil , ErrBranchNotExist {
RepoID : repoID ,
}
}
if branch . RepoID != repoID {
return nil , ErrBranchNotExist {
RepoID : repoID ,
}
}
if ! branch . IsDeleted {
return nil , ErrBranchNotExist {
RepoID : repoID ,
}
}
return & branch , nil
}
func DeleteBranches ( ctx context . Context , repoID , doerID int64 , branchIDs [ ] int64 ) error {
return db . WithTx ( ctx , func ( ctx context . Context ) error {
branches := make ( [ ] * Branch , 0 , len ( branchIDs ) )
if err := db . GetEngine ( ctx ) . In ( "id" , branchIDs ) . Find ( & branches ) ; err != nil {
return err
}
for _ , branch := range branches {
if err := AddDeletedBranch ( ctx , repoID , branch . Name , doerID ) ; err != nil {
return err
}
}
return nil
} )
}
Also sync DB branches on push if necessary (#28361)
Fix #28056
This PR will check whether the repo has zero branch when pushing a
branch. If that, it means this repository hasn't been synced.
The reason caused that is after user upgrade from v1.20 -> v1.21, he
just push branches without visit the repository user interface. Because
all repositories routers will check whether a branches sync is necessary
but push has not such check.
For every repository, it has two states, synced or not synced. If there
is zero branch for a repository, then it will be assumed as non-sync
state. Otherwise, it's synced state. So if we think it's synced, we just
need to update branch/insert new branch. Otherwise do a full sync. So
that, for every push, there will be almost no extra load added. It's
high performance than yours.
For the implementation, we in fact will try to update the branch first,
if updated success with affect records > 0, then all are done. Because
that means the branch has been in the database. If no record is
affected, that means the branch does not exist in database. So there are
two possibilities. One is this is a new branch, then we just need to
insert the record. Another is the branches haven't been synced, then we
need to sync all the branches into database.
12 months ago
// UpdateBranch updates the branch information in the database.
func UpdateBranch ( ctx context . Context , repoID , pusherID int64 , branchName string , commit * git . Commit ) ( int64 , error ) {
return db . GetEngine ( ctx ) . Where ( "repo_id=? AND name=?" , repoID , branchName ) .
Cols ( "commit_id, commit_message, pusher_id, commit_time, is_deleted, updated_unix" ) .
Update ( & Branch {
CommitID : commit . ID . String ( ) ,
CommitMessage : commit . Summary ( ) ,
PusherID : pusherID ,
CommitTime : timeutil . TimeStamp ( commit . Committer . When . Unix ( ) ) ,
IsDeleted : false ,
} )
}
// AddDeletedBranch adds a deleted branch to the database
func AddDeletedBranch ( ctx context . Context , repoID int64 , branchName string , deletedByID int64 ) error {
branch , err := GetBranch ( ctx , repoID , branchName )
if err != nil {
return err
}
if branch . IsDeleted {
return nil
}
cnt , err := db . GetEngine ( ctx ) . Where ( "repo_id=? AND name=? AND is_deleted=?" , repoID , branchName , false ) .
Cols ( "is_deleted, deleted_by_id, deleted_unix" ) .
Update ( & Branch {
IsDeleted : true ,
DeletedByID : deletedByID ,
DeletedUnix : timeutil . TimeStampNow ( ) ,
} )
if err != nil {
return err
}
if cnt == 0 {
return fmt . Errorf ( "branch %s not found or has been deleted" , branchName )
}
return err
}
func RemoveDeletedBranchByID ( ctx context . Context , repoID , branchID int64 ) error {
_ , err := db . GetEngine ( ctx ) . Where ( "repo_id=? AND id=? AND is_deleted = ?" , repoID , branchID , true ) . Delete ( new ( Branch ) )
return err
}
// RemoveOldDeletedBranches removes old deleted branches
func RemoveOldDeletedBranches ( ctx context . Context , olderThan time . Duration ) {
// Nothing to do for shutdown or terminate
log . Trace ( "Doing: DeletedBranchesCleanup" )
deleteBefore := time . Now ( ) . Add ( - olderThan )
_ , err := db . GetEngine ( ctx ) . Where ( "is_deleted=? AND deleted_unix < ?" , true , deleteBefore . Unix ( ) ) . Delete ( new ( Branch ) )
if err != nil {
log . Error ( "DeletedBranchesCleanup: %v" , err )
}
}
// RenamedBranch provide renamed branch log
// will check it when a branch can't be found
type RenamedBranch struct {
ID int64 ` xorm:"pk autoincr" `
RepoID int64 ` xorm:"INDEX NOT NULL" `
From string
To string
CreatedUnix timeutil . TimeStamp ` xorm:"created" `
}
// FindRenamedBranch check if a branch was renamed
func FindRenamedBranch ( ctx context . Context , repoID int64 , from string ) ( branch * RenamedBranch , exist bool , err error ) {
branch = & RenamedBranch {
RepoID : repoID ,
From : from ,
}
exist , err = db . GetEngine ( ctx ) . Get ( branch )
return branch , exist , err
}
// RenameBranch rename a branch
func RenameBranch ( ctx context . Context , repo * repo_model . Repository , from , to string , gitAction func ( ctx context . Context , isDefault bool ) error ) ( err error ) {
ctx , committer , err := db . TxContext ( ctx )
if err != nil {
return err
}
defer committer . Close ( )
sess := db . GetEngine ( ctx )
// check whether from branch exist
Also sync DB branches on push if necessary (#28361)
Fix #28056
This PR will check whether the repo has zero branch when pushing a
branch. If that, it means this repository hasn't been synced.
The reason caused that is after user upgrade from v1.20 -> v1.21, he
just push branches without visit the repository user interface. Because
all repositories routers will check whether a branches sync is necessary
but push has not such check.
For every repository, it has two states, synced or not synced. If there
is zero branch for a repository, then it will be assumed as non-sync
state. Otherwise, it's synced state. So if we think it's synced, we just
need to update branch/insert new branch. Otherwise do a full sync. So
that, for every push, there will be almost no extra load added. It's
high performance than yours.
For the implementation, we in fact will try to update the branch first,
if updated success with affect records > 0, then all are done. Because
that means the branch has been in the database. If no record is
affected, that means the branch does not exist in database. So there are
two possibilities. One is this is a new branch, then we just need to
insert the record. Another is the branches haven't been synced, then we
need to sync all the branches into database.
12 months ago
var branch Branch
exist , err := db . GetEngine ( ctx ) . Where ( "repo_id=? AND name=?" , repo . ID , from ) . Get ( & branch )
if err != nil {
return err
} else if ! exist || branch . IsDeleted {
return ErrBranchNotExist {
RepoID : repo . ID ,
BranchName : from ,
}
}
// check whether to branch exist or is_deleted
var dstBranch Branch
exist , err = db . GetEngine ( ctx ) . Where ( "repo_id=? AND name=?" , repo . ID , to ) . Get ( & dstBranch )
if err != nil {
return err
}
if exist {
if ! dstBranch . IsDeleted {
return ErrBranchAlreadyExists {
BranchName : to ,
}
}
if _ , err := db . GetEngine ( ctx ) . ID ( dstBranch . ID ) . NoAutoCondition ( ) . Delete ( & dstBranch ) ; err != nil {
return err
}
}
// 1. update branch in database
if n , err := sess . Where ( "repo_id=? AND name=?" , repo . ID , from ) . Update ( & Branch {
Name : to ,
} ) ; err != nil {
return err
} else if n <= 0 {
return ErrBranchNotExist {
RepoID : repo . ID ,
BranchName : from ,
}
}
// 2. update default branch if needed
isDefault := repo . DefaultBranch == from
if isDefault {
repo . DefaultBranch = to
_ , err = sess . ID ( repo . ID ) . Cols ( "default_branch" ) . Update ( repo )
if err != nil {
return err
}
}
// 3. Update protected branch if needed
protectedBranch , err := GetProtectedBranchRuleByName ( ctx , repo . ID , from )
if err != nil {
return err
}
if protectedBranch != nil {
// there is a protect rule for this branch
protectedBranch . RuleName = to
_ , err = sess . ID ( protectedBranch . ID ) . Cols ( "branch_name" ) . Update ( protectedBranch )
if err != nil {
return err
}
} else {
// some glob protect rules may match this branch
protected , err := IsBranchProtected ( ctx , repo . ID , from )
if err != nil {
return err
}
if protected {
return ErrBranchIsProtected
}
}
// 4. Update all not merged pull request base branch name
_ , err = sess . Table ( "pull_request" ) . Where ( "base_repo_id=? AND base_branch=? AND has_merged=?" ,
repo . ID , from , false ) .
Update ( map [ string ] any { "base_branch" : to } )
if err != nil {
return err
}
// 5. insert renamed branch record
renamedBranch := & RenamedBranch {
RepoID : repo . ID ,
From : from ,
To : to ,
}
err = db . Insert ( ctx , renamedBranch )
if err != nil {
return err
}
// 6. do git action
if err = gitAction ( ctx , isDefault ) ; err != nil {
return err
}
return committer . Commit ( )
}
type FindRecentlyPushedNewBranchesOptions struct {
Repo * repo_model . Repository
BaseRepo * repo_model . Repository
CommitAfterUnix int64
MaxCount int
}
type RecentlyPushedNewBranch struct {
BranchDisplayName string
BranchLink string
BranchCompareURL string
CommitTime timeutil . TimeStamp
}
// FindRecentlyPushedNewBranches return at most 2 new branches pushed by the user in 2 hours which has no opened PRs created
// if opts.CommitAfterUnix is 0, we will find the branches that were committed to in the last 2 hours
// if opts.ListOptions is not set, we will only display top 2 latest branch
func FindRecentlyPushedNewBranches ( ctx context . Context , doer * user_model . User , opts * FindRecentlyPushedNewBranchesOptions ) ( [ ] * RecentlyPushedNewBranch , error ) {
if doer == nil {
return [ ] * RecentlyPushedNewBranch { } , nil
}
// find all related repo ids
repoOpts := repo_model . SearchRepoOptions {
Actor : doer ,
Private : true ,
AllPublic : false , // Include also all public repositories of users and public organisations
AllLimited : false , // Include also all public repositories of limited organisations
Fork : optional . Some ( true ) ,
ForkFrom : opts . BaseRepo . ID ,
Archived : optional . Some ( false ) ,
}
repoCond := repo_model . SearchRepositoryCondition ( & repoOpts ) . And ( repo_model . AccessibleRepositoryCondition ( doer , unit . TypeCode ) )
if opts . Repo . ID == opts . BaseRepo . ID {
// should also include the base repo's branches
repoCond = repoCond . Or ( builder . Eq { "id" : opts . BaseRepo . ID } )
} else {
// in fork repo, we only detect the fork repo's branch
repoCond = repoCond . And ( builder . Eq { "id" : opts . Repo . ID } )
}
repoIDs := builder . Select ( "id" ) . From ( "repository" ) . Where ( repoCond )
if opts . CommitAfterUnix == 0 {
opts . CommitAfterUnix = time . Now ( ) . Add ( - time . Hour * 2 ) . Unix ( )
}
baseBranch , err := GetBranch ( ctx , opts . BaseRepo . ID , opts . BaseRepo . DefaultBranch )
if err != nil {
return nil , err
}
// find all related branches, these branches may already created PRs, we will check later
var branches [ ] * Branch
if err := db . GetEngine ( ctx ) .
Where ( builder . And (
builder . Eq {
"pusher_id" : doer . ID ,
"is_deleted" : false ,
} ,
builder . Gte { "commit_time" : opts . CommitAfterUnix } ,
builder . In ( "repo_id" , repoIDs ) ,
// newly created branch have no changes, so skip them
builder . Neq { "commit_id" : baseBranch . CommitID } ,
) ) .
OrderBy ( db . SearchOrderByRecentUpdated . String ( ) ) .
Find ( & branches ) ; err != nil {
return nil , err
}
newBranches := make ( [ ] * RecentlyPushedNewBranch , 0 , len ( branches ) )
if opts . MaxCount == 0 {
// by default we display 2 recently pushed new branch
opts . MaxCount = 2
}
for _ , branch := range branches {
// whether branch have already created PR
count , err := db . GetEngine ( ctx ) . Table ( "pull_request" ) .
// we should not only use branch name here, because if there are branches with same name in other repos,
// we can not detect them correctly
Where ( builder . Eq { "head_repo_id" : branch . RepoID , "head_branch" : branch . Name } ) . Count ( )
if err != nil {
return nil , err
}
// if no PR, we add to the result
if count == 0 {
if err := branch . LoadRepo ( ctx ) ; err != nil {
return nil , err
}
branchDisplayName := branch . Name
if branch . Repo . ID != opts . BaseRepo . ID && branch . Repo . ID != opts . Repo . ID {
branchDisplayName = fmt . Sprintf ( "%s:%s" , branch . Repo . FullName ( ) , branchDisplayName )
}
newBranches = append ( newBranches , & RecentlyPushedNewBranch {
BranchDisplayName : branchDisplayName ,
BranchLink : fmt . Sprintf ( "%s/src/branch/%s" , branch . Repo . Link ( ) , util . PathEscapeSegments ( branch . Name ) ) ,
BranchCompareURL : branch . Repo . ComposeBranchCompareURL ( opts . BaseRepo , branch . Name ) ,
CommitTime : branch . CommitTime ,
} )
}
if len ( newBranches ) == opts . MaxCount {
break
}
}
return newBranches , nil
}