Compare commits

...

30 Commits

Author SHA1 Message Date
AvengerMoJo 17462ede24
Merge d68a821455 into ecd1d96f49 2024-05-05 17:09:31 +08:00
yp05327 ecd1d96f49
Add result check in TestAPIEditUser (#29674)
Fix #29514
There are too many usage of `NewRequestWithValues`, so there's no need
to check all of them.
Just one is enough I think.
2024-05-05 02:10:20 +00:00
Neal Caffery bb0e4ce581
Update README.md (#30856)
fix typo for the Docker README
2024-05-03 23:53:18 -04:00
wxiaoguang c7bb3aa034
Fix markdown URL parsing for commit ID (#30812) 2024-05-04 09:48:16 +08:00
wxiaoguang 0f3e717a1a
Improve grep search (#30843)
Reduce the context line number to 1, make "git grep" search respect the
include/exclude patter, and fix #30785
2024-05-03 09:13:48 +00:00
Kemal Zebari 9f0ef3621a
Don't only list code-enabled repositories when using repository API (#30817)
We should be listing all repositories by default.

Fixes #28483.
2024-05-03 15:58:31 +08:00
yp05327 a50026e2f3
Fix no edit history after editing issue's title and content (#30814)
Fix #30807

reuse functions in services
2024-05-03 14:11:51 +08:00
wxiaoguang 53b55223d1
Ignore useless error message "broken pipe" (#30801)
Fix #30792
2024-05-03 02:39:36 +00:00
silverwind c4e875402b
Fix JS error on pull request page (#30838)
Fix this error seen on PR page, regression from
https://github.com/go-gitea/gitea/pull/30803:

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-05-03 10:12:10 +08:00
silverwind b30b7df9f4
Fix body margin shifting with modals, fix error on project column edit (#30831)
Fixes: https://github.com/go-gitea/gitea/issues/30816, regression from
https://github.com/go-gitea/gitea/pull/30723.
Fixes: https://github.com/go-gitea/gitea/pull/30815, regression from
https://github.com/go-gitea/gitea/pull/30723.

Fomantic [expects a
callback](59d9b40987/src/definitions/modules/modal.js (L530-L534))
to be called during `hide` which we did not do, so it could never remove
the margin it added to `body`.

I do observe the body content shifting to right by 1px when modal opens,
but this is a bug that existed on v1.21 as well, so not a regression.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-05-03 01:48:24 +00:00
silverwind c445a85528
Improve repo button row layout (#30668)
Since there is now a second `<input>` in the repo buttons, we can make a
better-looking layout with no empty space, except on mobile.

Also I fixed one bug with focus border on clone panel.

## Large

<img width="1163" alt="Screenshot 2024-04-23 at 22 25 22"
src="https://github.com/go-gitea/gitea/assets/115237/8135a572-aa67-4672-ad49-b76b06890b52">

## Medium
<img width="870" alt="Screenshot 2024-04-23 at 22 25 34"
src="https://github.com/go-gitea/gitea/assets/115237/9e93f61c-3315-4a78-8328-8cefad5b50fa">

## Mobile
<img width="416" alt="Screenshot 2024-04-23 at 22 25 52"
src="https://github.com/go-gitea/gitea/assets/115237/859e341f-807a-48e6-8bcf-31715963216c">
2024-05-02 19:10:49 +00:00
Bo-Yi Wu e67fbe4f15
refactor: merge ListActionTasks func to action.go file (#30811)
Just merge actions.go file to action.go

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2024-05-02 17:43:29 +00:00
Archer 5c542ca94c
Prevent automatic OAuth grants for public clients (#30790)
This commit forces the resource owner (user) to always approve OAuth 2.0
authorization requests if the client is public (e.g. native
applications).

As detailed in [RFC 6749 Section 10.2](https://www.rfc-editor.org/rfc/rfc6749.html#section-10.2),

> The authorization server SHOULD NOT process repeated authorization
requests automatically (without active resource owner interaction)
without authenticating the client or relying on other measures to ensure
that the repeated request comes from the original client and not an
impersonator.

With the implementation prior to this patch, attackers with access to
the redirect URI (e.g., the loopback interface for
`git-credential-oauth`) can get access to the user account without any
user interaction if they can redirect the user to the
`/login/oauth/authorize` endpoint somehow (e.g., with `xdg-open` on
Linux).

Fixes #25061.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-05-02 17:05:59 +00:00
Kemal Zebari 872caa17c0
Catch and handle unallowed file type errors in issue attachment API (#30791)
Before, we would just throw 500 if a user passes an attachment that is
not an allowed type. This commit catches this error and throws a 422
instead since this should be considered a validation error.
2024-05-02 16:33:31 +00:00
wxiaoguang 677032d36a
Fix incorrect message id for releaes email (#30825)
Make generateMessageIDForRelease outputs the same format as
generateMessageIDForIssue (old `createReference`)
2024-05-02 15:24:21 +00:00
silverwind 6f89d5e3a0
Add hover outline to heatmap squares (#30828)
Makes it easier to use because you see which square is currently
hovered:

<img width="314" alt="Screenshot 2024-05-02 at 15 38 20"
src="https://github.com/go-gitea/gitea/assets/115237/3a15dad1-2259-4f28-9fae-5cf6ad3d8798">

I did try a `scoped` style for this, but that did not work for some
reason.
2024-05-02 14:56:17 +00:00
silverwind 9235442ba5
Remove external API calls in `TestPassword` (#30716)
The test had a dependency on `https://api.pwnedpasswords.com` which
caused many failures on CI recently:

```
--- FAIL: TestPassword (2.37s)
    pwn_test.go:41: Get "https://api.pwnedpasswords.com/range/e6b6a": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
FAIL
coverage: 82.9% of statements
```
2024-05-02 14:43:23 +00:00
Lunny Xiao cb9e1a3ff6
Upgrade chi-binding (#30826)
Front port #30742
2024-05-02 14:09:38 +00:00
silverwind b1bb3642e5
Improve context popup rendering (#30824)
Before, lot of empty space when no labels or body:

<img width="281" alt="Screenshot 2024-05-02 at 13 51 29"
src="https://github.com/go-gitea/gitea/assets/115237/8a980ccd-d53c-43a3-a059-dc8c614621e1">

After, empty space collapsed:

<img width="306" alt="Screenshot 2024-05-02 at 13 51 16"
src="https://github.com/go-gitea/gitea/assets/115237/8d9c154d-5de1-43d0-8536-afd9194d99b3">

All `<p>` (unsuitable) and `<small>` (discouraged in favor of css) tags
are removed.
2024-05-02 15:42:33 +02:00
wxiaoguang eb8bb82e58
Fix activity heat map padding & locale (#30823)
Fix #30808

---------

Co-authored-by: silverwind <me@silverwind.io>
2024-05-02 13:22:55 +00:00
wxiaoguang 6ff2acc52c
Fix issue card layout (#30800)
Fix #30788
2024-05-02 11:19:44 +00:00
wxiaoguang ebe6f4cad7
Fix branch selector UI (#30803)
Fix  #30802
2024-05-02 10:45:23 +00:00
Alex Lau(AvengerMoJo) d68a821455
Fix the format
Signed-off-by: Alex Lau(AvengerMoJo) <avengermojo@gmail.com>
2024-04-29 15:03:35 +08:00
Alex Lau(AvengerMoJo) 325842db66
Merge branch 'main' into wip_require_action_feature_fail_post 2024-04-29 14:20:36 +08:00
Alex Lau(AvengerMoJo) c4b923886c
moving the js to a seperated file in web_src/js/features/
Signed-off-by: Alex Lau(AvengerMoJo) <avengermojo@gmail.com>
2024-04-22 13:40:37 +08:00
Alex Lau(AvengerMoJo) 32cb52662f
Remove formatting all the template
Signed-off-by: Alex Lau(AvengerMoJo) <avengermojo@gmail.com>
2024-04-11 14:15:16 +08:00
Alex Lau(AvengerMoJo) c10d144ca7
Remove duplicate error for the global enable flag remove formatting error.
Signed-off-by: Alex Lau(AvengerMoJo) <avengermojo@gmail.com>
2024-04-11 13:40:18 +08:00
Alex Lau(AvengerMoJo) d4fd5b553d
Add the delete function and remove formatting error.
Signed-off-by: Alex Lau(AvengerMoJo) <avengermojo@gmail.com>
2024-04-11 13:39:59 +08:00
Alex Lau(AvengerMoJo) 1fc998984f
Fix all the format of the commit
Signed-off-by: Alex Lau(AvengerMoJo) <avengermojo@gmail.com>
2024-04-10 17:07:46 +08:00
Alex Lau(AvengerMoJo) 5243490071
Enable the global workflow and require_action in org setting
Signed-off-by: Alex Lau(AvengerMoJo) <avengermojo@gmail.com>
2024-04-09 23:05:09 +08:00
75 changed files with 1343 additions and 571 deletions

View File

@ -1,7 +1,7 @@
# Gitea - Docker
Dockerfile is found in root of repository.
Dockerfile is found in the root of the repository.
Docker image can be found on [docker hub](https://hub.docker.com/r/gitea/gitea)
Docker image can be found on [docker hub](https://hub.docker.com/r/gitea/gitea).
Documentation on using docker image can be found on [Gitea Docs site](https://docs.gitea.com/installation/install-with-docker-rootless)
Documentation on using docker image can be found on [Gitea Docs site](https://docs.gitea.com/installation/install-with-docker-rootless).

4
go.mod
View File

@ -8,7 +8,7 @@ require (
code.gitea.io/sdk/gitea v0.17.1
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
connectrpc.com/connect v1.15.0
gitea.com/go-chi/binding v0.0.0-20240316035258-17450c5f3028
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed
gitea.com/go-chi/cache v0.2.0
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96
@ -59,6 +59,7 @@ require (
github.com/google/uuid v1.6.0
github.com/gorilla/feeds v1.1.2
github.com/gorilla/sessions v1.2.2
github.com/h2non/gock v1.2.0
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/huandu/xstrings v1.4.0
@ -209,6 +210,7 @@ require (
github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect

10
go.sum
View File

@ -20,8 +20,8 @@ git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4H
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
gitea.com/gitea/act v0.259.1 h1:8GG1o/xtUHl3qjn5f0h/2FXrT5ubBn05TJOM5ry+FBw=
gitea.com/gitea/act v0.259.1/go.mod h1:UxZWRYqQG2Yj4+4OqfGWW5a3HELwejyWFQyU7F1jUD8=
gitea.com/go-chi/binding v0.0.0-20240316035258-17450c5f3028 h1:6/QAx4+s0dyRwdaTFPTnhGppuiuu0OqxIH9szyTpvKw=
gitea.com/go-chi/binding v0.0.0-20240316035258-17450c5f3028/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw=
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso=
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw=
gitea.com/go-chi/cache v0.2.0 h1:E0npuTfDW6CT1yD8NMDVc1SK6IeRjfmRL2zlEsCEd7w=
gitea.com/go-chi/cache v0.2.0/go.mod h1:iQlVK2aKTZ/rE9UcHyz9pQWGvdP9i1eI2spOpzgCrtE=
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
@ -430,6 +430,10 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
@ -591,6 +595,8 @@ github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE=
github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek=
github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=

View File

@ -0,0 +1,82 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// WIP RequireAction
package actions
import (
"context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder"
)
type RequireAction struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"index"`
RepoName string `xorm:"VARCHAR(255)"`
WorkflowName string `xorm:"VARCHAR(255) UNIQUE(require_action) NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
type GlobalWorkflow struct {
RepoName string
Filename string
}
func init() {
db.RegisterModel(new(RequireAction))
}
type FindRequireActionOptions struct {
db.ListOptions
RequireActionID int64
OrgID int64
RepoName string
}
func (opts FindRequireActionOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.OrgID > 0 {
cond = cond.And(builder.Eq{"org_id": opts.OrgID})
}
if opts.RequireActionID > 0 {
cond = cond.And(builder.Eq{"id": opts.RequireActionID})
}
if opts.RepoName != "" {
cond = cond.And(builder.Eq{"repo_name": opts.RepoName})
}
return cond
}
// LoadAttributes loads the attributes of the require action
func (r *RequireAction) LoadAttributes(ctx context.Context) error {
// place holder for now.
return nil
}
// if the workflow is removable
func (r *RequireAction) Removable(orgID int64) bool {
// everyone can remove for now
return r.OrgID == orgID
}
func AddRequireAction(ctx context.Context, orgID int64, repoName, workflowName string) (*RequireAction, error) {
ra := &RequireAction{
OrgID: orgID,
RepoName: repoName,
WorkflowName: workflowName,
}
return ra, db.Insert(ctx, ra)
}
func DeleteRequireAction(ctx context.Context, requireActionID int64) error {
if _, err := db.DeleteByID[RequireAction](ctx, requireActionID); err != nil {
return err
}
return nil
}

View File

@ -429,62 +429,6 @@ func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_mo
return nil
}
// UpdateIssueByAPI updates all allowed fields of given issue.
// If the issue status is changed a statusChangeComment is returned
// similarly if the title is changed the titleChanged bool is set to true
func UpdateIssueByAPI(ctx context.Context, issue *Issue, doer *user_model.User) (statusChangeComment *Comment, titleChanged bool, err error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return nil, false, err
}
defer committer.Close()
if err := issue.LoadRepo(ctx); err != nil {
return nil, false, fmt.Errorf("loadRepo: %w", err)
}
// Reload the issue
currentIssue, err := GetIssueByID(ctx, issue.ID)
if err != nil {
return nil, false, err
}
if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(
"name", "content", "milestone_id", "priority",
"deadline_unix", "updated_unix", "is_locked").
Update(issue); err != nil {
return nil, false, err
}
titleChanged = currentIssue.Title != issue.Title
if titleChanged {
opts := &CreateCommentOptions{
Type: CommentTypeChangeTitle,
Doer: doer,
Repo: issue.Repo,
Issue: issue,
OldTitle: currentIssue.Title,
NewTitle: issue.Title,
}
_, err := CreateComment(ctx, opts)
if err != nil {
return nil, false, fmt.Errorf("createComment: %w", err)
}
}
if currentIssue.IsClosed != issue.IsClosed {
statusChangeComment, err = doChangeIssueStatus(ctx, issue, doer, false)
if err != nil {
return nil, false, err
}
}
if err := issue.AddCrossReferences(ctx, doer, true); err != nil {
return nil, false, err
}
return statusChangeComment, titleChanged, committer.Commit()
}
// UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it.
func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) {
// if the deadline hasn't changed do nothing

View File

@ -0,0 +1,22 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
import (
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
)
func AddRequireActionTable(x *xorm.Engine) error {
type RequireAction struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"index"`
RepoName string `xorm:"VARCHAR(255)"`
WorkflowName string `xorm:"VARCHAR(255) UNIQUE(require_action) NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(RequireAction))
}

View File

@ -169,13 +169,30 @@ func (cfg *PullRequestsConfig) GetDefaultMergeStyle() MergeStyle {
}
type ActionsConfig struct {
DisabledWorkflows []string
DisabledWorkflows []string
EnabledGlobalWorkflows []string
}
func (cfg *ActionsConfig) EnableWorkflow(file string) {
cfg.DisabledWorkflows = util.SliceRemoveAll(cfg.DisabledWorkflows, file)
}
func (cfg *ActionsConfig) DisableGlobalWorkflow(file string) {
cfg.EnabledGlobalWorkflows = util.SliceRemoveAll(cfg.EnabledGlobalWorkflows, file)
}
func (cfg *ActionsConfig) IsGlobalWorkflowEnabled(file string) bool {
return slices.Contains(cfg.EnabledGlobalWorkflows, file)
}
func (cfg *ActionsConfig) EnableGlobalWorkflow(file string) {
cfg.EnabledGlobalWorkflows = append(cfg.EnabledGlobalWorkflows, file)
}
func (cfg *ActionsConfig) GetGlobalWorkflow() []string {
return cfg.EnabledGlobalWorkflows
}
func (cfg *ActionsConfig) ToString() string {
return strings.Join(cfg.DisabledWorkflows, ",")
}

View File

@ -4,12 +4,11 @@
package pwn
import (
"math/rand/v2"
"net/http"
"strings"
"testing"
"time"
"github.com/h2non/gock"
"github.com/stretchr/testify/assert"
)
@ -18,86 +17,34 @@ var client = New(WithHTTP(&http.Client{
}))
func TestPassword(t *testing.T) {
// Check input error
_, err := client.CheckPassword("", false)
defer gock.Off()
count, err := client.CheckPassword("", false)
assert.ErrorIs(t, err, ErrEmptyPassword, "blank input should return ErrEmptyPassword")
assert.Equal(t, -1, count)
// Should fail
fail := "password1234"
count, err := client.CheckPassword(fail, false)
assert.NotEmpty(t, count, "%s should fail as a password", fail)
gock.New("https://api.pwnedpasswords.com").Get("/range/5c1d8").Times(1).Reply(200).BodyString("EAF2F254732680E8AC339B84F3266ECCBB5:1\r\nFC446EB88938834178CB9322C1EE273C2A7:2")
count, err = client.CheckPassword("pwned", false)
assert.NoError(t, err)
assert.Equal(t, 1, count)
// Should fail (with padding)
failPad := "administrator"
count, err = client.CheckPassword(failPad, true)
assert.NotEmpty(t, count, "%s should fail as a password", failPad)
gock.New("https://api.pwnedpasswords.com").Get("/range/ba189").Times(1).Reply(200).BodyString("FD4CB34F0378BCB15D23F6FFD28F0775C9E:3\r\nFDF342FCD8C3611DAE4D76E8A992A3E4169:4")
count, err = client.CheckPassword("notpwned", false)
assert.NoError(t, err)
assert.Equal(t, 0, count)
// Checking for a "good" password isn't going to be perfect, but we can give it a good try
// with hopefully minimal error. Try five times?
assert.Condition(t, func() bool {
for i := 0; i <= 5; i++ {
count, err = client.CheckPassword(testPassword(), false)
assert.NoError(t, err)
if count == 0 {
return true
}
}
return false
}, "no generated passwords passed. there is a chance this is a fluke")
gock.New("https://api.pwnedpasswords.com").Get("/range/a1733").Times(1).Reply(200).BodyString("C4CE0F1F0062B27B9E2F41AF0C08218017C:1\r\nFC446EB88938834178CB9322C1EE273C2A7:2\r\nFE81480327C992FE62065A827429DD1318B:0")
count, err = client.CheckPassword("paddedpwned", true)
assert.NoError(t, err)
assert.Equal(t, 1, count)
// Again, but with padded responses
assert.Condition(t, func() bool {
for i := 0; i <= 5; i++ {
count, err = client.CheckPassword(testPassword(), true)
assert.NoError(t, err)
if count == 0 {
return true
}
}
return false
}, "no generated passwords passed. there is a chance this is a fluke")
}
// Credit to https://golangbyexample.com/generate-random-password-golang/
// DO NOT USE THIS FOR AN ACTUAL PASSWORD GENERATOR
var (
lowerCharSet = "abcdedfghijklmnopqrst"
upperCharSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
specialCharSet = "!@#$%&*"
numberSet = "0123456789"
allCharSet = lowerCharSet + upperCharSet + specialCharSet + numberSet
)
func testPassword() string {
var password strings.Builder
// Set special character
for i := 0; i < 5; i++ {
random := rand.IntN(len(specialCharSet))
password.WriteString(string(specialCharSet[random]))
}
// Set numeric
for i := 0; i < 5; i++ {
random := rand.IntN(len(numberSet))
password.WriteString(string(numberSet[random]))
}
// Set uppercase
for i := 0; i < 5; i++ {
random := rand.IntN(len(upperCharSet))
password.WriteString(string(upperCharSet[random]))
}
for i := 0; i < 5; i++ {
random := rand.IntN(len(allCharSet))
password.WriteString(string(allCharSet[random]))
}
inRune := []rune(password.String())
rand.Shuffle(len(inRune), func(i, j int) {
inRune[i], inRune[j] = inRune[j], inRune[i]
})
return string(inRune)
gock.New("https://api.pwnedpasswords.com").Get("/range/5617b").Times(1).Reply(200).BodyString("FD4CB34F0378BCB15D23F6FFD28F0775C9E:3\r\nFDF342FCD8C3611DAE4D76E8A992A3E4169:4\r\nFE81480327C992FE62065A827429DD1318B:0")
count, err = client.CheckPassword("paddednotpwned", true)
assert.NoError(t, err)
assert.Equal(t, 0, count)
gock.New("https://api.pwnedpasswords.com").Get("/range/79082").Times(1).Reply(200).BodyString("FDF342FCD8C3611DAE4D76E8A992A3E4169:4\r\nFE81480327C992FE62065A827429DD1318B:0\r\nAFEF386F56EB0B4BE314E07696E5E6E6536:0")
count, err = client.CheckPassword("paddednotpwnedzero", true)
assert.NoError(t, err)
assert.Equal(t, 0, count)
}

View File

@ -29,6 +29,7 @@ type GrepOptions struct {
ContextLineNumber int
IsFuzzy bool
MaxLineLength int // the maximum length of a line to parse, exceeding chars will be truncated
PathspecList []string
}
func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
@ -62,6 +63,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
cmd.AddOptionValues("-e", strings.TrimLeft(search, "-"))
}
cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD"))
cmd.AddDashesAndList(opts.PathspecList...)
opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50)
stderr := bytes.Buffer{}
err = cmd.Run(&RunOpts{

View File

@ -31,6 +31,26 @@ func TestGrepSearch(t *testing.T) {
},
}, res)
res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{PathspecList: []string{":(glob)java-hello/*"}})
assert.NoError(t, err)
assert.Equal(t, []*GrepResult{
{
Filename: "java-hello/main.java",
LineNumbers: []int{3},
LineCodes: []string{" public static void main(String[] args)"},
},
}, res)
res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{PathspecList: []string{":(glob,exclude)java-hello/*"}})
assert.NoError(t, err)
assert.Equal(t, []*GrepResult{
{
Filename: "main.vendor.java",
LineNumbers: []int{3},
LineCodes: []string{" public static void main(String[] args)"},
},
}, res)
res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1})
assert.NoError(t, err)
assert.Equal(t, []*GrepResult{

View File

@ -10,6 +10,7 @@ import (
"path"
"path/filepath"
"regexp"
"slices"
"strings"
"sync"
@ -54,7 +55,7 @@ var (
shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
// anyHashPattern splits url containing SHA into parts
anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~_%.a-zA-Z0-9/]+)?(#[-+~_%.a-zA-Z0-9]+)?`)
anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`)
// comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash"
comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`)
@ -591,7 +592,8 @@ func replaceContentList(node *html.Node, i, j int, newNodes []*html.Node) {
func mentionProcessor(ctx *RenderContext, node *html.Node) {
start := 0
for node != nil {
nodeStop := node.NextSibling
for node != nodeStop {
found, loc := references.FindFirstMentionBytes(util.UnsafeStringToBytes(node.Data[start:]))
if !found {
node = node.NextSibling
@ -962,57 +964,68 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
}
}
type anyHashPatternResult struct {
PosStart int
PosEnd int
FullURL string
CommitID string
SubPath string
QueryHash string
}
func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) {
m := anyHashPattern.FindStringSubmatchIndex(s)
if m == nil {
return ret, false
}
ret.PosStart, ret.PosEnd = m[0], m[1]
ret.FullURL = s[ret.PosStart:ret.PosEnd]
if strings.HasSuffix(ret.FullURL, ".") {
// if url ends in '.', it's very likely that it is not part of the actual url but used to finish a sentence.
ret.PosEnd--
ret.FullURL = ret.FullURL[:len(ret.FullURL)-1]
for i := 0; i < len(m); i++ {
m[i] = min(m[i], ret.PosEnd)
}
}
ret.CommitID = s[m[2]:m[3]]
if m[5] > 0 {
ret.SubPath = s[m[4]:m[5]]
}
lastStart, lastEnd := m[len(m)-2], m[len(m)-1]
if lastEnd > 0 {
ret.QueryHash = s[lastStart:lastEnd][1:]
}
return ret, true
}
// fullHashPatternProcessor renders SHA containing URLs
func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.Metas == nil {
return
}
next := node.NextSibling
for node != nil && node != next {
m := anyHashPattern.FindStringSubmatchIndex(node.Data)
if m == nil {
return
nodeStop := node.NextSibling
for node != nodeStop {
if node.Type != html.TextNode {
node = node.NextSibling
continue
}
urlFull := node.Data[m[0]:m[1]]
text := base.ShortSha(node.Data[m[2]:m[3]])
// 3rd capture group matches a optional path
subpath := ""
if m[5] > 0 {
subpath = node.Data[m[4]:m[5]]
ret, ok := anyHashPatternExtract(node.Data)
if !ok {
node = node.NextSibling
continue
}
// 4th capture group matches a optional url hash
hash := ""
if m[7] > 0 {
hash = node.Data[m[6]:m[7]][1:]
text := base.ShortSha(ret.CommitID)
if ret.SubPath != "" {
text += ret.SubPath
}
start := m[0]
end := m[1]
// If url ends in '.', it's very likely that it is not part of the
// actual url but used to finish a sentence.
if strings.HasSuffix(urlFull, ".") {
end--
urlFull = urlFull[:len(urlFull)-1]
if hash != "" {
hash = hash[:len(hash)-1]
} else if subpath != "" {
subpath = subpath[:len(subpath)-1]
}
if ret.QueryHash != "" {
text += " (" + ret.QueryHash + ")"
}
if subpath != "" {
text += subpath
}
if hash != "" {
text += " (" + hash + ")"
}
replaceContent(node, start, end, createCodeLink(urlFull, text, "commit"))
replaceContent(node, ret.PosStart, ret.PosEnd, createCodeLink(ret.FullURL, text, "commit"))
node = node.NextSibling.NextSibling
}
}
@ -1021,19 +1034,16 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.Metas == nil {
return
}
next := node.NextSibling
for node != nil && node != next {
m := comparePattern.FindStringSubmatchIndex(node.Data)
if m == nil {
return
nodeStop := node.NextSibling
for node != nodeStop {
if node.Type != html.TextNode {
node = node.NextSibling
continue
}
// Ensure that every group (m[0]...m[7]) has a match
for i := 0; i < 8; i++ {
if m[i] == -1 {
return
}
m := comparePattern.FindStringSubmatchIndex(node.Data)
if m == nil || slices.Contains(m[:8], -1) { // ensure that every group (m[0]...m[7]) has a match
node = node.NextSibling
continue
}
urlFull := node.Data[m[0]:m[1]]

View File

@ -60,7 +60,8 @@ func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosSt
}
func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
for node != nil {
nodeStop := node.NextSibling
for node != nodeStop {
if node.Type != html.TextNode {
node = node.NextSibling
continue

View File

@ -399,36 +399,61 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) {
}
func TestRegExp_anySHA1Pattern(t *testing.T) {
testCases := map[string][]string{
testCases := map[string]anyHashPatternResult{
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js#L2703": {
"a644101ed04d0beacea864ce805e0c4f86ba1cd1",
"/test/unit/event.js",
"#L2703",
CommitID: "a644101ed04d0beacea864ce805e0c4f86ba1cd1",
SubPath: "/test/unit/event.js",
QueryHash: "L2703",
},
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js": {
"a644101ed04d0beacea864ce805e0c4f86ba1cd1",
"/test/unit/event.js",
"",
CommitID: "a644101ed04d0beacea864ce805e0c4f86ba1cd1",
SubPath: "/test/unit/event.js",
},
"https://github.com/jquery/jquery/commit/0705be475092aede1eddae01319ec931fb9c65fc": {
"0705be475092aede1eddae01319ec931fb9c65fc",
"",
"",
CommitID: "0705be475092aede1eddae01319ec931fb9c65fc",
},
"https://github.com/jquery/jquery/tree/0705be475092aede1eddae01319ec931fb9c65fc/src": {
"0705be475092aede1eddae01319ec931fb9c65fc",
"/src",
"",
CommitID: "0705be475092aede1eddae01319ec931fb9c65fc",
SubPath: "/src",
},
"https://try.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2": {
"d8a994ef243349f321568f9e36d5c3f444b99cae",
"",
"#diff-2",
CommitID: "d8a994ef243349f321568f9e36d5c3f444b99cae",
QueryHash: "diff-2",
},
"non-url": {},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b#L1-L2": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
QueryHash: "L1-L2",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678.": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678/sub.": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
SubPath: "/sub",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b.": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b&c=d": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678#hash.": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
QueryHash: "hash",
},
}
for k, v := range testCases {
assert.Equal(t, anyHashPattern.FindStringSubmatch(k)[1:], v)
ret, ok := anyHashPatternExtract(k)
if v.CommitID == "" {
assert.False(t, ok)
} else {
assert.EqualValues(t, strings.TrimSuffix(k, "."), ret.FullURL)
assert.EqualValues(t, v.CommitID, ret.CommitID)
assert.EqualValues(t, v.SubPath, ret.SubPath)
assert.EqualValues(t, v.QueryHash, ret.QueryHash)
}
}
}

View File

@ -124,6 +124,11 @@ func TestRender_CrossReferences(t *testing.T) {
test(
util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345"),
`<p><a href="`+util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345")+`" class="ref-issue" rel="nofollow">gogitea/some-repo-name#12345</a></p>`)
inputURL := "https://host/a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3"
test(
inputURL,
`<p><a href="`+inputURL+`" rel="nofollow"><code>0123456789/foo.txt (L2-L3)</code></a></p>`)
}
func TestMisc_IsSameDomain(t *testing.T) {
@ -695,7 +700,7 @@ func TestIssue18471(t *testing.T) {
}, strings.NewReader(data), &res)
assert.NoError(t, err)
assert.Equal(t, "<a href=\"http://domain/org/repo/compare/783b039...da951ce\" class=\"compare\"><code class=\"nohighlight\">783b039...da951ce</code></a>", res.String())
assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code class="nohighlight">783b039...da951ce</code></a>`, res.String())
}
func TestIsFullURL(t *testing.T) {

32
modules/setting/glob.go Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import "github.com/gobwas/glob"
type GlobMatcher struct {
compiledGlob glob.Glob
patternString string
}
var _ glob.Glob = (*GlobMatcher)(nil)
func (g *GlobMatcher) Match(s string) bool {
return g.compiledGlob.Match(s)
}
func (g *GlobMatcher) PatternString() string {
return g.patternString
}
func GlobMatcherCompile(pattern string, separators ...rune) (*GlobMatcher, error) {
g, err := glob.Compile(pattern, separators...)
if err != nil {
return nil, err
}
return &GlobMatcher{
compiledGlob: g,
patternString: pattern,
}, nil
}

View File

@ -10,8 +10,6 @@ import (
"time"
"code.gitea.io/gitea/modules/log"
"github.com/gobwas/glob"
)
// Indexer settings
@ -30,8 +28,8 @@ var Indexer = struct {
RepoConnStr string
RepoIndexerName string
MaxIndexerFileSize int64
IncludePatterns []glob.Glob
ExcludePatterns []glob.Glob
IncludePatterns []*GlobMatcher
ExcludePatterns []*GlobMatcher
ExcludeVendored bool
}{
IssueType: "bleve",
@ -93,12 +91,12 @@ func loadIndexerFrom(rootCfg ConfigProvider) {
}
// IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing
func IndexerGlobFromString(globstr string) []glob.Glob {
extarr := make([]glob.Glob, 0, 10)
func IndexerGlobFromString(globstr string) []*GlobMatcher {
extarr := make([]*GlobMatcher, 0, 10)
for _, expr := range strings.Split(strings.ToLower(globstr), ",") {
expr = strings.TrimSpace(expr)
if expr != "" {
if g, err := glob.Compile(expr, '.', '/'); err != nil {
if g, err := GlobMatcherCompile(expr, '.', '/'); err != nil {
log.Info("Invalid glob expression '%s' (skipped): %v", expr, err)
} else {
extarr = append(extarr, g)

View File

@ -85,7 +85,7 @@ type CreatePullRequestOption struct {
// EditPullRequestOption options when modify pull request
type EditPullRequestOption struct {
Title string `json:"title"`
Body string `json:"body"`
Body *string `json:"body"`
Base string `json:"base"`
Assignee string `json:"assignee"`
Assignees []string `json:"assignees"`

View File

@ -3647,11 +3647,38 @@ runs.no_workflows.documentation = For more information on Gitea Actions, see <a
runs.no_runs = The workflow has no runs yet.
runs.empty_commit_message = (empty commit message)
require_action = Require Actions
require_action.require_action_manage_panel = Require Actions Management Panel
require_action.enable_global_workflow = How to Enable Global Workflow
require_action.id = ID
require_action.add = Add Global Workflow
require_action.add_require_action = Enable selected Workflow
require_action.new = Create New
require_action.status = Status
require_action.search = Search...
require_action.version = Version
require_action.repo = Repo Name
require_action.workflow = Workflow Filename
require_action.link = Link
require_action.remove = Remove
require_action.none = No Require Actions Available.
require_action.creation.failed = Create Global Require Action %s Failed.
require_action.creation.success = Create Global Require Action %s successfully.
require_action.deletion = Delete
require_action.deletion.description = Removing the Global Require Action is permanent and cannot be undone. Continue?
require_action.deletion.success = The Global Require Action has been removed.
workflow.disable = Disable Workflow
workflow.disable_success = Workflow '%s' disabled successfully.
workflow.enable = Enable Workflow
workflow.enable_success = Workflow '%s' enabled successfully.
workflow.disabled = Workflow is disabled.
workflow.global = Global
workflow.global_disable = Disable Global Require
workflow.global_disable_success = Global Require '%s' disabled successfully.
workflow.global_enable = Enable Global Require
workflow.global_enable_success = Global Require '%s' enabled successfully.
workflow.global_enabled = Global Require is disabled.
need_approval_desc = Need approval to run workflows for fork pull request.

26
package-lock.json generated
View File

@ -14,6 +14,7 @@
"@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.9.0",
"@silverwind/vue3-calendar-heatmap": "2.0.6",
"add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2",
"asciinema-player": "3.7.1",
@ -57,7 +58,6 @@
"vue-bar-graph": "2.0.0",
"vue-chartjs": "5.3.1",
"vue-loader": "17.4.2",
"vue3-calendar-heatmap": "2.0.5",
"webpack": "5.91.0",
"webpack-cli": "5.1.4",
"wrap-ansi": "9.0.0"
@ -1626,6 +1626,18 @@
"win32"
]
},
"node_modules/@silverwind/vue3-calendar-heatmap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@silverwind/vue3-calendar-heatmap/-/vue3-calendar-heatmap-2.0.6.tgz",
"integrity": "sha512-efX+nf2GR7EfA7iNgZDeM9Jue5ksglSXvN0C/ja0M1bTmkCpAxKlGJ3vki7wfTPQgX1O0nCfAM62IKqUUEM0cQ==",
"engines": {
"node": ">=16"
},
"peerDependencies": {
"tippy.js": "^6.3.7",
"vue": "^3.2.29"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@ -12200,18 +12212,6 @@
}
}
},
"node_modules/vue3-calendar-heatmap": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/vue3-calendar-heatmap/-/vue3-calendar-heatmap-2.0.5.tgz",
"integrity": "sha512-qvveNQlTS5Aw7AvRLs0zOyu3uP5iGJlXJAnkrkG2ElDdyQ8H1TJhQ8rL702CROjAg16ezIveUY10nCO7lqZ25w==",
"engines": {
"node": ">=16"
},
"peerDependencies": {
"tippy.js": "^6.3.7",
"vue": "^3.2.29"
}
},
"node_modules/watchpack": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",

View File

@ -13,6 +13,7 @@
"@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.9.0",
"@silverwind/vue3-calendar-heatmap": "2.0.6",
"add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2",
"asciinema-player": "3.7.1",
@ -56,7 +57,6 @@
"vue-bar-graph": "2.0.0",
"vue-chartjs": "5.3.1",
"vue-loader": "17.4.2",
"vue3-calendar-heatmap": "2.0.5",
"webpack": "5.91.0",
"webpack-cli": "5.1.4",
"wrap-ansi": "9.0.0"

View File

@ -140,9 +140,7 @@ func serveMavenMetadata(ctx *context.Context, params parameters) {
ctx.Resp.Header().Set("Content-Length", strconv.Itoa(len(xmlMetadataWithHeader)))
ctx.Resp.Header().Set("Content-Type", contentTypeXML)
if _, err := ctx.Resp.Write(xmlMetadataWithHeader); err != nil {
log.Error("write bytes failed: %v", err)
}
_, _ = ctx.Resp.Write(xmlMetadataWithHeader)
}
func servePackageFile(ctx *context.Context, params parameters, serveContent bool) {

View File

@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
secret_service "code.gitea.io/gitea/services/secrets"
)
@ -517,3 +518,68 @@ type Action struct{}
func NewAction() actions_service.API {
return Action{}
}
// ListActionTasks list all the actions of a repository
func ListActionTasks(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/tasks repository ListActionTasks
// ---
// summary: List a repository's action tasks
// 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: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results, default maximum page size is 50
// type: integer
// responses:
// "200":
// "$ref": "#/responses/TasksList"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/conflict"
// "422":
// "$ref": "#/responses/validationError"
tasks, total, err := db.FindAndCount[actions_model.ActionTask](ctx, &actions_model.FindTaskOptions{
ListOptions: utils.GetListOptions(ctx),
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "ListActionTasks", err)
return
}
res := new(api.ActionTaskResponse)
res.TotalCount = total
res.Entries = make([]*api.ActionTask, len(tasks))
for i := range tasks {
convertedTask, err := convert.ToActionTask(ctx, tasks[i])
if err != nil {
ctx.Error(http.StatusInternalServerError, "ToActionTask", err)
return
}
res.Entries[i] = convertedTask
}
ctx.JSON(http.StatusOK, &res)
}

View File

@ -1,80 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
// ListActionTasks list all the actions of a repository
func ListActionTasks(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/tasks repository ListActionTasks
// ---
// summary: List a repository's action tasks
// 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: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results, default maximum page size is 50
// type: integer
// responses:
// "200":
// "$ref": "#/responses/TasksList"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/conflict"
// "422":
// "$ref": "#/responses/validationError"
tasks, total, err := db.FindAndCount[actions_model.ActionTask](ctx, &actions_model.FindTaskOptions{
ListOptions: utils.GetListOptions(ctx),
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "ListActionTasks", err)
return
}
res := new(api.ActionTaskResponse)
res.TotalCount = total
res.Entries = make([]*api.ActionTask, len(tasks))
for i := range tasks {
convertedTask, err := convert.ToActionTask(ctx, tasks[i])
if err != nil {
ctx.Error(http.StatusInternalServerError, "ToActionTask", err)
return
}
res.Entries[i] = convertedTask
}
ctx.JSON(http.StatusOK, &res)
}

View File

@ -29,7 +29,6 @@ import (
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
notify_service "code.gitea.io/gitea/services/notify"
)
// SearchIssues searches for issues across the repositories that the user has access to
@ -803,12 +802,19 @@ func EditIssue(ctx *context.APIContext) {
return
}
oldTitle := issue.Title
if len(form.Title) > 0 {
issue.Title = form.Title
err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeTitle", err)
return
}
}
if form.Body != nil {
issue.Content = *form.Body
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
return
}
}
if form.Ref != nil {
err = issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, *form.Ref)
@ -880,24 +886,14 @@ func EditIssue(ctx *context.APIContext) {
return
}
}
issue.IsClosed = api.StateClosed == api.StateType(*form.State)
}
statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(ctx, issue, ctx.Doer)
if err != nil {
if issues_model.IsErrDependenciesLeft(err) {
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", api.StateClosed == api.StateType(*form.State)); err != nil {
if issues_model.IsErrDependenciesLeft(err) {
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
return
}
ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
return
}
ctx.Error(http.StatusInternalServerError, "UpdateIssueByAPI", err)
return
}
if titleChanged {
notify_service.IssueChangeTitle(ctx, ctx.Doer, issue, oldTitle)
}
if statusChangeComment != nil {
notify_service.IssueChangeStatus(ctx, ctx.Doer, "", issue, statusChangeComment, issue.IsClosed)
}
// Refetch from database to assign some automatic values

View File

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
)
@ -153,6 +154,8 @@ func CreateIssueAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
@ -185,7 +188,11 @@ func CreateIssueAttachment(ctx *context.APIContext) {
IssueID: issue.ID,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)
if upload.IsErrFileTypeForbidden(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)
}
return
}

View File

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
)
@ -160,6 +161,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
@ -194,9 +197,14 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
CommentID: comment.ID,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)
if upload.IsErrFileTypeForbidden(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)
}
return
}
if err := comment.LoadAttachments(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
return

View File

@ -602,12 +602,19 @@ func EditPullRequest(ctx *context.APIContext) {
return
}
oldTitle := issue.Title
if len(form.Title) > 0 {
issue.Title = form.Title
err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeTitle", err)
return
}
}
if len(form.Body) > 0 {
issue.Content = form.Body
if form.Body != nil {
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
return
}
}
// Update or remove deadline if set
@ -686,24 +693,14 @@ func EditPullRequest(ctx *context.APIContext) {
ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged")
return
}
issue.IsClosed = api.StateClosed == api.StateType(*form.State)
}
statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(ctx, issue, ctx.Doer)
if err != nil {
if issues_model.IsErrDependenciesLeft(err) {
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies")
if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", api.StateClosed == api.StateType(*form.State)); err != nil {
if issues_model.IsErrDependenciesLeft(err) {
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies")
return
}
ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
return
}
ctx.Error(http.StatusInternalServerError, "UpdateIssueByAPI", err)
return
}
if titleChanged {
notify_service.IssueChangeTitle(ctx, ctx.Doer, issue, oldTitle)
}
if statusChangeComment != nil {
notify_service.IssueChangeStatus(ctx, ctx.Doer, "", issue, statusChangeComment, issue.IsClosed)
}
// change pull target branch

View File

@ -6,10 +6,8 @@ package user
import (
"net/http"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
@ -44,7 +42,7 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
return
}
if ctx.IsSigned && ctx.Doer.IsAdmin || permission.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeRead {
if ctx.IsSigned && ctx.Doer.IsAdmin || permission.HasAnyUnitAccess() {
apiRepos = append(apiRepos, convert.ToRepo(ctx, repos[i], permission))
}
}

View File

@ -470,8 +470,9 @@ func AuthorizeOAuth(ctx *context.Context) {
return
}
// Redirect if user already granted access
if grant != nil {
// Redirect if user already granted access and the application is confidential.
// I.e. always require authorization for public clients as recommended by RFC 6749 Section 10.2
if app.ConfidentialClient && grant != nil {
code, err := grant.GenerateNewAuthorizationCode(ctx, form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)

View File

@ -0,0 +1,14 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// WIP RequireAction
package setting
import (
"code.gitea.io/gitea/services/context"
)
func RedirectToRepoSetting(ctx *context.Context) {
ctx.Redirect(ctx.Org.OrgLink + "/settings/actions/require_action")
}

View File

@ -145,6 +145,7 @@ func List(ctx *context.Context) {
workflow := ctx.FormString("workflow")
actorID := ctx.FormInt64("actor")
status := ctx.FormInt("status")
isGlobal := false
ctx.Data["CurWorkflow"] = workflow
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
@ -153,6 +154,8 @@ func List(ctx *context.Context) {
if len(workflow) > 0 && ctx.Repo.IsAdmin() {
ctx.Data["AllowDisableOrEnableWorkflow"] = true
ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(workflow)
ctx.Data["CurGlobalWorkflowEnable"] = actionsConfig.IsGlobalWorkflowEnabled(workflow)
isGlobal = actionsConfig.IsGlobalWorkflowEnabled(workflow)
}
// if status or actor query param is not given to frontend href, (href="/<repoLink>/actions")
@ -209,6 +212,9 @@ func List(ctx *context.Context) {
pager.AddParamString("workflow", workflow)
pager.AddParamString("actor", fmt.Sprint(actorID))
pager.AddParamString("status", fmt.Sprint(status))
if isGlobal {
pager.AddParamString("global", fmt.Sprint(isGlobal))
}
ctx.Data["Page"] = pager
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0

View File

@ -685,33 +685,60 @@ func EnableWorkflowFile(ctx *context_module.Context) {
}
func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) {
disableOrEnable(ctx, isEnable, false)
}
func disableOrEnable(ctx *context_module.Context, isEnable, isglobal bool) {
workflow := ctx.FormString("workflow")
if len(workflow) == 0 {
ctx.ServerError("workflow", nil)
return
}
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
cfg := cfgUnit.ActionsConfig()
if isEnable {
cfg.EnableWorkflow(workflow)
if isglobal {
if isEnable {
cfg.DisableGlobalWorkflow(workflow)
} else {
cfg.EnableGlobalWorkflow(workflow)
}
} else {
cfg.DisableWorkflow(workflow)
if isEnable {
cfg.EnableWorkflow(workflow)
} else {
cfg.DisableWorkflow(workflow)
}
}
if err := repo_model.UpdateRepoUnit(ctx, cfgUnit); err != nil {
ctx.ServerError("UpdateRepoUnit", err)
return
}
if isEnable {
ctx.Flash.Success(ctx.Tr("actions.workflow.enable_success", workflow))
if isglobal {
if isEnable {
ctx.Flash.Success(ctx.Tr("actions.workflow.global_disable_success", workflow))
} else {
ctx.Flash.Success(ctx.Tr("actions.workflow.global_enable_success", workflow))
}
} else {
ctx.Flash.Success(ctx.Tr("actions.workflow.disable_success", workflow))
if isEnable {
ctx.Flash.Success(ctx.Tr("actions.workflow.enable_success", workflow))
} else {
ctx.Flash.Success(ctx.Tr("actions.workflow.disable_success", workflow))
}
}
redirectURL := fmt.Sprintf("%s/actions?workflow=%s&actor=%s&status=%s", ctx.Repo.RepoLink, url.QueryEscape(workflow),
url.QueryEscape(ctx.FormString("actor")), url.QueryEscape(ctx.FormString("status")))
ctx.JSONRedirect(redirectURL)
}
func DisableGlobalWorkflowFile(ctx *context_module.Context) {
disableOrEnableGlobalWorkflowFile(ctx, true)
}
func EnableGlobalWorkflowFile(ctx *context_module.Context) {
disableOrEnableGlobalWorkflowFile(ctx, false)
}
func disableOrEnableGlobalWorkflowFile(ctx *context_module.Context, isEnable bool) {
disableOrEnable(ctx, isEnable, true)
}

View File

@ -17,6 +17,16 @@ import (
const tplSearch base.TplName = "repo/search"
func indexSettingToGitGrepPathspecList() (list []string) {
for _, expr := range setting.Indexer.IncludePatterns {
list = append(list, ":(glob)"+expr.PatternString())
}
for _, expr := range setting.Indexer.ExcludePatterns {
list = append(list, ":(glob,exclude)"+expr.PatternString())
}
return list
}
// Search render repository search page
func Search(ctx *context.Context) {
language := ctx.FormTrim("l")
@ -65,8 +75,14 @@ func Search(ctx *context.Context) {
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
}
} else {
res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{ContextLineNumber: 3, IsFuzzy: isFuzzy})
res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{
ContextLineNumber: 1,
IsFuzzy: isFuzzy,
RefName: git.RefNameFromBranch(ctx.Repo.BranchName).String(), // BranchName should be default branch or the first existing branch
PathspecList: indexSettingToGitGrepPathspecList(),
})
if err != nil {
// TODO: if no branch exists, it reports: exit status 128, fatal: this operation must be run in a work tree.
ctx.ServerError("GrepSearch", err)
return
}

View File

@ -0,0 +1,19 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
func TestIndexSettingToGitGrepPathspecList(t *testing.T) {
defer test.MockVariableValue(&setting.Indexer.IncludePatterns, setting.IndexerGlobFromString("a"))()
defer test.MockVariableValue(&setting.Indexer.ExcludePatterns, setting.IndexerGlobFromString("b"))()
assert.Equal(t, []string{":(glob)a", ":(glob,exclude)b"}, indexSettingToGitGrepPathspecList())
}

View File

@ -0,0 +1,87 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// WIP RequireAction
package setting
import (
"errors"
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
shared "code.gitea.io/gitea/routers/web/shared/actions"
"code.gitea.io/gitea/services/context"
)
const (
// let start with org WIP
tplOrgRequireAction base.TplName = "org/settings/actions"
)
type requireActionsCtx struct {
OrgID int64
IsOrg bool
RequireActionTemplate base.TplName
RedirectLink string
}
func getRequireActionCtx(ctx *context.Context) (*requireActionsCtx, error) {
if ctx.Data["PageIsOrgSettings"] == true {
return &requireActionsCtx{
OrgID: ctx.Org.Organization.ID,
IsOrg: true,
RequireActionTemplate: tplOrgRequireAction,
RedirectLink: ctx.Org.OrgLink + "/settings/actions/require_action",
}, nil
}
return nil, errors.New("unable to set Require Actions context")
}
// Listing all RequireAction
func RequireAction(ctx *context.Context) {
ctx.Data["ActionsTitle"] = ctx.Tr("actions.requires")
ctx.Data["PageType"] = "require_action"
ctx.Data["PageIsSharedSettingsRequireAction"] = true
vCtx, err := getRequireActionCtx(ctx)
if err != nil {
ctx.ServerError("getRequireActionCtx", err)
return
}
page := ctx.FormInt("page")
if page <= 1 {
page = 1
}
opts := actions_model.FindRequireActionOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: 10,
},
}
shared.SetRequireActionContext(ctx, opts)
ctx.Data["Link"] = vCtx.RedirectLink
shared.GlobalEnableWorkflow(ctx, ctx.Org.Organization.ID)
ctx.HTML(http.StatusOK, vCtx.RequireActionTemplate)
}
func RequireActionCreate(ctx *context.Context) {
vCtx, err := getRequireActionCtx(ctx)
if err != nil {
ctx.ServerError("getRequireActionCtx", err)
return
}
shared.CreateRequireAction(ctx, vCtx.OrgID, vCtx.RedirectLink)
}
func RequireActionDelete(ctx *context.Context) {
vCtx, err := getRequireActionCtx(ctx)
if err != nil {
ctx.ServerError("getRequireActionCtx", err)
return
}
shared.DeleteRequireAction(ctx, vCtx.RedirectLink)
}

View File

@ -0,0 +1,84 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// WIP RequireAction
package actions
import (
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
org_model "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
// SetRequireActionDeletePost response for deleting a require action workflow
func SetRequireActionContext(ctx *context.Context, opts actions_model.FindRequireActionOptions) {
requireActions, count, err := db.FindAndCount[actions_model.RequireAction](ctx, opts)
if err != nil {
ctx.ServerError("CountRequireActions", err)
return
}
ctx.Data["RequireActions"] = requireActions
ctx.Data["Total"] = count
ctx.Data["OrgID"] = ctx.Org.Organization.ID
ctx.Data["OrgName"] = ctx.Org.Organization.Name
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
ctx.Data["Page"] = pager
}
// get all the available enable global workflow in the org's repo
func GlobalEnableWorkflow(ctx *context.Context, orgID int64) {
var gwfList []actions_model.GlobalWorkflow
orgRepos, err := org_model.GetOrgRepositories(ctx, orgID)
if err != nil {
ctx.ServerError("GlobalEnableWorkflows get org repos: ", err)
return
}
for _, repo := range orgRepos {
err := repo.LoadUnits(ctx)
if err != nil {
ctx.ServerError("GlobalEnableWorkflows LoadUnits : ", err)
}
actionsConfig := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
enabledWorkflows := actionsConfig.GetGlobalWorkflow()
for _, workflow := range enabledWorkflows {
gwf := actions_model.GlobalWorkflow{
RepoName: repo.Name,
Filename: workflow,
}
gwfList = append(gwfList, gwf)
}
}
ctx.Data["GlobalEnableWorkflows"] = gwfList
}
func CreateRequireAction(ctx *context.Context, orgID int64, redirectURL string) {
ctx.Data["OrgID"] = ctx.Org.Organization.ID
form := web.GetForm(ctx).(*forms.RequireActionForm)
v, err := actions_service.CreateRequireAction(ctx, orgID, form.RepoName, form.WorkflowName)
if err != nil {
log.Error("CreateRequireAction: %v", err)
ctx.JSONError(ctx.Tr("actions.require_action.creation.failed"))
return
}
ctx.Flash.Success(ctx.Tr("actions.require_action.creation.success", v.WorkflowName))
ctx.JSONRedirect(redirectURL)
}
func DeleteRequireAction(ctx *context.Context, redirectURL string) {
id := ctx.ParamsInt64(":require_action_id")
if err := actions_service.DeleteRequireActionByID(ctx, id); err != nil {
log.Error("Delete RequireAction [%d] failed: %v", id, err)
ctx.JSONError(ctx.Tr("actions.require_action.deletion.failed"))
return
}
ctx.Flash.Success(ctx.Tr("actions.require_action.deletion.success"))
ctx.JSONRedirect(redirectURL)
}

View File

@ -457,6 +457,15 @@ func registerRoutes(m *web.Route) {
})
}
// WIP RequireAction
addSettingsRequireActionRoutes := func() {
m.Group("/require_action", func() {
m.Get("", repo_setting.RequireAction)
m.Post("/add", web.Bind(forms.RequireActionForm{}), repo_setting.RequireActionCreate)
m.Post("/{require_action_id}/delete", repo_setting.RequireActionDelete)
})
}
// FIXME: not all routes need go through same middleware.
// Especially some AJAX requests, we can reduce middleware number to improve performance.
@ -628,6 +637,7 @@ func registerRoutes(m *web.Route) {
m.Group("/actions", func() {
m.Get("", user_setting.RedirectToDefaultSetting)
addSettingsRequireActionRoutes()
addSettingsRunnersRoutes()
addSettingsSecretsRoutes()
addSettingsVariablesRoutes()
@ -926,6 +936,7 @@ func registerRoutes(m *web.Route) {
m.Group("/actions", func() {
m.Get("", org_setting.RedirectToDefaultSetting)
addSettingsRequireActionRoutes()
addSettingsRunnersRoutes()
addSettingsSecretsRoutes()
addSettingsVariablesRoutes()
@ -1371,10 +1382,12 @@ func registerRoutes(m *web.Route) {
}, ignSignIn, context.RepoAssignment, reqRepoProjectsReader, repo.MustEnableRepoProjects)
// end "/{username}/{reponame}/projects"
m.Group("/{username}/{reponame}/actions", func() {
m.Group("/actions", func() {
m.Get("", actions.List)
m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile)
m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile)
m.Post("/global_disable", reqRepoAdmin, actions.DisableGlobalWorkflowFile)
m.Post("/global_enable", reqRepoAdmin, actions.EnableGlobalWorkflowFile)
m.Group("/runs/{run}", func() {
m.Combo("").

View File

@ -0,0 +1,22 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"context"
actions_model "code.gitea.io/gitea/models/actions"
)
func CreateRequireAction(ctx context.Context, orgID int64, repoName, workflowName string) (*actions_model.RequireAction, error) {
v, err := actions_model.AddRequireAction(ctx, orgID, repoName, workflowName)
if err != nil {
return nil, err
}
return v, nil
}
func DeleteRequireActionByID(ctx context.Context, requireActionID int64) error {
return actions_model.DeleteRequireAction(ctx, requireActionID)
}

View File

@ -234,9 +234,7 @@ func (b *Base) plainTextInternal(skip, status int, bs []byte) {
b.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
b.Resp.Header().Set("X-Content-Type-Options", "nosniff")
b.Resp.WriteHeader(status)
if _, err := b.Resp.Write(bs); err != nil {
log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err)
}
_, _ = b.Resp.Write(bs)
}
// PlainTextBytes renders bytes as plain text

View File

@ -13,6 +13,7 @@ import (
"path"
"strconv"
"strings"
"syscall"
"time"
user_model "code.gitea.io/gitea/models/user"
@ -77,7 +78,7 @@ func (ctx *Context) HTML(status int, name base.TplName) {
}
err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data, ctx.TemplateContext)
if err == nil {
if err == nil || errors.Is(err, syscall.EPIPE) {
return
}

View File

@ -344,6 +344,12 @@ func (f *EditVariableForm) Validate(req *http.Request, errs binding.Errors) bind
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
// WIP RequireAction create form
type RequireActionForm struct {
RepoName string `binding:"Required;MaxSize(255)"`
WorkflowName string `binding:"Required;MaxSize(255)"`
}
// NewAccessTokenForm form for creating access token
type NewAccessTokenForm struct {
Name string `binding:"Required;MaxSize(255)" locale:"settings.token_name"`

View File

@ -289,8 +289,8 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
}
// Make sure to compose independent messages to avoid leaking user emails
msgID := createReference(ctx.Issue, ctx.Comment, ctx.ActionType)
reference := createReference(ctx.Issue, nil, activities_model.ActionType(0))
msgID := generateMessageIDForIssue(ctx.Issue, ctx.Comment, ctx.ActionType)
reference := generateMessageIDForIssue(ctx.Issue, nil, activities_model.ActionType(0))
var replyPayload []byte
if ctx.Comment != nil {
@ -362,7 +362,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
return msgs, nil
}
func createReference(issue *issues_model.Issue, comment *issues_model.Comment, actionType activities_model.ActionType) string {
func generateMessageIDForIssue(issue *issues_model.Issue, comment *issues_model.Comment, actionType activities_model.ActionType) string {
var path string
if issue.IsPull {
path = "pulls"
@ -389,6 +389,10 @@ func createReference(issue *issues_model.Issue, comment *issues_model.Comment, a
return fmt.Sprintf("<%s/%s/%d%s@%s>", issue.Repo.FullName(), path, issue.Index, extra, setting.Domain)
}
func generateMessageIDForRelease(release *repo_model.Release) string {
return fmt.Sprintf("<%s/releases/%d@%s>", release.Repo.FullName(), release.ID, setting.Domain)
}
func generateAdditionalHeaders(ctx *mailCommentContext, reason string, recipient *user_model.User) map[string]string {
repo := ctx.Issue.Repo

View File

@ -86,11 +86,11 @@ func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_mo
msgs := make([]*Message, 0, len(tos))
publisherName := rel.Publisher.DisplayName()
relURL := "<" + rel.HTMLURL() + ">"
msgID := generateMessageIDForRelease(rel)
for _, to := range tos {
msg := NewMessageFrom(to, publisherName, setting.MailService.FromEmail, subject, mailBody.String())
msg.Info = subject
msg.SetHeader("Message-ID", relURL)
msg.SetHeader("Message-ID", msgID)
msgs = append(msgs, msg)
}

View File

@ -288,7 +288,7 @@ func TestGenerateAdditionalHeaders(t *testing.T) {
}
}
func Test_createReference(t *testing.T) {
func TestGenerateMessageIDForIssue(t *testing.T) {
_, _, issue, comment := prepareMailerTest(t)
_, _, pullIssue, _ := prepareMailerTest(t)
pullIssue.IsPull = true
@ -388,10 +388,18 @@ func Test_createReference(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := createReference(tt.args.issue, tt.args.comment, tt.args.actionType)
got := generateMessageIDForIssue(tt.args.issue, tt.args.comment, tt.args.actionType)
if !strings.HasPrefix(got, tt.prefix) {
t.Errorf("createReference() = %v, want %v", got, tt.prefix)
t.Errorf("generateMessageIDForIssue() = %v, want %v", got, tt.prefix)
}
})
}
}
func TestGenerateMessageIDForRelease(t *testing.T) {
msgID := generateMessageIDForRelease(&repo_model.Release{
ID: 1,
Repo: &repo_model.Repository{OwnerName: "owner", Name: "repo"},
})
assert.Equal(t, "<owner/repo/releases/1@localhost>", msgID)
}

View File

@ -1,6 +1,8 @@
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings actions")}}
<div class="org-setting-content">
{{if eq .PageType "runners"}}
{{if eq .PageType "require_action"}}
{{template "shared/actions/require_action_list" .}}
{{else if eq .PageType "runners"}}
{{template "shared/actions/runner_list" .}}
{{else if eq .PageType "secrets"}}
{{template "shared/secrets/add_list" .}}

View File

@ -29,6 +29,9 @@
<details class="item toggleable-item" {{if or .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
<summary>{{ctx.Locale.Tr "actions.actions"}}</summary>
<div class="menu">
<a class="{{if .PageIsSharedSettingsRequireAction}}active {{end}}item" href="{{.OrgLink}}/settings/actions/require_action">
{{ctx.Locale.Tr "actions.require_action"}}
</a>
<a class="{{if .PageIsSharedSettingsRunners}}active {{end}}item" href="{{.OrgLink}}/settings/actions/runners">
{{ctx.Locale.Tr "actions.runners"}}
</a>

View File

@ -20,6 +20,9 @@
{{if $.ActionsConfig.IsWorkflowDisabled .Entry.Name}}
<div class="ui red label">{{ctx.Locale.Tr "disabled"}}</div>
{{end}}
{{if $.ActionsConfig.IsGlobalWorkflowEnabled .Entry.Name}}
<div class="ui red label">{{ctx.Locale.Tr "Global Enabled"}}</div>
{{end}}
</a>
{{end}}
</div>
@ -65,6 +68,10 @@
</div>
</div>
<!-- IsGlobalWorkflowEnabled -->
<div class="ui dropdown jump item">
<span class="text">{{ctx.Locale.Tr "actions.workflow.global"}}</span>
</div>
{{if .AllowDisableOrEnableWorkflow}}
<button class="ui jump dropdown btn interact-bg tw-p-2">
{{svg "octicon-kebab-horizontal"}}
@ -72,6 +79,9 @@
<a class="item link-action" data-url="{{$.Link}}/{{if .CurWorkflowDisabled}}enable{{else}}disable{{end}}?workflow={{$.CurWorkflow}}&actor={{.CurActor}}&status={{$.CurStatus}}">
{{if .CurWorkflowDisabled}}{{ctx.Locale.Tr "actions.workflow.enable"}}{{else}}{{ctx.Locale.Tr "actions.workflow.disable"}}{{end}}
</a>
<a class="item link-action" data-url="{{$.Link}}/{{if .CurGlobalWorkflowEnable}}global_disable{{else}}global_enable{{end}}?workflow={{$.CurWorkflow}}&actor={{.CurActor}}&status={{$.CurStatus}}">
{{if .CurGlobalWorkflowEnable}}{{ctx.Locale.Tr "actions.workflow.global_disable"}}{{else}}{{ctx.Locale.Tr "actions.workflow.global_enable"}}{{end}}
</a>
</div>
</button>
{{end}}

View File

@ -69,9 +69,9 @@
<div class="js-branch-tag-selector {{if .ContainerClasses}}{{.ContainerClasses}}{{end}}">
{{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}}
<div class="ui dropdown custom">
<button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex tw-m-0">
<span class="text tw-flex tw-items-center tw-mr-1 gt-ellipsis">
<div class="ui dropdown custom branch-selector-dropdown">
<div class="ui button branch-dropdown-button">
<span class="flex-text-block gt-ellipsis">
{{if .release}}
{{ctx.Locale.Tr "repo.release.compare"}}
{{else}}
@ -84,6 +84,6 @@
{{end}}
</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
</button>
</div>
</div>
</div>

View File

@ -9,7 +9,7 @@
SSH
</button>
{{end}}
<input id="repo-clone-url" size="20" class="js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" readonly>
<input id="repo-clone-url" size="10" class="js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" readonly>
<button class="ui small icon button" id="clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{ctx.Locale.Tr "copy_url"}}">
{{svg "octicon-copy" 14}}
</button>

View File

@ -46,7 +46,7 @@
{{$l := Eval $n "-" 1}}
{{$isHomepage := (eq $n 0)}}
<div class="repo-button-row">
<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-y-2">
<div class="repo-button-row-left">
{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "tw-mr-1"}}
{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
{{$cmpBranch := ""}}
@ -66,7 +66,7 @@
{{end}}
{{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}}
<button class="ui dropdown basic compact jump button tw-mr-1"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
<button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
{{ctx.Locale.Tr "repo.editor.add_file"}}
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
@ -93,9 +93,9 @@
{{if $isHomepage}}
{{/* only show the "code search" on the repo home page, it only does global search,
so do not show it when viewing file or directory to avoid misleading users (it doesn't search in a directory) */}}
<form class="ignore-dirty" action="{{.RepoLink}}/search" method="get">
<div class="ui small action input">
<input name="q" placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
<form class="ignore-dirty tw-flex tw-flex-1" action="{{.RepoLink}}/search" method="get">
<div class="ui small action input tw-flex-1">
<input name="q" size="10" placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
{{template "shared/search/button"}}
</div>
</form>
@ -113,7 +113,7 @@
</span>
{{end}}
</div>
<div class="tw-flex tw-items-center">
<div class="repo-button-row-right">
<!-- Only show clone panel in repository home page -->
{{if $isHomepage}}
<div class="clone-panel ui action tiny input">

View File

@ -4,10 +4,12 @@
<form method="post" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/ref" id="update_issueref_form">
{{$.CsrfTokenHtml}}
</form>
{{/* TODO: share this branch selector dropdown with the same in repo page */}}
<div class="ui {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}} floating filter select-branch dropdown tw-max-w-full" data-no-results="{{ctx.Locale.Tr "no_results_found"}}">
<div class="ui basic small button">
<span class="text branch-name gt-ellipsis">{{if .Reference}}{{$.RefEndName}}{{else}}{{ctx.Locale.Tr "repo.issues.no_ref"}}{{end}}</span>
<div class="ui dropdown select-branch branch-selector-dropdown {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}}"
data-no-results="{{ctx.Locale.Tr "no_results_found"}}"
{{if not .Issue}}data-for-new-issue="true"{{end}}
>
<div class="ui button branch-dropdown-button">
<span class="text-branch-name gt-ellipsis">{{if .Reference}}{{$.RefEndName}}{{else}}{{ctx.Locale.Tr "repo.issues.no_ref"}}{{end}}</span>
{{if .HasIssuesOrPullsWritePermission}}{{svg "octicon-triangle-down" 14 "dropdown icon"}}{{end}}
</div>
<div class="menu">
@ -15,26 +17,18 @@
<i class="icon">{{svg "octicon-filter" 16}}</i>
<input name="search" placeholder="{{ctx.Locale.Tr "repo.filter_branch_and_tag"}}...">
</div>
<div class="header">
<div class="ui grid">
<div class="two column row">
<a class="reference column muted" href="#" data-target="#branch-list">
<span class="text black">
{{svg "octicon-git-branch" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.branches"}}
</span>
</a>
<a class="reference column muted" href="#" data-target="#tag-list">
<span class="text">
{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.tags"}}
</span>
</a>
</div>
</div>
<div class="branch-tag-tab">
<a class="branch-tag-item reference column muted active" href="#" data-target="#branch-list">
{{svg "octicon-git-branch" 16 "tw-mr-1"}} {{ctx.Locale.Tr "repo.branches"}}
</a>
<a class="branch-tag-item reference column muted" href="#" data-target="#tag-list">
{{svg "octicon-tag" 16 "tw-mr-1"}} {{ctx.Locale.Tr "repo.tags"}}
</a>
</div>
<div class="branch-tag-divider"></div>
<div id="branch-list" class="scrolling menu reference-list-menu {{if not .Issue}}new-issue{{end}}">
{{if .Reference}}
<div class="item text small" data-id="" data-id-selector="#ref_selector"><strong><a href="#">{{ctx.Locale.Tr "repo.clear_ref"}}</a></strong></div>
<div id="branch-list" class="scrolling menu reference-list-menu">
{{if or .Reference (not .Issue)}}
<div class="item text small" data-id="" data-name="{{ctx.Locale.Tr "repo.issues.no_ref"}}" data-id-selector="#ref_selector"><strong><a href="#">{{ctx.Locale.Tr "repo.clear_ref"}}</a></strong></div>
{{end}}
{{range .Branches}}
<div class="item" data-id="refs/heads/{{.}}" data-name="{{.}}" data-id-selector="#ref_selector" title="{{.}}">{{.}}</div>
@ -42,9 +36,9 @@
<div class="item">{{ctx.Locale.Tr "no_results_found"}}</div>
{{end}}
</div>
<div id="tag-list" class="scrolling menu reference-list-menu {{if not .Issue}}new-issue{{end}} tw-hidden">
{{if .Reference}}
<div class="item text small" data-id="" data-id-selector="#ref_selector"><strong><a href="#">{{ctx.Locale.Tr "repo.clear_ref"}}</a></strong></div>
<div id="tag-list" class="scrolling menu reference-list-menu tw-hidden">
{{if or .Reference (not .Issue)}}
<div class="item text small" data-id="" data-name="{{ctx.Locale.Tr "repo.issues.no_ref"}}" data-id-selector="#ref_selector"><strong><a href="#">{{ctx.Locale.Tr "repo.clear_ref"}}</a></strong></div>
{{end}}
{{range .Tags}}
<div class="item" data-id="refs/tags/{{.}}" data-name="tags/{{.}}" data-id-selector="#ref_selector">{{.}}</div>

View File

@ -62,13 +62,13 @@
</div>
{{if or .Labels .Assignees}}
<div class="tw-flex tw-justify-between">
<div class="labels-list tw-flex-1">
<div class="issue-card-bottom">
<div class="labels-list">
{{range .Labels}}
<a target="_blank" href="{{$.Issue.Repo.Link}}/issues?labels={{.ID}}">{{RenderLabel ctx ctx.Locale .}}</a>
{{end}}
</div>
<div class="tw-flex tw-flex-wrap tw-content-start tw-gap-1">
<div class="issue-card-assignees">
{{range .Assignees}}
<a target="_blank" href="{{.HomeLink}}" data-tooltip-content="{{ctx.Locale.Tr "repo.projects.column.assigned_to"}} {{.Name}}">{{ctx.AvatarUtils.Avatar . 28}}</a>
{{end}}

View File

@ -1,6 +1,6 @@
<div class="ui labels list">
<span class="no-select item {{if .root.HasSelectedLabel}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span>
<span class="labels-list">
<span class="no-select {{if .root.HasSelectedLabel}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span>
{{range .root.Labels}}
{{template "repo/issue/labels/label" dict "root" $.root "label" .}}
{{end}}

View File

@ -0,0 +1,147 @@
<div class="require-actions-container">
<h4 class="ui top attached header">
{{ctx.Locale.Tr "actions.require_action.require_action_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
<div class="ui right">
<div class="ui top right">
<button class="ui primary tiny button show-modal"
data-modal="#add-require-actions-modal"
data-modal-form.action="{{.Link}}/add"
data-modal-header="{{ctx.Locale.Tr "actions.require_action.add"}}">
{{ctx.Locale.Tr "actions.require_action.add"}}
</button>
</div>
</div>
</h4>
<div class="ui attached segment">
<form class="ui form ignore-dirty" id="require-action-list-search-form" action="{{$.Link}}">
<!-- Search Text -->
{{template "shared/search/combo" dict "Value" .Keyword}}
<button class="ui primary button">{{ctx.Locale.Tr "actions.require_action.search"}}</button>
</form>
</div>
<div class="ui attached table segment">
<table class="ui very basic striped table unstackable">
<thead>
<tr>
<th data-sortt-asc="newest" data-sortt-desc="oldest">
{{ctx.Locale.Tr "actions.require_action.id"}}
</th>
<th data-sortt-asc="alphabetically" data-sortt-desc="reversealphabetically">
{{ctx.Locale.Tr "actions.require_action.workflow"}}
</th>
<th>{{ctx.Locale.Tr "actions.require_action.repo"}}</th>
<th>{{ctx.Locale.Tr "actions.require_action.link"}}</th>
<th>{{ctx.Locale.Tr "actions.require_action.remove"}}</th>
</tr>
</thead>
<tbody>
{{if .RequireActions}}
{{range .RequireActions}}
<tr>
<td>{{.ID}}</td>
<td><p data-tooltip-content="{{.RepoName}}">{{.WorkflowName}}</p></td>
<td>{{.RepoName}}</td>
<td><a href="/{{$.OrgName}}/{{.RepoName}}">Workflow Link</a></td>
<td class="require_action-ops">
{{if .Removable $.OrgID}}
<button class="btn interact-bg tw-p-2 link-action"
data-tooltip-content="{{ctx.Locale.Tr "actions.require_action.deletion"}}"
data-url="{{$.Link}}/{{.ID}}/delete"
data-modal-confirm="{{ctx.Locale.Tr "actions.require_action.deletion.description"}}"
>
{{svg "octicon-trash"}}
</button>
<!-- <a href="{{$.Link}}/{{.ID}}/delete">{{svg "octicon-x-circle-fill"}}</a>-->
{{end}}
</td>
</tr>
{{end}}
{{else}}
<tr>
<td class="center aligned" colspan="8">{{ctx.Locale.Tr "actions.require_action.none"}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{template "base/paginate"}}
</div>
{{/* Add RequireAction dialog */}}
<div class="ui small modal" id="add-require-actions-modal">
<div class="header">
<span id="actions-modal-header">Enable Workflows</span>
</div>
<form class="ui form form-fetch-action" method="post">
<div class="content">
<div class="item">
<a href="https://docs.gitea.com/usage/actions/require-action">{{ctx.Locale.Tr "actions.require_action.enable_global_workflow"}}</a>
</div>
<div class="divider"></div>
<table class="ui very basic striped table unstackable">
<thead>
<tr>
<th data-sortt-asc="alphabetically" data-sortt-desc="reversealphabetically">
{{ctx.Locale.Tr "actions.require_action.workflow"}}
</th>
<th>
{{ctx.Locale.Tr "actions.require_action.repo"}}
</th>
</tr>
</thead>
<tbody>
{{if .GlobalEnableWorkflows}}
{{range .GlobalEnableWorkflows}}
<tr>
<td><div class="field">
<div class="ui radio checkbox">
<input class="select-org-radio" name="workflow_name" type="radio" value="{{.Filename}}">
<label>{{.Filename}}</label>
</div>
<input name="repo_name" type="hidden" value="{{.RepoName}}">
</div></td>
<td><div class="field">
<a href="/{{$.OrgName}}/{{.RepoName}}">
<label>{{.RepoName}}</label>
</a>
</div></td>
</tr>
{{end}}
{{else}}
<tr>
<td class="center aligned" colspan="8">{{ctx.Locale.Tr "actions.require_action.none"}}</td>
</tr>
{{end}}
</tbody>
</table>
<div class="divider"></div>
<div class="item">
<a href="{{$.Link}}/add">{{ctx.Locale.Tr "actions.require_action.add_require_action"}}</a>
</div>
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>
<!--
<script>
window.addEventListener('DOMContentLoaded', function() {
var checkboxes = document.querySelectorAll('.ui.radio.checkbox');
checkboxes.forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
var hiddenInput = this.nextElementSibling;
var isChecked = this.querySelector('input[type="radio"]').checked;
hiddenInput.disabled = !isChecked;
// Disable other hidden inputs
checkboxes.forEach(function(otherCheckbox) {
var otherHiddenInput = otherCheckbox.nextElementSibling;
if (otherCheckbox !== checkbox) {
otherHiddenInput.disabled = isChecked;
}
});
});
});
});
</script>
-->

View File

@ -7478,6 +7478,9 @@
"404": {
"$ref": "#/responses/error"
},
"422": {
"$ref": "#/responses/validationError"
},
"423": {
"$ref": "#/responses/repoArchivedError"
}
@ -8097,6 +8100,9 @@
"404": {
"$ref": "#/responses/error"
},
"422": {
"$ref": "#/responses/validationError"
},
"423": {
"$ref": "#/responses/repoArchivedError"
}

View File

@ -195,14 +195,17 @@ func TestAPIEditUser(t *testing.T) {
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
urlStr := fmt.Sprintf("/api/v1/admin/users/%s", "user2")
fullNameToChange := "Full Name User 2"
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
// required
"login_name": "user2",
"source_id": "0",
// to change
"full_name": "Full Name User 2",
"full_name": fullNameToChange,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
assert.Equal(t, fullNameToChange, user2.FullName)
empty := ""
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
@ -216,7 +219,7 @@ func TestAPIEditUser(t *testing.T) {
json.Unmarshal(resp.Body.Bytes(), &errMap)
assert.EqualValues(t, "e-mail invalid [email: ]", errMap["message"].(string))
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
user2 = unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
assert.False(t, user2.IsRestricted)
bTrue := true
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{

View File

@ -120,6 +120,34 @@ func TestAPICreateCommentAttachment(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, CommentID: comment.ID})
}
func TestAPICreateCommentAttachmentWithUnallowedFile(t *testing.T) {
defer tests.PrepareTestEnv(t)()
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
filename := "file.bad"
body := &bytes.Buffer{}
// Setup multi-part.
writer := multipart.NewWriter(body)
_, err := writer.CreateFormFile("attachment", filename)
assert.NoError(t, err)
err = writer.Close()
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets", repoOwner.Name, repo.Name, comment.ID), body).
AddTokenAuth(token).
SetHeader("Content-Type", writer.FormDataContentType())
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
}
func TestAPIEditCommentAttachment(t *testing.T) {
defer tests.PrepareTestEnv(t)()

View File

@ -96,6 +96,33 @@ func TestAPICreateIssueAttachment(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
}
func TestAPICreateIssueAttachmentWithUnallowedFile(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
filename := "file.bad"
body := &bytes.Buffer{}
// Setup multi-part.
writer := multipart.NewWriter(body)
_, err := writer.CreateFormFile("attachment", filename)
assert.NoError(t, err)
err = writer.Close()
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets", repoOwner.Name, repo.Name, issue.Index), body).
AddTokenAuth(token)
req.Header.Add("Content-Type", writer.FormDataContentType())
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
}
func TestAPIEditIssueAttachment(t *testing.T) {
defer tests.PrepareTestEnv(t)()

View File

@ -194,6 +194,10 @@ func TestAPIEditIssue(t *testing.T) {
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
// check comment history
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: issueAfter.ID, OldTitle: issueBefore.Title, NewTitle: title})
unittest.AssertExistsAndLoadBean(t, &issues_model.ContentHistory{IssueID: issueAfter.ID, ContentText: body, IsFirstCreated: false})
// check deleted user
assert.Equal(t, int64(500), issueAfter.PosterID)
assert.NoError(t, issueAfter.LoadAttributes(db.DefaultContext))

View File

@ -223,23 +223,33 @@ func TestAPIEditPull(t *testing.T) {
session := loginUser(t, owner10.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
title := "create a success pr"
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &api.CreatePullRequestOption{
Head: "develop",
Base: "master",
Title: "create a success pr",
Title: title,
}).AddTokenAuth(token)
pull := new(api.PullRequest)
apiPull := new(api.PullRequest)
resp := MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, pull)
assert.EqualValues(t, "master", pull.Base.Name)
DecodeJSON(t, resp, apiPull)
assert.EqualValues(t, "master", apiPull.Base.Name)
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), &api.EditPullRequestOption{
newTitle := "edit a this pr"
newBody := "edited body"
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, apiPull.Index), &api.EditPullRequestOption{
Base: "feature/1",
Title: "edit a this pr",
Title: newTitle,
Body: &newBody,
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, pull)
assert.EqualValues(t, "feature/1", pull.Base.Name)
DecodeJSON(t, resp, apiPull)
assert.EqualValues(t, "feature/1", apiPull.Base.Name)
// check comment history
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
err := pull.LoadIssue(db.DefaultContext)
assert.NoError(t, err)
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: pull.Issue.ID, OldTitle: title, NewTitle: newTitle})
unittest.AssertExistsAndLoadBean(t, &issues_model.ContentHistory{IssueID: pull.Issue.ID, ContentText: newBody, IsFirstCreated: false})
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), &api.EditPullRequestOption{
Base: "not-exist",

View File

@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/models/db"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
@ -326,6 +327,39 @@ func TestAPIOrgRepos(t *testing.T) {
}
}
// See issue #28483. Tests to make sure we consider more than just code unit-enabled repositories.
func TestAPIOrgReposWithCodeUnitDisabled(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: "repo21"})
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo21.OwnerID})
// Disable code repository unit.
var units []unit_model.Type
units = append(units, unit_model.TypeCode)
if err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo21, nil, units); err != nil {
assert.Fail(t, "should have been able to delete code repository unit; failed to %v", err)
}
assert.False(t, repo21.UnitEnabled(db.DefaultContext, unit_model.TypeCode))
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization)
req := NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", org3.Name).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var apiRepos []*api.Repository
DecodeJSON(t, resp, &apiRepos)
var repoNames []string
for _, r := range apiRepos {
repoNames = append(repoNames, r.Name)
}
assert.Contains(t, repoNames, repo21.Name)
}
func TestAPIGetRepoByIDUnauthorized(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})

View File

@ -871,6 +871,7 @@ input:-webkit-autofill:active,
.ui.dropdown .scrolling.menu {
border-color: var(--color-secondary);
border-radius: 0 0 var(--border-radius) var(--border-radius) !important;
}
.color-preview {

View File

@ -31,6 +31,10 @@
padding: 0 5px;
}
#user-heatmap .vch__day__square:hover {
outline: 1.5px solid var(--color-text);
}
/* move the "? contributions in the last ? months" text from top to bottom */
#user-heatmap .total-contributions {
font-size: 11px;

View File

@ -188,8 +188,8 @@
.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection:hover,
.ui.action.input:not([class*="left action"]) > input:focus + .button,
.ui.action.input:not([class*="left action"]) > input:focus + .button:hover,
.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button,
.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button:hover {
.ui.action.input:not([class*="left action"]) > input:focus + i.icon + .button,
.ui.action.input:not([class*="left action"]) > input:focus + i.icon + .button:hover {
border-left-color: var(--color-primary);
}
.ui.action.input:not([class*="left action"]) > input:focus {

View File

@ -128,15 +128,22 @@
margin-bottom: 12px;
}
.repository .clone-panel #repo-clone-url {
width: 320px;
border-radius: 0;
.repository .clone-panel {
display: flex;
flex: 1;
}
@media (max-width: 991.98px) {
.repository .clone-panel #repo-clone-url {
width: 200px;
}
.repository.wiki .clone-panel {
flex: 0;
}
.repository.wiki .clone-panel input {
width: 20ch;
}
.repository .clone-panel #repo-clone-url {
border-radius: 0;
flex: 1;
}
.repository .ui.action.input.clone-panel > button + button,
@ -2195,18 +2202,12 @@ td .commit-summary {
display: inline-flex;
flex-wrap: wrap;
gap: 2.5px;
}
.labels-list a {
display: flex;
text-decoration: none;
align-items: center;
}
.labels-list .label {
padding: 0 6px;
margin: 0 !important;
min-height: 20px;
display: inline-flex !important;
line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */
}
@ -2235,17 +2236,37 @@ td .commit-summary {
}
.repo-button-row {
margin: 10px 0;
margin: 8px 0;
display: flex;
align-items: center;
gap: 0.5em;
flex-wrap: wrap;
gap: 8px;
justify-content: space-between;
}
.repo-button-row-left,
.repo-button-row-right {
display: flex;
flex: 1;
align-items: center;
gap: 0.5rem;
}
.repo-button-row-right {
justify-content: flex-end;
}
@media (max-width: 991px) {
.repository:not(.wiki) .repo-button-row {
flex-direction: column;
align-items: stretch;
}
}
.repo-button-row .button {
padding: 6px 10px !important;
height: 30px;
flex-shrink: 0;
margin: 0;
}
.repo-button-row .button.dropdown:not(.icon) {
@ -2256,6 +2277,12 @@ td .commit-summary {
height: 30px;
}
@media (max-width: 600px) {
.repo-button-row-left {
flex-wrap: wrap;
}
}
tbody.commit-list {
vertical-align: baseline;
}
@ -2748,23 +2775,6 @@ tbody.commit-list {
}
}
.branch-dropdown-button {
max-width: 340px;
vertical-align: bottom !important;
}
@media (min-width: 768px) and (max-width: 991.98px) {
.branch-dropdown-button {
max-width: 185px;
}
}
@media (max-width: 767.98px) {
.branch-dropdown-button {
max-width: 165px;
}
}
.commit-status-header {
/* reset the default ".ui.attached.header" styles, to use the outer border */
border: none !important;
@ -2841,32 +2851,70 @@ tbody.commit-list {
max-height: 200px;
}
/* Branch tag selector - TODO: Merge this into the same selector on repo page */
.repository .issue-content .issue-content-right .ui.grid .column.row {
padding: 10px;
padding-bottom: 0;
.branch-selector-dropdown {
max-width: 100%;
}
.repository .issue-content .issue-content-right .ui.grid .column.muted {
padding: 0;
.ui.dropdown.branch-selector-dropdown > .menu {
margin-top: 4px;
}
.repository .issue-content .issue-content-right .ui.grid .column.muted .text {
.branch-selector-dropdown .branch-dropdown-button {
margin: 0;
max-width: 340px;
line-height: var(--line-height-default);
}
/* FIXME: These media selectors are not ideal (just keep them from old code).
There are many different pages, some need the max-width while some others don't,
they should be tested and improved in the future. */
@media (min-width: 768px) and (max-width: 991.98px) {
.branch-selector-dropdown .branch-dropdown-button {
max-width: 185px;
}
}
@media (max-width: 767.98px) {
.branch-selector-dropdown .branch-dropdown-button {
max-width: 165px;
}
}
.branch-selector-dropdown .branch-tag-tab {
padding: 0 10px;
}
.branch-selector-dropdown .branch-tag-item {
display: inline-block;
padding: 10px;
width: 100%;
text-align: center;
border: 1px solid transparent;
border-bottom: none;
}
.repository .issue-content .issue-content-right .ui.grid .column.muted .text.black {
.branch-selector-dropdown .branch-tag-item.active {
border-color: var(--color-secondary);
background: var(--color-menu);
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
.repository .issue-content .issue-content-right .ui.dropdown .scrolling.menu {
border-top: none;
}
.repository .issue-content .issue-content-right .branch-tag-divider {
margin-top: -1px;
.branch-selector-dropdown .branch-tag-divider {
margin-top: -1px !important;
border-top: 1px solid var(--color-secondary);
}
.branch-selector-dropdown .scrolling.menu {
border-top: none !important;
}
.branch-selector-dropdown .menu .item .rss-icon {
visibility: hidden; /* only show RSS icon on hover */
}
.branch-selector-dropdown .menu .item:hover .rss-icon {
visibility: visible;
}
.branch-selector-dropdown .scrolling.menu .loading-indicator {
height: 4em;
}

View File

@ -23,3 +23,18 @@
.issue-card.sortable-chosen .issue-card-title {
cursor: inherit;
}
.issue-card-bottom {
display: flex;
width: 100%;
justify-content: space-between;
gap: 0.25em;
}
.issue-card-assignees {
display: flex;
align-items: center;
gap: 0.25em;
justify-content: end;
flex-wrap: wrap;
}

View File

@ -18,4 +18,5 @@ rules:
vue/attributes-order: [0]
vue/html-closing-bracket-spacing: [2, {startTag: never, endTag: never, selfClosingTag: never}]
vue/max-attributes-per-line: [0]
vue/singleline-html-element-content-newline: [0]
vue-scoped-css/enforce-style-type: [0]

View File

@ -1,5 +1,6 @@
<script>
import {CalendarHeatmap} from 'vue3-calendar-heatmap';
// TODO: Switch to upstream after https://github.com/razorness/vue3-calendar-heatmap/pull/34 is merged
import {CalendarHeatmap} from '@silverwind/vue3-calendar-heatmap';
export default {
components: {CalendarHeatmap},
@ -55,15 +56,16 @@ export default {
</script>
<template>
<div class="total-contributions">
{{ locale.contributions_in_the_last_12_months }}
{{ locale.textTotalContributions }}
</div>
<calendar-heatmap
:locale="locale"
:no-data-text="locale.no_contributions"
:tooltip-unit="locale.contributions"
:locale="locale.heatMapLocale"
:no-data-text="locale.noDataText"
:tooltip-unit="locale.tooltipUnit"
:end-date="endDate"
:values="values"
:range-color="colorRange"
@day-click="handleDayClick($event)"
:tippy-props="{theme: 'tooltip'}"
/>
</template>

View File

@ -91,16 +91,22 @@ export default {
<template>
<div ref="root">
<div v-if="loading" class="tw-h-12 tw-w-12 is-loading"/>
<div v-if="!loading && issue !== null">
<p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p>
<p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p>
<p>{{ body }}</p>
<div v-if="!loading && issue !== null" class="tw-flex tw-flex-col tw-gap-2">
<div class="tw-text-12">{{ issue.repository.full_name }} on {{ createdAt }}</div>
<div class="flex-text-block">
<svg-icon :name="icon" :class="['text', color]"/>
<span class="issue-title tw-font-semibold tw-break-anywhere">
{{ issue.title }}
<span class="index">#{{ issue.number }}</span>
</span>
</div>
<div v-if="body">{{ body }}</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="renderedLabels"/>
<div v-if="issue.labels.length" v-html="renderedLabels"/>
</div>
<div v-if="!loading && issue === null">
<p><small>{{ i18nErrorOccurred }}</small></p>
<p>{{ i18nErrorMessage }}</p>
<div class="tw-flex tw-flex-col tw-gap-2" v-if="!loading && issue === null">
<div class="tw-text-12">{{ i18nErrorOccurred }}</div>
<div>{{ i18nErrorMessage }}</div>
</div>
</div>
</template>

View File

@ -246,9 +246,9 @@ export function initRepoBranchTagSelector(selector) {
export default sfc; // activate IDE's Vue plugin
</script>
<template>
<div class="ui dropdown custom">
<button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex tw-m-0" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
<span class="text tw-flex tw-items-center tw-mr-1 gt-ellipsis">
<div class="ui dropdown custom branch-selector-dropdown">
<div class="ui button branch-dropdown-button" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
<span class="flex-text-block gt-ellipsis">
<template v-if="release">{{ textReleaseCompare }}</template>
<template v-else>
<svg-icon v-if="isViewTag" name="octicon-tag"/>
@ -257,7 +257,7 @@ export default sfc; // activate IDE's Vue plugin
</template>
</span>
<svg-icon name="octicon-triangle-down" :size="14" class-name="dropdown icon"/>
</button>
</div>
<div class="menu transition" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak>
<div class="ui icon search input">
<i class="icon"><svg-icon name="octicon-filter" :size="16"/></i>
@ -317,43 +317,3 @@ export default sfc; // activate IDE's Vue plugin
</div>
</div>
</template>
<style scoped>
.branch-tag-tab {
padding: 0 10px;
}
.branch-tag-item {
display: inline-block;
padding: 10px;
border: 1px solid transparent;
border-bottom: none;
}
.branch-tag-item.active {
border-color: var(--color-secondary);
background: var(--color-menu);
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
.branch-tag-divider {
margin-top: -1px !important;
border-top: 1px solid var(--color-secondary);
}
.scrolling.menu {
border-top: none !important;
}
.menu .item .rss-icon {
display: none; /* only show RSS icon on hover */
}
.menu .item:hover .rss-icon {
display: inline-block;
}
.scrolling.menu .loading-indicator {
height: 4em;
}
</style>

View File

@ -20,13 +20,16 @@ export function initHeatmap() {
// last heatmap tooltip localization attempt https://github.com/go-gitea/gitea/pull/24131/commits/a83761cbbae3c2e3b4bced71e680f44432073ac8
const locale = {
months: new Array(12).fill().map((_, idx) => translateMonth(idx)),
days: new Array(7).fill().map((_, idx) => translateDay(idx)),
contributions: 'contributions',
contributions_in_the_last_12_months: el.getAttribute('data-locale-total-contributions'),
no_contributions: el.getAttribute('data-locale-no-contributions'),
more: el.getAttribute('data-locale-more'),
less: el.getAttribute('data-locale-less'),
heatMapLocale: {
months: new Array(12).fill().map((_, idx) => translateMonth(idx)),
days: new Array(7).fill().map((_, idx) => translateDay(idx)),
on: ' - ', // no correct locale support for it, because in many languages the sentence is not "something on someday"
more: el.getAttribute('data-locale-more'),
less: el.getAttribute('data-locale-less'),
},
tooltipUnit: 'contributions',
textTotalContributions: el.getAttribute('data-locale-total-contributions'),
noDataText: el.getAttribute('data-locale-no-contributions'),
};
const View = createApp(ActivityHeatmap, {values, locale});

View File

@ -19,7 +19,7 @@ import {initCompReactionSelector} from './comp/ReactionSelector.js';
import {initRepoSettingBranches} from './repo-settings.js';
import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.js';
import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.js';
import {hideElem, showElem} from '../utils/dom.js';
import {hideElem, queryElemChildren, showElem} from '../utils/dom.js';
import {POST} from '../modules/fetch.js';
import {initRepoIssueCommentEdit} from './repo-issue-edit.js';
@ -56,16 +56,20 @@ export function initRepoCommentForm() {
}
function initBranchSelector() {
const $selectBranch = $('.ui.select-branch');
const elSelectBranch = document.querySelector('.ui.dropdown.select-branch');
if (!elSelectBranch) return;
const isForNewIssue = elSelectBranch.getAttribute('data-for-new-issue') === 'true';
const $selectBranch = $(elSelectBranch);
const $branchMenu = $selectBranch.find('.reference-list-menu');
const $isNewIssue = $branchMenu.hasClass('new-issue');
$branchMenu.find('.item:not(.no-select)').on('click', async function () {
const selectedValue = $(this).data('id');
$branchMenu.find('.item:not(.no-select)').on('click', async function (e) {
e.preventDefault();
const selectedValue = $(this).data('id'); // eg: "refs/heads/my-branch"
const editMode = $('#editing_mode').val();
$($(this).data('id-selector')).val(selectedValue);
if ($isNewIssue) {
$selectBranch.find('.ui .branch-name').text($(this).data('name'));
return;
if (isForNewIssue) {
elSelectBranch.querySelector('.text-branch-name').textContent = this.getAttribute('data-name');
return; // only update UI&form, do not send request/reload
}
if (editMode === 'true') {
@ -84,9 +88,9 @@ export function initRepoCommentForm() {
});
$selectBranch.find('.reference.column').on('click', function () {
hideElem($selectBranch.find('.scrolling.reference-list-menu'));
$selectBranch.find('.reference .text').removeClass('black');
showElem($($(this).data('target')));
$(this).find('.text').addClass('black');
showElem(this.getAttribute('data-target'));
queryElemChildren(this.parentNode, '.branch-tag-item', (el) => el.classList.remove('active'));
this.classList.add('active');
return false;
});
}

View File

@ -0,0 +1,19 @@
export function initRequireActionsSelect() {
const raselect = document.getElementById('add-require-actions-modal');
if (!raselect) return;
const checkboxes = document.querySelectorAll('.ui.radio.checkbox');
for (const box of checkboxes) {
box.addEventListener('change', function() {
const hiddenInput = this.nextElementSibling;
const isChecked = this.querySelector('input[type="radio"]').checked;
hiddenInput.disabled = !isChecked;
// Disable other hidden inputs
for (const otherbox of checkboxes) {
const otherHiddenInput = otherbox.nextElementSibling;
if (otherbox !== box) {
otherHiddenInput.disabled = isChecked;
}
}
});
}
}

View File

@ -54,6 +54,7 @@ import {initRepoCodeView} from './features/repo-code.js';
import {initSshKeyFormParser} from './features/sshkey-helper.js';
import {initUserSettings} from './features/user-settings.js';
import {initRepoArchiveLinks} from './features/repo-common.js';
import {initRequireActionsSelect} from './features/require-actions-select.js';
import {initRepoMigrationStatusChecker} from './features/repo-migrate.js';
import {
initRepoSettingGitHook,
@ -143,6 +144,7 @@ onDomReady(() => {
initRepoActivityTopAuthorsChart();
initRepoArchiveLinks();
initRequireActionsSelect();
initRepoBranchButton();
initRepoCodeView();
initRepoCommentForm();

View File

@ -3,11 +3,12 @@ import {queryElemChildren} from '../../utils/dom.js';
export function initFomanticDimmer() {
// stand-in for removed dimmer module
$.fn.dimmer = function (arg0, $el) {
$.fn.dimmer = function (arg0, arg1) {
if (arg0 === 'add content') {
const $el = arg1;
const existingDimmer = document.querySelector('body > .ui.dimmer');
if (existingDimmer) {
queryElemChildren(existingDimmer, '*', (el) => el.remove());
queryElemChildren(existingDimmer, '*', (el) => el.classList.add('hidden'));
this._dimmer = existingDimmer;
} else {
this._dimmer = document.createElement('div');
@ -21,8 +22,10 @@ export function initFomanticDimmer() {
this._dimmer.classList.add('active');
document.body.classList.add('tw-overflow-hidden');
} else if (arg0 === 'hide') {
const cb = arg1;
this._dimmer.classList.remove('active');
document.body.classList.remove('tw-overflow-hidden');
cb();
}
return this;
};