Compare commits

...

32 Commits

Author SHA1 Message Date
zam.
0d5a8475e4
Merge 5576b10604c39c2323521edf36160e4885c84bd8 into 40faa6dc78a5b5730a1609ba39daefddac08aa63 2025-02-19 19:35:16 +01:00
ericLemanissier
40faa6dc78
git graph: don't show detached commits (#33645)
Some checks failed
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-docker-rootful (push) Has been cancelled
release-nightly / nightly-docker-rootless (push) Has been cancelled
Current git graph is not usable for mirrors of repos having a lot of
PRs, as can be seen at
https://demo.gitea.com/ericLemanissier/conan-center-index/graph

![image](https://github.com/user-attachments/assets/ace40dd2-3eea-4d69-8e19-10fb7224e326)


Manually running `git log --graph --date-order --all` on such a repo
indeed shows:
```
*   commit c4a34bd39d7977c8630177c5f88507000ea3e943
|\  Merge: a4bbd3ad6b 35a102c77c
| | Author: toge <toge.mail@gmail.com>
| | Date:   Wed Feb 19 08:36:41 2025 +0000
| |
| |     Merge 35a102c77cbc38d84baca0ca63466fb410336ea8 into a4bbd3ad6bb5a0f8e5117a897d8c55941f533d98
| |
| * commit 35a102c77cbc38d84baca0ca63466fb410336ea8
| | Author: toge <toge.mail@gmail.com>
| | Date:   Wed Feb 19 17:36:35 2025 +0900
| |
| |     update 4.4.2
| |
| | * commit 5d610f4fd3c0428731e402a2f618fad9ce055875
| |/| Merge: a4bbd3ad6b fe916fb70a
|/| | Author: Antony Peacock <ant.peacock@gmail.com>
| | | Date:   Wed Feb 19 08:31:30 2025 +0000
| | |
| | |     Merge fe916fb70a8bf49503cce70a5c7124bcc4314ddc into a4bbd3ad6bb5a0f8e5117a897d8c55941f533d98
| | |
| | * commit fe916fb70a8bf49503cce70a5c7124bcc4314ddc
| | | Author: Antony Peacock <ant.peacock@gmail.com>
| | | Date:   Wed Feb 19 08:31:18 2025 +0000
| | |
| | |     Remove parquet cmakelist patch
| | |
| | | * commit 9f6d2759d650ec3c86d01bb940e829e7e14220c2
| |_|/| Merge: a4bbd3ad6b f0963429b0
|/| | | Author: Thomas Sedlmair <thomas.sedlmair@googlemail.com>
| | | | Date:   Wed Feb 19 08:03:08 2025 +0100
| | | |
| | | |     Merge f0963429b0952499da0da7e559f8d53387097307 into a4bbd3ad6bb5a0f8e5117a897d8c55941f533d98
| | | |
| | | * commit f0963429b0952499da0da7e559f8d53387097307
| |_|/  Author: Thomas Sedlmair <thomas.sedlmair@googlemail.com>
|/| |   Date:   Wed Feb 19 08:01:43 2025 +0100
| | |
| | |       added cwt-cucumber 2.5
| | |
```

On the other hand, running `git log --graph --date-order --branches
--tags` returns the expected:
```
* commit a4bbd3ad6bb5a0f8e5117a897d8c55941f533d98 (HEAD -> master)
| Author: Dan <mstr.danila@gmail.com>
| Date:   Fri Feb 14 18:46:11 2025 +0200
|
|     grpc: add version 1.69.0 (#26446)
|
|     * grpc: add version 1.69.0
|
|     * add cmake tool requires
|
|     ---------
|
|     Co-authored-by: Luis Caro Campos <3535649+jcar87@users.noreply.github.com>
|
* commit a7868807cb2e21206ebf95278cb588f29a3e2205
| Author: Guillaume Egles <gegles@users.noreply.github.com>
| Date:   Thu Feb 13 05:44:35 2025 -0800
|
|     openssl: add versions `3.0.16`, `3.1.8`, `3.2.4`, `3.3.3`, `3.4.1`, stop publishing revisions for version `3.0.15` (#26578)
|
* commit 86057d3e63ac71e2fe48c07bb301f2d54187044d
| Author: Luis Caro Campos <3535649+jcar87@users.noreply.github.com>
| Date:   Thu Feb 13 13:34:41 2025 +0000
|
|     android-ndk: dont set LD and AS variables (#26581)
|
|     * android-ndk: dont set LD and AS variables
|
|     * android-ndk: refactor test package
|
* commit 123e382fafd2f5e811e10faac02efc275c45ec2a
| Author: Nikita <root.kidik@gmail.com>
| Date:   Thu Feb 13 12:29:39 2025 +0300
|
|     libffi: fix conditionals when building on Windows (#26500)
|
|     * fix: add missing or `clang`
|
|     * fix: libffi - always require as tool `automake`
```
2025-02-19 10:35:08 -08:00
silverwind
5576b10604
Merge branch 'main' into feat/actions-delete-workflow-run 2025-01-23 19:10:33 +01:00
zsbahtiar
530360bd1a fix: check backend 2025-01-09 09:55:09 +07:00
zsbahtiar
f389529950 fix: job 2025-01-09 09:42:23 +07:00
zsbahtiar
95924a17b9 fix: lint 2025-01-09 09:28:30 +07:00
zsbahtiar
e17f73aae2 test(integration): add assert error equal 2025-01-09 09:04:05 +07:00
zsbahtiar
8c742ab72a test(integration): add TestRepoActionDelete 2025-01-09 08:35:29 +07:00
zsbahtiar
1a7a7e7127 refactor: change to DeleteActionRunAndChild 2025-01-09 05:33:29 +07:00
zsbahtiar
71b773ac2e fix: lint 2025-01-08 18:34:52 +07:00
zsbahtiar
965087e6a9 feat: add removeActionTaskLogFilenames 2025-01-08 18:02:58 +07:00
zsbahtiar
1390874e35 feat: add GetRunTasksByJobIDs 2025-01-08 18:02:36 +07:00
zsbahtiar
80377f8ae4 rm: invalid test 2025-01-08 17:14:04 +07:00
zsbahtiar
05af60bff7 refactor: change to GetRunsByIDsAndTriggerUserID 2025-01-08 17:11:46 +07:00
zsbahtiar
5dd6245fa1 fix: job id 2025-01-08 16:46:53 +07:00
zsbahtiar
050a6b6fb0 feat: add GetRunJobsByRunIDs 2025-01-08 16:46:18 +07:00
zsbahtiar
0b27261d31 revert 2025-01-08 16:14:46 +07:00
zsbahtiar
3faea229b6 revert 2025-01-08 16:13:24 +07:00
zsbahtiar
7f511d9813 feat(DeleteRunByIDs): add delete action_task_step, action_task_output, action_task 2025-01-08 16:05:59 +07:00
zsbahtiar
8413beba4b refactor(action.delete): change to support plural delete action runs 2025-01-08 16:05:59 +07:00
zsbahtiar
b6afdc6cc7 refactor(run_list): change from button delete workflow to checkbox 2025-01-08 16:05:59 +07:00
zsbahtiar
aa8a3d0789 feat(action.list): add checkbox all and button delete 2025-01-08 16:05:59 +07:00
zsbahtiar
1e752123e9 feat(websrc): add repo action for handling delete 2025-01-08 16:05:59 +07:00
zsbahtiar
c4bdf78bda refactor: change middleware to reqRepoActionsWriter 2025-01-08 16:05:59 +07:00
zam.
60982c7d72
Merge branch 'main' into feat/actions-delete-workflow-run 2025-01-08 02:18:07 +07:00
zsbahtiar
8170c47b07 feat(DeleteRunByID): add delete action_run_job 2025-01-08 02:15:50 +07:00
zsbahtiar
6c58a02362 fix: lint 2025-01-08 01:41:45 +07:00
zsbahtiar
3938aeadd5 feat(initGlobalDeleteButton): add option http method delete when data-method is delete 2025-01-08 01:16:45 +07:00
zsbahtiar
c8dbc29c9a feat(web): add path DELETE /username/reponame/actions/runs/{id} 2025-01-08 01:16:00 +07:00
zsbahtiar
5358fc5054 feat(action_model): add query DeleteRunByID 2025-01-08 01:14:49 +07:00
zsbahtiar
862237cd05 fix: lint 2025-01-07 23:33:48 +07:00
zsbahtiar
fcd6948539 feat(runs_list): add button delete workflow run 2025-01-07 23:01:23 +07:00
10 changed files with 518 additions and 3 deletions

View File

@ -435,3 +435,56 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
}
type ActionRunIndex db.ResourceIndex
// DeleteActionRunAndChild delete action_task_step, action_task_output, action_task, action_run and action_run_job.
func DeleteActionRunAndChild(ctx context.Context, runIDs, jobIDs, taskIDs []int64) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
_, err = db.GetEngine(ctx).In("task_id", taskIDs).
Delete(ActionTaskStep{})
if err != nil {
return err
}
_, err = db.GetEngine(ctx).In("task_id", taskIDs).
Delete(ActionTaskOutput{})
if err != nil {
return err
}
_, err = db.GetEngine(ctx).In("id", taskIDs).Delete(ActionTask{})
if err != nil {
return err
}
_, err = db.GetEngine(ctx).In("id", jobIDs).Delete(ActionRunJob{})
if err != nil {
return err
}
_, err = db.GetEngine(ctx).In("id", runIDs).Delete(ActionRun{})
if err != nil {
return err
}
return committer.Commit()
}
// GetRunsByIDsAndTriggerUserID -- get all action run by trigger user with selected ids
func GetRunsByIDsAndTriggerUserID(ctx context.Context, ids []int64, triggerUserID int64) ([]*ActionRun, error) {
var runs []*ActionRun
err := db.GetEngine(ctx).Where("trigger_user_id=?", triggerUserID).
In("id", ids).Find(&runs)
if err != nil {
return nil, err
}
if len(runs) < 1 {
return nil, fmt.Errorf("run with ids %d: %w", ids, util.ErrNotExist)
}
return runs, nil
}

