Merge branch 'main' into rename

This commit is contained in:
Giteabot 2024-05-03 00:43:16 +08:00 committed by GitHub
commit 4a213f892e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 341 additions and 252 deletions

4
go.mod
View File

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

10
go.sum
View File

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

View File

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

View File

@ -17,11 +17,14 @@ import (
"time"
charsetModule "code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
"github.com/klauspost/compress/gzhttp"
)
type ServeHeaderOptions struct {
@ -38,6 +41,11 @@ type ServeHeaderOptions struct {
func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
header := w.Header()
skipCompressionExts := container.SetOf(".gz", ".bz2", ".zip", ".xz", ".zst", ".deb", ".apk", ".jar", ".png", ".jpg", ".webp")
if skipCompressionExts.Contains(strings.ToLower(path.Ext(opts.Filename))) {
w.Header().Add(gzhttp.HeaderNoCompression, "1")
}
contentType := typesniffer.ApplicationOctetStream
if opts.ContentType != "" {
if opts.ContentTypeCharset != "" {

26
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

@ -54,7 +54,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
)
const GzipMinSize = 1400 // min size to compress for the body size of response
var GzipMinSize = 1400 // min size to compress for the body size of response
// optionsCorsHandler return a http handler which sets CORS options if enabled by config, it blocks non-CORS OPTIONS requests.
func optionsCorsHandler() func(next http.Handler) http.Handler {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,33 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"io"
"net/http"
"testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/routers/web"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestRepoDownloadArchive(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.EnableGzip, true)()
defer test.MockVariableValue(&web.GzipMinSize, 10)()
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
req := NewRequest(t, "GET", "/user2/repo1/archive/master.zip")
req.Header.Set("Accept-Encoding", "gzip")
resp := MakeRequest(t, req, http.StatusOK)
bs, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Empty(t, resp.Header().Get("Content-Encoding"))
assert.Equal(t, 320, len(bs))
}

View File

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

View File

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

View File

@ -152,6 +152,7 @@
}
.ui.attached.segment:has(+ .ui[class*="top attached"].header),
.ui.attached.segment:has(+ .page.buttons),
.ui.attached.segment:last-child,
.ui.segment:has(+ .ui.segment:not(.attached)),
.ui.attached.segment:has(+ .ui.modal) {

View File

@ -2195,18 +2195,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 */
}
@ -2748,23 +2742,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 +2818,70 @@ tbody.commit-list {
max-height: 200px;
}
/* Branch tag selector - TODO: Merge this into the same selector on repo page */
.repository .issue-content .issue-content-right .ui.grid .column.row {
padding: 10px;
padding-bottom: 0;
.branch-selector-dropdown {
max-width: 100%;
}
.repository .issue-content .issue-content-right .ui.grid .column.muted {
padding: 0;
.ui.dropdown.branch-selector-dropdown > .menu {
margin-top: 4px;
}
.repository .issue-content .issue-content-right .ui.grid .column.muted .text {
.branch-selector-dropdown .branch-dropdown-button {
margin: 0;
max-width: 340px;
line-height: var(--line-height-default);
}
/* FIXME: These media selectors are not ideal (just keep them from old code).
There are many different pages, some need the max-width while some others don't,
they should be tested and improved in the future. */
@media (min-width: 768px) and (max-width: 991.98px) {
.branch-selector-dropdown .branch-dropdown-button {
max-width: 185px;
}
}
@media (max-width: 767.98px) {
.branch-selector-dropdown .branch-dropdown-button {
max-width: 165px;
}
}
.branch-selector-dropdown .branch-tag-tab {
padding: 0 10px;
}
.branch-selector-dropdown .branch-tag-item {
display: inline-block;
padding: 10px;
width: 100%;
text-align: center;
border: 1px solid transparent;
border-bottom: none;
}
.repository .issue-content .issue-content-right .ui.grid .column.muted .text.black {
.branch-selector-dropdown .branch-tag-item.active {
border-color: var(--color-secondary);
background: var(--color-menu);
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
.repository .issue-content .issue-content-right .ui.dropdown .scrolling.menu {
border-top: none;
}
.repository .issue-content .issue-content-right .branch-tag-divider {
margin-top: -1px;
.branch-selector-dropdown .branch-tag-divider {
margin-top: -1px !important;
border-top: 1px solid var(--color-secondary);
}
.branch-selector-dropdown .scrolling.menu {
border-top: none !important;
}
.branch-selector-dropdown .menu .item .rss-icon {
visibility: hidden; /* only show RSS icon on hover */
}
.branch-selector-dropdown .menu .item:hover .rss-icon {
visibility: visible;
}
.branch-selector-dropdown .scrolling.menu .loading-indicator {
height: 4em;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ import {initCompReactionSelector} from './comp/ReactionSelector.js';
import {initRepoSettingBranches} from './repo-settings.js';
import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.js';
import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.js';
import {hideElem, showElem} from '../utils/dom.js';
import {hideElem, queryElemChildren, showElem} from '../utils/dom.js';
import {POST} from '../modules/fetch.js';
import {initRepoIssueCommentEdit} from './repo-issue-edit.js';
@ -56,16 +56,19 @@ export function initRepoCommentForm() {
}
function initBranchSelector() {
const $selectBranch = $('.ui.select-branch');
const elSelectBranch = document.querySelector('.ui.dropdown.select-branch');
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 +87,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;
});
}