Merge pull request '[gitea] week 2024-51 cherry pick (gitea/main -> forgejo)' (#6271) from earl-warren/wcp/2024-51 into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6271
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2024-12-19 07:08:01 +00:00
commit 380e266440
28 changed files with 526 additions and 32 deletions

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

@ -137,7 +137,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
if err != nil { if err != nil {
return 0, err return 0, err
} }
run.Status = aggregateJobStatus(jobs) run.Status = AggregateJobStatus(jobs)
if run.Started.IsZero() && run.Status.IsRunning() { if run.Started.IsZero() && run.Status.IsRunning() {
run.Started = timeutil.TimeStampNow() run.Started = timeutil.TimeStampNow()
} }
@ -152,7 +152,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
return affected, nil return affected, nil
} }
func aggregateJobStatus(jobs []*ActionRunJob) Status { func AggregateJobStatus(jobs []*ActionRunJob) Status {
allDone := true allDone := true
allWaiting := true allWaiting := true
hasFailure := false hasFailure := false

View file

@ -4,7 +4,7 @@
package db // it's not db_test, because this file is for testing the private type halfCommitter package db // it's not db_test, because this file is for testing the private type halfCommitter
import ( import (
"fmt" "errors"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -80,7 +80,7 @@ func Test_halfCommitter(t *testing.T) {
testWithCommitter(mockCommitter, func(committer Committer) error { testWithCommitter(mockCommitter, func(committer Committer) error {
defer committer.Close() defer committer.Close()
if true { if true {
return fmt.Errorf("error") return errors.New("error")
} }
return committer.Commit() return committer.Commit()
}) })
@ -94,7 +94,7 @@ func Test_halfCommitter(t *testing.T) {
testWithCommitter(mockCommitter, func(committer Committer) error { testWithCommitter(mockCommitter, func(committer Committer) error {
committer.Close() committer.Close()
committer.Commit() committer.Commit()
return fmt.Errorf("error") return errors.New("error")
}) })
mockCommitter.Assert(t) mockCommitter.Assert(t)

View file

@ -413,6 +413,25 @@
}, },
"total_commits": 0 "total_commits": 0
} }
-
id: 793
title: "job output"
repo_id: 4
owner_id: 1
workflow_id: "test.yaml"
index: 189
trigger_user_id: 1
ref: "refs/heads/master"
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
event: "push"
is_fork_pull_request: 0
status: 1
started: 1683636528
stopped: 1683636626
created: 1683636108
updated: 1683636626
need_approval: 0
approved_by: 0
- -
id: 891 id: 891
title: "update actions" title: "update actions"

View file

@ -26,6 +26,49 @@
status: 1 status: 1
started: 1683636528 started: 1683636528
stopped: 1683636626 stopped: 1683636626
-
id: 194
run_id: 793
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: 0
name: job1 (1)
attempt: 1
job_id: job1
task_id: 49
status: 1
started: 1683636528
stopped: 1683636626
-
id: 195
run_id: 793
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: 0
name: job1 (2)
attempt: 1
job_id: job1
task_id: 50
status: 1
started: 1683636528
stopped: 1683636626
-
id: 196
run_id: 793
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: 0
name: job2
attempt: 1
job_id: job2
needs: [job1]
task_id: 51
status: 5
started: 1683636528
stopped: 1683636626
- -
id: 292 id: 292
run_id: 891 run_id: 891

View file

@ -57,3 +57,63 @@
log_length: 707 log_length: 707
log_size: 90179 log_size: 90179
log_expired: 0 log_expired: 0
-
id: 49
job_id: 194
attempt: 1
runner_id: 1
status: 1 # success
started: 1683636528
stopped: 1683636626
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: 0
token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784220
token_salt: ffffffffff
token_last_eight: ffffffff
log_filename: artifact-test2/2f/47.log
log_in_storage: 1
log_length: 707
log_size: 90179
log_expired: 0
-
id: 50
job_id: 195
attempt: 1
runner_id: 1
status: 1 # success
started: 1683636528
stopped: 1683636626
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: 0
token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784221
token_salt: ffffffffff
token_last_eight: ffffffff
log_filename: artifact-test2/2f/47.log
log_in_storage: 1
log_length: 707
log_size: 90179
log_expired: 0
-
id: 51
job_id: 196
attempt: 1
runner_id: 1
status: 6 # running
started: 1683636528
stopped: 1683636626
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: 0
token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784222
token_salt: ffffffffff
token_last_eight: ffffffff
log_filename: artifact-test2/2f/47.log
log_in_storage: 1
log_length: 707
log_size: 90179
log_expired: 0

View file

@ -0,0 +1,20 @@
-
id: 1
task_id: 49
output_key: output_a
output_value: abc
-
id: 2
task_id: 49
output_key: output_b
output_value: ''
-
id: 3
task_id: 50
output_key: output_a
output_value: ''
-
id: 4
task_id: 50
output_key: output_b
output_value: bbb

View file

@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
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/timeutil" "code.gitea.io/gitea/modules/timeutil"
@ -162,9 +163,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

@ -72,7 +72,10 @@ func (c *HTTPClient) batch(ctx context.Context, operation string, objects []Poin
url := fmt.Sprintf("%s/objects/batch", c.endpoint) url := fmt.Sprintf("%s/objects/batch", c.endpoint)
request := &BatchRequest{operation, c.transferNames(), nil, objects} // `ref` is an "optional object describing the server ref that the objects belong to"
// but some (incorrect) lfs servers require it, so maybe adding an empty ref here doesn't break the correct ones.
// https://github.com/git-lfs/git-lfs/blob/a32a02b44bf8a511aa14f047627c49e1a7fd5021/docs/api/batch.md?plain=1#L37
request := &BatchRequest{operation, c.transferNames(), &Reference{}, objects}
payload := new(bytes.Buffer) payload := new(bytes.Buffer)
err := json.NewEncoder(payload).Encode(request) err := json.NewEncoder(payload).Encode(request)
if err != nil { if err != nil {
@ -236,6 +239,7 @@ func createRequest(ctx context.Context, method, url string, headers map[string]s
req.Header.Set(key, value) req.Header.Set(key, value)
} }
req.Header.Set("Accept", AcceptHeader) req.Header.Set("Accept", AcceptHeader)
req.Header.Set("User-Agent", UserAgentHeader)
return req, nil return req, nil
} }

View file

@ -14,8 +14,12 @@ import (
const ( const (
// MediaType contains the media type for LFS server requests // MediaType contains the media type for LFS server requests
MediaType = "application/vnd.git-lfs+json" MediaType = "application/vnd.git-lfs+json"
// Some LFS servers offer content with other types, so fallback to '*/*' if application/vnd.git-lfs+json cannot be served // AcceptHeader Some LFS servers offer content with other types, so fallback to '*/*' if application/vnd.git-lfs+json cannot be served
AcceptHeader = "application/vnd.git-lfs+json;q=0.9, */*;q=0.8" AcceptHeader = "application/vnd.git-lfs+json;q=0.9, */*;q=0.8"
// UserAgentHeader Add User-Agent for gitea's self-implemented lfs client,
// and the version is consistent with the latest version of git lfs can be avoided incompatibilities.
// Some lfs servers will check this
UserAgentHeader = "git-lfs/3.6.0 (Forgejo)"
) )
// BatchRequest contains multiple requests processed in one batch operation. // BatchRequest contains multiple requests processed in one batch operation.

View file

@ -290,6 +290,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 {

4
release-notes/6271.md Normal file
View file

@ -0,0 +1,4 @@
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/96a7f0a3f065c5db8fdf352c93c8367e24d259de) Fix missing outputs for jobs with matrix
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/2b5c69c451a684b20119e2521dc23734c7869241) Detect whether action view branch was deleted
feat: [commit](https://codeberg.org/forgejo/forgejo/commit/b0d6a7f07bff836190a8e87fe5645d5557893e32) Implement update branch API
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/bf934c96c92d643678ac7a18697b6563bc9d20a5) Add standard-compliant route to serve outdated R packages

View file

@ -0,0 +1,16 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package runner
import (
"testing"
"code.gitea.io/gitea/models/unittest"
_ "code.gitea.io/gitea/models/forgefed"
)
func TestMain(m *testing.M) {
unittest.MainTest(m)
}

View file

@ -162,28 +162,56 @@ func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[str
return nil, fmt.Errorf("FindRunJobs: %w", err) return nil, fmt.Errorf("FindRunJobs: %w", err)
} }
ret := make(map[string]*runnerv1.TaskNeed, len(needs)) jobIDJobs := make(map[string][]*actions_model.ActionRunJob)
for _, job := range jobs { for _, job := range jobs {
if !needs.Contains(job.JobID) { jobIDJobs[job.JobID] = append(jobIDJobs[job.JobID], job)
}
ret := make(map[string]*runnerv1.TaskNeed, len(needs))
for jobID, jobsWithSameID := range jobIDJobs {
if !needs.Contains(jobID) {
continue continue
} }
var jobOutputs map[string]string
for _, job := range jobsWithSameID {
if job.TaskID == 0 || !job.Status.IsDone() { if job.TaskID == 0 || !job.Status.IsDone() {
// it shouldn't happen, or the job has been rerun // it shouldn't happen, or the job has been rerun
continue continue
} }
outputs := make(map[string]string)
got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID) got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID)
if err != nil { if err != nil {
return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err) return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err)
} }
outputs := make(map[string]string, len(got))
for _, v := range got { for _, v := range got {
outputs[v.OutputKey] = v.OutputValue outputs[v.OutputKey] = v.OutputValue
} }
ret[job.JobID] = &runnerv1.TaskNeed{ if len(jobOutputs) == 0 {
Outputs: outputs, jobOutputs = outputs
Result: runnerv1.Result(job.Status), } else {
jobOutputs = mergeTwoOutputs(outputs, jobOutputs)
}
}
ret[jobID] = &runnerv1.TaskNeed{
Outputs: jobOutputs,
Result: runnerv1.Result(actions_model.AggregateJobStatus(jobsWithSameID)),
} }
} }
return ret, nil return ret, nil
} }
// mergeTwoOutputs merges two outputs from two different ActionRunJobs
// Values with the same output name may be overridden. The user should ensure the output names are unique.
// See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#using-job-outputs-in-a-matrix-job
func mergeTwoOutputs(o1, o2 map[string]string) map[string]string {
ret := make(map[string]string, len(o1))
for k1, v1 := range o1 {
if len(v1) > 0 {
ret[k1] = v1
} else {
ret[k1] = o2[k1]
}
}
return ret
}