View File

@ -184,3 +184,19 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
return StatusUnknown // it shouldn't happen
}
}
func GetRunJobsByRunIDs(ctx context.Context, runIDs []int64) ([]*ActionRunJob, error) {
var jobs []*ActionRunJob
if err := db.GetEngine(ctx).In("run_id", runIDs).Find(&jobs); err != nil {
return nil, err
}
return jobs, nil
}
func GetRunTasksByJobIDs(ctx context.Context, jobIDs []int64) ([]*ActionTask, error) {
var tasks []*ActionTask
if err := db.GetEngine(ctx).In("job_id", jobIDs).Find(&tasks); err != nil {
return nil, err
}
return tasks, nil
}

View File

@ -7,6 +7,8 @@ import (
"bytes"
stdCtx "context"
"net/http"
"os"
"path/filepath"
"slices"
"strings"
@ -18,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
@ -28,6 +31,7 @@ import (
"code.gitea.io/gitea/services/convert"
"github.com/nektos/act/pkg/model"
"golang.org/x/sync/errgroup"
"gopkg.in/yaml.v3"
)
@ -312,6 +316,7 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin)
}
// loadIsRefDeleted loads the IsRefDeleted field for each run in the list.
@ -424,3 +429,94 @@ func decodeNode(node yaml.Node, out any) bool {
}
return true
}
func DeleteRuns(ctx *context.Context) {
rd := ctx.Req.Body
defer rd.Close()
req := DeleteRunsRequest{}
if err := json.NewDecoder(rd).Decode(&req); err != nil {
ctx.ServerError("failed to decode request body into delte runs request", err)
return
}
if len(req.ActionIDs) < 1 {
ctx.ServerError("missing action_run.id for delete action run", nil)
return
}
var (
eg = new(errgroup.Group)
actionRuns []*actions_model.ActionRun
jobIDs, taskIDs []int64
taskLogFileNames []string
)
eg.Go(func() error {
var err error
actionRuns, err = actions_model.GetRunsByIDsAndTriggerUserID(ctx, req.ActionIDs, ctx.Doer.ID)
return err
})
eg.Go(func() error {
actionRunJobs, err := actions_model.GetRunJobsByRunIDs(ctx, req.ActionIDs)
if err != nil {
return err
}
for _, actionRunJob := range actionRunJobs {
jobIDs = append(jobIDs, actionRunJob.ID)
}
actionTasks, err := actions_model.GetRunTasksByJobIDs(ctx, jobIDs)
if err != nil {
return err
}
for _, actionTask := range actionTasks {
taskIDs = append(taskIDs, actionTask.ID)
taskLogFileNames = append(taskLogFileNames, actionTask.LogFilename)
}
return nil
})
err := eg.Wait()
if err != nil {
ctx.ServerError("failed to get action runs and action run jobs", err)
return
}
if len(actionRuns) != len(req.ActionIDs) {
ctx.ServerError("action ids not match with request", nil)
return
}
err = actions_model.DeleteActionRunAndChild(ctx, req.ActionIDs, jobIDs, taskIDs)
if err != nil {
ctx.ServerError("failed to delete action_run", err)
return
}
removeActionTaskLogFilenames(taskLogFileNames)
ctx.Status(http.StatusNoContent)
}
type DeleteRunsRequest struct {
ActionIDs []int64 `json:"actionIds"`
}
func removeActionTaskLogFilenames(taskLogFileNames []string) {
dirNameActionLog := "actions_log"
go func() {
for _, taskLogFileName := range taskLogFileNames {
var fileName string
if filepath.IsAbs(setting.AppDataPath) {
fileName = filepath.Join(setting.AppDataPath, dirNameActionLog, taskLogFileName)
} else {
fileName = filepath.Join(setting.AppWorkPath, setting.AppDataPath, dirNameActionLog, taskLogFileName)
}
if err := os.Remove(fileName); err != nil {
log.Error("failed to remove actions_log file %s: %v", fileName, err)
}
}
}()
}

