1
1
mirror of https://github.com/go-gitea/gitea synced 2025-02-11 01:04:46 +00:00

Merge branch 'main' into lunny/merge_getuserfork

This commit is contained in:
Lunny Xiao 2024-12-12 12:00:36 -08:00
commit 09e02c1c4f
33 changed files with 463 additions and 161 deletions

View File

@ -10,7 +10,7 @@ concurrency:
jobs: jobs:
nightly-binary: nightly-binary:
runs-on: nscloud runs-on: namespace-profile-gitea-release-binary
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -58,7 +58,7 @@ jobs:
run: | run: |
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
nightly-docker-rootful: nightly-docker-rootful:
runs-on: ubuntu-latest runs-on: namespace-profile-gitea-release-docker
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -95,7 +95,7 @@ jobs:
push: true push: true
tags: gitea/gitea:${{ steps.clean_name.outputs.branch }} tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}
nightly-docker-rootless: nightly-docker-rootless:
runs-on: ubuntu-latest runs-on: namespace-profile-gitea-release-docker
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions

View File

@ -11,7 +11,7 @@ concurrency:
jobs: jobs:
binary: binary:
runs-on: nscloud runs-on: namespace-profile-gitea-release-binary
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -68,7 +68,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
docker-rootful: docker-rootful:
runs-on: ubuntu-latest runs-on: namespace-profile-gitea-release-docker
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -99,7 +99,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
docker-rootless: docker-rootless:
runs-on: ubuntu-latest runs-on: namespace-profile-gitea-release-docker
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions

View File

@ -13,7 +13,7 @@ concurrency:
jobs: jobs:
binary: binary:
runs-on: nscloud runs-on: namespace-profile-gitea-release-binary
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -70,7 +70,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
docker-rootful: docker-rootful:
runs-on: ubuntu-latest runs-on: namespace-profile-gitea-release-docker
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -105,7 +105,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
docker-rootless: docker-rootless:
runs-on: ubuntu-latest runs-on: namespace-profile-gitea-release-docker
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions

View File

@ -37,6 +37,7 @@ type ActionRun struct {
TriggerUser *user_model.User `xorm:"-"` TriggerUser *user_model.User `xorm:"-"`
ScheduleID int64 ScheduleID int64
Ref string `xorm:"index"` // the commit/tag/… that caused the run Ref string `xorm:"index"` // the commit/tag/… that caused the run
IsRefDeleted bool `xorm:"-"`
CommitSHA string CommitSHA string
IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow. IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow.
NeedApproval bool // may need approval if it's a fork pull request NeedApproval bool // may need approval if it's a fork pull request

View File

@ -12,6 +12,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
@ -169,9 +170,22 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
return &branch, nil return &branch, nil
} }
func GetBranches(ctx context.Context, repoID int64, branchNames []string) ([]*Branch, error) { func GetBranches(ctx context.Context, repoID int64, branchNames []string, includeDeleted bool) ([]*Branch, error) {
branches := make([]*Branch, 0, len(branchNames)) branches := make([]*Branch, 0, len(branchNames))
return branches, db.GetEngine(ctx).Where("repo_id=?", repoID).In("name", branchNames).Find(&branches)
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 { func AddBranches(ctx context.Context, branches []*Branch) error {

View File

@ -474,3 +474,17 @@ func (c *Commit) GetRepositoryDefaultPublicGPGKey(forceUpdate bool) (*GPGSetting
} }
return c.repo.GetDefaultPublicGPGKey(forceUpdate) return c.repo.GetDefaultPublicGPGKey(forceUpdate)
} }
func IsStringLikelyCommitID(objFmt ObjectFormat, s string, minLength ...int) bool {
minLen := util.OptionalArg(minLength, objFmt.FullLength())
if len(s) < minLen || len(s) > objFmt.FullLength() {
return false
}
for _, c := range s {
isHex := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')
if !isHex {
return false
}
}
return true
}

View File

@ -142,7 +142,6 @@ func (ref RefName) RemoteName() string {
// ShortName returns the short name of the reference name // ShortName returns the short name of the reference name
func (ref RefName) ShortName() string { func (ref RefName) ShortName() string {
refName := string(ref)
if ref.IsBranch() { if ref.IsBranch() {
return ref.BranchName() return ref.BranchName()
} }
@ -158,8 +157,7 @@ func (ref RefName) ShortName() string {
if ref.IsFor() { if ref.IsFor() {
return ref.ForBranchName() return ref.ForBranchName()
} }
return string(ref) // usually it is a commit ID
return refName
} }
// RefGroup returns the group type of the reference // RefGroup returns the group type of the reference

View File

@ -61,3 +61,31 @@ func parseTags(refs []string) []string {
} }
return results return results
} }
// UnstableGuessRefByShortName does the best guess to see whether a "short name" provided by user is a branch, tag or commit.
// It could guess wrongly if the input is already ambiguous. For example:
// * "refs/heads/the-name" vs "refs/heads/refs/heads/the-name"
// * "refs/tags/1234567890" vs commit "1234567890"
// In most cases, it SHOULD AVOID using this function, unless there is an irresistible reason (eg: make API friendly to end users)
// If the function is used, the caller SHOULD CHECK the ref type carefully.
func (repo *Repository) UnstableGuessRefByShortName(shortName string) RefName {
if repo.IsBranchExist(shortName) {
return RefNameFromBranch(shortName)
}
if repo.IsTagExist(shortName) {
return RefNameFromTag(shortName)
}
if strings.HasPrefix(shortName, "refs/") {
if repo.IsReferenceExist(shortName) {
return RefName(shortName)
}
}
commit, err := repo.GetCommit(shortName)
if err == nil {
commitIDString := commit.ID.String()
if strings.HasPrefix(commitIDString, shortName) {
return RefName(commitIDString)
}
}
return ""
}

View File