View file

@ -0,0 +1,29 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package runner
import (
"context"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_findTaskNeeds(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 51})
ret, err := findTaskNeeds(context.Background(), task)
require.NoError(t, err)
assert.Len(t, ret, 1)
assert.Contains(t, ret, "job1")
assert.Len(t, ret["job1"].Outputs, 2)
assert.Equal(t, "abc", ret["job1"].Outputs["output_a"])
assert.Equal(t, "bbb", ret["job1"].Outputs["output_b"])
}

View file

@ -337,6 +337,7 @@ func CommonRoutes() *web.Route {
r.Get("/PACKAGES", cran.EnumerateSourcePackages) r.Get("/PACKAGES", cran.EnumerateSourcePackages)
r.Get("/PACKAGES{format}", cran.EnumerateSourcePackages) r.Get("/PACKAGES{format}", cran.EnumerateSourcePackages)
r.Get("/{filename}", cran.DownloadSourcePackageFile) r.Get("/{filename}", cran.DownloadSourcePackageFile)
r.Get("/Archive/{packagename}/{filename}", cran.DownloadSourcePackageFile)
}) })
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), cran.UploadSourcePackageFile) r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), cran.UploadSourcePackageFile)
}) })

View file

@ -1153,6 +1153,7 @@ func Routes() *web.Route {
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{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.CreateBranch) m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), 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

