diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 6e1b6e07584..2264c9e822d 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -10,7 +10,7 @@ concurrency: jobs: nightly-binary: - runs-on: nscloud + runs-on: namespace-profile-gitea-release-binary steps: - uses: actions/checkout@v4 # 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: | aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress nightly-docker-rootful: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # 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 tags: gitea/gitea:${{ steps.clean_name.outputs.branch }} nightly-docker-rootless: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml index 41037df29cb..a406602dc0a 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -11,7 +11,7 @@ concurrency: jobs: binary: - runs-on: nscloud + runs-on: namespace-profile-gitea-release-binary steps: - uses: actions/checkout@v4 # 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: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} docker-rootful: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # 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 }} labels: ${{ steps.meta.outputs.labels }} docker-rootless: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml index a23e6982000..f67b76f4087 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -13,7 +13,7 @@ concurrency: jobs: binary: - runs-on: nscloud + runs-on: namespace-profile-gitea-release-binary steps: - uses: actions/checkout@v4 # 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: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} docker-rootful: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # 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 }} labels: ${{ steps.meta.outputs.labels }} docker-rootless: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions diff --git a/models/actions/run.go b/models/actions/run.go index 732fb48bb9a..f40bc1eb3db 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -37,6 +37,7 @@ type ActionRun struct { TriggerUser *user_model.User `xorm:"-"` ScheduleID int64 Ref string `xorm:"index"` // the commit/tag/… that caused the run + IsRefDeleted bool `xorm:"-"` 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. NeedApproval bool // may need approval if it's a fork pull request diff --git a/models/git/branch.go b/models/git/branch.go index ba1ada5517d..e683ce47e65 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -12,6 +12,7 @@ import ( 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/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" @@ -169,9 +170,22 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e 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)) - 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 { diff --git a/modules/git/commit.go b/modules/git/commit.go index 010b56948ef..0ed268e3469 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -474,3 +474,17 @@ func (c *Commit) GetRepositoryDefaultPublicGPGKey(forceUpdate bool) (*GPGSetting } 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 +} diff --git a/modules/git/ref.go b/modules/git/ref.go index 2db630e2ea9..aab4c5d77d7 100644 --- a/modules/git/ref.go +++ b/modules/git/ref.go @@ -142,7 +142,6 @@ func (ref RefName) RemoteName() string { // ShortName returns the short name of the reference name func (ref RefName) ShortName() string { - refName := string(ref) if ref.IsBranch() { return ref.BranchName() } @@ -158,8 +157,7 @@ func (ref RefName) ShortName() string { if ref.IsFor() { return ref.ForBranchName() } - - return refName + return string(ref) // usually it is a commit ID } // RefGroup returns the group type of the reference diff --git a/modules/git/repo_ref.go b/modules/git/repo_ref.go index 8eaa17cb041..850ec655029 100644 --- a/modules/git/repo_ref.go +++ b/modules/git/repo_ref.go @@ -61,3 +61,31 @@ func parseTags(refs []string) []string { } 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 "" +} diff --git a/modules/globallock/globallock_test.go b/modules/globallock/globallock_test.go index 88a555c86f3..f14c7d656b6 100644 --- a/modules/globallock/globallock_test.go +++ b/modules/globallock/globallock_test.go @@ -64,7 +64,7 @@ func TestLockAndDo(t *testing.T) { } func testLockAndDo(t *testing.T) { - const concurrency = 1000 + const concurrency = 50 ctx := context.Background() count := 0 diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 832ffa8bcc9..fb784bd8b37 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -278,6 +278,16 @@ type CreateBranchRepoOption struct { 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 // swagger:model type TransferRepoOption struct { diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index f28ee980e10..96365e7c14d 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1195,6 +1195,7 @@ func Routes() *web.Router { m.Get("/*", repo.GetBranch) m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch) 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)) m.Group("/branch_protections", func() { m.Get("", repo.ListBranchProtections) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 53f3b4648a5..946203e97ec 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -386,6 +386,77 @@ func ListBranches(ctx *context.APIContext) { 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 func GetBranchProtection(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/branch_protections/{name} repository repoGetBranchProtection diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index 38e5330b3ac..1678bc033c6 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -64,22 +64,19 @@ func CompareDiff(ctx *context.APIContext) { } } - _, headGitRepo, ci, _, _ := parseCompareInfo(ctx, api.CreatePullRequestOption{ - Base: infos[0], - Head: infos[1], - }) + compareResult, closer := parseCompareInfo(ctx, api.CreatePullRequestOption{Base: infos[0], Head: infos[1]}) if ctx.Written() { return } - defer headGitRepo.Close() + defer closer() verification := ctx.FormString("verification") == "" || ctx.FormBool("verification") 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) - for i := 0; i < len(ci.Commits); i++ { - apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ci.Commits[i], userCache, + for i := 0; i < len(compareResult.compareInfo.Commits); i++ { + apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, compareResult.compareInfo.Commits[i], userCache, convert.ToCommitOptions{ Stat: true, Verification: verification, @@ -93,7 +90,7 @@ func CompareDiff(ctx *context.APIContext) { } ctx.JSON(http.StatusOK, &api.Compare{ - TotalCommits: len(ci.Commits), + TotalCommits: len(compareResult.compareInfo.Commits), Commits: apiCommits, }) } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index bad078414e6..fc690bee022 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -389,8 +389,7 @@ func CreatePullRequest(ctx *context.APIContext) { form := *web.GetForm(ctx).(*api.CreatePullRequestOption) if form.Head == form.Base { - ctx.Error(http.StatusUnprocessableEntity, "BaseHeadSame", - "Invalid PullRequest: There are no changes between the head and the base") + ctx.Error(http.StatusUnprocessableEntity, "BaseHeadSame", "Invalid PullRequest: There are no changes between the head and the base") return } @@ -401,14 +400,22 @@ func CreatePullRequest(ctx *context.APIContext) { ) // Get repo/branch information - headRepo, headGitRepo, compareInfo, baseBranch, headBranch := parseCompareInfo(ctx, form) + compareResult, closer := parseCompareInfo(ctx, form) if ctx.Written() { 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 - 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 !issues_model.IsErrPullRequestNotExist(err) { ctx.Error(http.StatusInternalServerError, "GetUnmergedPullRequest", err) @@ -484,13 +491,13 @@ func CreatePullRequest(ctx *context.APIContext) { DeadlineUnix: deadlineUnix, } pr := &issues_model.PullRequest{ - HeadRepoID: headRepo.ID, + HeadRepoID: compareResult.headRepo.ID, BaseRepoID: repo.ID, - HeadBranch: headBranch, - BaseBranch: baseBranch, - HeadRepo: headRepo, + HeadBranch: compareResult.headRef.ShortName(), + BaseBranch: compareResult.baseRef.ShortName(), + HeadRepo: compareResult.headRepo, BaseRepo: repo, - MergeBase: compareInfo.MergeBase, + MergeBase: compareResult.compareInfo.MergeBase, Type: issues_model.PullRequestGitea, } @@ -1080,32 +1087,32 @@ func MergePullRequest(ctx *context.APIContext) { ctx.Status(http.StatusOK) } -func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (*repo_model.Repository, *git.Repository, *git.CompareInfo, string, string) { - baseRepo := ctx.Repo.Repository +type parseCompareInfoResult struct { + 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 // format: ...[:] // base<-head: master...head:feature // same repo: master...feature + baseRepo := ctx.Repo.Repository + baseRefToGuess := form.Base - // TODO: Validate form first? - - baseBranch := form.Base - - var ( - 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] + headUser := ctx.Repo.Owner + headRefToGuess := form.Head + if headInfos := strings.Split(form.Head, ":"); len(headInfos) == 1 { + // If there is no head repository, it means pull request between same repository. + // Do nothing here because the head variables have been assigned above. } 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]) if err != nil { if user_model.IsErrUserNotExist(err) { @@ -1113,23 +1120,14 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) } else { 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 { ctx.NotFound() - return nil, nil, nil, "", "" + return nil, nil } - ctx.Repo.PullRequest.SameRepo = isSameRepo - 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, "", "" - } + isSameRepo := ctx.Repo.Owner.ID == headUser.ID // Check if current user has fork of repository or in the same repository. 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, "", "" } if headRepo == nil && !isSameRepo { - err := baseRepo.GetBaseRepo(ctx) + err = baseRepo.GetBaseRepo(ctx) if err != nil { 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. if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID { log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID) ctx.NotFound("GetBaseRepo") - return nil, nil, nil, "", "" + return nil, nil } // Assign headRepo so it can be used below. headRepo = baseRepo.BaseRepo @@ -1158,67 +1156,68 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) if isSameRepo { headRepo = ctx.Repo.Repository headGitRepo = ctx.Repo.GitRepo + closer = func() {} // no need to close the head repo because it shares the base repo } else { headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo) if err != nil { 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 permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer) if err != nil { - headGitRepo.Close() ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) - return nil, 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, "", "" + return 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) if err != nil { - headGitRepo.Close() ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) - return nil, nil, nil, "", "" + return nil, nil } 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) - } - headGitRepo.Close() + log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", ctx.Doer, headRepo, permHead) ctx.NotFound("Can't read headRepo UnitTypeCode") - return nil, nil, nil, "", "" + return nil, nil } - // Check if head branch is valid. - if !headGitRepo.IsBranchExist(headBranch) && !headGitRepo.IsTagExist(headBranch) { - headGitRepo.Close() + baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefToGuess) + headRef := headGitRepo.UnstableGuessRefByShortName(headRefToGuess) + + 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() - 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 { - headGitRepo.Close() 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 diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 39c98c666e5..125605d98f5 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -90,6 +90,8 @@ type swaggerParameterBodies struct { // in:body EditRepoOption api.EditRepoOption // in:body + UpdateBranchRepoOption api.UpdateBranchRepoOption + // in:body TransferRepoOption api.TransferRepoOption // in:body CreateForkOption api.CreateForkOption diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index ad16b9fb4e4..7ed37ea26b2 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -245,6 +245,10 @@ func List(ctx *context.Context) { return } + if err := loadIsRefDeleted(ctx, runs); err != nil { + log.Error("LoadIsRefDeleted", err) + } + ctx.Data["Runs"] = runs actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID) @@ -267,6 +271,34 @@ func List(ctx *context.Context) { 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 { Name string `yaml:"name"` Description string `yaml:"description"` diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 73c6e54fbf5..b711038da06 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -19,6 +19,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -136,8 +137,9 @@ type ViewUser struct { } type ViewBranch struct { - Name string `json:"name"` - Link string `json:"link"` + Name string `json:"name"` + Link string `json:"link"` + IsDeleted bool `json:"isDeleted"` } type ViewJobStep struct { @@ -236,6 +238,16 @@ func ViewPost(ctx *context_module.Context) { Name: run.PrettyRef(), 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{ ShortSha: base.ShortSha(run.CommitSHA), Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA), diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 833f59981b4..5397411b59c 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -9,7 +9,6 @@ import ( "fmt" "html/template" "net/http" - "net/url" "strconv" "strings" @@ -114,7 +113,6 @@ func MustAllowPulls(ctx *context.Context) { // User can send pull request if owns a forked repository. if ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) { ctx.Repo.PullRequest.Allowed = true - ctx.Repo.PullRequest.HeadInfoSubURL = url.PathEscape(ctx.Doer.Name) + ":" + util.PathEscapeSegments(ctx.Repo.BranchName) } } diff --git a/services/context/repo.go b/services/context/repo.go index cf328ca97b7..9b544391103 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -39,10 +39,9 @@ import ( // PullRequest contains information to make a pull request type PullRequest struct { - BaseRepo *repo_model.Repository - Allowed bool - SameRepo bool - HeadInfoSubURL string // [:] url segment + BaseRepo *repo_model.Repository + Allowed bool // it only used by the web tmpl: "PullRequestCtx.Allowed" + SameRepo bool // it only used by the web tmpl: "PullRequestCtx.SameRepo" } // 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 func RepoAssignment(ctx *Context) context.CancelFunc { 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 ...") return nil } @@ -697,7 +697,6 @@ func RepoAssignment(ctx *Context) context.CancelFunc { ctx.Data["BaseRepo"] = repo.BaseRepo ctx.Repo.PullRequest.BaseRepo = repo.BaseRepo 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) { // Or, this is repository accepts pull requests between branches. canCompare = true @@ -705,7 +704,6 @@ func RepoAssignment(ctx *Context) context.CancelFunc { ctx.Repo.PullRequest.BaseRepo = repo ctx.Repo.PullRequest.Allowed = canPush ctx.Repo.PullRequest.SameRepo = true - ctx.Repo.PullRequest.HeadInfoSubURL = util.PathEscapeSegments(ctx.Repo.BranchName) } ctx.Data["CanCompareOrPull"] = canCompare ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequest @@ -771,20 +769,6 @@ func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool 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) { extraRef := util.OptionalArg(optionalExtraRef) reqPath := ctx.PathParam("*") @@ -799,7 +783,7 @@ func getRefNameLegacy(ctx *Base, repo *Repository, optionalExtraRef ...string) ( // For legacy support only full commit sha 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 repo.TreePath = strings.Join(parts[1:], "/") return parts[0], RepoRefCommit @@ -849,7 +833,7 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string { return getRefNameFromPath(repo, path, repo.GitRepo.IsTagExist) case RepoRefCommit: 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 repo.TreePath = strings.Join(parts[1:], "/") return parts[0] @@ -985,7 +969,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func return cancel } 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.CommitID = refName diff --git a/services/repository/branch.go b/services/repository/branch.go index 508817c83e6..3a95aab2649 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -305,7 +305,7 @@ func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames, } 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 { return fmt.Errorf("git_model.GetBranches: %v", err) } diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl index 5537e9e6172..fa1adb3e3ba 100644 --- a/templates/repo/actions/runs_list.tmpl +++ b/templates/repo/actions/runs_list.tmpl @@ -27,10 +27,10 @@
- {{if .RefLink}} - {{.PrettyRef}} + {{if .IsRefDeleted}} + {{.PrettyRef}} {{else}} - {{.PrettyRef}} + {{.PrettyRef}} {{end}}
{{svg "octicon-calendar" 16}}{{DateUtils.TimeSince .Updated}}
diff --git a/templates/repo/contributors.tmpl b/templates/repo/contributors.tmpl index 6b8a63fe996..882d8b205cf 100644 --- a/templates/repo/contributors.tmpl +++ b/templates/repo/contributors.tmpl @@ -1,6 +1,7 @@ {{if .Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
{{svg "octicon-kebab-horizontal" 18}}
{{if not (or $file.IsIncomplete $file.IsBin $file.IsSubmodule)}} - - + + {{end}} {{if and (not $file.IsSubmodule) (not $.PageIsWiki)}} {{if $file.IsDeleted}} diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index 2bb0a4f0068..843a977e3e5 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -44,13 +44,13 @@
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index c06c0ad1541..82a301da2fe 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -5045,6 +5045,63 @@ "$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": { @@ -24968,6 +25025,22 @@ }, "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": { "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", diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go index 8e49516aa72..24a041de17e 100644 --- a/tests/integration/api_branch_test.go +++ b/tests/integration/api_branch_test.go @@ -5,6 +5,7 @@ package integration import ( "net/http" + "net/http/httptest" "net/url" "testing" @@ -186,6 +187,37 @@ func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBran 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) { defer tests.PrepareTestEnv(t)() diff --git a/tests/integration/api_repo_compare_test.go b/tests/integration/api_repo_compare_test.go index f3188eb49f6..9565e4d2090 100644 --- a/tests/integration/api_repo_compare_test.go +++ b/tests/integration/api_repo_compare_test.go @@ -24,15 +24,27 @@ func TestAPICompareBranches(t *testing.T) { session := loginUser(t, user.Name) 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). - AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) + var apiResp *api.Compare + DecodeJSON(t, resp, &apiResp) - var apiResp *api.Compare - DecodeJSON(t, resp, &apiResp) + assert.Equal(t, 2, apiResp.TotalCommits) + assert.Len(t, apiResp.Commits, 2) + }) - assert.Equal(t, 2, apiResp.TotalCommits) - assert.Len(t, apiResp.Commits, 2) + t.Run("CompareCommits", func(t *testing.T) { + 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) + }) } diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 4338e196174..8b6605eac8f 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -440,7 +440,7 @@ func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v any) { t.Helper() 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) { diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 7f647b668a6..eece2efaf86 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -429,7 +429,8 @@ export function initRepositoryActionView() { {{ run.commit.pusher.displayName }} - {{ run.commit.branch.name }} + {{ run.commit.branch.name }} + {{ run.commit.branch.name }}
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue index 97678b9a13b..e79fc80d8e3 100644 --- a/web_src/js/components/RepoContributors.vue +++ b/web_src/js/components/RepoContributors.vue @@ -1,5 +1,6 @@