mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-29 01:21:13 +02:00
This PR replaces a set of struct-based `Get` lookups with explicit `db.Get` / `db.Exist` conditions in places where zero-value fields can lead to ambiguous matches or incorrect records being returned. The main goal is to make read paths deterministic and avoid accidentally matching the wrong row when only part of a struct is populated. ### What changed - replace many `db.GetEngine(ctx).Get(bean)` calls with explicit `builder.Eq` conditions across models such as actions, admin tasks, issues, pull requests, repositories, users, packages, redirects, watches, stars, and follows - use quoted column names where needed for reserved fields like `index`, `type`, and `name` - add dedicated user lookup helpers for: - primary email - OAuth login source / login name - update sign-in and OAuth-related flows to use explicit individual-user lookups instead of partially populated `User` structs - tighten package property and Terraform lock lookups to avoid ambiguous reads and updates - keep existing fallback behavior where needed, while removing reliance on zero-value struct matching ### User-facing impact These changes primarily affect authentication and account lookup paths: - email/username sign-in now re-fetches users through explicit keys - OAuth2 auto-linking now resolves users by name or primary email explicitly - OAuth2 login/sync now looks up users by login source, login type, and login name explicitly - non-individual accounts are no longer implicitly matched through partial user lookups in these flows This should reduce the risk of incorrect account matches and make query behavior more predictable across the codebase. --------- Co-authored-by: bircni <bircni@icloud.com>
589 lines
18 KiB
Go
589 lines
18 KiB
Go
// Copyright 2016 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package git
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"gitea.dev/models/db"
|
|
repo_model "gitea.dev/models/repo"
|
|
"gitea.dev/models/unit"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/container"
|
|
"gitea.dev/modules/git"
|
|
"gitea.dev/modules/log"
|
|
"gitea.dev/modules/optional"
|
|
"gitea.dev/modules/timeutil"
|
|
"gitea.dev/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,
|
|
}
|
|
}
|
|
// FIXME: this design is not right: it doesn't check `branch.IsDeleted`, it doesn't make sense to make callers to check IsDeleted again and again.
|
|
// It causes inconsistency with `GetBranches` and `git.GetBranch`, and will lead to strange bugs
|
|
// In the future, there should be 2 functions: `GetBranchExisting` and `GetBranchWithDeleted`
|
|
return &branch, nil
|
|
}
|
|
|
|
// IsBranchExist returns true if the branch exists in the repository.
|
|
func IsBranchExist(ctx context.Context, repoID int64, branchName string) (bool, error) {
|
|
var branch Branch
|
|
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("name=?", branchName).Get(&branch)
|
|
if err != nil {
|
|
return false, err
|
|
} else if !has {
|
|
return false, nil
|
|
}
|
|
return !branch.IsDeleted, nil
|
|
}
|
|
|
|
func GetBranches(ctx context.Context, repoID int64, branchNames []string, includeDeleted bool) ([]*Branch, error) {
|
|
branches := make([]*Branch, 0, len(branchNames))
|
|
|
|
sess := db.GetEngine(ctx).Where("repo_id=?", repoID).In("name", branchNames)
|
|
if !includeDeleted {
|
|
sess.And("is_deleted=?", false)
|
|
}
|
|
return branches, sess.Find(&branches)
|
|
}
|
|
|
|
func BranchesToNamesSet(branches []*Branch) container.Set[string] {
|
|
names := make(container.Set[string], len(branches))
|
|
for _, branch := range branches {
|
|
names.Add(branch.Name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
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 DeleteRepoBranches(ctx context.Context, repoID int64) error {
|
|
_, err := db.GetEngine(ctx).Where("repo_id=?", repoID).Delete(new(Branch))
|
|
return err
|
|
}
|
|
|
|
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 := MarkBranchAsDeleted(ctx, repoID, branch.Name, doerID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// 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.MessageTitle(),
|
|
PusherID: pusherID,
|
|
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
|
|
IsDeleted: false,
|
|
})
|
|
}
|
|
|
|
// MarkBranchAsDeleted marks branch as deleted
|
|
func MarkBranchAsDeleted(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) {
|
|
return db.Get[RenamedBranch](ctx, builder.Eq{"repo_id": repoID, "`from`": from})
|
|
}
|
|
|
|
// 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) {
|
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
sess := db.GetEngine(ctx)
|
|
|
|
// check whether from branch exist
|
|
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).Cols("name").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
|
|
existingRule, err := GetProtectedBranchRuleByName(ctx, repo.ID, to)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if existingRule == nil || existingRule.ID == protectedBranch.ID {
|
|
protectedBranch.RuleName = to
|
|
if _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch); 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
|
|
}
|
|
|
|
// 4.1 Update all not merged pull request head branch name
|
|
if _, err = sess.Table("pull_request").Where("head_repo_id=? AND head_branch=? AND has_merged=?",
|
|
repo.ID, from, false).
|
|
Update(map[string]any{"head_branch": to}); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 5. insert renamed branch record
|
|
if err = db.Insert(ctx, &RenamedBranch{
|
|
RepoID: repo.ID,
|
|
From: from,
|
|
To: to,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 6. do git action
|
|
return gitAction(ctx, isDefault)
|
|
})
|
|
}
|
|
|
|
type FindRecentlyPushedNewBranchesOptions struct {
|
|
Repo *repo_model.Repository
|
|
BaseRepo *repo_model.Repository
|
|
PushedAfterUnix int64
|
|
MaxCount int
|
|
}
|
|
|
|
type RecentlyPushedNewBranch struct {
|
|
BranchRepo *repo_model.Repository
|
|
BranchName string
|
|
BranchDisplayName string
|
|
BranchLink string
|
|
BranchCompareURL string
|
|
PushedTime timeutil.TimeStamp
|
|
}
|
|
|
|
// FindRecentlyPushedNewBranches return at most 2 new branches pushed by the user in 2 hours which has no opened PRs created
|
|
// if opts.PushedAfterUnix is 0, we will find the branches that were pushed in the last 2 hours
|
|
// if opts.ListOptions is not set, we will only display top 2 latest branches.
|
|
// Protected branches will be skipped since they are unlikely to be used to create new PRs.
|
|
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.PushedAfterUnix == 0 {
|
|
opts.PushedAfterUnix = time.Now().Add(-time.Hour * 2).Unix()
|
|
}
|
|
|
|
var ignoredCommitIDs []string
|
|
baseDefaultBranch, err := GetBranch(ctx, opts.BaseRepo.ID, opts.BaseRepo.DefaultBranch)
|
|
if err != nil {
|
|
log.Warn("GetBranch:DefaultBranch: %v", err)
|
|
} else {
|
|
ignoredCommitIDs = append(ignoredCommitIDs, baseDefaultBranch.CommitID)
|
|
}
|
|
|
|
baseDefaultTargetBranchName := opts.BaseRepo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig().DefaultTargetBranch
|
|
if baseDefaultTargetBranchName != "" && baseDefaultTargetBranchName != opts.BaseRepo.DefaultBranch {
|
|
baseDefaultTargetBranch, err := GetBranch(ctx, opts.BaseRepo.ID, baseDefaultTargetBranchName)
|
|
if err != nil {
|
|
log.Warn("GetBranch:DefaultTargetBranch: %v", err)
|
|
} else {
|
|
ignoredCommitIDs = append(ignoredCommitIDs, baseDefaultTargetBranch.CommitID)
|
|
}
|
|
}
|
|
|
|
// find all related branches, these branches may already have 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{"updated_unix": opts.PushedAfterUnix},
|
|
builder.In("repo_id", repoIDs),
|
|
// newly created branch have no changes, so skip them
|
|
builder.NotIn("commit_id", ignoredCommitIDs),
|
|
)).
|
|
OrderBy(db.SearchOrderByRecentUpdated.String()).
|
|
Find(&branches); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newBranches := make([]*RecentlyPushedNewBranch, 0, len(branches))
|
|
opts.MaxCount = util.IfZero(opts.MaxCount, 2) // by default, we display 2 recently pushed new branch
|
|
baseTargetBranchName := opts.BaseRepo.GetPullRequestTargetBranch(ctx)
|
|
for _, branch := range branches {
|
|
// whether the branch is protected
|
|
protected, err := IsBranchProtected(ctx, branch.RepoID, branch.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("IsBranchProtected: %v", err)
|
|
}
|
|
if protected {
|
|
// Skip protected branches,
|
|
// since updates to protected branches often come from PR merges,
|
|
// and they are unlikely to be used to create new PRs.
|
|
continue
|
|
}
|
|
|
|
// 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{
|
|
BranchRepo: branch.Repo,
|
|
BranchDisplayName: branchDisplayName,
|
|
BranchName: branch.Name,
|
|
BranchLink: fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)),
|
|
BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, baseTargetBranchName, branch.Name),
|
|
PushedTime: branch.UpdatedUnix,
|
|
})
|
|
}
|
|
if len(newBranches) == opts.MaxCount {
|
|
break
|
|
}
|
|
}
|
|
|
|
return newBranches, nil
|
|
}
|
|
|
|
// CountBranches returns the number of branches in the repository
|
|
func CountBranches(ctx context.Context, repoID int64, includeDeleted bool) (int64, error) {
|
|
sess := db.GetEngine(ctx).Where("repo_id=?", repoID)
|
|
if !includeDeleted {
|
|
sess.And("is_deleted=?", false)
|
|
}
|
|
return sess.Count(new(Branch))
|
|
}
|