@ -393,6 +393,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.Params("*")
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

@ -101,6 +101,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

@ -12,11 +12,13 @@ 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/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container" "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/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -222,6 +224,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
ctx.Data["Repo"] = ctx.Repo ctx.Data["Repo"] = ctx.Repo
@ -245,3 +251,31 @@ 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
}

View file

@ -20,10 +20,13 @@ 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"
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"
"code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
@ -159,6 +162,7 @@ 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 {
@ -227,6 +231,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{
LocaleCommit: ctx.Locale.TrString("actions.runs.commit"), LocaleCommit: ctx.Locale.TrString("actions.runs.commit"),
LocalePushedBy: ctx.Locale.TrString("actions.runs.pushed_by"), LocalePushedBy: ctx.Locale.TrString("actions.runs.pushed_by"),

View file

@ -254,7 +254,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

@ -5820,6 +5820,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": {
@ -27302,6 +27359,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"
@ -187,6 +188,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

@ -116,6 +116,14 @@ func TestPackageCran(t *testing.T) {
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusOK)
}) })
t.Run("DownloadArchived", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", fmt.Sprintf("%s/src/contrib/Archive/%s/%s_%s.tar.gz", url, packageName, packageName, packageVersion)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusOK)
})
t.Run("Enumerate", func(t *testing.T) { t.Run("Enumerate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()

View file

@ -166,6 +166,11 @@ func TestMain(m *testing.M) {
os.Unsetenv("GIT_COMMITTER_EMAIL") os.Unsetenv("GIT_COMMITTER_EMAIL")
os.Unsetenv("GIT_COMMITTER_DATE") os.Unsetenv("GIT_COMMITTER_DATE")
// Avoid loading the default system config. On MacOS, this config
// sets the osxkeychain credential helper, which will cause tests
// to freeze with a dialog.
os.Setenv("GIT_CONFIG_NOSYSTEM", "true")
err := unittest.InitFixtures( err := unittest.InitFixtures(
unittest.FixturesOptions{ unittest.FixturesOptions{
Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"), Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"),

View file

@ -444,7 +444,8 @@ export function initRepositoryActionView() {
{{ run.commit.localePushedBy }} {{ run.commit.localePushedBy }}
<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>
<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 class="action-summary"> <div class="action-summary">