View File

@ -1447,6 +1447,8 @@ func registerRoutes(m *web.Router) {
m.Group("/workflows/{workflow_name}", func() {
m.Get("/badge.svg", actions.GetWorkflowBadge)
})
m.Post("/runs/delete", reqRepoActionsWriter, actions.DeleteRuns)
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions)
// end "/{username}/{reponame}/actions"

View File

@ -29,7 +29,7 @@ func GetCommitGraph(r *git.Repository, page, maxAllowedColors int, hidePRRefs bo
}
if len(branches) == 0 {
graphCmd.AddArguments("--all")
graphCmd.AddArguments("--tags", "--branches")
}
graphCmd.AddArguments("-C", "-M", "--date=iso-strict").

View File

@ -25,7 +25,19 @@
</div>
</div>
<div class="twelve wide column content">
<div class="ui secondary filter menu tw-justify-end tw-flex tw-items-center">
<div id="action-filter" class="ui secondary filter menu tw-flex tw-items-center tw-justify-between">
<!-- Checkbox -->
<div class="action-list-toolbar-left">
<input type="checkbox" autocomplete="off" class="action-checkbox-all tw-mr-4 tw-ml-4" title="{{ctx.Locale.Tr "repo.issues.action_check_all"}}">
</div>
<div class="tw-flex tw-items-center">
{{if $.IsRepoAdmin}}
<div id="action-delete" class="ui jump item tw-hidden">
<button class="ui red button action-action" data-action="delete" data-url="{{$.RepoLink}}/actions/runs/delete" data-action-delete-confirm="{{ctx.Locale.Tr "confirm_delete_selected"}}">
{{ctx.Locale.Tr "repo.issues.delete"}}
</button>
</div>
{{end}}
<!-- Actor -->
<div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item">
<span class="text">{{ctx.Locale.Tr "actions.runs.actor"}}</span>
@ -63,6 +75,7 @@
</a>
{{end}}
</div>
</div>
</div>
{{if .AllowDisableOrEnableWorkflow}}