@ -64,7 +64,7 @@ func TestLockAndDo(t *testing.T) {
} }
func testLockAndDo(t *testing.T) { func testLockAndDo(t *testing.T) {
const concurrency = 1000 const concurrency = 50
ctx := context.Background() ctx := context.Background()
count := 0 count := 0

View File

@ -278,6 +278,16 @@ type CreateBranchRepoOption struct {
OldRefName string `json:"old_ref_name" binding:"GitRefName;MaxSize(100)"` OldRefName string `json:"old_ref_name" binding:"GitRefName;MaxSize(100)"`
} }
// UpdateBranchRepoOption options when updating a branch in a repository
// swagger:model
type UpdateBranchRepoOption struct {
// New branch name
//
// required: true
// unique: true
Name string `json:"name" binding:"Required;GitRefName;MaxSize(100)"`
}
// TransferRepoOption options when transfer a repository's ownership // TransferRepoOption options when transfer a repository's ownership
// swagger:model // swagger:model
type TransferRepoOption struct { type TransferRepoOption struct {

View File

@ -1195,6 +1195,7 @@ func Routes() *web.Router {
m.Get("/*", repo.GetBranch) m.Get("/*", repo.GetBranch)
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch) m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch)
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), repo.CreateBranch) m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
m.Patch("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.UpdateBranchRepoOption{}), repo.UpdateBranch)
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode)) }, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
m.Group("/branch_protections", func() { m.Group("/branch_protections", func() {
m.Get("", repo.ListBranchProtections) m.Get("", repo.ListBranchProtections)

View File

@ -386,6 +386,77 @@ func ListBranches(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, apiBranches) ctx.JSON(http.StatusOK, apiBranches)
} }
// UpdateBranch updates a repository's branch.
func UpdateBranch(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/branches/{branch} repository repoUpdateBranch
// ---
// summary: Update a branch
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: branch
// in: path
// description: name of the branch
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UpdateBranchRepoOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
opt := web.GetForm(ctx).(*api.UpdateBranchRepoOption)
oldName := ctx.PathParam("*")
repo := ctx.Repo.Repository
if repo.IsEmpty {
ctx.Error(http.StatusNotFound, "", "Git Repository is empty.")
return
}
if repo.IsMirror {
ctx.Error(http.StatusForbidden, "", "Git Repository is a mirror.")
return
}
msg, err := repo_service.RenameBranch(ctx, repo, ctx.Doer, ctx.Repo.GitRepo, oldName, opt.Name)
if err != nil {
ctx.Error(http.StatusInternalServerError, "RenameBranch", err)
return
}
if msg == "target_exist" {
ctx.Error(http.StatusUnprocessableEntity, "", "Cannot rename a branch using the same name or rename to a branch that already exists.")
return
}
if msg == "from_not_exist" {
ctx.Error(http.StatusNotFound, "", "Branch doesn't exist.")
return
}
ctx.Status(http.StatusNoContent)
}
// GetBranchProtection gets a branch protection // GetBranchProtection gets a branch protection
func GetBranchProtection(ctx *context.APIContext) { func GetBranchProtection(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/branch_protections/{name} repository repoGetBranchProtection // swagger:operation GET /repos/{owner}/{repo}/branch_protections/{name} repository repoGetBranchProtection

View File

@ -64,22 +64,19 @@ func CompareDiff(ctx *context.APIContext) {
} }
} }
_, headGitRepo, ci, _, _ := parseCompareInfo(ctx, api.CreatePullRequestOption{ compareResult, closer := parseCompareInfo(ctx, api.CreatePullRequestOption{Base: infos[0], Head: infos[1]})
Base: infos[0],
Head: infos[1],
})
if ctx.Written() { if ctx.Written() {
return return
} }
defer headGitRepo.Close() defer closer()
verification := ctx.FormString("verification") == "" || ctx.FormBool("verification") verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
files := ctx.FormString("files") == "" || ctx.FormBool("files") files := ctx.FormString("files") == "" || ctx.FormBool("files")
apiCommits := make([]*api.Commit, 0, len(ci.Commits)) apiCommits := make([]*api.Commit, 0, len(compareResult.compareInfo.Commits))
userCache := make(map[string]*user_model.User) userCache := make(map[string]*user_model.User)
for i := 0; i < len(ci.Commits); i++ { for i := 0; i < len(compareResult.compareInfo.Commits); i++ {
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ci.Commits[i], userCache, apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, compareResult.compareInfo.Commits[i], userCache,
convert.ToCommitOptions{ convert.ToCommitOptions{
Stat: true, Stat: true,
Verification: verification, Verification: verification,
@ -93,7 +90,7 @@ func CompareDiff(ctx *context.APIContext) {
} }
ctx.JSON(http.StatusOK, &api.Compare{ ctx.JSON(http.StatusOK, &api.Compare{
TotalCommits: len(ci.Commits), TotalCommits: len(compareResult.compareInfo.Commits),
Commits: apiCommits, Commits: apiCommits,
}) })
} }

View File

@ -389,8 +389,7 @@ func CreatePullRequest(ctx *context.APIContext) {
form := *web.GetForm(ctx).(*api.CreatePullRequestOption) form := *web.GetForm(ctx).(*api.CreatePullRequestOption)
if form.Head == form.Base { if form.Head == form.Base {
ctx.Error(http.StatusUnprocessableEntity, "BaseHeadSame", ctx.Error(http.StatusUnprocessableEntity, "BaseHeadSame", "Invalid PullRequest: There are no changes between the head and the base")
"Invalid PullRequest: There are no changes between the head and the base")
return return
} }
@ -401,14 +400,22 @@ func CreatePullRequest(ctx *context.APIContext) {
) )
// Get repo/branch information // Get repo/branch information
headRepo, headGitRepo, compareInfo, baseBranch, headBranch := parseCompareInfo(ctx, form) compareResult, closer := parseCompareInfo(ctx, form)
if ctx.Written() { if ctx.Written() {
return return
} }
defer headGitRepo.Close() defer closer()
if !compareResult.baseRef.IsBranch() || !compareResult.headRef.IsBranch() {
ctx.Error(http.StatusUnprocessableEntity, "BaseHeadInvalidRefType", "Invalid PullRequest: base and head must be branches")
return
}
// Check if another PR exists with the same targets // Check if another PR exists with the same targets
existingPr, err := issues_model.GetUnmergedPullRequest(ctx, headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, issues_model.PullRequestFlowGithub) existingPr, err := issues_model.GetUnmergedPullRequest(ctx, compareResult.headRepo.ID, ctx.Repo.Repository.ID,
compareResult.headRef.ShortName(), compareResult.baseRef.ShortName(),
issues_model.PullRequestFlowGithub,
)
if err != nil { if err != nil {
if !issues_model.IsErrPullRequestNotExist(err) { if !issues_model.IsErrPullRequestNotExist(err) {
ctx.Error(http.StatusInternalServerError, "GetUnmergedPullRequest", err) ctx.Error(http.StatusInternalServerError, "GetUnmergedPullRequest", err)
@ -484,13 +491,13 @@ func CreatePullRequest(ctx *context.APIContext) {
DeadlineUnix: deadlineUnix, DeadlineUnix: deadlineUnix,
} }
pr := &issues_model.PullRequest{ pr := &issues_model.PullRequest{
HeadRepoID: headRepo.ID, HeadRepoID: compareResult.headRepo.ID,
BaseRepoID: repo.ID, BaseRepoID: repo.ID,
HeadBranch: headBranch, HeadBranch: compareResult.headRef.ShortName(),
BaseBranch: baseBranch, BaseBranch: compareResult.baseRef.ShortName(),
HeadRepo: headRepo, HeadRepo: compareResult.headRepo,
BaseRepo: repo, BaseRepo: repo,
MergeBase: compareInfo.MergeBase, MergeBase: compareResult.compareInfo.MergeBase,
Type: issues_model.PullRequestGitea, Type: issues_model.PullRequestGitea,
} }
@ -1080,32 +1087,32 @@ func MergePullRequest(ctx *context.APIContext) {
ctx.Status(http.StatusOK) ctx.Status(http.StatusOK)
} }
func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (*repo_model.Repository, *git.Repository, *git.CompareInfo, string, string) { type parseCompareInfoResult struct {
baseRepo := ctx.Repo.Repository headRepo *repo_model.Repository
headGitRepo *git.Repository
compareInfo *git.CompareInfo
baseRef git.RefName
headRef git.RefName
}
// parseCompareInfo returns non-nil if it succeeds, it always writes to the context and returns nil if it fails
func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (result *parseCompareInfoResult, closer func()) {
var err error
// Get compared branches information // Get compared branches information
// format: <base branch>...[<head repo>:]<head branch> // format: <base branch>...[<head repo>:]<head branch>
// base<-head: master...head:feature // base<-head: master...head:feature
// same repo: master...feature // same repo: master...feature
baseRepo := ctx.Repo.Repository
baseRefToGuess := form.Base
// TODO: Validate form first? headUser := ctx.Repo.Owner
headRefToGuess := form.Head
baseBranch := form.Base if headInfos := strings.Split(form.Head, ":"); len(headInfos) == 1 {
// If there is no head repository, it means pull request between same repository.
var ( // Do nothing here because the head variables have been assigned above.
headUser *user_model.User
headBranch string
isSameRepo bool
err error
)
// If there is no head repository, it means pull request between same repository.
headInfos := strings.Split(form.Head, ":")
if len(headInfos) == 1 {
isSameRepo = true
headUser = ctx.Repo.Owner
headBranch = headInfos[0]
} else if len(headInfos) == 2 { } else if len(headInfos) == 2 {
// There is a head repository (the head repository could also be the same base repo)
headRefToGuess = headInfos[1]
headUser, err = user_model.GetUserByName(ctx, headInfos[0]) headUser, err = user_model.GetUserByName(ctx, headInfos[0])
if err != nil { if err != nil {
if user_model.IsErrUserNotExist(err) { if user_model.IsErrUserNotExist(err) {
@ -1113,23 +1120,14 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
} else { } else {
ctx.Error(http.StatusInternalServerError, "GetUserByName", err) ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
} }
return nil, nil, nil, "", "" return nil, nil
} }
headBranch = headInfos[1]
// The head repository can also point to the same repo
isSameRepo = ctx.Repo.Owner.ID == headUser.ID
} else { } else {
ctx.NotFound() ctx.NotFound()
return nil, nil, nil, "", "" return nil, nil
} }
ctx.Repo.PullRequest.SameRepo = isSameRepo isSameRepo := ctx.Repo.Owner.ID == headUser.ID
log.Trace("Repo path: %q, base branch: %q, head branch: %q", ctx.Repo.GitRepo.Path, baseBranch, headBranch)
// Check if base branch is valid.
if !ctx.Repo.GitRepo.IsBranchExist(baseBranch) && !ctx.Repo.GitRepo.IsTagExist(baseBranch) {
ctx.NotFound("BaseNotExist")
return nil, nil, nil, "", ""
}
// Check if current user has fork of repository or in the same repository. // Check if current user has fork of repository or in the same repository.
headRepo, err := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID) headRepo, err := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID)
@ -1138,17 +1136,17 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
return nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
if headRepo == nil && !isSameRepo { if headRepo == nil && !isSameRepo {
err := baseRepo.GetBaseRepo(ctx) err = baseRepo.GetBaseRepo(ctx)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetBaseRepo", err) ctx.Error(http.StatusInternalServerError, "GetBaseRepo", err)
return nil, nil, nil, "", "" return nil, nil
} }
// Check if baseRepo's base repository is the same as headUser's repository. // Check if baseRepo's base repository is the same as headUser's repository.
if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID { if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID {
log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID) log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
ctx.NotFound("GetBaseRepo") ctx.NotFound("GetBaseRepo")
return nil, nil, nil, "", "" return nil, nil
} }
// Assign headRepo so it can be used below. // Assign headRepo so it can be used below.
headRepo = baseRepo.BaseRepo headRepo = baseRepo.BaseRepo
@ -1158,67 +1156,68 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
if isSameRepo { if isSameRepo {
headRepo = ctx.Repo.Repository headRepo = ctx.Repo.Repository
headGitRepo = ctx.Repo.GitRepo headGitRepo = ctx.Repo.GitRepo
closer = func() {} // no need to close the head repo because it shares the base repo
} else { } else {
headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo) headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "OpenRepository", err) ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
return nil, nil, nil, "", "" return nil, nil
} }
closer = func() { _ = headGitRepo.Close() }
} }
defer func() {
if result == nil && !isSameRepo {
_ = headGitRepo.Close()
}
}()
// user should have permission to read baseRepo's codes and pulls, NOT headRepo's // user should have permission to read baseRepo's codes and pulls, NOT headRepo's
permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer) permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
if err != nil { if err != nil {
headGitRepo.Close()
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
return nil, nil, nil, "", "" return nil, nil
}
if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) {
if log.IsTrace() {
log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v",
ctx.Doer,
baseRepo,
permBase)
}
headGitRepo.Close()
ctx.NotFound("Can't read pulls or can't read UnitTypeCode")
return nil, nil, nil, "", ""
} }
// user should have permission to read headrepo's codes if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) {
log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase)
ctx.NotFound("Can't read pulls or can't read UnitTypeCode")
return nil, nil
}
// user should have permission to read headRepo's codes
// TODO: could the logic be simplified if the headRepo is the same as the baseRepo? Need to think more about it.
permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer) permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer)
if err != nil { if err != nil {
headGitRepo.Close()
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
return nil, nil, nil, "", "" return nil, nil
} }
if !permHead.CanRead(unit.TypeCode) { if !permHead.CanRead(unit.TypeCode) {
if log.IsTrace() { log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", ctx.Doer, headRepo, permHead)
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
ctx.Doer,
headRepo,
permHead)
}
headGitRepo.Close()
ctx.NotFound("Can't read headRepo UnitTypeCode") ctx.NotFound("Can't read headRepo UnitTypeCode")
return nil, nil, nil, "", "" return nil, nil
} }
// Check if head branch is valid. baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefToGuess)
if !headGitRepo.IsBranchExist(headBranch) && !headGitRepo.IsTagExist(headBranch) { headRef := headGitRepo.UnstableGuessRefByShortName(headRefToGuess)
headGitRepo.Close()
log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.GitRepo.Path, baseRefToGuess, baseRef, headRefToGuess, headRef)
baseRefValid := baseRef.IsBranch() || baseRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName), baseRef.ShortName())
headRefValid := headRef.IsBranch() || headRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(headRepo.ObjectFormatName), headRef.ShortName())
// Check if base&head ref are valid.
if !baseRefValid || !headRefValid {
ctx.NotFound() ctx.NotFound()
return nil, nil, nil, "", "" return nil, nil
} }
compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch, false, false) compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseRef.ShortName(), headRef.ShortName(), false, false)
if err != nil { if err != nil {
headGitRepo.Close()
ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err) ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err)
return nil, nil, nil, "", "" return nil, nil
} }
return headRepo, headGitRepo, compareInfo, baseBranch, headBranch result = &parseCompareInfoResult{headRepo: headRepo, headGitRepo: headGitRepo, compareInfo: compareInfo, baseRef: baseRef, headRef: headRef}
return result, closer
} }
// UpdatePullRequest merge PR's baseBranch into headBranch // UpdatePullRequest merge PR's baseBranch into headBranch

