diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go
index ebe0266d1f..97f9bb7f06 100644
--- a/cmd/admin_user_create.go
+++ b/cmd/admin_user_create.go
@@ -81,6 +81,10 @@ var microcmdUserCreate = &cli.Command{
Name: "restricted",
Usage: "Make a restricted user account",
},
+ &cli.StringFlag{
+ Name: "fullname",
+ Usage: `The full, human-readable name of the user`,
+ },
},
}
@@ -191,6 +195,7 @@ func runCreateUser(c *cli.Context) error {
Passwd: password,
MustChangePassword: mustChangePassword,
Visibility: visibility,
+ FullName: c.String("fullname"),
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
diff --git a/cmd/admin_user_create_test.go b/cmd/admin_user_create_test.go
index 6fd6f8c226..d5952412c3 100644
--- a/cmd/admin_user_create_test.go
+++ b/cmd/admin_user_create_test.go
@@ -50,17 +50,17 @@ func TestAdminUserCreate(t *testing.T) {
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
})
- createUser := func(name, args string) error {
- return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args)))
+ createUser := func(name string, args ...string) error {
+ return app.Run(append([]string{"./gitea", "admin", "user", "create", "--username", name, "--email", name + "@gitea.local"}, args...))
}
t.Run("UserType", func(t *testing.T) {
reset()
- assert.ErrorContains(t, createUser("u", "--user-type invalid"), "invalid user type")
- assert.ErrorContains(t, createUser("u", "--user-type bot --password 123"), "can only be set for individual users")
- assert.ErrorContains(t, createUser("u", "--user-type bot --must-change-password"), "can only be set for individual users")
+ assert.ErrorContains(t, createUser("u", "--user-type", "invalid"), "invalid user type")
+ assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--password", "123"), "can only be set for individual users")
+ assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--must-change-password"), "can only be set for individual users")
- assert.NoError(t, createUser("u", "--user-type bot"))
+ assert.NoError(t, createUser("u", "--user-type", "bot"))
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
assert.Equal(t, user_model.UserTypeBot, u.Type)
assert.Empty(t, u.Passwd)
@@ -75,7 +75,7 @@ func TestAdminUserCreate(t *testing.T) {
// using "--access-token" only means "all" access
reset()
- assert.NoError(t, createUser("u", "--random-password --access-token"))
+ assert.NoError(t, createUser("u", "--random-password", "--access-token"))
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"})
@@ -85,7 +85,7 @@ func TestAdminUserCreate(t *testing.T) {
// using "--access-token" with name & scopes
reset()
- assert.NoError(t, createUser("u", "--random-password --access-token --access-token-name new-token-name --access-token-scopes read:issue,read:user"))
+ assert.NoError(t, createUser("u", "--random-password", "--access-token", "--access-token-name", "new-token-name", "--access-token-scopes", "read:issue,read:user"))
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"})
@@ -98,23 +98,38 @@ func TestAdminUserCreate(t *testing.T) {
// using "--access-token-name" without "--access-token"
reset()
- err = createUser("u", "--random-password --access-token-name new-token-name")
+ err = createUser("u", "--random-password", "--access-token-name", "new-token-name")
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
// using "--access-token-scopes" without "--access-token"
reset()
- err = createUser("u", "--random-password --access-token-scopes read:issue")
+ err = createUser("u", "--random-password", "--access-token-scopes", "read:issue")
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
// empty permission
reset()
- err = createUser("u", "--random-password --access-token --access-token-scopes public-only")
+ err = createUser("u", "--random-password", "--access-token", "--access-token-scopes", "public-only")
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.ErrorContains(t, err, "access token does not have any permission")
})
+
+ t.Run("UserFields", func(t *testing.T) {
+ reset()
+ assert.NoError(t, createUser("u-FullNameWithSpace", "--random-password", "--fullname", "First O'Middle Last"))
+ unittest.AssertExistsAndLoadBean(t, &user_model.User{
+ Name: "u-FullNameWithSpace",
+ LowerName: "u-fullnamewithspace",
+ FullName: "First O'Middle Last",
+ Email: "u-FullNameWithSpace@gitea.local",
+ })
+
+ assert.NoError(t, createUser("u-FullNameEmpty", "--random-password", "--fullname", ""))
+ u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u-fullnameempty"})
+ assert.Empty(t, u.FullName)
+ })
}
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index c10de95953..53e25a8c3b 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -59,27 +59,16 @@ RUN_USER = ; git
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
-;; The protocol the server listens on. One of 'http', 'https', 'http+unix', 'fcgi' or 'fcgi+unix'. Defaults to 'http'
-;; Note: Value must be lowercase.
+;; The protocol the server listens on. One of "http", "https", "http+unix", "fcgi" or "fcgi+unix".
;PROTOCOL = http
;;
-;; Expect PROXY protocol headers on connections
-;USE_PROXY_PROTOCOL = false
-;;
-;; Use PROXY protocol in TLS Bridging mode
-;PROXY_PROTOCOL_TLS_BRIDGING = false
-;;
-; Timeout to wait for PROXY protocol header (set to 0 to have no timeout)
-;PROXY_PROTOCOL_HEADER_TIMEOUT=5s
-;;
-; Accept PROXY protocol headers with UNKNOWN type
-;PROXY_PROTOCOL_ACCEPT_UNKNOWN=false
-;;
-;; Set the domain for the server
+;; Set the domain for the server.
+;; Most users should set it to the real website domain of their Gitea instance.
;DOMAIN = localhost
;;
;; The AppURL used by Gitea to generate absolute links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/".
-;; Most users should set it to the real website URL of their Gitea instance.
+;; Most users should set it to the real website URL of their Gitea instance when there is a reverse proxy.
+;; When it is empty, Gitea will use HTTP "Host" header to generate ROOT_URL, and fall back to the default one if no "Host" header.
;ROOT_URL =
;;
;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy.
@@ -90,13 +79,25 @@ RUN_USER = ; git
;STATIC_URL_PREFIX =
;;
;; The address to listen on. Either a IPv4/IPv6 address or the path to a unix socket.
-;; If PROTOCOL is set to `http+unix` or `fcgi+unix`, this should be the name of the Unix socket file to use.
+;; If PROTOCOL is set to "http+unix" or "fcgi+unix", this should be the name of the Unix socket file to use.
;; Relative paths will be made absolute against the _`AppWorkPath`_.
;HTTP_ADDR = 0.0.0.0
;;
-;; The port to listen on. Leave empty when using a unix socket.
+;; The port to listen on for "http" or "https" protocol. Leave empty when using a unix socket.
;HTTP_PORT = 3000
;;
+;; Expect PROXY protocol headers on connections
+;USE_PROXY_PROTOCOL = false
+;;
+;; Use PROXY protocol in TLS Bridging mode
+;PROXY_PROTOCOL_TLS_BRIDGING = false
+;;
+;; Timeout to wait for PROXY protocol header (set to 0 to have no timeout)
+;PROXY_PROTOCOL_HEADER_TIMEOUT = 5s
+;;
+;; Accept PROXY protocol headers with UNKNOWN type
+;PROXY_PROTOCOL_ACCEPT_UNKNOWN = false
+;;
;; If REDIRECT_OTHER_PORT is true, and PROTOCOL is set to https an http server
;; will be started on PORT_TO_REDIRECT and it will redirect plain, non-secure http requests to the main
;; ROOT_URL. Defaults are false for REDIRECT_OTHER_PORT and 80 for
diff --git a/go.mod b/go.mod
index bbe0d5a4c9..496f387f98 100644
--- a/go.mod
+++ b/go.mod
@@ -120,7 +120,7 @@ require (
gitlab.com/gitlab-org/api/client-go v0.126.0
golang.org/x/crypto v0.36.0
golang.org/x/image v0.25.0
- golang.org/x/net v0.37.0
+ golang.org/x/net v0.38.0
golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.12.0
golang.org/x/sys v0.31.0
diff --git a/go.sum b/go.sum
index 9dbb7df060..4d5f37d697 100644
--- a/go.sum
+++ b/go.sum
@@ -875,8 +875,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
-golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
-golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
diff --git a/models/actions/runner.go b/models/actions/runner.go
index 0411a48393..b55723efa0 100644
--- a/models/actions/runner.go
+++ b/models/actions/runner.go
@@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/models/shared/types"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
@@ -123,8 +124,15 @@ func (r *ActionRunner) IsOnline() bool {
return false
}
-// Editable checks if the runner is editable by the user
-func (r *ActionRunner) Editable(ownerID, repoID int64) bool {
+// EditableInContext checks if the runner is editable by the "context" owner/repo
+// ownerID == 0 and repoID == 0 means "admin" context, any runner including global runners could be edited
+// ownerID == 0 and repoID != 0 means "repo" context, any runner belonging to the given repo could be edited
+// ownerID != 0 and repoID == 0 means "owner(org/user)" context, any runner belonging to the given user/org could be edited
+// ownerID != 0 and repoID != 0 means "owner" OR "repo" context, legacy behavior, but we should forbid using it
+func (r *ActionRunner) EditableInContext(ownerID, repoID int64) bool {
+ if ownerID != 0 && repoID != 0 {
+ setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
+ }
if ownerID == 0 && repoID == 0 {
return true
}
@@ -168,6 +176,12 @@ func init() {
db.RegisterModel(&ActionRunner{})
}
+// FindRunnerOptions
+// ownerID == 0 and repoID == 0 means any runner including global runners
+// repoID != 0 and WithAvailable == false means any runner for the given repo
+// repoID != 0 and WithAvailable == true means any runner for the given repo, parent user/org, and global runners
+// ownerID != 0 and repoID == 0 and WithAvailable == false means any runner for the given user/org
+// ownerID != 0 and repoID == 0 and WithAvailable == true means any runner for the given user/org and global runners
type FindRunnerOptions struct {
db.ListOptions
IDs []int64
diff --git a/models/fixtures/action_runner.yml b/models/fixtures/action_runner.yml
new file mode 100644
index 0000000000..dce2d41cfb
--- /dev/null
+++ b/models/fixtures/action_runner.yml
@@ -0,0 +1,40 @@
+-
+ id: 34346
+ name: runner_to_be_deleted-user
+ uuid: 3EF231BD-FBB7-4E4B-9602-E6F28363EF18
+ token_hash: 3EF231BD-FBB7-4E4B-9602-E6F28363EF18
+ version: "1.0.0"
+ owner_id: 1
+ repo_id: 0
+ description: "This runner is going to be deleted"
+ agent_labels: '["runner_to_be_deleted","linux"]'
+-
+ id: 34347
+ name: runner_to_be_deleted-org
+ uuid: 3EF231BD-FBB7-4E4B-9602-E6F28363EF19
+ token_hash: 3EF231BD-FBB7-4E4B-9602-E6F28363EF19
+ version: "1.0.0"
+ owner_id: 3
+ repo_id: 0
+ description: "This runner is going to be deleted"
+ agent_labels: '["runner_to_be_deleted","linux"]'
+-
+ id: 34348
+ name: runner_to_be_deleted-repo1
+ uuid: 3EF231BD-FBB7-4E4B-9602-E6F28363EF20
+ token_hash: 3EF231BD-FBB7-4E4B-9602-E6F28363EF20
+ version: "1.0.0"
+ owner_id: 0
+ repo_id: 1
+ description: "This runner is going to be deleted"
+ agent_labels: '["runner_to_be_deleted","linux"]'
+-
+ id: 34349
+ name: runner_to_be_deleted
+ uuid: 3EF231BD-FBB7-4E4B-9602-E6F28363EF17
+ token_hash: 3EF231BD-FBB7-4E4B-9602-E6F28363EF17
+ version: "1.0.0"
+ owner_id: 0
+ repo_id: 0
+ description: "This runner is going to be deleted"
+ agent_labels: '["runner_to_be_deleted","linux"]'
diff --git a/models/packages/package_version.go b/models/packages/package_version.go
index 278e8e3a86..bb7fd895f8 100644
--- a/models/packages/package_version.go
+++ b/models/packages/package_version.go
@@ -279,9 +279,7 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) {
default:
e.Desc("package_version.created_unix")
}
-
- // Sort by id for stable order with duplicates in the other field
- e.Asc("package_version.id")
+ e.Desc("package_version.id") // Sort by id for stable order with duplicates in the other field
}
// SearchVersions gets all versions of packages matching the search options
diff --git a/modules/httplib/url.go b/modules/httplib/url.go
index 5d5b64dc0c..dabc1f5f45 100644
--- a/modules/httplib/url.go
+++ b/modules/httplib/url.go
@@ -70,11 +70,16 @@ func GuessCurrentHostURL(ctx context.Context) string {
// 1. The reverse proxy is configured correctly, it passes "X-Forwarded-Proto/Host" headers. Perfect, Gitea can handle it correctly.
// 2. The reverse proxy is not configured correctly, doesn't pass "X-Forwarded-Proto/Host" headers, eg: only one "proxy_pass http://gitea:3000" in Nginx.
// 3. There is no reverse proxy.
- // Without an extra config option, Gitea is impossible to distinguish between case 2 and case 3,
- // then case 2 would result in wrong guess like guessed AppURL becomes "http://gitea:3000/", which is not accessible by end users.
- // So in the future maybe it should introduce a new config option, to let site admin decide how to guess the AppURL.
+ // Without more information, Gitea is impossible to distinguish between case 2 and case 3, then case 2 would result in
+ // wrong guess like guessed AppURL becomes "http://gitea:3000/" behind a "https" reverse proxy, which is not accessible by end users.
+ // So we introduced "UseHostHeader" option, it could be enabled by setting "ROOT_URL" to empty
reqScheme := getRequestScheme(req)
if reqScheme == "" {
+ // if no reverse proxy header, try to use "Host" header for absolute URL
+ if setting.UseHostHeader && req.Host != "" {
+ return util.Iif(req.TLS == nil, "http://", "https://") + req.Host
+ }
+ // fall back to default AppURL
return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
}
// X-Forwarded-Host has many problems: non-standard, not well-defined (X-Forwarded-Port or not), conflicts with Host header.
diff --git a/modules/httplib/url_test.go b/modules/httplib/url_test.go
index d57653646b..0e198d7d73 100644
--- a/modules/httplib/url_test.go
+++ b/modules/httplib/url_test.go
@@ -5,6 +5,7 @@ package httplib
import (
"context"
+ "crypto/tls"
"net/http"
"testing"
@@ -39,6 +40,25 @@ func TestIsRelativeURL(t *testing.T) {
}
}
+func TestGuessCurrentHostURL(t *testing.T) {
+ defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")()
+ defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
+ defer test.MockVariableValue(&setting.UseHostHeader, false)()
+
+ ctx := t.Context()
+ assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx))
+
+ ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "localhost:3000"})
+ assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx))
+
+ defer test.MockVariableValue(&setting.UseHostHeader, true)()
+ ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "http-host:3000"})
+ assert.Equal(t, "http://http-host:3000", GuessCurrentHostURL(ctx))
+
+ ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "http-host", TLS: &tls.ConnectionState{}})
+ assert.Equal(t, "https://http-host", GuessCurrentHostURL(ctx))
+}
+
func TestMakeAbsoluteURL(t *testing.T) {
defer test.MockVariableValue(&setting.Protocol, "http")()
defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")()
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 0e074cbcfa..7c3bd93699 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -71,7 +71,8 @@ var globalVars = sync.OnceValue(func() *globalVarsType {
// it is still accepted by the CommonMark specification, as well as the HTML5 spec:
// http://spec.commonmark.org/0.28/#email-address
// https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
- v.emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))")
+ // At the moment, we use stricter rule for rendering purpose: only allow the "name" part starting after the word boundary
+ v.emailRegex = regexp.MustCompile(`\b([-\w.!#$%&'*+/=?^{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)\b`)
// emojiShortCodeRegex find emoji by alias like :smile:
v.emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`)
diff --git a/modules/markup/html_email.go b/modules/markup/html_email.go
index cbfae8b829..cf18e99d98 100644
--- a/modules/markup/html_email.go
+++ b/modules/markup/html_email.go
@@ -3,7 +3,11 @@
package markup
-import "golang.org/x/net/html"
+import (
+ "strings"
+
+ "golang.org/x/net/html"
+)
// emailAddressProcessor replaces raw email addresses with a mailto: link.
func emailAddressProcessor(ctx *RenderContext, node *html.Node) {
@@ -14,6 +18,14 @@ func emailAddressProcessor(ctx *RenderContext, node *html.Node) {
return
}
+ var nextByte byte
+ if len(node.Data) > m[3] {
+ nextByte = node.Data[m[3]]
+ }
+ if strings.IndexByte(":/", nextByte) != -1 {
+ // for cases: "git@gitea.com:owner/repo.git", "https://git@gitea.com/owner/repo.git"
+ return
+ }
mail := node.Data[m[2]:m[3]]
replaceContent(node, m[2], m[3], createLink(ctx, "mailto:"+mail, mail, "" /*mailto*/))
node = node.NextSibling.NextSibling
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index aab9fddd91..58f71bdd7b 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -225,10 +225,10 @@ func TestRender_email(t *testing.T) {
test := func(input, expected string) {
res, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
assert.NoError(t, err)
- assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res))
+ assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res), "input: %s", input)
}
- // Text that should be turned into email link
+ // Text that should be turned into email link
test(
"info@gitea.com",
`
info@gitea.com
`)
@@ -260,28 +260,48 @@ func TestRender_email(t *testing.T) {
j.doe@example.com?
j.doe@example.com!`)
+ // match GitHub behavior
+ test("email@domain@domain.com", `email@domain@domain.com
`)
+
+ // match GitHub behavior
+ test(`"info@gitea.com"`, `"info@gitea.com"
`)
+
// Test that should *not* be turned into email links
- test(
- "\"info@gitea.com\"",
- `"info@gitea.com"
`)
test(
"/home/gitea/mailstore/info@gitea/com",
`/home/gitea/mailstore/info@gitea/com
`)
test(
"git@try.gitea.io:go-gitea/gitea.git",
`git@try.gitea.io:go-gitea/gitea.git
`)
+ test(
+ "https://foo:bar@gitea.io",
+ `https://foo:bar@gitea.io
`)
test(
"gitea@3",
`gitea@3
`)
test(
"gitea@gmail.c",
`gitea@gmail.c
`)
- test(
- "email@domain@domain.com",
- `email@domain@domain.com
`)
test(
"email@domain..com",
`email@domain..com
`)
+
+ cases := []struct {
+ input, expected string
+ }{
+ // match GitHub behavior
+ {"?a@d.zz", `?a@d.zz
`},
+ {"*a@d.zz", `*a@d.zz
`},
+ {"~a@d.zz", `~a@d.zz
`},
+
+ // the following cases don't match GitHub behavior, but they are valid email addresses ...
+ // maybe we should reduce the candidate characters for the "name" part in the future
+ {"a*a@d.zz", `a*a@d.zz
`},
+ {"a~a@d.zz", `a~a@d.zz
`},
+ }
+ for _, c := range cases {
+ test(c.input, c.expected)
+ }
}
func TestRender_emoji(t *testing.T) {
diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index 0d7180c6b1..79df547c2c 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -86,20 +86,15 @@ func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.C
preClasses += " is-loading"
}
- err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, ``, preClasses)
- if err != nil {
- return
- }
-
// include language-x class as part of commonmark spec, "chroma" class is used to highlight the code
// the "display" class is used by "js/markup/math.ts" to render the code element as a block
// the "math.ts" strictly depends on the structure: ...
- err = r.ctx.RenderInternal.FormatWithSafeAttrs(w, ``, languageStr)
+ err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `")
if err != nil {
return
}
diff --git a/modules/setting/server.go b/modules/setting/server.go
index ca635c8abe..41b0ca8959 100644
--- a/modules/setting/server.go
+++ b/modules/setting/server.go
@@ -46,25 +46,37 @@ var (
// AppURL is the Application ROOT_URL. It always has a '/' suffix
// It maps to ini:"ROOT_URL"
AppURL string
- // AppSubURL represents the sub-url mounting point for gitea. It is either "" or starts with '/' and ends without '/', such as '/{subpath}'.
+
+ // AppSubURL represents the sub-url mounting point for gitea, parsed from "ROOT_URL"
+ // It is either "" or starts with '/' and ends without '/', such as '/{sub-path}'.
// This value is empty if site does not have sub-url.
AppSubURL string
- // UseSubURLPath makes Gitea handle requests with sub-path like "/sub-path/owner/repo/...", to make it easier to debug sub-path related problems without a reverse proxy.
+
+ // UseSubURLPath makes Gitea handle requests with sub-path like "/sub-path/owner/repo/...",
+ // to make it easier to debug sub-path related problems without a reverse proxy.
UseSubURLPath bool
+
+ // UseHostHeader makes Gitea prefer to use the "Host" request header for construction of absolute URLs.
+ UseHostHeader bool
+
// AppDataPath is the default path for storing data.
// It maps to ini:"APP_DATA_PATH" in [server] and defaults to AppWorkPath + "/data"
AppDataPath string
+
// LocalURL is the url for locally running applications to contact Gitea. It always has a '/' suffix
// It maps to ini:"LOCAL_ROOT_URL" in [server]
LocalURL string
- // AssetVersion holds a opaque value that is used for cache-busting assets
+
+ // AssetVersion holds an opaque value that is used for cache-busting assets
AssetVersion string
- appTempPathInternal string // the temporary path for the app, it is only an internal variable, do not use it, always use AppDataTempDir
+ // appTempPathInternal is the temporary path for the app, it is only an internal variable
+ // DO NOT use it directly, always use AppDataTempDir
+ appTempPathInternal string
Protocol Scheme
- UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"`
- ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"`
+ UseProxyProtocol bool
+ ProxyProtocolTLSBridging bool
ProxyProtocolHeaderTimeout time.Duration
ProxyProtocolAcceptUnknown bool
Domain string
@@ -181,13 +193,14 @@ func loadServerFrom(rootCfg ConfigProvider) {
EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false)
}
- Protocol = HTTP
protocolCfg := sec.Key("PROTOCOL").String()
if protocolCfg != "https" && EnableAcme {
log.Fatal("ACME could only be used with HTTPS protocol")
}
switch protocolCfg {
+ case "", "http":
+ Protocol = HTTP
case "https":
Protocol = HTTPS
if EnableAcme {
@@ -243,7 +256,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
case "unix":
log.Warn("unix PROTOCOL value is deprecated, please use http+unix")
fallthrough
- case "http+unix":
+ default: // "http+unix"
Protocol = HTTPUnix
}
UnixSocketPermissionRaw := sec.Key("UNIX_SOCKET_PERMISSION").MustString("666")
@@ -256,6 +269,8 @@ func loadServerFrom(rootCfg ConfigProvider) {
if !filepath.IsAbs(HTTPAddr) {
HTTPAddr = filepath.Join(AppWorkPath, HTTPAddr)
}
+ default:
+ log.Fatal("Invalid PROTOCOL %q", Protocol)
}
UseProxyProtocol = sec.Key("USE_PROXY_PROTOCOL").MustBool(false)
ProxyProtocolTLSBridging = sec.Key("PROXY_PROTOCOL_TLS_BRIDGING").MustBool(false)
@@ -268,12 +283,16 @@ func loadServerFrom(rootCfg ConfigProvider) {
PerWritePerKbTimeout = sec.Key("PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout)
defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort
- AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL)
+ AppURL = sec.Key("ROOT_URL").String()
+ if AppURL == "" {
+ UseHostHeader = true
+ AppURL = defaultAppURL
+ }
// Check validity of AppURL
appURL, err := url.Parse(AppURL)
if err != nil {
- log.Fatal("Invalid ROOT_URL '%s': %s", AppURL, err)
+ log.Fatal("Invalid ROOT_URL %q: %s", AppURL, err)
}
// Remove default ports from AppURL.
// (scheme-based URL normalization, RFC 3986 section 6.2.3)
@@ -309,13 +328,15 @@ func loadServerFrom(rootCfg ConfigProvider) {
defaultLocalURL = AppURL
case FCGIUnix:
defaultLocalURL = AppURL
- default:
+ case HTTP, HTTPS:
defaultLocalURL = string(Protocol) + "://"
if HTTPAddr == "0.0.0.0" {
defaultLocalURL += net.JoinHostPort("localhost", HTTPPort) + "/"
} else {
defaultLocalURL += net.JoinHostPort(HTTPAddr, HTTPPort) + "/"
}
+ default:
+ log.Fatal("Invalid PROTOCOL %q", Protocol)
}
LocalURL = sec.Key("LOCAL_ROOT_URL").MustString(defaultLocalURL)
LocalURL = strings.TrimRight(LocalURL, "/") + "/"
diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go
index 22409b4aff..75f8e188dd 100644
--- a/modules/structs/repo_actions.go
+++ b/modules/structs/repo_actions.go
@@ -133,3 +133,26 @@ type ActionWorkflowJob struct {
// swagger:strfmt date-time
CompletedAt time.Time `json:"completed_at,omitempty"`
}
+
+// ActionRunnerLabel represents a Runner Label
+type ActionRunnerLabel struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+}
+
+// ActionRunner represents a Runner
+type ActionRunner struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+ Status string `json:"status"`
+ Busy bool `json:"busy"`
+ Ephemeral bool `json:"ephemeral"`
+ Labels []*ActionRunnerLabel `json:"labels"`
+}
+
+// ActionRunnersResponse returns Runners
+type ActionRunnersResponse struct {
+ Entries []*ActionRunner `json:"runners"`
+ TotalCount int64 `json:"total_count"`
+}
diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index e98c453744..58069f8613 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -117,6 +117,7 @@ files=ファイル
error=エラー
error404=アクセスしようとしたページは存在しないか、閲覧が許可されていません。
+error503=サーバーはリクエストを完了できませんでした。 後でもう一度お試しください。
go_back=戻る
invalid_data=無効なデータ: %v
@@ -730,6 +731,8 @@ public_profile=公開プロフィール
biography_placeholder=自己紹介してください!(Markdownを使うことができます)
location_placeholder=おおよその場所を他の人と共有
profile_desc=あなたのプロフィールが他のユーザーにどのように表示されるかを制御します。あなたのプライマリメールアドレスは、通知、パスワードの回復、WebベースのGit操作に使用されます。
+password_username_disabled=ユーザー名の変更は許可されていません。詳細はサイト管理者にお問い合わせください。
+password_full_name_disabled=フルネームの変更は許可されていません。詳細はサイト管理者にお問い合わせください。
full_name=フルネーム
website=Webサイト
location=場所
@@ -924,6 +927,9 @@ permission_not_set=設定なし
permission_no_access=アクセス不可
permission_read=読み取り
permission_write=読み取りと書き込み
+permission_anonymous_read=匿名の読み込み
+permission_everyone_read=全員の読み込み
+permission_everyone_write=全員の書き込み
access_token_desc=選択したトークン権限に応じて、関連するAPIルートのみに許可が制限されます。 詳細はドキュメントを参照してください。
at_least_one_permission=トークンを作成するには、少なくともひとつの許可を選択する必要があります
permissions_list=許可:
@@ -1136,6 +1142,7 @@ transfer.no_permission_to_reject=この移転を拒否する権限がありま
desc.private=プライベート
desc.public=公開
+desc.public_access=公開アクセス
desc.template=テンプレート
desc.internal=内部
desc.archived=アーカイブ
@@ -1544,6 +1551,7 @@ issues.filter_project=プロジェクト
issues.filter_project_all=すべてのプロジェクト
issues.filter_project_none=プロジェクトなし
issues.filter_assignee=担当者
+issues.filter_assignee_no_assignee=担当者なし
issues.filter_assignee_any_assignee=担当者あり
issues.filter_poster=作成者
issues.filter_user_placeholder=ユーザーを検索
@@ -1647,6 +1655,8 @@ issues.label_archived_filter=アーカイブされたラベルを表示
issues.label_archive_tooltip=アーカイブされたラベルは、ラベルによる検索時のサジェストからデフォルトで除外されます。
issues.label_exclusive_desc=ラベル名を スコープ/アイテム
の形にすることで、他の スコープ/
ラベルと排他的になります。
issues.label_exclusive_warning=イシューやプルリクエストのラベル編集では、競合するスコープ付きラベルは解除されます。
+issues.label_exclusive_order=ソート順
+issues.label_exclusive_order_tooltip=同じスコープ内の排他的なラベルは、この数値順にソートされます。
issues.label_count=ラベル %d件
issues.label_open_issues=オープン中のイシュー %d件
issues.label_edit=編集
@@ -2129,6 +2139,12 @@ contributors.contribution_type.deletions=削除
settings=設定
settings.desc=設定では、リポジトリの設定を管理することができます。
settings.options=リポジトリ
+settings.public_access=公開アクセス
+settings.public_access_desc=外部からの訪問者のアクセス権限について、このリポジトリのデフォルト設定を上書きします。
+settings.public_access.docs.not_set=設定なし: 公開アクセス権限はありません。訪問者の権限は、リポジトリの公開範囲とメンバーの権限に従います。
+settings.public_access.docs.anonymous_read=匿名の読み込み: ログインしていないユーザーは読み取り権限でユニットにアクセスできます。
+settings.public_access.docs.everyone_read=全員の読み込み: すべてのログインユーザーは読み取り権限でユニットにアクセスできます。イシュー/プルリクエストユニットの読み取り権限は、ユーザーが新しいイシュー/プルリクエストを作成できることを意味します。
+settings.public_access.docs.everyone_write=全員の書き込み: すべてのログインユーザーに書き込み権限があります。Wikiユニットのみがこの権限をサポートします。
settings.collaboration=共同作業者
settings.collaboration.admin=管理者
settings.collaboration.write=書き込み
@@ -2719,6 +2735,7 @@ branch.restore_success=ブランチ "%s" を復元しました。
branch.restore_failed=ブランチ "%s" の復元に失敗しました。
branch.protected_deletion_failed=ブランチ "%s" は保護されています。 削除できません。
branch.default_deletion_failed=ブランチ "%s" はデフォルトブランチです。 削除できません。
+branch.default_branch_not_exist=デフォルトブランチ "%s" がありません。
branch.restore=ブランチ "%s" の復元
branch.download=ブランチ "%s" をダウンロード
branch.rename=ブランチ名 "%s" を変更
diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go
index 4d7fb8b1a6..47439c4c3b 100644
--- a/routers/api/packages/swift/swift.go
+++ b/routers/api/packages/swift/swift.go
@@ -290,7 +290,24 @@ func DownloadManifest(ctx *context.Context) {
})
}
-// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6
+// formFileOptionalReadCloser returns (nil, nil) if the formKey is not present.
+func formFileOptionalReadCloser(ctx *context.Context, formKey string) (io.ReadCloser, error) {
+ multipartFile, _, err := ctx.Req.FormFile(formKey)
+ if err != nil && !errors.Is(err, http.ErrMissingFile) {
+ return nil, err
+ }
+ if multipartFile != nil {
+ return multipartFile, nil
+ }
+
+ content := ctx.Req.FormValue(formKey)
+ if content == "" {
+ return nil, nil
+ }
+ return io.NopCloser(strings.NewReader(content)), nil
+}
+
+// UploadPackageFile refers to https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6
func UploadPackageFile(ctx *context.Context) {
packageScope := ctx.PathParam("scope")
packageName := ctx.PathParam("name")
@@ -304,9 +321,9 @@ func UploadPackageFile(ctx *context.Context) {
packageVersion := v.Core().String()
- file, _, err := ctx.Req.FormFile("source-archive")
- if err != nil {
- apiError(ctx, http.StatusBadRequest, err)
+ file, err := formFileOptionalReadCloser(ctx, "source-archive")
+ if file == nil || err != nil {
+ apiError(ctx, http.StatusBadRequest, "unable to read source-archive file")
return
}
defer file.Close()
@@ -318,10 +335,13 @@ func UploadPackageFile(ctx *context.Context) {
}
defer buf.Close()
- var mr io.Reader
- metadata := ctx.Req.FormValue("metadata")
- if metadata != "" {
- mr = strings.NewReader(metadata)
+ mr, err := formFileOptionalReadCloser(ctx, "metadata")
+ if err != nil {
+ apiError(ctx, http.StatusBadRequest, "unable to read metadata file")
+ return
+ }
+ if mr != nil {
+ defer mr.Close()
}
pck, err := swift_module.ParsePackage(buf, buf.Size(), mr)
diff --git a/routers/api/v1/admin/runners.go b/routers/api/v1/admin/runners.go
index 329242d9f6..736c421229 100644
--- a/routers/api/v1/admin/runners.go
+++ b/routers/api/v1/admin/runners.go
@@ -24,3 +24,81 @@ func GetRegistrationToken(ctx *context.APIContext) {
shared.GetRegistrationToken(ctx, 0, 0)
}
+
+// CreateRegistrationToken returns the token to register global runners
+func CreateRegistrationToken(ctx *context.APIContext) {
+ // swagger:operation POST /admin/actions/runners/registration-token admin adminCreateRunnerRegistrationToken
+ // ---
+ // summary: Get an global actions runner registration token
+ // produces:
+ // - application/json
+ // parameters:
+ // responses:
+ // "200":
+ // "$ref": "#/responses/RegistrationToken"
+
+ shared.GetRegistrationToken(ctx, 0, 0)
+}
+
+// ListRunners get all runners
+func ListRunners(ctx *context.APIContext) {
+ // swagger:operation GET /admin/actions/runners admin getAdminRunners
+ // ---
+ // summary: Get all runners
+ // produces:
+ // - application/json
+ // responses:
+ // "200":
+ // "$ref": "#/definitions/ActionRunnersResponse"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ shared.ListRunners(ctx, 0, 0)
+}
+
+// GetRunner get an global runner
+func GetRunner(ctx *context.APIContext) {
+ // swagger:operation GET /admin/actions/runners/{runner_id} admin getAdminRunner
+ // ---
+ // summary: Get an global runner
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: runner_id
+ // in: path
+ // description: id of the runner
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/definitions/ActionRunner"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ shared.GetRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id"))
+}
+
+// DeleteRunner delete an global runner
+func DeleteRunner(ctx *context.APIContext) {
+ // swagger:operation DELETE /admin/actions/runners/{runner_id} admin deleteAdminRunner
+ // ---
+ // summary: Delete an global runner
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: runner_id
+ // in: path
+ // description: id of the runner
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // description: runner has been deleted
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ shared.DeleteRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id"))
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index b9b590725b..e77118f4ff 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -912,7 +912,11 @@ func Routes() *web.Router {
})
m.Group("/runners", func() {
+ m.Get("", reqToken(), reqChecker, act.ListRunners)
m.Get("/registration-token", reqToken(), reqChecker, act.GetRegistrationToken)
+ m.Post("/registration-token", reqToken(), reqChecker, act.CreateRegistrationToken)
+ m.Get("/{runner_id}", reqToken(), reqChecker, act.GetRunner)
+ m.Delete("/{runner_id}", reqToken(), reqChecker, act.DeleteRunner)
})
})
}
@@ -1043,7 +1047,11 @@ func Routes() *web.Router {
})
m.Group("/runners", func() {
+ m.Get("", reqToken(), user.ListRunners)
m.Get("/registration-token", reqToken(), user.GetRegistrationToken)
+ m.Post("/registration-token", reqToken(), user.CreateRegistrationToken)
+ m.Get("/{runner_id}", reqToken(), user.GetRunner)
+ m.Delete("/{runner_id}", reqToken(), user.DeleteRunner)
})
})
@@ -1689,6 +1697,12 @@ func Routes() *web.Router {
Patch(bind(api.EditHookOption{}), admin.EditHook).
Delete(admin.DeleteHook)
})
+ m.Group("/actions/runners", func() {
+ m.Get("", admin.ListRunners)
+ m.Post("/registration-token", admin.CreateRegistrationToken)
+ m.Get("/{runner_id}", admin.GetRunner)
+ m.Delete("/{runner_id}", admin.DeleteRunner)
+ })
m.Group("/runners", func() {
m.Get("/registration-token", admin.GetRegistrationToken)
})
diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go
index b1cd2f0c3c..700a5ef8ea 100644
--- a/routers/api/v1/org/action.go
+++ b/routers/api/v1/org/action.go
@@ -190,6 +190,27 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) {
shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0)
}
+// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
+// CreateRegistrationToken returns the token to register org runners
+func (Action) CreateRegistrationToken(ctx *context.APIContext) {
+ // swagger:operation POST /orgs/{org}/actions/runners/registration-token organization orgCreateRunnerRegistrationToken
+ // ---
+ // summary: Get an organization's actions runner registration token
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/RegistrationToken"
+
+ shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0)
+}
+
// ListVariables list org-level variables
func (Action) ListVariables(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList
@@ -470,6 +491,85 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
+// ListRunners get org-level runners
+func (Action) ListRunners(ctx *context.APIContext) {
+ // swagger:operation GET /orgs/{org}/actions/runners organization getOrgRunners
+ // ---
+ // summary: Get org-level runners
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/definitions/ActionRunnersResponse"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ shared.ListRunners(ctx, ctx.Org.Organization.ID, 0)
+}
+
+// GetRunner get an org-level runner
+func (Action) GetRunner(ctx *context.APIContext) {
+ // swagger:operation GET /orgs/{org}/actions/runners/{runner_id} organization getOrgRunner
+ // ---
+ // summary: Get an org-level runner
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: runner_id
+ // in: path
+ // description: id of the runner
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/definitions/ActionRunner"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ shared.GetRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id"))
+}
+
+// DeleteRunner delete an org-level runner
+func (Action) DeleteRunner(ctx *context.APIContext) {
+ // swagger:operation DELETE /orgs/{org}/actions/runners/{runner_id} organization deleteOrgRunner
+ // ---
+ // summary: Delete an org-level runner
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: runner_id
+ // in: path
+ // description: id of the runner
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // description: runner has been deleted
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ shared.DeleteRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id"))
+}
+
var _ actions_service.API = new(Action)
// Action implements actions_service.API
diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go
index ed2017a372..6aef529f98 100644
--- a/routers/api/v1/repo/action.go
+++ b/routers/api/v1/repo/action.go
@@ -183,7 +183,7 @@ func (Action) DeleteSecret(ctx *context.APIContext) {
// required: true
// responses:
// "204":
- // description: delete one secret of the organization
+ // description: delete one secret of the repository
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -531,6 +531,125 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) {
shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID)
}
+// CreateRegistrationToken returns the token to register repo runners
+func (Action) CreateRegistrationToken(ctx *context.APIContext) {
+ // swagger:operation POST /repos/{owner}/{repo}/actions/runners/registration-token repository repoCreateRunnerRegistrationToken
+ // ---
+ // summary: Get a repository's actions runner registration token
+ // 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
+ // responses:
+ // "200":
+ // "$ref": "#/responses/RegistrationToken"
+
+ shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID)
+}
+
+// ListRunners get repo-level runners
+func (Action) ListRunners(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/actions/runners repository getRepoRunners
+ // ---
+ // summary: Get repo-level runners
+ // 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
+ // responses:
+ // "200":
+ // "$ref": "#/definitions/ActionRunnersResponse"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ shared.ListRunners(ctx, 0, ctx.Repo.Repository.ID)
+}
+
+// GetRunner get an repo-level runner
+func (Action) GetRunner(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/actions/runners/{runner_id} repository getRepoRunner
+ // ---
+ // summary: Get an repo-level runner
+ // 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: runner_id
+ // in: path
+ // description: id of the runner
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/definitions/ActionRunner"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ shared.GetRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
+}
+
+// DeleteRunner delete an repo-level runner
+func (Action) DeleteRunner(ctx *context.APIContext) {
+ // swagger:operation DELETE /repos/{owner}/{repo}/actions/runners/{runner_id} repository deleteRepoRunner
+ // ---
+ // summary: Delete an repo-level runner
+ // 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: runner_id
+ // in: path
+ // description: id of the runner
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // description: runner has been deleted
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ shared.DeleteRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
+}
+
var _ actions_service.API = new(Action)
// Action implements actions_service.API
diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go
index 67dd6c913d..d5840b4149 100644
--- a/routers/api/v1/repo/wiki.go
+++ b/routers/api/v1/repo/wiki.go
@@ -193,7 +193,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi
}
// get commit count - wiki revisions
- commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
+ commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
// Get last change information.
lastCommit, err := wikiRepo.GetCommitByPath(pageFilename)
@@ -432,7 +432,7 @@ func ListPageRevisions(ctx *context.APIContext) {
}
// get commit count - wiki revisions
- commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
+ commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
page := ctx.FormInt("page")
if page <= 1 {
@@ -442,7 +442,7 @@ func ListPageRevisions(ctx *context.APIContext) {
// get Commit Count
commitsHistory, err := wikiRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
- Revision: "master",
+ Revision: ctx.Repo.Repository.DefaultWikiBranch,
File: pageFilename,
Page: page,
})
@@ -486,7 +486,7 @@ func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit)
return nil, nil
}
- commit, err := wikiRepo.GetBranchCommit("master")
+ commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
if err != nil {
if git.IsErrNotExist(err) {
ctx.APIErrorNotFound(err)
diff --git a/routers/api/v1/shared/runners.go b/routers/api/v1/shared/runners.go
index f31d9e5d0b..d42f330d1c 100644
--- a/routers/api/v1/shared/runners.go
+++ b/routers/api/v1/shared/runners.go
@@ -8,8 +8,13 @@ import (
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/convert"
)
// RegistrationToken is response related to registration token
@@ -30,3 +35,84 @@ func GetRegistrationToken(ctx *context.APIContext, ownerID, repoID int64) {
ctx.JSON(http.StatusOK, RegistrationToken{Token: token.Token})
}
+
+// ListRunners lists runners for api route validated ownerID and repoID
+// ownerID == 0 and repoID == 0 means all runners including global runners, does not appear in sql where clause
+// ownerID == 0 and repoID != 0 means all runners for the given repo
+// ownerID != 0 and repoID == 0 means all runners for the given user/org
+// ownerID != 0 and repoID != 0 undefined behavior
+// Access rights are checked at the API route level
+func ListRunners(ctx *context.APIContext, ownerID, repoID int64) {
+ if ownerID != 0 && repoID != 0 {
+ setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
+ }
+ runners, total, err := db.FindAndCount[actions_model.ActionRunner](ctx, &actions_model.FindRunnerOptions{
+ OwnerID: ownerID,
+ RepoID: repoID,
+ ListOptions: utils.GetListOptions(ctx),
+ })
+ if err != nil {
+ ctx.APIErrorInternal(err)
+ return
+ }
+
+ res := new(api.ActionRunnersResponse)
+ res.TotalCount = total
+
+ res.Entries = make([]*api.ActionRunner, len(runners))
+ for i, runner := range runners {
+ res.Entries[i] = convert.ToActionRunner(ctx, runner)
+ }
+
+ ctx.JSON(http.StatusOK, &res)
+}
+
+// GetRunner get the runner for api route validated ownerID and repoID
+// ownerID == 0 and repoID == 0 means any runner including global runners
+// ownerID == 0 and repoID != 0 means any runner for the given repo
+// ownerID != 0 and repoID == 0 means any runner for the given user/org
+// ownerID != 0 and repoID != 0 undefined behavior
+// Access rights are checked at the API route level
+func GetRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
+ if ownerID != 0 && repoID != 0 {
+ setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
+ }
+ runner, err := actions_model.GetRunnerByID(ctx, runnerID)
+ if err != nil {
+ ctx.APIErrorNotFound(err)
+ return
+ }
+ if !runner.EditableInContext(ownerID, repoID) {
+ ctx.APIErrorNotFound("No permission to get this runner")
+ return
+ }
+ ctx.JSON(http.StatusOK, convert.ToActionRunner(ctx, runner))
+}
+
+// DeleteRunner deletes the runner for api route validated ownerID and repoID
+// ownerID == 0 and repoID == 0 means any runner including global runners
+// ownerID == 0 and repoID != 0 means any runner for the given repo
+// ownerID != 0 and repoID == 0 means any runner for the given user/org
+// ownerID != 0 and repoID != 0 undefined behavior
+// Access rights are checked at the API route level
+func DeleteRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
+ if ownerID != 0 && repoID != 0 {
+ setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
+ }
+ runner, err := actions_model.GetRunnerByID(ctx, runnerID)
+ if err != nil {
+ ctx.APIErrorInternal(err)
+ return
+ }
+ if !runner.EditableInContext(ownerID, repoID) {
+ ctx.APIErrorNotFound("No permission to delete this runner")
+ return
+ }
+
+ err = actions_model.DeleteRunner(ctx, runner.ID)
+ if err != nil {
+ ctx.APIErrorInternal(err)
+ return
+ }
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go
index 25f137f3bf..df0c8a805a 100644
--- a/routers/api/v1/swagger/repo.go
+++ b/routers/api/v1/swagger/repo.go
@@ -457,6 +457,20 @@ type swaggerRepoArtifact struct {
Body api.ActionArtifact `json:"body"`
}
+// RunnerList
+// swagger:response RunnerList
+type swaggerRunnerList struct {
+ // in:body
+ Body api.ActionRunnersResponse `json:"body"`
+}
+
+// Runner
+// swagger:response Runner
+type swaggerRunner struct {
+ // in:body
+ Body api.ActionRunner `json:"body"`
+}
+
// swagger:response Compare
type swaggerCompare struct {
// in:body
diff --git a/routers/api/v1/user/runners.go b/routers/api/v1/user/runners.go
index 899218473e..be3f63cc5e 100644
--- a/routers/api/v1/user/runners.go
+++ b/routers/api/v1/user/runners.go
@@ -24,3 +24,81 @@ func GetRegistrationToken(ctx *context.APIContext) {
shared.GetRegistrationToken(ctx, ctx.Doer.ID, 0)
}
+
+// CreateRegistrationToken returns the token to register user runners
+func CreateRegistrationToken(ctx *context.APIContext) {
+ // swagger:operation POST /user/actions/runners/registration-token user userCreateRunnerRegistrationToken
+ // ---
+ // summary: Get an user's actions runner registration token
+ // produces:
+ // - application/json
+ // parameters:
+ // responses:
+ // "200":
+ // "$ref": "#/responses/RegistrationToken"
+
+ shared.GetRegistrationToken(ctx, ctx.Doer.ID, 0)
+}
+
+// ListRunners get user-level runners
+func ListRunners(ctx *context.APIContext) {
+ // swagger:operation GET /user/actions/runners user getUserRunners
+ // ---
+ // summary: Get user-level runners
+ // produces:
+ // - application/json
+ // responses:
+ // "200":
+ // "$ref": "#/definitions/ActionRunnersResponse"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ shared.ListRunners(ctx, ctx.Doer.ID, 0)
+}
+
+// GetRunner get an user-level runner
+func GetRunner(ctx *context.APIContext) {
+ // swagger:operation GET /user/actions/runners/{runner_id} user getUserRunner
+ // ---
+ // summary: Get an user-level runner
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: runner_id
+ // in: path
+ // description: id of the runner
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/definitions/ActionRunner"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ shared.GetRunner(ctx, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id"))
+}
+
+// DeleteRunner delete an user-level runner
+func DeleteRunner(ctx *context.APIContext) {
+ // swagger:operation DELETE /user/actions/runners/{runner_id} user deleteUserRunner
+ // ---
+ // summary: Delete an user-level runner
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: runner_id
+ // in: path
+ // description: id of the runner
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // description: runner has been deleted
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ shared.DeleteRunner(ctx, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id"))
+}
diff --git a/routers/web/admin/admin_test.go b/routers/web/admin/admin_test.go
index a568c7c5c8..04fad4663c 100644
--- a/routers/web/admin/admin_test.go
+++ b/routers/web/admin/admin_test.go
@@ -76,6 +76,7 @@ func TestShadowPassword(t *testing.T) {
func TestSelfCheckPost(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")()
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
+ defer test.MockVariableValue(&setting.UseHostHeader, false)()
ctx, resp := contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend")
SelfCheckPost(ctx)
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 0124c163f3..a33542fd37 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -181,6 +181,7 @@ func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) {
// GetPullDiffStats get Pull Requests diff stats
func GetPullDiffStats(ctx *context.Context) {
+ // FIXME: this getPullInfo seems to be a duplicate call with other route handlers
issue, ok := getPullInfo(ctx)
if !ok {
return
@@ -188,21 +189,19 @@ func GetPullDiffStats(ctx *context.Context) {
pull := issue.PullRequest
mergeBaseCommitID := GetMergedBaseCommitID(ctx, issue)
-
if mergeBaseCommitID == "" {
- ctx.NotFound(nil)
- return
+ return // no merge base, do nothing, do not stop the route handler, see below
}
+ // do not report 500 server error to end users if error occurs, otherwise a PR missing ref won't be able to view.
headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitRefName())
if err != nil {
- ctx.ServerError("GetRefCommitID", err)
+ log.Error("Failed to GetRefCommitID: %v, repo: %v", err, ctx.Repo.Repository.FullName())
return
}
-
diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, mergeBaseCommitID, headCommitID)
if err != nil {
- ctx.ServerError("GetDiffShortStat", err)
+ log.Error("Failed to GetDiffShortStat: %v, repo: %v", err, ctx.Repo.Repository.FullName())
return
}
diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go
index a87f6ce4dc..a642cfd66d 100644
--- a/routers/web/shared/actions/runners.go
+++ b/routers/web/shared/actions/runners.go
@@ -197,7 +197,7 @@ func RunnersEdit(ctx *context.Context) {
ctx.ServerError("LoadAttributes", err)
return
}
- if !runner.Editable(ownerID, repoID) {
+ if !runner.EditableInContext(ownerID, repoID) {
err = errors.New("no permission to edit this runner")
ctx.NotFound(err)
return
@@ -250,7 +250,7 @@ func RunnersEditPost(ctx *context.Context) {
ctx.ServerError("RunnerDetailsEditPost.GetRunnerByID", err)
return
}
- if !runner.Editable(ownerID, repoID) {
+ if !runner.EditableInContext(ownerID, repoID) {
ctx.NotFound(util.NewPermissionDeniedErrorf("no permission to edit this runner"))
return
}
@@ -304,7 +304,7 @@ func RunnerDeletePost(ctx *context.Context) {
return
}
- if !runner.Editable(rCtx.OwnerID, rCtx.RepoID) {
+ if !runner.EditableInContext(rCtx.OwnerID, rCtx.RepoID) {
ctx.NotFound(util.NewPermissionDeniedErrorf("no permission to delete this runner"))
return
}
diff --git a/services/actions/interface.go b/services/actions/interface.go
index d4fa782fec..b407f5c6c8 100644
--- a/services/actions/interface.go
+++ b/services/actions/interface.go
@@ -25,4 +25,12 @@ type API interface {
UpdateVariable(*context.APIContext)
// GetRegistrationToken get registration token
GetRegistrationToken(*context.APIContext)
+ // CreateRegistrationToken get registration token
+ CreateRegistrationToken(*context.APIContext)
+ // ListRunners list runners
+ ListRunners(*context.APIContext)
+ // GetRunner get a runner
+ GetRunner(*context.APIContext)
+ // DeleteRunner delete runner
+ DeleteRunner(*context.APIContext)
}
diff --git a/services/convert/convert.go b/services/convert/convert.go
index ac2680766c..9d2afdea30 100644
--- a/services/convert/convert.go
+++ b/services/convert/convert.go
@@ -30,6 +30,8 @@ import (
"code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/gitdiff"
+
+ runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
)
// ToEmail convert models.EmailAddress to api.Email
@@ -252,6 +254,30 @@ func ToActionArtifact(repo *repo_model.Repository, art *actions_model.ActionArti
}, nil
}
+func ToActionRunner(ctx context.Context, runner *actions_model.ActionRunner) *api.ActionRunner {
+ status := runner.Status()
+ apiStatus := "offline"
+ if runner.IsOnline() {
+ apiStatus = "online"
+ }
+ labels := make([]*api.ActionRunnerLabel, len(runner.AgentLabels))
+ for i, label := range runner.AgentLabels {
+ labels[i] = &api.ActionRunnerLabel{
+ ID: int64(i),
+ Name: label,
+ Type: "custom",
+ }
+ }
+ return &api.ActionRunner{
+ ID: runner.ID,
+ Name: runner.Name,
+ Status: apiStatus,
+ Busy: status == runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE,
+ Ephemeral: runner.Ephemeral,
+ Labels: labels,
+ }
+}
+
// ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification {
verif := asymkey_service.ParseCommitWithSignature(ctx, c)
diff --git a/services/feed/feed.go b/services/feed/feed.go
index 38a4e25308..1dbd2e0e26 100644
--- a/services/feed/feed.go
+++ b/services/feed/feed.go
@@ -6,7 +6,7 @@ package feed
import (
"context"
"fmt"
- "strconv"
+ "strings"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
@@ -14,15 +14,10 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
-func userFeedCacheKey(userID int64) string {
- return fmt.Sprintf("user_feed_%d", userID)
-}
-
func GetFeedsForDashboard(ctx context.Context, opts activities_model.GetFeedsOptions) (activities_model.ActionList, int, error) {
opts.DontCount = opts.RequestedTeam == nil && opts.Date == ""
results, cnt, err := activities_model.GetFeeds(ctx, opts)
@@ -40,7 +35,18 @@ func GetFeeds(ctx context.Context, opts activities_model.GetFeedsOptions) (activ
// * Organization action: UserID=100 (the repo's org), ActUserID=1
// * Watcher action: UserID=20 (a user who is watching a repo), ActUserID=1
func notifyWatchers(ctx context.Context, act *activities_model.Action, watchers []*repo_model.Watch, permCode, permIssue, permPR []bool) error {
- // Add feed for actioner.
+ // MySQL has TEXT length limit 65535.
+ // Sometimes the content is "field1|field2|field3", sometimes the content is JSON (ActionMirrorSyncPush, ActionCommitRepo, ActionPushTag, etc...)
+ if left, right := util.EllipsisDisplayStringX(act.Content, 65535); right != "" {
+ if strings.HasPrefix(act.Content, `{"`) && strings.HasSuffix(act.Content, `}`) {
+ // FIXME: at the moment we can do nothing if the content is JSON and it is too long
+ act.Content = "{}"
+ } else {
+ act.Content = left
+ }
+ }
+
+ // Add feed for actor.
act.UserID = act.ActUserID
if err := db.Insert(ctx, act); err != nil {
return fmt.Errorf("insert new actioner: %w", err)
@@ -76,24 +82,18 @@ func notifyWatchers(ctx context.Context, act *activities_model.Action, watchers
if !permPR[i] {
continue
}
+ default:
}
if err := db.Insert(ctx, act); err != nil {
return fmt.Errorf("insert new action: %w", err)
}
-
- total, err := activities_model.CountUserFeeds(ctx, act.UserID)
- if err != nil {
- return fmt.Errorf("count user feeds: %w", err)
- }
-
- _ = cache.GetCache().Put(userFeedCacheKey(act.UserID), strconv.FormatInt(total, 10), setting.CacheService.TTLSeconds())
}
return nil
}
-// NotifyWatchersActions creates batch of actions for every watcher.
+// NotifyWatchers creates batch of actions for every watcher.
func NotifyWatchers(ctx context.Context, acts ...*activities_model.Action) error {
return db.WithTx(ctx, func(ctx context.Context) error {
if len(acts) == 0 {
diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl
index 5b40268761..7a435cc433 100644
--- a/templates/devtest/gitea-ui.tmpl
+++ b/templates/devtest/gitea-ui.tmpl
@@ -9,16 +9,16 @@
silenced
Button
-