View File

@ -1,4 +1,4 @@
<div class="flex-list run-list">
<div id="action-actions" class="flex-list run-list">
{{if not .Runs}}
<div class="empty-placeholder">
{{svg "octicon-no-entry" 48}}
@ -7,6 +7,7 @@
{{end}}
{{range .Runs}}
<div class="flex-item tw-items-center">
<input type="checkbox" autocomplete="off" class="action-checkbox tw-mr-4 tw-ml-4" data-action-id={{.ID}} aria-label="{{ctx.Locale.Tr "repo.issues.action_check"}} &quot;{{.Title}}&quot;"{{if or (eq .Status 6) (eq .Status 5)}}disabled{{end}}>
<div class="flex-item-leading">
{{template "repo/actions/status" (dict "status" .Status.String)}}
</div>

View File

@ -0,0 +1,235 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/tests"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
)
func TestRepoActionDelete(t *testing.T) {
defer tests.PrepareTestEnv(t)()
testCases := []struct {
treePath string
fileContent string
outcomes map[string]*mockTaskOutcome
expectedTaskNeeds map[string]*runnerv1.TaskNeed // jobID => TaskNeed
}{
{
treePath: ".gitea/workflows/jobs-outputs-with-matrix.yml",
fileContent: `name: jobs-outputs-with-matrix
on:
push:
paths:
- '.gitea/workflows/jobs-outputs-with-matrix.yml'
jobs:
job1:
runs-on: ubuntu-latest
outputs:
output_1: ${{ steps.gen_output.outputs.output_1 }}
output_2: ${{ steps.gen_output.outputs.output_2 }}
output_3: ${{ steps.gen_output.outputs.output_3 }}
strategy:
matrix:
version: [1, 2, 3]
steps:
- name: Generate output
id: gen_output
run: |
version="${{ matrix.version }}"
echo "output_${version}=${version}" >> "$GITHUB_OUTPUT"
job2:
runs-on: ubuntu-latest
needs: [job1]
steps:
- run: echo '${{ toJSON(needs.job1.outputs) }}'
`,
outcomes: map[string]*mockTaskOutcome{
"job1 (1)": {
result: runnerv1.Result_RESULT_SUCCESS,
outputs: map[string]string{
"output_1": "1",
"output_2": "",
"output_3": "",
},
},
"job1 (2)": {
result: runnerv1.Result_RESULT_SUCCESS,
outputs: map[string]string{
"output_1": "",
"output_2": "2",
"output_3": "",
},
},
"job1 (3)": {
result: runnerv1.Result_RESULT_SUCCESS,
outputs: map[string]string{
"output_1": "",
"output_2": "",
"output_3": "3",
},
},
},
expectedTaskNeeds: map[string]*runnerv1.TaskNeed{
"job1": {
Result: runnerv1.Result_RESULT_SUCCESS,
Outputs: map[string]string{
"output_1": "1",
"output_2": "2",
"output_3": "3",
},
},
},
},
{
treePath: ".gitea/workflows/jobs-outputs-with-matrix-failure.yml",
fileContent: `name: jobs-outputs-with-matrix-failure
on:
push:
paths:
- '.gitea/workflows/jobs-outputs-with-matrix-failure.yml'
jobs:
job1:
runs-on: ubuntu-latest
outputs:
output_1: ${{ steps.gen_output.outputs.output_1 }}
output_2: ${{ steps.gen_output.outputs.output_2 }}
output_3: ${{ steps.gen_output.outputs.output_3 }}
strategy:
matrix:
version: [1, 2, 3]
steps:
- name: Generate output
id: gen_output
run: |
version="${{ matrix.version }}"
echo "output_${version}=${version}" >> "$GITHUB_OUTPUT"
job2:
runs-on: ubuntu-latest
if: ${{ always() }}
needs: [job1]
steps:
- run: echo '${{ toJSON(needs.job1.outputs) }}'
`,
outcomes: map[string]*mockTaskOutcome{
"job1 (1)": {
result: runnerv1.Result_RESULT_SUCCESS,
outputs: map[string]string{
"output_1": "1",
"output_2": "",
"output_3": "",
},
},
"job1 (2)": {
result: runnerv1.Result_RESULT_FAILURE,
outputs: map[string]string{
"output_1": "",
"output_2": "",
"output_3": "",
},
},
"job1 (3)": {
result: runnerv1.Result_RESULT_SUCCESS,
outputs: map[string]string{
"output_1": "",
"output_2": "",
"output_3": "3",
},
},
},
expectedTaskNeeds: map[string]*runnerv1.TaskNeed{
"job1": {
Result: runnerv1.Result_RESULT_FAILURE,
Outputs: map[string]string{
"output_1": "1",
"output_2": "",
"output_3": "3",
},
},
},
},
}
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user2.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
apiRepo := createActionsTestRepo(t, token, "actions-jobs-outputs-with-matrix", false)
runner := newMockRunner()
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"})
for _, tc := range testCases {
t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) {
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent)
createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts)
for i := 0; i < len(tc.outcomes); i++ {
task := runner.fetchTask(t)
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
outcome := tc.outcomes[jobName]
assert.NotNil(t, outcome)
runner.execTask(t, task, outcome)
}
task := runner.fetchTask(t)
actualTaskNeeds := task.Needs
assert.Len(t, actualTaskNeeds, len(tc.expectedTaskNeeds))
for jobID, tn := range tc.expectedTaskNeeds {
actualNeed := actualTaskNeeds[jobID]
assert.Equal(t, tn.Result, actualNeed.Result)
assert.Len(t, actualNeed.Outputs, len(tn.Outputs))
for outputKey, outputValue := range tn.Outputs {
assert.Equal(t, outputValue, actualNeed.Outputs[outputKey])
}
}
})
}
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository)
// check result
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions", httpContext.Username, httpContext.Reponame))
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
var runIDs []int64
list := htmlDoc.doc.Find("#action-actions input.action-checkbox:not(:disabled)")
list.Each(func(i int, s *goquery.Selection) {
idStr, exists := s.Attr("data-action-id")
if exists {
runID, err := strconv.ParseInt(idStr, 10, 64)
assert.NoError(t, err)
runIDs = append(runIDs, runID)
}
})
assert.NotEmpty(t, runIDs)
csrf := GetUserCSRFToken(t, session)
reqDelete := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/%s/%s/actions/runs/delete", httpContext.Username, httpContext.Reponame), map[string]any{
"actionIds": runIDs,
"_csrf": csrf,
}).AddTokenAuth(token).
SetHeader("X-Csrf-Token", csrf)
session.MakeRequest(t, reqDelete, http.StatusNoContent)
// should not found
_, err := actions_model.GetRunsByIDsAndTriggerUserID(context.Background(), runIDs, user2.ID)
assert.EqualError(t, err, fmt.Errorf("run with ids %d: %w", runIDs, util.ErrNotExist).Error())
doAPIDeleteRepository(httpContext)
})
}