View File

@ -90,6 +90,8 @@ type swaggerParameterBodies struct {
// in:body // in:body
EditRepoOption api.EditRepoOption EditRepoOption api.EditRepoOption
// in:body // in:body
UpdateBranchRepoOption api.UpdateBranchRepoOption
// in:body
TransferRepoOption api.TransferRepoOption TransferRepoOption api.TransferRepoOption
// in:body // in:body
CreateForkOption api.CreateForkOption CreateForkOption api.CreateForkOption

View File

@ -245,6 +245,10 @@ func List(ctx *context.Context) {
return return
} }
if err := loadIsRefDeleted(ctx, runs); err != nil {
log.Error("LoadIsRefDeleted", err)
}
ctx.Data["Runs"] = runs ctx.Data["Runs"] = runs
actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID) actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
@ -267,6 +271,34 @@ func List(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplListActions) ctx.HTML(http.StatusOK, tplListActions)
} }
// loadIsRefDeleted loads the IsRefDeleted field for each run in the list.
// TODO: move this function to models/actions/run_list.go but now it will result in a circular import.
func loadIsRefDeleted(ctx *context.Context, runs actions_model.RunList) error {
branches := make(container.Set[string], len(runs))
for _, run := range runs {
refName := git.RefName(run.Ref)
if refName.IsBranch() {
branches.Add(refName.ShortName())
}
}
if len(branches) == 0 {
return nil
}
branchInfos, err := git_model.GetBranches(ctx, ctx.Repo.Repository.ID, branches.Values(), false)
if err != nil {
return err
}
branchSet := git_model.BranchesToNamesSet(branchInfos)
for _, run := range runs {
refName := git.RefName(run.Ref)
if refName.IsBranch() && !branchSet.Contains(run.Ref) {
run.IsRefDeleted = true
}
}
return nil
}
type WorkflowDispatchInput struct { type WorkflowDispatchInput struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Description string `yaml:"description"` Description string `yaml:"description"`

View File

@ -19,6 +19,7 @@ import (
actions_model "code.gitea.io/gitea/models/actions" actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
@ -136,8 +137,9 @@ type ViewUser struct {
} }
type ViewBranch struct { type ViewBranch struct {
Name string `json:"name"` Name string `json:"name"`
Link string `json:"link"` Link string `json:"link"`
IsDeleted bool `json:"isDeleted"`
} }
type ViewJobStep struct { type ViewJobStep struct {
@ -236,6 +238,16 @@ func ViewPost(ctx *context_module.Context) {
Name: run.PrettyRef(), Name: run.PrettyRef(),
Link: run.RefLink(), Link: run.RefLink(),
} }
refName := git.RefName(run.Ref)
if refName.IsBranch() {
b, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, refName.ShortName())
if err != nil && !git_model.IsErrBranchNotExist(err) {
log.Error("GetBranch: %v", err)
} else if git_model.IsErrBranchNotExist(err) || (b != nil && b.IsDeleted) {
branch.IsDeleted = true
}
}
resp.State.Run.Commit = ViewCommit{ resp.State.Run.Commit = ViewCommit{
ShortSha: base.ShortSha(run.CommitSHA), ShortSha: base.ShortSha(run.CommitSHA),
Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA), Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),

View File

@ -9,7 +9,6 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"strings" "strings"
@ -114,7 +113,6 @@ func MustAllowPulls(ctx *context.Context) {
// User can send pull request if owns a forked repository. // User can send pull request if owns a forked repository.
if ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) { if ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) {
ctx.Repo.PullRequest.Allowed = true ctx.Repo.PullRequest.Allowed = true
ctx.Repo.PullRequest.HeadInfoSubURL = url.PathEscape(ctx.Doer.Name) + ":" + util.PathEscapeSegments(ctx.Repo.BranchName)
} }
} }

