Compare commits

...

8 Commits

Author SHA1 Message Date
Giteabot 0001e58e6b fix: OIDC discovery advertises unsupported id_token response_type (#39498) (#39689)
Backport #39498 by @SHIVANSHGARG07

The OIDC discovery document at `/.well-known/openid-configuration`
advertised `id_token` in `response_types_supported`, but
`/login/oauth/authorize` only implements the authorization code flow and
rejects any other response_type with `unsupported_response_type`. This
mismatch caused OIDC client libraries that rely on discovery to attempt
the implicit flow and fail silently.

This removes `id_token` from `response_types_supported` so discovery
matches actual server behavior, and adds an integration test asserting
`response_type=id_token` is rejected consistently.

Manually verified: rebuilt Gitea, registered an OAuth2 app, confirmed
`/.well-known/openid-configuration` no longer lists `id_token`, and
confirmed `/login/oauth/authorize?...&response_type=id_token` still
correctly returns `error=unsupported_response_type`.

Fixes #39482.

<!--
Before submitting:
- Target the `main` branch; release branches are for backports only.
- Use a Conventional Commits title, e.g. `fix(repo): handle empty branch
names`.
- Read the contributing guidelines:
https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md
- Documentation changes go to https://gitea.com/gitea/docs

Describe your change below and link any issue it fixes.
-->

Co-authored-by: Shivansh Garg <shivanshgarg587@gmail.com>
2026-10-08 18:57:06 +00:00
Giteabot 79c4237cb2 chore(deps): update go toolchain directive to v1.27.2 (#39685) (#39687)
Backport #39685 by @GiteaBot

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [go](https://go.dev/)
([source](https://redirect.github.com/golang/go)) | toolchain | patch |
`1.27.1` → `1.27.2` |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend Renovate
CLI](https://redirect.github.com/renovatebot/renovate).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMjEuNCIsInVwZGF0ZWRJblZlciI6IjQ0LjEyMS40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2026-10-08 11:25:28 -07:00
Giteabot 20f73b2230 ci: skip reverted commits in release notes, publish tagged snaps to stable (#39657) (#39678)
Backport #39657 by @bircni

Release notes: revert commits and the commits they revert are now left
out of the git-cliff changelog. A step before git-cliff finds commits
whose subject starts with "revert", reads the PR numbers on their revert
lines, and writes both SHAs to `.cliffignore`.

Snap: since the move from Launchpad to GitHub Actions, every build was
uploaded to `latest/edge`, including the stable-grade build that
`part-gitea-pull.sh` makes for an unreleased tag. Launchpad used to
release those builds to candidate and stable automatically. Without
that, v28.0.0 had to be promoted by hand and v28.1.0 stayed in edge.
Stable-grade snaps now go to `latest/stable` and `latest/candidate`;
main builds still go to edge. Candidate has to be updated too because
the pull script compares against it.

Signed-off-by: bircni <bircni@icloud.com>
Co-authored-by: bircni <bircni@icloud.com>
2026-10-08 15:57:37 +02:00
Giteabot 175bc89bf9 fix: correct "go get" URL ssh scheme, fix form binding (#39674, #39528) (#39676)
* Backport #39674
* Fix form binding errors (found by #39528)
* Fix #39680

---------

Co-authored-by: Roland Singer <10167163+r0l1@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-08 12:30:59 +00:00
Giteabot 793e77b073 fix(webhook): keep line breaks in Telegram rich messages (#39650) (#39677)
Backport #39650 by @Kshot3000

Since the Telegram webhook switched to Bot API rich messages
(https://github.com/go-gitea/gitea/pull/38298), a bare newline in
`rich_message.html` is treated as insignificant whitespace, so
multi-line messages — issue and PR bodies, comments, push commit lists —
arrive in Telegram as a single paragraph. This restores the old layout
by converting line breaks (`\n`, `\r\n`, `\r`) to `<br>` in
`createTelegramPayloadHTML`, after sanitizing, covering every Telegram
payload type at once.

Verified: the new `Line breaks are kept in rich messages` test plus the
updated Push/Issue/IssueComment/PullRequest/Review expectations fail on
unpatched code (literal `\n` in the payload) and pass with the fix; the
full `services/webhook` package passes, gofmt/vet clean.

Fixes https://github.com/go-gitea/gitea/issues/39649

---
Tips welcome: PayPal kyleblake0659@gmail.com · BTC
3GnR7TWBXAB3pPztBWpNF4LMNEX5yX8vZK

Co-authored-by: KShot <kshot9000@gmail.com>
2026-10-08 13:31:11 +02:00
wxiaoguang 5018ae1b29 fix: correct websocket notification for change password page (#39671) (#39670)
* Backport #39671
* Fix #39669

And also fix a UI bug
2026-10-07 19:26:34 +00:00
Giteabot c60b13b2dd fix(markup): display MathML has no space below it (#39663) (#39668)
Backport #39663 by @nschloe

Since MathML is allowed in markup (#36352, #38034, #39337), a
display-style `<math display="block">` sits flush against the text below
it: it gets no margin of its own, and markup paragraphs have
`margin-top: 0`. Display math rendered by KaTeX gets 16px above and
below.

This adds `math[display="block"]` to the markup block elements that get
`margin-top: 0; margin-bottom: 16px`, like `p`, `pre` and `table`.

To test, view a Markdown file containing

```markdown
Text before.

<math display="block">
<mi>x</mi><mo>=</mo><mn>1</mn>
</math>

Text after.
```

Before, the equation has 16px above and 0px below; after, 16px on both
sides.

Before:

<img width="1544" height="238" alt="before"
src="https://github.com/user-attachments/assets/318238d9-018c-4945-974c-91bd1b282ff2"
/>

After:

<img width="1544" height="283" alt="after"
src="https://github.com/user-attachments/assets/9afde2be-b5ed-4624-a33f-4933e7fae79b"
/>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Nico Schlömer <nschloe@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-10-07 18:23:27 +00:00
Giteabot 27bd022b34 fix: various bugs (#39661) (#39667)
Backport #39661

1. fix #39660: relax email validation
2. fix #39658: use "int64" instead of time.Duration (for JSON v2)

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-07 10:11:58 -07:00
29 changed files with 142 additions and 81 deletions
@@ -37,8 +37,15 @@ jobs:
# retry snapcraft uploads which can be flaky # retry snapcraft uploads which can be flaky
- name: Publish snap - name: Publish snap
run: | run: |
# part-gitea-pull.sh decides what is built, see the comments there:
# * grade devel (main or a prerelease tag) goes to latest/edge
# * grade stable (a new release tag) goes to latest/stable, and to latest/candidate which marks it as released
channel=latest/edge
if unsquashfs -cat "$SNAP" meta/snap.yaml | grep -qx 'grade: stable'; then
channel=latest/stable,latest/candidate
fi
for attempt in 1 2 3 4 5; do for attempt in 1 2 3 4 5; do
snapcraft upload "$SNAP" --release latest/edge && exit 0 snapcraft upload "$SNAP" --release "$channel" && exit 0
echo "::warning::snap upload attempt $attempt failed, retrying in 15s" echo "::warning::snap upload attempt $attempt failed, retrying in 15s"
sleep 15 sleep 15
done done
@@ -76,6 +76,10 @@ jobs:
run: | run: |
previous=$(git tag --list --sort=-v:refname | grep -xE 'v[0-9]+\.[0-9]+\.[0-9]+' | grep -A1 -xF "$GITHUB_REF_NAME" | tail -1) # highest stable version below this one previous=$(git tag --list --sort=-v:refname | grep -xE 'v[0-9]+\.[0-9]+\.[0-9]+' | grep -A1 -xF "$GITHUB_REF_NAME" | tail -1) # highest stable version below this one
echo "range=$previous..$GITHUB_SHA" >> "$GITHUB_OUTPUT" echo "range=$previous..$GITHUB_SHA" >> "$GITHUB_OUTPUT"
- name: skip reverts and reverted commits in changelog
env:
RANGE: ${{ steps.range.outputs.range }}
run: ./tools/generate-cliffignore.sh "$RANGE" | sort -u > .cliffignore
- uses: orhun/git-cliff-action@a9a95522b26fe6403f7bb24031f21fb573d0f5ff # v4.9.1 - uses: orhun/git-cliff-action@a9a95522b26fe6403f7bb24031f21fb573d0f5ff # v4.9.1
with: with:
args: --tag ${{ github.ref_name }} ${{ steps.range.outputs.range }} args: --tag ${{ github.ref_name }} ${{ steps.range.outputs.range }}
+1 -1
View File
@@ -6,7 +6,7 @@ SHASUM ?= shasum -a 256
AIR_PACKAGE ?= github.com/air-verse/air@v1.67.4 # renovate: datasource=go AIR_PACKAGE ?= github.com/air-verse/air@v1.67.4 # renovate: datasource=go
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v4/cmd/editorconfig-checker@v4.0.2 # renovate: datasource=go EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v4/cmd/editorconfig-checker@v4.0.2 # renovate: datasource=go
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.2 # renovate: datasource=go GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.14.0 # renovate: datasource=go
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.17 # renovate: datasource=go GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.17 # renovate: datasource=go
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.36.6 # renovate: datasource=go SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.36.6 # renovate: datasource=go
+3 -2
View File
@@ -1098,8 +1098,9 @@ LEVEL = Info
;; Force ssh:// clone url instead of scp-style uri when default SSH port is used ;; Force ssh:// clone url instead of scp-style uri when default SSH port is used
;USE_COMPAT_SSH_URI = false ;USE_COMPAT_SSH_URI = false
;; ;;
;; Value for the "go get" request returns the repository url as https or ssh, default is https ;; Scheme of the returned URL for the "go get" response.
;GO_GET_CLONE_URL_PROTOCOL = https ;; Default is "https" if DISABLE_HTTP_GIT=false or SSH is disabled, otherwise "ssh".
;GO_GET_CLONE_URL_PROTOCOL =
;; ;;
;; Close issues as long as a commit on any branch marks it as fixed ;; Close issues as long as a commit on any branch marks it as fixed
;DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false ;DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false
+1 -1
View File
@@ -2,7 +2,7 @@ module gitea.dev
go 1.27 go 1.27
toolchain go1.27.1 toolchain go1.27.2
require ( require (
connectrpc.com/connect v1.21.0 connectrpc.com/connect v1.21.0
+10 -1
View File
@@ -616,6 +616,15 @@ func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string {
// ComposeSSHCloneURL returns SSH clone URL based on the given owner and repository name. // ComposeSSHCloneURL returns SSH clone URL based on the given owner and repository name.
func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string { func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string {
return composeSSHCloneURL(doer, ownerName, repoName, setting.Repository.UseCompatSSHURI)
}
// ComposeSSHCloneURI is like ComposeSSHCloneURL but always returns the "ssh://" form, because "go get" rejects scp-style addresses
func ComposeSSHCloneURI(doer *user_model.User, ownerName, repoName string) string {
return composeSSHCloneURL(doer, ownerName, repoName, true)
}
func composeSSHCloneURL(doer *user_model.User, ownerName, repoName string, useURI bool) string {
sshUser := setting.SSH.User sshUser := setting.SSH.User
sshDomain := setting.SSH.Domain sshDomain := setting.SSH.Domain
@@ -642,7 +651,7 @@ func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) strin
if ip := net.ParseIP(sshHost); ip != nil && ip.To4() == nil { if ip := net.ParseIP(sshHost); ip != nil && ip.To4() == nil {
sshHost = "[" + sshHost + "]" // for IPv6 address, wrap it with brackets sshHost = "[" + sshHost + "]" // for IPv6 address, wrap it with brackets
} }
if setting.Repository.UseCompatSSHURI { if useURI {
return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName)) return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
} }
return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName)) return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
+2
View File
@@ -185,6 +185,8 @@ func TestComposeSSHCloneURL(t *testing.T) {
assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo")) assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
setting.Repository.UseCompatSSHURI = true setting.Repository.UseCompatSSHURI = true
assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo")) assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
setting.Repository.UseCompatSSHURI = false
assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURI(nil, "user", "repo"))
// test SSH_DOMAIN while use non-standard SSH port // test SSH_DOMAIN while use non-standard SSH port
setting.SSH.Port = 123 setting.SSH.Port = 123
setting.Repository.UseCompatSSHURI = false setting.Repository.UseCompatSSHURI = false
+16 -4
View File
@@ -152,15 +152,27 @@ func TestListEmails(t *testing.T) {
func TestEmailAddressValidate(t *testing.T) { func TestEmailAddressValidate(t *testing.T) {
cases := map[string]bool{ cases := map[string]bool{
"": false, "": false,
"root@localhost": true,
"user@[192.168.1.2]": true,
"@a": false, "@a": false,
// "_" shouldn't appear in domain but can appear in hostname, since we can't stop site admins from doing so, just accept it
"root@local_host": true,
"root@localhost": true,
"root@LOCALHOST": true,
"user@[192.168.1.2]": true,
"user@[IPv6:FFff::1]": true,
"abc@gmail.com": true, "abc@gmail.com": true,
"abc@gmail.com.": false,
"abc@gmail.com-": false,
"abc@gmail.com\n": false, "abc@gmail.com\n": false,
"abc@gmail..com": false,
"abc@gmail com": false,
"abc@gmail*com": false,
"Foo <foo@bar.com>": false, "Foo <foo@bar.com>": false,
"abc@gmail.com (x)": false, "abc@gmail.com (x)": false,
"jürgen@example.com": false, "jürgen@example.com": false, // utf8 address is not supported yet
"a@foo_bar.com": false,
} }
for tc, isValid := range cases { for tc, isValid := range cases {
t.Run(tc, func(t *testing.T) { t.Run(tc, func(t *testing.T) {
+3 -3
View File
@@ -102,11 +102,11 @@ func NewEmbeddedFS(data []byte) fs.ReadDirFS {
efs := &embeddedFS{data: data, files: make(map[string]*embeddedFileInfo)} efs := &embeddedFS{data: data, files: make(map[string]*embeddedFileInfo)}
efs.meta = sync.OnceValue(func() *EmbeddedMeta { efs.meta = sync.OnceValue(func() *EmbeddedMeta {
var meta EmbeddedMeta var meta EmbeddedMeta
p := bytes.LastIndexByte(data, '\n') _, metaJSON, ok := bytes.CutLast(data, []byte{'\n'})
if p < 0 { if !ok {
return &meta return &meta
} }
if err := json.Unmarshal(data[p+1:], &meta); err != nil { if err := json.Unmarshal(metaJSON, &meta); err != nil {
panic("embedded file is not valid") panic("embedded file is not valid")
} }
return &meta return &meta
+2 -2
View File
@@ -38,14 +38,14 @@ func ReloadTemplates(ctx context.Context) ResponseExtra {
// FlushOptions represents the options for the flush call // FlushOptions represents the options for the flush call
type FlushOptions struct { type FlushOptions struct {
Timeout time.Duration Timeout int64
NonBlocking bool NonBlocking bool
} }
// FlushQueues calls the internal flush-queues function // FlushQueues calls the internal flush-queues function
func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) ResponseExtra { func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) ResponseExtra {
reqURL := setting.LocalURL + "api/internal/manager/flush-queues" reqURL := setting.LocalURL + "api/internal/manager/flush-queues"
req := newInternalRequestAPI(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking}) req := newInternalRequestAPI(ctx, reqURL, "POST", FlushOptions{Timeout: int64(timeout), NonBlocking: nonBlocking})
if timeout > 0 { if timeout > 0 {
req.SetReadWriteTimeout(timeout + 10*time.Second) req.SetReadWriteTimeout(timeout + 10*time.Second)
} }
+1 -1
View File
@@ -303,7 +303,7 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("repository") sec := rootCfg.Section("repository")
Repository.DisableHTTPGit = sec.Key("DISABLE_HTTP_GIT").MustBool() Repository.DisableHTTPGit = sec.Key("DISABLE_HTTP_GIT").MustBool()
Repository.UseCompatSSHURI = sec.Key("USE_COMPAT_SSH_URI").MustBool() Repository.UseCompatSSHURI = sec.Key("USE_COMPAT_SSH_URI").MustBool()
Repository.GoGetCloneURLProtocol = sec.Key("GO_GET_CLONE_URL_PROTOCOL").MustString("https") Repository.GoGetCloneURLProtocol = sec.Key("GO_GET_CLONE_URL_PROTOCOL").String()
// MAX_CREATION_LIMIT is a shortcut that sets the default for the two per-type limits below. // MAX_CREATION_LIMIT is a shortcut that sets the default for the two per-type limits below.
// USER_/ORG_MAX_CREATION_LIMIT take precedence when explicitly set. // USER_/ORG_MAX_CREATION_LIMIT take precedence when explicitly set.
Repository.MaxCreationLimit = sec.Key("MAX_CREATION_LIMIT").MustInt(-1) // FIXME: INI-MUST-SIDE-EFFECT Repository.MaxCreationLimit = sec.Key("MAX_CREATION_LIMIT").MustInt(-1) // FIXME: INI-MUST-SIDE-EFFECT
+11 -7
View File
@@ -14,8 +14,6 @@ import (
"gitea.dev/modules/glob" "gitea.dev/modules/glob"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
"golang.org/x/net/idna"
) )
type globalVarsStruct struct { type globalVarsStruct struct {
@@ -24,6 +22,8 @@ type globalVarsStruct struct {
invalidUsernamePattern *regexp.Regexp invalidUsernamePattern *regexp.Regexp
validBadgeSlugPattern *regexp.Regexp validBadgeSlugPattern *regexp.Regexp
invalidBadgeSlugPattern *regexp.Regexp invalidBadgeSlugPattern *regexp.Regexp
validEmailHostName *regexp.Regexp
validEmailHostIP *regexp.Regexp
} }
var globalVars = sync.OnceValue(func() *globalVarsStruct { var globalVars = sync.OnceValue(func() *globalVarsStruct {
@@ -33,6 +33,8 @@ var globalVars = sync.OnceValue(func() *globalVarsStruct {
invalidUsernamePattern: regexp.MustCompile(`[-._]{2,}|[-._]$`), // No consecutive or trailing non-alphanumeric chars invalidUsernamePattern: regexp.MustCompile(`[-._]{2,}|[-._]$`), // No consecutive or trailing non-alphanumeric chars
validBadgeSlugPattern: regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`), validBadgeSlugPattern: regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`),
invalidBadgeSlugPattern: regexp.MustCompile(`[-._]{2,}|[-._]$`), invalidBadgeSlugPattern: regexp.MustCompile(`[-._]{2,}|[-._]$`),
validEmailHostName: regexp.MustCompile(`^[a-zA-Z0-9][-.\w]*$`),
validEmailHostIP: regexp.MustCompile(`(?i)^\[([0-9.]+|ipv6:[0-9a-f:.]+)\]$`),
} }
}) })
@@ -124,10 +126,12 @@ func IsEmailAddressValid(email string) bool {
return false return false
} }
_, domain, _ := strings.Cut(email, "@") _, domain, _ := strings.Cut(email, "@")
if strings.HasPrefix(domain, "[") { if !globalVars().validEmailHostName.MatchString(domain) && !globalVars().validEmailHostIP.MatchString(domain) {
// address like "foo@[192.168.1.2]" return false
}
if strings.HasPrefix(domain, "-") || strings.HasSuffix(domain, "-") ||
strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") {
return false
}
return true return true
} }
_, err = idna.Registration.ToASCII(domain)
return err == nil
}
+2 -2
View File
@@ -135,8 +135,8 @@ func shortenFilename(filename, fallback string) string {
if filename == "" { if filename == "" {
return fallback return fallback
} }
if lastIndex := strings.LastIndexByte(filename, '/'); lastIndex >= 0 { if dir, _, ok := strings.CutLast(filename, "/"); ok {
if secondLastIndex := strings.LastIndexByte(filename[:lastIndex], '/'); secondLastIndex >= 0 { if secondLastIndex := strings.LastIndexByte(dir, '/'); secondLastIndex >= 0 {
return filename[secondLastIndex+1:] return filename[secondLastIndex+1:]
} }
} }
+3 -2
View File
@@ -5,6 +5,7 @@ package private
import ( import (
"net/http" "net/http"
"time"
"gitea.dev/models/db" "gitea.dev/models/db"
"gitea.dev/modules/graceful" "gitea.dev/modules/graceful"
@@ -34,7 +35,7 @@ func FlushQueues(ctx *context.PrivateContext) {
// Save the hammer ctx here - as a new one is created each time you call this. // Save the hammer ctx here - as a new one is created each time you call this.
baseCtx := graceful.GetManager().HammerContext() baseCtx := graceful.GetManager().HammerContext()
go func() { go func() {
err := queue.GetManager().FlushAll(baseCtx, opts.Timeout) err := queue.GetManager().FlushAll(baseCtx, time.Duration(opts.Timeout))
if err != nil { if err != nil {
log.Error("Flushing request timed-out with error: %v", err) log.Error("Flushing request timed-out with error: %v", err)
} }
@@ -44,7 +45,7 @@ func FlushQueues(ctx *context.PrivateContext) {
}) })
return return
} }
err := queue.GetManager().FlushAll(ctx, opts.Timeout) err := queue.GetManager().FlushAll(ctx, time.Duration(opts.Timeout))
if err != nil { if err != nil {
ctx.PrivateUserErrorf(http.StatusRequestTimeout, "%v", err) ctx.PrivateUserErrorf(http.StatusRequestTimeout, "%v", err)
return return
-1
View File
@@ -31,7 +31,6 @@ func OIDCWellKnown(ctx *context.Context) {
"introspection_endpoint": oidcBaseUrl + "/login/oauth/introspect", "introspection_endpoint": oidcBaseUrl + "/login/oauth/introspect",
"response_types_supported": []string{ "response_types_supported": []string{
"code", "code",
"id_token",
}, },
"id_token_signing_alg_values_supported": []string{ "id_token_signing_alg_values_supported": []string{
oauth2_provider.DefaultSigningKey.SigningMethod().Alg(), oauth2_provider.DefaultSigningKey.SigningMethod().Alg(),
+1 -7
View File
@@ -69,13 +69,7 @@ func goGet(ctx *context.Context) {
goGetImport := context.ComposeGoGetImport(ctx, ownerName, trimmedRepoName) goGetImport := context.ComposeGoGetImport(ctx, ownerName, trimmedRepoName)
var cloneURL string goImportContent := fmt.Sprintf("%s git %s", goGetImport, context.ComposeGoGetCloneURL(ctx, ownerName, trimmedRepoName))
if setting.Repository.GoGetCloneURLProtocol == "ssh" {
cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, ownerName, repoName)
} else {
cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, ownerName, repoName)
}
goImportContent := fmt.Sprintf("%s git %s", goGetImport, cloneURL /*CloneLink*/)
goSourceContent := fmt.Sprintf("%s _ %s %s", goGetImport, prefix+"{/dir}" /*GoDocDirectory*/, prefix+"{/dir}/{file}#L{line}" /*GoDocFile*/) goSourceContent := fmt.Sprintf("%s _ %s %s", goGetImport, prefix+"{/dir}" /*GoDocDirectory*/, prefix+"{/dir}/{file}#L{line}" /*GoDocFile*/)
goGetCli := fmt.Sprintf("go get %s%s", insecure, goGetImport) goGetCli := fmt.Sprintf("go get %s%s", insecure, goGetImport)
-1
View File
@@ -197,7 +197,6 @@ func verifyAuthWithOptionsWeb(options *common.VerifyOptions) func(ctx *context.C
ctx.HTTPError(http.StatusUnauthorized, ctx.Locale.TrString("auth.must_change_password")) ctx.HTTPError(http.StatusUnauthorized, ctx.Locale.TrString("auth.must_change_password"))
return return
} }
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
ctx.Redirect(setting.AppSubURL + "/user/settings/change_password") ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
return return
} }
+10 -7
View File
@@ -389,6 +389,15 @@ func ComposeGoGetImport(ctx context.Context, owner, repo string) string {
return path.Join(curAppURL.Host, setting.AppSubURL, url.PathEscape(owner), url.PathEscape(repo)) return path.Join(curAppURL.Host, setting.AppSubURL, url.PathEscape(owner), url.PathEscape(repo))
} }
// ComposeGoGetCloneURL returns the clone URL for the go-import meta content.
func ComposeGoGetCloneURL(ctx *Context, owner, repo string) string {
useSSH := setting.Repository.GoGetCloneURLProtocol == "ssh" || (setting.Repository.DisableHTTPGit && !setting.SSH.Disabled)
if useSSH {
return repo_model.ComposeSSHCloneURI(ctx.Doer, owner, repo)
}
return repo_model.ComposeHTTPSCloneURL(ctx, owner, repo)
}
// EarlyResponseForGoGetMeta responses appropriate go-get meta with status 200 // EarlyResponseForGoGetMeta responses appropriate go-get meta with status 200
// if user does not have actual access to the requested repository, // if user does not have actual access to the requested repository,
// or the owner or repository does not exist at all. // or the owner or repository does not exist at all.
@@ -402,13 +411,7 @@ func EarlyResponseForGoGetMeta(ctx *Context) {
return return
} }
var cloneURL string goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(ctx, username, reponame), ComposeGoGetCloneURL(ctx, username, reponame))
if setting.Repository.GoGetCloneURLProtocol == "ssh" {
cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, username, reponame)
} else {
cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, username, reponame)
}
goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(ctx, username, reponame), cloneURL)
htmlMeta := fmt.Sprintf(`<meta name="go-import" content="%s">`, html.EscapeString(goImportContent)) htmlMeta := fmt.Sprintf(`<meta name="go-import" content="%s">`, html.EscapeString(goImportContent))
ctx.PlainText(http.StatusOK, htmlMeta) ctx.PlainText(http.StatusOK, htmlMeta)
} }
+1 -1
View File
@@ -65,6 +65,6 @@ type AdminEditUserForm struct {
// AdminDashboardForm form for admin dashboard operations // AdminDashboardForm form for admin dashboard operations
type AdminDashboardForm struct { type AdminDashboardForm struct {
middleware.FormDefaultValidator middleware.FormDefaultValidator
Op string `binding:"required"` Op string `binding:"Required"`
From string From string
} }
-6
View File
@@ -571,12 +571,6 @@ type AddTimeManuallyForm struct {
Minutes int `binding:"Range(0,1000)"` Minutes int `binding:"Range(0,1000)"`
} }
// SaveTopicForm form for save topics for repository
type SaveTopicForm struct {
middleware.FormDefaultValidator
Topics []string `binding:"topics;Required;"`
}
// AddDeployTokenForm form for adding a deploy token to a repository // AddDeployTokenForm form for adding a deploy token to a repository
type AddDeployTokenForm struct { type AddDeployTokenForm struct {
middleware.FormDefaultValidator middleware.FormDefaultValidator
+4 -1
View File
@@ -201,9 +201,12 @@ func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload
func createTelegramPayloadHTML(msgHTML string) TelegramPayload { func createTelegramPayloadHTML(msgHTML string) TelegramPayload {
// https://core.telegram.org/bots/api#formatting-options // https://core.telegram.org/bots/api#formatting-options
sanitized := strings.TrimSpace(string(markup.Sanitize(msgHTML)))
// Rich messages collapse bare newlines like HTML, so keep line breaks as <br>.
sanitized = strings.NewReplacer("\r\n", "<br>", "\r", "<br>", "\n", "<br>").Replace(sanitized)
return TelegramPayload{ return TelegramPayload{
RichMessage: InputRichMessage{ RichMessage: InputRichMessage{
HTML: strings.TrimSpace(string(markup.Sanitize(msgHTML))), HTML: sanitized,
}, },
} }
} }
+13 -17
View File
@@ -27,6 +27,12 @@ func TestTelegramPayload(t *testing.T) {
}, p) }, p)
}) })
t.Run("Line breaks are kept in rich messages", func(t *testing.T) {
// Rich messages collapse bare newlines, so they must become <br>.
p := createTelegramPayloadHTML("first line\r\nsecond line\nthird line")
assert.Equal(t, "first line<br>second line<br>third line", p.RichMessage.HTML)
})
t.Run("Create", func(t *testing.T) { t.Run("Create", func(t *testing.T) {
p := createTestPayload() p := createTestPayload()
@@ -60,9 +66,7 @@ func TestTelegramPayload(t *testing.T) {
pl, err := tc.Push(p) pl, err := tc.Push(p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>:<a href="http://localhost:3000/test/repo/src/test" rel="nofollow">test</a>] 2 new commits assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>:<a href="http://localhost:3000/test/repo/src/test" rel="nofollow">test</a>] 2 new commits<br>[<a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778" rel="nofollow">2020558</a>] commit message - user1<br>[<a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778" rel="nofollow">2020558</a>] commit message - user1`, pl.RichMessage.HTML)
[<a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778" rel="nofollow">2020558</a>] commit message - user1
[<a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778" rel="nofollow">2020558</a>] commit message - user1`, pl.RichMessage.HTML)
}) })
t.Run("Issue", func(t *testing.T) { t.Run("Issue", func(t *testing.T) {
@@ -72,9 +76,7 @@ func TestTelegramPayload(t *testing.T) {
pl, err := tc.Issue(p) pl, err := tc.Issue(p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] Issue opened: <a href="http://localhost:3000/test/repo/issues/2" rel="nofollow">#2 crash</a> by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a> assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] Issue opened: <a href="http://localhost:3000/test/repo/issues/2" rel="nofollow">#2 crash</a> by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a><br><br>issue body`, pl.RichMessage.HTML)
issue body`, pl.RichMessage.HTML)
p.Action = api.HookIssueClosed p.Action = api.HookIssueClosed
pl, err = tc.Issue(p) pl, err = tc.Issue(p)
@@ -89,8 +91,7 @@ issue body`, pl.RichMessage.HTML)
pl, err := tc.IssueComment(p) pl, err := tc.IssueComment(p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] New comment on issue <a href="http://localhost:3000/test/repo/issues/2" rel="nofollow">#2 crash</a> by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a> assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] New comment on issue <a href="http://localhost:3000/test/repo/issues/2" rel="nofollow">#2 crash</a> by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a><br>more info needed`, pl.RichMessage.HTML)
more info needed`, pl.RichMessage.HTML)
}) })
t.Run("PullRequest", func(t *testing.T) { t.Run("PullRequest", func(t *testing.T) {
@@ -99,8 +100,7 @@ more info needed`, pl.RichMessage.HTML)
pl, err := tc.PullRequest(p) pl, err := tc.PullRequest(p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] Pull request opened: <a href="http://localhost:3000/test/repo/pulls/12" rel="nofollow">#12 Fix bug</a> by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a> assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] Pull request opened: <a href="http://localhost:3000/test/repo/pulls/12" rel="nofollow">#12 Fix bug</a> by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a><br>fixes bug #2`, pl.RichMessage.HTML)
fixes bug #2`, pl.RichMessage.HTML)
}) })
t.Run("PullRequestComment", func(t *testing.T) { t.Run("PullRequestComment", func(t *testing.T) {
@@ -109,8 +109,7 @@ fixes bug #2`, pl.RichMessage.HTML)
pl, err := tc.IssueComment(p) pl, err := tc.IssueComment(p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] New comment on pull request <a href="http://localhost:3000/test/repo/pulls/12" rel="nofollow">#12 Fix bug</a> by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a> assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] New comment on pull request <a href="http://localhost:3000/test/repo/pulls/12" rel="nofollow">#12 Fix bug</a> by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a><br>changes requested`, pl.RichMessage.HTML)
changes requested`, pl.RichMessage.HTML)
}) })
t.Run("Review", func(t *testing.T) { t.Run("Review", func(t *testing.T) {
@@ -120,8 +119,7 @@ changes requested`, pl.RichMessage.HTML)
pl, err := tc.Review(p, webhook_module.HookEventPullRequestReviewApproved) pl, err := tc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, `[test/repo] Pull request review approved: #12 Fix bug assert.Equal(t, `[test/repo] Pull request review approved: #12 Fix bug<br>good job`, pl.RichMessage.HTML)
good job`, pl.RichMessage.HTML)
}) })
t.Run("Repository", func(t *testing.T) { t.Run("Repository", func(t *testing.T) {
@@ -206,7 +204,5 @@ func TestTelegramJSONPayload(t *testing.T) {
var body TelegramPayload var body TelegramPayload
err = json.NewDecoder(req.Body).Decode(&body) err = json.NewDecoder(req.Body).Decode(&body)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>:<a href="http://localhost:3000/test/repo/src/test" rel="nofollow">test</a>] 2 new commits assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>:<a href="http://localhost:3000/test/repo/src/test" rel="nofollow">test</a>] 2 new commits<br>[<a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778" rel="nofollow">2020558</a>] commit message - user1<br>[<a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778" rel="nofollow">2020558</a>] commit message - user1`, body.RichMessage.HTML)
[<a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778" rel="nofollow">2020558</a>] commit message - user1
[<a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778" rel="nofollow">2020558</a>] commit message - user1`, body.RichMessage.HTML)
} }
+6 -5
View File
@@ -16,13 +16,14 @@ else
fi fi
# How it works: # How it works:
# * snapcraft.io checks out the default branch (e.g.: main during 1.27 dev period) # * release-nightly-snapcraft.yml builds the snap on every push to the default branch (e.g.: main during 1.27 dev period)
# * "override-pull" step gets the latest tag by date (e.g.: v1.26.1) # * "override-pull" step gets the latest tag by date (e.g.: v1.26.1)
# * use "snap info gitea" to get the latest released tag # * use "snap info gitea" to get the latest released tag, which is the version in the "latest/candidate" channel
# * if the latest tag is not released to stable, checkout that tag and build it for "stable" # * if the latest tag is not released yet, checkout that tag and build it with grade "stable",
# * otherwise, build the main branch for "devel" # the workflow publishes it to "latest/stable" and "latest/candidate"
# * otherwise, build the main branch with grade "devel", the workflow publishes it to "latest/edge"
# * "override-build" step uses build script from the checked out commit to build # * "override-build" step uses build script from the checked out commit to build
# This approach highly depends on the "main" branch's push. # This approach highly depends on the "main" branch's push: a new tag is only published by the next push after it.
# To debug the logic: # To debug the logic:
# * last_committed_tag=v1.26.1 last_released_tag=v1.26.0 ./snap/part-gitea-pull.sh # * last_committed_tag=v1.26.1 last_released_tag=v1.26.0 ./snap/part-gitea-pull.sh
+2 -2
View File
@@ -44,8 +44,8 @@
<div class="navbar-right"> <div class="navbar-right">
{{if and .IsSigned .MustChangePassword}} {{if and .IsSigned .MustChangePassword}}
<div class="ui dropdown jump item" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}"> <div class="ui dropdown jump item" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
<span class="text"> <span class="flex-text-block">
{{ctx.AvatarUtils.Avatar .SignedUser 24 "tw-mr-1"}} {{ctx.AvatarUtils.Avatar .SignedUser 24}}
<span class="only-mobile">{{.SignedUser.Name}}</span> <span class="only-mobile">{{.SignedUser.Name}}</span>
<span class="not-mobile flex-text-block">{{svg "octicon-triangle-down"}}</span> <span class="not-mobile flex-text-block">{{svg "octicon-triangle-down"}}</span>
</span> </span>
+1 -1
View File
@@ -1,6 +1,6 @@
{{- $itemExtraClass := .ItemExtraClass -}} {{- $itemExtraClass := .ItemExtraClass -}}
{{- $data := .PageGlobalData -}} {{- $data := .PageGlobalData -}}
{{if and $data $data.IsSigned}}{{/* data may not exist, for example: rendering 503 page before the PageGlobalData middleware */}} {{if and $data $data.IsSigned (not ctx.RootData.MustChangePassword)}}{{/* data may not exist, for example: rendering 503 page before the PageGlobalData middleware */}}
{{- $activeStopwatch := call $data.GetActiveStopwatch -}} {{- $activeStopwatch := call $data.GetActiveStopwatch -}}
{{- $notificationUnreadCount := call $data.GetNotificationUnreadCount -}} {{- $notificationUnreadCount := call $data.GetNotificationUnreadCount -}}
{{/* always rendered so a real-time push can reveal it without a reload */}} {{/* always rendered so a real-time push can reveal it without a reload */}}
+5
View File
@@ -58,6 +58,11 @@ func TestGoGetForSSH(t *testing.T) {
</html>`, setting.AppDomain, setting.HTTPPort, setting.AppURL, setting.SSH.Domain, setting.SSH.Port) </html>`, setting.AppDomain, setting.HTTPPort, setting.AppURL, setting.SSH.Domain, setting.SSH.Port)
assert.Equal(t, expected, resp.Body.String()) assert.Equal(t, expected, resp.Body.String())
// go rejects scp-style addresses, so the standard port must still produce an ssh:// URL
defer test.MockVariableValue(&setting.SSH.Port, 22)()
resp = MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), fmt.Sprintf(`git ssh://git@%s/blah/glah.git">`, setting.SSH.Domain))
} }
// TestGoGetPrivateRepoBranchNotLeaked ensures the go-get meta endpoint does not disclose a // TestGoGetPrivateRepoBranchNotLeaked ensures the go-get meta endpoint does not disclose a
+11
View File
@@ -108,6 +108,7 @@ func TestOAuth2(t *testing.T) {
t.Run("AuthorizeNoClientID", testAuthorizeNoClientID) t.Run("AuthorizeNoClientID", testAuthorizeNoClientID)
t.Run("AuthorizeUnregisteredRedirect", testAuthorizeUnregisteredRedirect) t.Run("AuthorizeUnregisteredRedirect", testAuthorizeUnregisteredRedirect)
t.Run("AuthorizeUnsupportedResponseType", testAuthorizeUnsupportedResponseType) t.Run("AuthorizeUnsupportedResponseType", testAuthorizeUnsupportedResponseType)
t.Run("AuthorizeUnsupportedResponseTypeIDToken", testAuthorizeUnsupportedResponseTypeIDToken)
t.Run("AuthorizeUnsupportedCodeChallengeMethod", testAuthorizeUnsupportedCodeChallengeMethod) t.Run("AuthorizeUnsupportedCodeChallengeMethod", testAuthorizeUnsupportedCodeChallengeMethod)
t.Run("AuthorizeLoginRedirect", testAuthorizeLoginRedirect) t.Run("AuthorizeLoginRedirect", testAuthorizeLoginRedirect)
t.Run("AuthorizeShow", testAuthorizeShow) t.Run("AuthorizeShow", testAuthorizeShow)
@@ -163,6 +164,16 @@ func testAuthorizeUnsupportedResponseType(t *testing.T) {
assert.Equal(t, "Only code response type is supported.", u.Query().Get("error_description")) assert.Equal(t, "Only code response type is supported.", u.Query().Get("error_description"))
} }
func testAuthorizeUnsupportedResponseTypeIDToken(t *testing.T) {
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=https://example.com&response_type=id_token&state=thestate")
ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
u, err := resp.Result().Location()
assert.NoError(t, err)
assert.Equal(t, "unsupported_response_type", u.Query().Get("error"))
assert.Equal(t, "Only code response type is supported.", u.Query().Get("error_description"))
}
func testAuthorizeUnsupportedCodeChallengeMethod(t *testing.T) { func testAuthorizeUnsupportedCodeChallengeMethod(t *testing.T) {
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=https://example.com&response_type=code&state=thestate&code_challenge_method=UNEXPECTED") req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=https://example.com&response_type=code&state=thestate&code_challenge_method=UNEXPECTED")
ctx := loginUser(t, "user1") ctx := loginUser(t, "user1")
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
set -euo pipefail
# Prints the revert commits in the given range and the commits they revert, for git-cliff's .cliffignore
range="${1:?usage: $0 <revision-range>}"
commits=$(git log --format='%H %s' "$range")
shopt -s nocasematch
while read -r sha subject; do
[[ "$subject" == revert* ]] || continue
echo "$sha"
for pr in $(git show -s --format=%B "$sha" | grep -i revert | grep -oE '(#|/pull/)[0-9]+' | grep -oE '[0-9]+' || true); do
grep -F "(#$pr)" <<< "$commits" | cut -d' ' -f1 || true
done
done <<< "$commits"
+2 -1
View File
@@ -125,7 +125,8 @@
.markup ol, .markup ol,
.markup dl, .markup dl,
.markup table, .markup table,
.markup pre { .markup pre,
.markup math[display="block" i] {
margin-top: 0; margin-top: 0;
margin-bottom: 16px; margin-bottom: 16px;
} }