View File

@ -0,0 +1,97 @@
import {queryElems, toggleElem} from '../utils/dom.ts';
import {confirmModal} from './comp/ConfirmModal.ts';
import {showErrorToast} from '../modules/toast.ts';
import {POST} from '../modules/fetch.ts';
function initRepoActionListCheckboxes() {
const actionListSelectAll = document.querySelector<HTMLInputElement>('.action-checkbox-all');
if (!actionListSelectAll) return; // logged out state
const issueCheckboxes = document.querySelectorAll<HTMLInputElement>('.action-checkbox:not([disabled])');
const actionDelete = document.querySelector('#action-delete');
const syncIssueSelectionState = () => {
const enabledCheckboxes = Array.from(issueCheckboxes).filter((el) => !el.disabled);
const checkedCheckboxes = enabledCheckboxes.filter((el) => el.checked);
const anyChecked = Boolean(checkedCheckboxes.length);
const allChecked = anyChecked && checkedCheckboxes.length === enabledCheckboxes.length;
if (allChecked) {
actionListSelectAll.checked = true;
actionListSelectAll.indeterminate = false;
} else if (anyChecked) {
actionListSelectAll.checked = false;
actionListSelectAll.indeterminate = true;
} else {
actionListSelectAll.checked = false;
actionListSelectAll.indeterminate = false;
}
if (actionDelete) {
toggleElem('#action-delete', anyChecked);
}
};
for (const el of issueCheckboxes) {
el.addEventListener('change', syncIssueSelectionState);
}
actionListSelectAll.addEventListener('change', () => {
for (const el of issueCheckboxes) {
if (!el.disabled) {
el.checked = actionListSelectAll.checked;
}
}
syncIssueSelectionState();
});
queryElems(document, '.action-action', (el) => el.addEventListener('click',
async (e: MouseEvent) => {
e.preventDefault();
const action = el.getAttribute('data-action');
const url = el.getAttribute('data-url');
const actionIDList: number[] = [];
const radix = 10;
for (const el of document.querySelectorAll<HTMLInputElement>('.action-checkbox:checked:not([disabled])')) {
const id = el.getAttribute('data-action-id');
if (id) {
actionIDList.push(parseInt(id, radix));
}
}
if (actionIDList.length < 1) return;
// for delete
if (action === 'delete') {
const confirmText = el.getAttribute('data-action-delete-confirm');
if (!await confirmModal({content: confirmText, confirmButtonColor: 'red'})) {
return;
}
}
try {
await deleteActions(url, actionIDList);
window.location.reload();
} catch (err) {
showErrorToast(err.responseJSON?.error ?? err.message);
}
},
));
}
async function deleteActions(url: string, actionIds: number[]) {
try {
const response = await POST(url, {
data: {
actionIds,
},
});
if (!response.ok) {
throw new Error('failed to delete actions');
}
} catch (error) {
console.error(error);
}
}
export function initRepoActionList() {
if (document.querySelector('.page-content.repository.actions')) {
initRepoActionListCheckboxes();
}
}

View File

@ -85,6 +85,7 @@ import {
initGlobalEnterQuickSubmit,
initGlobalFormDirtyLeaveConfirm,
} from './features/common-form.ts';
import {initRepoActionList} from './features/repo-action-list.ts';
initGiteaFomantic();
initDirAuto();
@ -213,5 +214,6 @@ onDomReady(() => {
initColorPickers,
initOAuth2SettingsDisableCheckbox,
initRepoActionList,
]);
});