View File

@ -39,10 +39,9 @@ import (
// PullRequest contains information to make a pull request // PullRequest contains information to make a pull request
type PullRequest struct { type PullRequest struct {
BaseRepo *repo_model.Repository BaseRepo *repo_model.Repository
Allowed bool Allowed bool // it only used by the web tmpl: "PullRequestCtx.Allowed"
SameRepo bool SameRepo bool // it only used by the web tmpl: "PullRequestCtx.SameRepo"
HeadInfoSubURL string // [<user>:]<branch> url segment
} }
// Repository contains information to operate a repository // Repository contains information to operate a repository
@ -401,6 +400,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
// RepoAssignment returns a middleware to handle repository assignment // RepoAssignment returns a middleware to handle repository assignment
func RepoAssignment(ctx *Context) context.CancelFunc { func RepoAssignment(ctx *Context) context.CancelFunc {
if _, repoAssignmentOnce := ctx.Data["repoAssignmentExecuted"]; repoAssignmentOnce { if _, repoAssignmentOnce := ctx.Data["repoAssignmentExecuted"]; repoAssignmentOnce {
// FIXME: it should panic in dev/test modes to have a clear behavior
log.Trace("RepoAssignment was exec already, skipping second call ...") log.Trace("RepoAssignment was exec already, skipping second call ...")
return nil return nil
} }
@ -697,7 +697,6 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
ctx.Data["BaseRepo"] = repo.BaseRepo ctx.Data["BaseRepo"] = repo.BaseRepo
ctx.Repo.PullRequest.BaseRepo = repo.BaseRepo ctx.Repo.PullRequest.BaseRepo = repo.BaseRepo
ctx.Repo.PullRequest.Allowed = canPush ctx.Repo.PullRequest.Allowed = canPush
ctx.Repo.PullRequest.HeadInfoSubURL = url.PathEscape(ctx.Repo.Owner.Name) + ":" + util.PathEscapeSegments(ctx.Repo.BranchName)
} else if repo.AllowsPulls(ctx) { } else if repo.AllowsPulls(ctx) {
// Or, this is repository accepts pull requests between branches. // Or, this is repository accepts pull requests between branches.
canCompare = true canCompare = true
@ -705,7 +704,6 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
ctx.Repo.PullRequest.BaseRepo = repo ctx.Repo.PullRequest.BaseRepo = repo
ctx.Repo.PullRequest.Allowed = canPush ctx.Repo.PullRequest.Allowed = canPush
ctx.Repo.PullRequest.SameRepo = true ctx.Repo.PullRequest.SameRepo = true
ctx.Repo.PullRequest.HeadInfoSubURL = util.PathEscapeSegments(ctx.Repo.BranchName)
} }
ctx.Data["CanCompareOrPull"] = canCompare ctx.Data["CanCompareOrPull"] = canCompare
ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequest ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequest
@ -771,20 +769,6 @@ func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool
return "" return ""
} }
func isStringLikelyCommitID(objFmt git.ObjectFormat, s string, minLength ...int) bool {
minLen := util.OptionalArg(minLength, objFmt.FullLength())
if len(s) < minLen || len(s) > objFmt.FullLength() {
return false
}
for _, c := range s {
isHex := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')
if !isHex {
return false
}
}
return true
}
func getRefNameLegacy(ctx *Base, repo *Repository, optionalExtraRef ...string) (string, RepoRefType) { func getRefNameLegacy(ctx *Base, repo *Repository, optionalExtraRef ...string) (string, RepoRefType) {
extraRef := util.OptionalArg(optionalExtraRef) extraRef := util.OptionalArg(optionalExtraRef)
reqPath := ctx.PathParam("*") reqPath := ctx.PathParam("*")
@ -799,7 +783,7 @@ func getRefNameLegacy(ctx *Base, repo *Repository, optionalExtraRef ...string) (
// For legacy support only full commit sha // For legacy support only full commit sha
parts := strings.Split(reqPath, "/") parts := strings.Split(reqPath, "/")
if isStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), parts[0]) { if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), parts[0]) {
// FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists // FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists
repo.TreePath = strings.Join(parts[1:], "/") repo.TreePath = strings.Join(parts[1:], "/")
return parts[0], RepoRefCommit return parts[0], RepoRefCommit
@ -849,7 +833,7 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
return getRefNameFromPath(repo, path, repo.GitRepo.IsTagExist) return getRefNameFromPath(repo, path, repo.GitRepo.IsTagExist)
case RepoRefCommit: case RepoRefCommit:
parts := strings.Split(path, "/") parts := strings.Split(path, "/")
if isStringLikelyCommitID(repo.GetObjectFormat(), parts[0], 7) { if git.IsStringLikelyCommitID(repo.GetObjectFormat(), parts[0], 7) {
// FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists // FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists
repo.TreePath = strings.Join(parts[1:], "/") repo.TreePath = strings.Join(parts[1:], "/")
return parts[0] return parts[0]
@ -985,7 +969,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
return cancel return cancel
} }
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
} else if isStringLikelyCommitID(ctx.Repo.GetObjectFormat(), refName, 7) { } else if git.IsStringLikelyCommitID(ctx.Repo.GetObjectFormat(), refName, 7) {
ctx.Repo.IsViewCommit = true ctx.Repo.IsViewCommit = true
ctx.Repo.CommitID = refName ctx.Repo.CommitID = refName

View File

@ -305,7 +305,7 @@ func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames,
} }
return db.WithTx(ctx, func(ctx context.Context) error { return db.WithTx(ctx, func(ctx context.Context) error {
branches, err := git_model.GetBranches(ctx, repoID, branchNames) branches, err := git_model.GetBranches(ctx, repoID, branchNames, true)
if err != nil { if err != nil {
return fmt.Errorf("git_model.GetBranches: %v", err) return fmt.Errorf("git_model.GetBranches: %v", err)
} }

View File

@ -27,10 +27,10 @@
</div> </div>
</div> </div>
<div class="flex-item-trailing"> <div class="flex-item-trailing">
{{if .RefLink}} {{if .IsRefDeleted}}
<a class="ui label run-list-ref gt-ellipsis" href="{{.RefLink}}">{{.PrettyRef}}</a> <span class="ui label run-list-ref gt-ellipsis tw-line-through" data-tooltip-content="{{.PrettyRef}}">{{.PrettyRef}}</span>
{{else}} {{else}}
<span class="ui label run-list-ref gt-ellipsis">{{.PrettyRef}}</span> <a class="ui label run-list-ref gt-ellipsis" href="{{.RefLink}}" data-tooltip-content="{{.PrettyRef}}">{{.PrettyRef}}</a>
{{end}} {{end}}
<div class="run-list-item-right"> <div class="run-list-item-right">
<div class="run-list-meta">{{svg "octicon-calendar" 16}}{{DateUtils.TimeSince .Updated}}</div> <div class="run-list-meta">{{svg "octicon-calendar" 16}}{{DateUtils.TimeSince .Updated}}</div>

View File

@ -1,6 +1,7 @@
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeCode}} {{if .Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
<div id="repo-contributors-chart" <div id="repo-contributors-chart"
data-repo-link="{{.RepoLink}}" data-repo-link="{{.RepoLink}}"
data-repo-default-branch-name="{{.Repository.DefaultBranch}}"
data-locale-filter-label="{{ctx.Locale.Tr "repo.contributors.contribution_type.filter_label"}}" data-locale-filter-label="{{ctx.Locale.Tr "repo.contributors.contribution_type.filter_label"}}"
data-locale-contribution-type-commits="{{ctx.Locale.Tr "repo.contributors.contribution_type.commits"}}" data-locale-contribution-type-commits="{{ctx.Locale.Tr "repo.contributors.contribution_type.commits"}}"
data-locale-contribution-type-additions="{{ctx.Locale.Tr "repo.contributors.contribution_type.additions"}}" data-locale-contribution-type-additions="{{ctx.Locale.Tr "repo.contributors.contribution_type.additions"}}"

View File

@ -167,8 +167,8 @@
<button class="btn diff-header-popup-btn tw-p-1">{{svg "octicon-kebab-horizontal" 18}}</button> <button class="btn diff-header-popup-btn tw-p-1">{{svg "octicon-kebab-horizontal" 18}}</button>
<div class="tippy-target"> <div class="tippy-target">
{{if not (or $file.IsIncomplete $file.IsBin $file.IsSubmodule)}} {{if not (or $file.IsIncomplete $file.IsBin $file.IsSubmodule)}}
<button class="unescape-button item" data-file-content-elem-id="diff-{{$file.NameHash}}">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button> <button class="unescape-button item" data-unicode-content-selector="#diff-{{$file.NameHash}}">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
<button class="escape-button tw-hidden item" data-file-content-elem-id="diff-{{$file.NameHash}}">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button> <button class="escape-button tw-hidden item" data-unicode-content-selector="#diff-{{$file.NameHash}}">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
{{end}} {{end}}
{{if and (not $file.IsSubmodule) (not $.PageIsWiki)}} {{if and (not $file.IsSubmodule) (not $.PageIsWiki)}}
{{if $file.IsDeleted}} {{if $file.IsDeleted}}

View File

@ -44,13 +44,13 @@
</div> </div>
<div class="repo-button-row"> <div class="repo-button-row">
{{if .EscapeStatus.Escaped}} {{if .EscapeStatus.Escaped}}
<a class="ui small button unescape-button tw-m-0 tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a> <a class="ui small button unescape-button tw-hidden" data-unicode-content-selector=".wiki-content-parts">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a>
<a class="ui small button escape-button tw-m-0">{{ctx.Locale.Tr "repo.escape_control_characters"}}</a> <a class="ui small button escape-button" data-unicode-content-selector=".wiki-content-parts">{{ctx.Locale.Tr "repo.escape_control_characters"}}</a>
{{end}} {{end}}
{{if and .CanWriteWiki (not .Repository.IsMirror)}} {{if and .CanWriteWiki (not .Repository.IsMirror)}}
<a class="ui small button" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_edit">{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}</a> <a class="ui small button" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_edit">{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}</a>
<a class="ui small primary button" href="{{.RepoLink}}/wiki?action=_new">{{ctx.Locale.Tr "repo.wiki.new_page_button"}}</a> <a class="ui small primary button" href="{{.RepoLink}}/wiki?action=_new">{{ctx.Locale.Tr "repo.wiki.new_page_button"}}</a>
<a class="ui small red button tw-m-0 delete-button" href="" data-url="{{.RepoLink}}/wiki/{{.PageURL}}?action=_delete" data-id="{{.PageURL}}">{{ctx.Locale.Tr "repo.wiki.delete_page_button"}}</a> <a class="ui small red button delete-button" href="" data-url="{{.RepoLink}}/wiki/{{.PageURL}}?action=_delete" data-id="{{.PageURL}}">{{ctx.Locale.Tr "repo.wiki.delete_page_button"}}</a>
{{end}} {{end}}
</div> </div>
</div> </div>

View File

@ -5045,6 +5045,63 @@
"$ref": "#/responses/repoArchivedError" "$ref": "#/responses/repoArchivedError"
} }
} }
},
"patch": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Update a branch",
"operationId": "repoUpdateBranch",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the branch",
"name": "branch",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/UpdateBranchRepoOption"
}
}
],
"responses": {
"204": {
"$ref": "#/responses/empty"
},
"403": {
"$ref": "#/responses/forbidden"
},
"404": {
"$ref": "#/responses/notFound"
},
"422": {
"$ref": "#/responses/validationError"
}
}
} }
}, },
"/repos/{owner}/{repo}/collaborators": { "/repos/{owner}/{repo}/collaborators": {
@ -24968,6 +25025,22 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"UpdateBranchRepoOption": {
"description": "UpdateBranchRepoOption options when updating a branch in a repository",
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"description": "New branch name",
"type": "string",
"uniqueItems": true,
"x-go-name": "Name"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"UpdateFileOptions": { "UpdateFileOptions": {
"description": "UpdateFileOptions options for updating files\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", "description": "UpdateFileOptions options for updating files\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)",
"type": "object", "type": "object",

View File

@ -5,6 +5,7 @@ package integration
import ( import (
"net/http" "net/http"
"net/http/httptest"
"net/url" "net/url"
"testing" "testing"
@ -186,6 +187,37 @@ func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBran
return resp.Result().StatusCode == status return resp.Result().StatusCode == status
} }
func TestAPIUpdateBranch(t *testing.T) {
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
t.Run("UpdateBranchWithEmptyRepo", func(t *testing.T) {
testAPIUpdateBranch(t, "user10", "repo6", "master", "test", http.StatusNotFound)
})
t.Run("UpdateBranchWithSameBranchNames", func(t *testing.T) {
resp := testAPIUpdateBranch(t, "user2", "repo1", "master", "master", http.StatusUnprocessableEntity)
assert.Contains(t, resp.Body.String(), "Cannot rename a branch using the same name or rename to a branch that already exists.")
})
t.Run("UpdateBranchThatAlreadyExists", func(t *testing.T) {
resp := testAPIUpdateBranch(t, "user2", "repo1", "master", "branch2", http.StatusUnprocessableEntity)
assert.Contains(t, resp.Body.String(), "Cannot rename a branch using the same name or rename to a branch that already exists.")
})
t.Run("UpdateBranchWithNonExistentBranch", func(t *testing.T) {
resp := testAPIUpdateBranch(t, "user2", "repo1", "i-dont-exist", "new-branch-name", http.StatusNotFound)
assert.Contains(t, resp.Body.String(), "Branch doesn't exist.")
})
t.Run("RenameBranchNormalScenario", func(t *testing.T) {
testAPIUpdateBranch(t, "user2", "repo1", "branch2", "new-branch-name", http.StatusNoContent)
})
})
}
func testAPIUpdateBranch(t *testing.T, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder {
token := getUserToken(t, ownerName, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, "PATCH", "api/v1/repos/"+ownerName+"/"+repoName+"/branches/"+from, &api.UpdateBranchRepoOption{
Name: to,
}).AddTokenAuth(token)
return MakeRequest(t, req, expectedHTTPStatus)
}
func TestAPIBranchProtection(t *testing.T) { func TestAPIBranchProtection(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()

View File

@ -24,15 +24,27 @@ func TestAPICompareBranches(t *testing.T) {
session := loginUser(t, user.Name) session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
repoName := "repo20" t.Run("CompareBranches", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b").AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
req := NewRequestf(t, "GET", "/api/v1/repos/user2/%s/compare/add-csv...remove-files-b", repoName). var apiResp *api.Compare
AddTokenAuth(token) DecodeJSON(t, resp, &apiResp)
resp := MakeRequest(t, req, http.StatusOK)
var apiResp *api.Compare assert.Equal(t, 2, apiResp.TotalCommits)
DecodeJSON(t, resp, &apiResp) assert.Len(t, apiResp.Commits, 2)
})
assert.Equal(t, 2, apiResp.TotalCommits) t.Run("CompareCommits", func(t *testing.T) {
assert.Len(t, apiResp.Commits, 2) defer tests.PrintCurrentTest(t)()
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f").AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var apiResp *api.Compare
DecodeJSON(t, resp, &apiResp)
assert.Equal(t, 1, apiResp.TotalCommits)
assert.Len(t, apiResp.Commits, 1)
})
} }

View File

@ -440,7 +440,7 @@ func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v any) {
t.Helper() t.Helper()
decoder := json.NewDecoder(resp.Body) decoder := json.NewDecoder(resp.Body)
assert.NoError(t, decoder.Decode(v)) require.NoError(t, decoder.Decode(v))
} }
func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile string) { func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile string) {

View File

@ -429,7 +429,8 @@ export function initRepositoryActionView() {
<a class="muted" :href="run.commit.pusher.link">{{ run.commit.pusher.displayName }}</a> <a class="muted" :href="run.commit.pusher.link">{{ run.commit.pusher.displayName }}</a>
</template> </template>
<span class="ui label tw-max-w-full" v-if="run.commit.shortSHA"> <span class="ui label tw-max-w-full" v-if="run.commit.shortSHA">
<a class="gt-ellipsis" :href="run.commit.branch.link">{{ run.commit.branch.name }}</a> <span v-if="run.commit.branch.isDeleted" class="gt-ellipsis tw-line-through" :data-tooltip-content="run.commit.branch.name">{{ run.commit.branch.name }}</span>
<a v-else class="gt-ellipsis" :href="run.commit.branch.link" :data-tooltip-content="run.commit.branch.name">{{ run.commit.branch.name }}</a>
</span> </span>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import {SvgIcon} from '../svg.ts'; import {SvgIcon} from '../svg.ts';
import dayjs from 'dayjs';
import { import {
Chart, Chart,
Title, Title,
@ -26,6 +27,7 @@ import {sleep} from '../utils.ts';
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
import {fomanticQuery} from '../modules/fomantic/base.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts';
import type {Entries} from 'type-fest'; import type {Entries} from 'type-fest';
import {pathEscapeSegments} from '../utils/url.ts';
const customEventListener: Plugin = { const customEventListener: Plugin = {
id: 'customEventListener', id: 'customEventListener',
@ -65,6 +67,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
repoDefaultBranchName: {
type: String,
required: true,
},
}, },
data: () => ({ data: () => ({
isLoading: false, isLoading: false,
@ -100,6 +106,15 @@ export default {
.slice(0, 100); .slice(0, 100);
}, },
getContributorSearchQuery(contributorEmail: string) {
const min = dayjs(this.xAxisMin).format('YYYY-MM-DD');
const max = dayjs(this.xAxisMax).format('YYYY-MM-DD');
const params = new URLSearchParams({
'q': `after:${min}, before:${max}, author:${contributorEmail}`,
});
return `${this.repoLink}/commits/branch/${pathEscapeSegments(this.repoDefaultBranchName)}/search?${params.toString()}`;
},
async fetchGraphData() { async fetchGraphData() {
this.isLoading = true; this.isLoading = true;
try { try {
@ -167,7 +182,7 @@ export default {
// for details. // for details.
user.max_contribution_type += 1; user.max_contribution_type += 1;
filteredData[key] = {...user, weeks: filteredWeeks}; filteredData[key] = {...user, weeks: filteredWeeks, email: key};
} }
return filteredData; return filteredData;
@ -215,7 +230,7 @@ export default {
}; };
}, },
updateOtherCharts({chart}: {chart: Chart}, reset?: boolean = false) { updateOtherCharts({chart}: {chart: Chart}, reset: boolean = false) {
const minVal = chart.options.scales.x.min; const minVal = chart.options.scales.x.min;
const maxVal = chart.options.scales.x.max; const maxVal = chart.options.scales.x.max;
if (reset) { if (reset) {
@ -381,7 +396,7 @@ export default {
<div class="ui top attached header tw-flex tw-flex-1"> <div class="ui top attached header tw-flex tw-flex-1">
<b class="ui right">#{{ index + 1 }}</b> <b class="ui right">#{{ index + 1 }}</b>
<a :href="contributor.home_link"> <a :href="contributor.home_link">
<img class="ui avatar tw-align-middle" height="40" width="40" :src="contributor.avatar_link"> <img class="ui avatar tw-align-middle" height="40" width="40" :src="contributor.avatar_link" alt="">
</a> </a>
<div class="tw-ml-2"> <div class="tw-ml-2">
<a v-if="contributor.home_link !== ''" :href="contributor.home_link"><h4>{{ contributor.name }}</h4></a> <a v-if="contributor.home_link !== ''" :href="contributor.home_link"><h4>{{ contributor.name }}</h4></a>
@ -389,7 +404,11 @@ export default {
{{ contributor.name }} {{ contributor.name }}
</h4> </h4>
<p class="tw-text-12 tw-flex tw-gap-1"> <p class="tw-text-12 tw-flex tw-gap-1">
<strong v-if="contributor.total_commits">{{ contributor.total_commits.toLocaleString() }} {{ locale.contributionType.commits }}</strong> <strong v-if="contributor.total_commits">
<a class="silenced" :href="getContributorSearchQuery(contributor.email)">
{{ contributor.total_commits.toLocaleString() }} {{ locale.contributionType.commits }}
</a>
</strong>
<strong v-if="contributor.total_additions" class="text green">{{ contributor.total_additions.toLocaleString() }}++ </strong> <strong v-if="contributor.total_additions" class="text green">{{ contributor.total_additions.toLocaleString() }}++ </strong>
<strong v-if="contributor.total_deletions" class="text red"> <strong v-if="contributor.total_deletions" class="text red">
{{ contributor.total_deletions.toLocaleString() }}--</strong> {{ contributor.total_deletions.toLocaleString() }}--</strong>

View File

@ -178,6 +178,7 @@ export function initTextareaEvents(textarea, dropzoneEl) {
}); });
textarea.addEventListener('drop', (e) => { textarea.addEventListener('drop', (e) => {
if (!e.dataTransfer.files.length) return; if (!e.dataTransfer.files.length) return;
if (!dropzoneEl) return;
handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, e.dataTransfer.files, e); handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, e.dataTransfer.files, e);
}); });
dropzoneEl?.dropzone.on(DropzoneCustomEventRemovedFile, ({fileUuid}) => { dropzoneEl?.dropzone.on(DropzoneCustomEventRemovedFile, ({fileUuid}) => {

View File

@ -8,6 +8,7 @@ export async function initRepoContributors() {
try { try {
const View = createApp(RepoContributors, { const View = createApp(RepoContributors, {
repoLink: el.getAttribute('data-repo-link'), repoLink: el.getAttribute('data-repo-link'),
repoDefaultBranchName: el.getAttribute('data-repo-default-branch-name'),
locale: { locale: {
filterLabel: el.getAttribute('data-locale-filter-label'), filterLabel: el.getAttribute('data-locale-filter-label'),
contributionType: { contributionType: {

View File

@ -1,27 +1,28 @@
import {addDelegatedEventListener, hideElem, queryElemSiblings, showElem, toggleElem} from '../utils/dom.ts'; import {addDelegatedEventListener, hideElem, queryElemSiblings, showElem, toggleElem} from '../utils/dom.ts';
export function initUnicodeEscapeButton() { export function initUnicodeEscapeButton() {
// buttons might appear on these pages: file view (code, rendered markdown), diff (commit, pr conversation, pr diff), blame, wiki
addDelegatedEventListener(document, 'click', '.escape-button, .unescape-button, .toggle-escape-button', (btn, e) => { addDelegatedEventListener(document, 'click', '.escape-button, .unescape-button, .toggle-escape-button', (btn, e) => {
e.preventDefault(); e.preventDefault();
const fileContentElemId = btn.getAttribute('data-file-content-elem-id'); const unicodeContentSelector = btn.getAttribute('data-unicode-content-selector');
const fileContent = fileContentElemId ? const container = unicodeContentSelector ?
document.querySelector(`#${fileContentElemId}`) : document.querySelector(unicodeContentSelector) :
btn.closest('.file-content, .non-diff-file-content'); btn.closest('.file-content, .non-diff-file-content');
const fileView = fileContent?.querySelectorAll('.file-code, .file-view'); const fileView = container.querySelector('.file-code, .file-view') ?? container;
if (btn.matches('.escape-button')) { if (btn.matches('.escape-button')) {
for (const el of fileView) el.classList.add('unicode-escaped'); fileView.classList.add('unicode-escaped');
hideElem(btn); hideElem(btn);
showElem(queryElemSiblings(btn, '.unescape-button')); showElem(queryElemSiblings(btn, '.unescape-button'));
} else if (btn.matches('.unescape-button')) { } else if (btn.matches('.unescape-button')) {
for (const el of fileView) el.classList.remove('unicode-escaped'); fileView.classList.remove('unicode-escaped');
hideElem(btn); hideElem(btn);
showElem(queryElemSiblings(btn, '.escape-button')); showElem(queryElemSiblings(btn, '.escape-button'));
} else if (btn.matches('.toggle-escape-button')) { } else if (btn.matches('.toggle-escape-button')) {
const isEscaped = fileView[0]?.classList.contains('unicode-escaped'); const isEscaped = fileView.classList.contains('unicode-escaped');
for (const el of fileView) el.classList.toggle('unicode-escaped', !isEscaped); fileView.classList.toggle('unicode-escaped', !isEscaped);
toggleElem(fileContent.querySelectorAll('.unescape-button'), !isEscaped); toggleElem(container.querySelectorAll('.unescape-button'), !isEscaped);
toggleElem(fileContent.querySelectorAll('.escape-button'), isEscaped); toggleElem(container.querySelectorAll('.escape-button'), isEscaped);
} }
}); });
} }