mirror of https://github.com/go-gitea/gitea.git
Compare commits
30 Commits
eb70bbea80
...
17462ede24
Author | SHA1 | Date |
---|---|---|
AvengerMoJo | 17462ede24 | |
yp05327 | ecd1d96f49 | |
Neal Caffery | bb0e4ce581 | |
wxiaoguang | c7bb3aa034 | |
wxiaoguang | 0f3e717a1a | |
Kemal Zebari | 9f0ef3621a | |
yp05327 | a50026e2f3 | |
wxiaoguang | 53b55223d1 | |
silverwind | c4e875402b | |
silverwind | b30b7df9f4 | |
silverwind | c445a85528 | |
Bo-Yi Wu | e67fbe4f15 | |
Archer | 5c542ca94c | |
Kemal Zebari | 872caa17c0 | |
wxiaoguang | 677032d36a | |
silverwind | 6f89d5e3a0 | |
silverwind | 9235442ba5 | |
Lunny Xiao | cb9e1a3ff6 | |
silverwind | b1bb3642e5 | |
wxiaoguang | eb8bb82e58 | |
wxiaoguang | 6ff2acc52c | |
wxiaoguang | ebe6f4cad7 | |
Alex Lau(AvengerMoJo) | d68a821455 | |
Alex Lau(AvengerMoJo) | 325842db66 | |
Alex Lau(AvengerMoJo) | c4b923886c | |
Alex Lau(AvengerMoJo) | 32cb52662f | |
Alex Lau(AvengerMoJo) | c10d144ca7 | |
Alex Lau(AvengerMoJo) | d4fd5b553d | |
Alex Lau(AvengerMoJo) | 1fc998984f | |
Alex Lau(AvengerMoJo) | 5243490071 |
|
@ -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
4
go.mod
|
@ -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
10
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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, ",")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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("").
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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" .}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
-->
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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)()
|
||||
|
||||
|
|
|
@ -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)()
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue