0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-07-21 18:54:39 +02:00

Merge branch 'go-gitea:main' into main

This commit is contained in:
badhezi 2025-04-20 20:33:21 +03:00 committed by GitHub
commit e11a3398b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
90 changed files with 2120 additions and 409 deletions

View File

@ -81,6 +81,10 @@ var microcmdUserCreate = &cli.Command{
Name: "restricted", Name: "restricted",
Usage: "Make a restricted user account", 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, Passwd: password,
MustChangePassword: mustChangePassword, MustChangePassword: mustChangePassword,
Visibility: visibility, Visibility: visibility,
FullName: c.String("fullname"),
} }
overwriteDefault := &user_model.CreateUserOverwriteOptions{ overwriteDefault := &user_model.CreateUserOverwriteOptions{

View File

@ -50,17 +50,17 @@ func TestAdminUserCreate(t *testing.T) {
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false")) assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
}) })
createUser := func(name, args string) error { createUser := func(name string, args ...string) error {
return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args))) return app.Run(append([]string{"./gitea", "admin", "user", "create", "--username", name, "--email", name + "@gitea.local"}, args...))
} }
t.Run("UserType", func(t *testing.T) { t.Run("UserType", func(t *testing.T) {
reset() reset()
assert.ErrorContains(t, createUser("u", "--user-type invalid"), "invalid user type") 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", "--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", "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"}) u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
assert.Equal(t, user_model.UserTypeBot, u.Type) assert.Equal(t, user_model.UserTypeBot, u.Type)
assert.Empty(t, u.Passwd) assert.Empty(t, u.Passwd)
@ -75,7 +75,7 @@ func TestAdminUserCreate(t *testing.T) {
// using "--access-token" only means "all" access // using "--access-token" only means "all" access
reset() 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, &user_model.User{}))
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{})) assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"}) 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 // using "--access-token" with name & scopes
reset() 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, &user_model.User{}))
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{})) assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"}) 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" // using "--access-token-name" without "--access-token"
reset() 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, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{})) 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") 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" // using "--access-token-scopes" without "--access-token"
reset() 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, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{})) 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") assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
// empty permission // empty permission
reset() 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, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{})) assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.ErrorContains(t, err, "access token does not have any permission") 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)
})
} }

View File

@ -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' ;; The protocol the server listens on. One of "http", "https", "http+unix", "fcgi" or "fcgi+unix".
;; Note: Value must be lowercase.
;PROTOCOL = http ;PROTOCOL = http
;; ;;
;; Expect PROXY protocol headers on connections ;; Set the domain for the server.
;USE_PROXY_PROTOCOL = false ;; Most users should set it to the real website domain of their Gitea instance.
;;
;; 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
;DOMAIN = localhost ;DOMAIN = localhost
;; ;;
;; The AppURL used by Gitea to generate absolute links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/". ;; 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 = ;ROOT_URL =
;; ;;
;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy. ;; 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 = ;STATIC_URL_PREFIX =
;; ;;
;; The address to listen on. Either a IPv4/IPv6 address or the path to a unix socket. ;; 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`_. ;; Relative paths will be made absolute against the _`AppWorkPath`_.
;HTTP_ADDR = 0.0.0.0 ;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 ;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 ;; 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 ;; 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 ;; ROOT_URL. Defaults are false for REDIRECT_OTHER_PORT and 80 for

2
go.mod
View File

@ -120,7 +120,7 @@ require (
gitlab.com/gitlab-org/api/client-go v0.126.0 gitlab.com/gitlab-org/api/client-go v0.126.0
golang.org/x/crypto v0.36.0 golang.org/x/crypto v0.36.0
golang.org/x/image v0.25.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/oauth2 v0.28.0
golang.org/x/sync v0.12.0 golang.org/x/sync v0.12.0
golang.org/x/sys v0.31.0 golang.org/x/sys v0.31.0

4
go.sum
View File

@ -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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 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.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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 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 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 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= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/models/shared/types" "code.gitea.io/gitea/models/shared/types"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -123,8 +124,15 @@ func (r *ActionRunner) IsOnline() bool {
return false return false
} }
// Editable checks if the runner is editable by the user // EditableInContext checks if the runner is editable by the "context" owner/repo
func (r *ActionRunner) Editable(ownerID, repoID int64) bool { // 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 { if ownerID == 0 && repoID == 0 {
return true return true
} }
@ -168,6 +176,12 @@ func init() {
db.RegisterModel(&ActionRunner{}) 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 { type FindRunnerOptions struct {
db.ListOptions db.ListOptions
IDs []int64 IDs []int64

View File

@ -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"]'

View File

@ -279,9 +279,7 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) {
default: default:
e.Desc("package_version.created_unix") e.Desc("package_version.created_unix")
} }
e.Desc("package_version.id") // Sort by id for stable order with duplicates in the other field
// Sort by id for stable order with duplicates in the other field
e.Asc("package_version.id")
} }
// SearchVersions gets all versions of packages matching the search options // SearchVersions gets all versions of packages matching the search options

View File

@ -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. // 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. // 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. // 3. There is no reverse proxy.
// Without an extra config option, Gitea is impossible to distinguish between case 2 and case 3, // Without more information, Gitea is impossible to distinguish between case 2 and case 3, then case 2 would result in
// then case 2 would result in wrong guess like guessed AppURL becomes "http://gitea:3000/", which is not accessible by end users. // wrong guess like guessed AppURL becomes "http://gitea:3000/" behind a "https" reverse proxy, 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. // So we introduced "UseHostHeader" option, it could be enabled by setting "ROOT_URL" to empty
reqScheme := getRequestScheme(req) reqScheme := getRequestScheme(req)
if reqScheme == "" { 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+"/") 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. // X-Forwarded-Host has many problems: non-standard, not well-defined (X-Forwarded-Port or not), conflicts with Host header.

View File

@ -5,6 +5,7 @@ package httplib
import ( import (
"context" "context"
"crypto/tls"
"net/http" "net/http"
"testing" "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) { func TestMakeAbsoluteURL(t *testing.T) {
defer test.MockVariableValue(&setting.Protocol, "http")() defer test.MockVariableValue(&setting.Protocol, "http")()
defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")() defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")()

View File

@ -71,7 +71,8 @@ var globalVars = sync.OnceValue(func() *globalVarsType {
// it is still accepted by the CommonMark specification, as well as the HTML5 spec: // it is still accepted by the CommonMark specification, as well as the HTML5 spec:
// http://spec.commonmark.org/0.28/#email-address // http://spec.commonmark.org/0.28/#email-address
// https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail) // 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: // emojiShortCodeRegex find emoji by alias like :smile:
v.emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`) v.emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`)

View File

@ -3,7 +3,11 @@
package markup package markup
import "golang.org/x/net/html" import (
"strings"
"golang.org/x/net/html"
)
// emailAddressProcessor replaces raw email addresses with a mailto: link. // emailAddressProcessor replaces raw email addresses with a mailto: link.
func emailAddressProcessor(ctx *RenderContext, node *html.Node) { func emailAddressProcessor(ctx *RenderContext, node *html.Node) {
@ -14,6 +18,14 @@ func emailAddressProcessor(ctx *RenderContext, node *html.Node) {
return 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]] mail := node.Data[m[2]:m[3]]
replaceContent(node, m[2], m[3], createLink(ctx, "mailto:"+mail, mail, "" /*mailto*/)) replaceContent(node, m[2], m[3], createLink(ctx, "mailto:"+mail, mail, "" /*mailto*/))
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling

View File

@ -225,10 +225,10 @@ func TestRender_email(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
res, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input) res, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
assert.NoError(t, err) 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( test(
"info@gitea.com", "info@gitea.com",
`<p><a href="mailto:info@gitea.com" rel="nofollow">info@gitea.com</a></p>`) `<p><a href="mailto:info@gitea.com" rel="nofollow">info@gitea.com</a></p>`)
@ -260,28 +260,48 @@ func TestRender_email(t *testing.T) {
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>? <a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>?
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>!</p>`) <a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>!</p>`)
// match GitHub behavior
test("email@domain@domain.com", `<p>email@<a href="mailto:domain@domain.com" rel="nofollow">domain@domain.com</a></p>`)
// match GitHub behavior
test(`"info@gitea.com"`, `<p>&#34;<a href="mailto:info@gitea.com" rel="nofollow">info@gitea.com</a>&#34;</p>`)
// Test that should *not* be turned into email links // Test that should *not* be turned into email links
test(
"\"info@gitea.com\"",
`<p>&#34;info@gitea.com&#34;</p>`)
test( test(
"/home/gitea/mailstore/info@gitea/com", "/home/gitea/mailstore/info@gitea/com",
`<p>/home/gitea/mailstore/info@gitea/com</p>`) `<p>/home/gitea/mailstore/info@gitea/com</p>`)
test( test(
"git@try.gitea.io:go-gitea/gitea.git", "git@try.gitea.io:go-gitea/gitea.git",
`<p>git@try.gitea.io:go-gitea/gitea.git</p>`) `<p>git@try.gitea.io:go-gitea/gitea.git</p>`)
test(
"https://foo:bar@gitea.io",
`<p><a href="https://foo:bar@gitea.io" rel="nofollow">https://foo:bar@gitea.io</a></p>`)
test( test(
"gitea@3", "gitea@3",
`<p>gitea@3</p>`) `<p>gitea@3</p>`)
test( test(
"gitea@gmail.c", "gitea@gmail.c",
`<p>gitea@gmail.c</p>`) `<p>gitea@gmail.c</p>`)
test(
"email@domain@domain.com",
`<p>email@domain@domain.com</p>`)
test( test(
"email@domain..com", "email@domain..com",
`<p>email@domain..com</p>`) `<p>email@domain..com</p>`)
cases := []struct {
input, expected string
}{
// match GitHub behavior
{"?a@d.zz", `<p>?<a href="mailto:a@d.zz" rel="nofollow">a@d.zz</a></p>`},
{"*a@d.zz", `<p>*<a href="mailto:a@d.zz" rel="nofollow">a@d.zz</a></p>`},
{"~a@d.zz", `<p>~<a href="mailto:a@d.zz" rel="nofollow">a@d.zz</a></p>`},
// 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", `<p><a href="mailto:a*a@d.zz" rel="nofollow">a*a@d.zz</a></p>`},
{"a~a@d.zz", `<p><a href="mailto:a~a@d.zz" rel="nofollow">a~a@d.zz</a></p>`},
}
for _, c := range cases {
test(c.input, c.expected)
}
} }
func TestRender_emoji(t *testing.T) { func TestRender_emoji(t *testing.T) {

View File

@ -86,20 +86,15 @@ func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.C
preClasses += " is-loading" preClasses += " is-loading"
} }
err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `<pre class="%s">`, preClasses)
if err != nil {
return
}
// include language-x class as part of commonmark spec, "chroma" class is used to highlight the code // 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 "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: <pre class="code-block is-loading"><code class="language-math display">...</code></pre> // the "math.ts" strictly depends on the structure: <pre class="code-block is-loading"><code class="language-math display">...</code></pre>
err = r.ctx.RenderInternal.FormatWithSafeAttrs(w, `<code class="chroma language-%s display">`, languageStr) err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `<div class="code-block-container code-overflow-scroll"><pre class="%s"><code class="chroma language-%s display">`, preClasses, languageStr)
if err != nil { if err != nil {
return return
} }
} else { } else {
_, err := w.WriteString("</code></pre>") _, err := w.WriteString("</code></pre></div>")
if err != nil { if err != nil {
return return
} }

View File

@ -46,25 +46,37 @@ var (
// AppURL is the Application ROOT_URL. It always has a '/' suffix // AppURL is the Application ROOT_URL. It always has a '/' suffix
// It maps to ini:"ROOT_URL" // It maps to ini:"ROOT_URL"
AppURL string 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. // This value is empty if site does not have sub-url.
AppSubURL string 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 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. // AppDataPath is the default path for storing data.
// It maps to ini:"APP_DATA_PATH" in [server] and defaults to AppWorkPath + "/data" // It maps to ini:"APP_DATA_PATH" in [server] and defaults to AppWorkPath + "/data"
AppDataPath string AppDataPath string
// LocalURL is the url for locally running applications to contact Gitea. It always has a '/' suffix // 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] // It maps to ini:"LOCAL_ROOT_URL" in [server]
LocalURL string 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 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 Protocol Scheme
UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"` UseProxyProtocol bool
ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"` ProxyProtocolTLSBridging bool
ProxyProtocolHeaderTimeout time.Duration ProxyProtocolHeaderTimeout time.Duration
ProxyProtocolAcceptUnknown bool ProxyProtocolAcceptUnknown bool
Domain string Domain string
@ -181,13 +193,14 @@ func loadServerFrom(rootCfg ConfigProvider) {
EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false) EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false)
} }
Protocol = HTTP
protocolCfg := sec.Key("PROTOCOL").String() protocolCfg := sec.Key("PROTOCOL").String()
if protocolCfg != "https" && EnableAcme { if protocolCfg != "https" && EnableAcme {
log.Fatal("ACME could only be used with HTTPS protocol") log.Fatal("ACME could only be used with HTTPS protocol")
} }
switch protocolCfg { switch protocolCfg {
case "", "http":
Protocol = HTTP
case "https": case "https":
Protocol = HTTPS Protocol = HTTPS
if EnableAcme { if EnableAcme {
@ -243,7 +256,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
case "unix": case "unix":
log.Warn("unix PROTOCOL value is deprecated, please use http+unix") log.Warn("unix PROTOCOL value is deprecated, please use http+unix")
fallthrough fallthrough
case "http+unix": default: // "http+unix"
Protocol = HTTPUnix Protocol = HTTPUnix
} }
UnixSocketPermissionRaw := sec.Key("UNIX_SOCKET_PERMISSION").MustString("666") UnixSocketPermissionRaw := sec.Key("UNIX_SOCKET_PERMISSION").MustString("666")
@ -256,6 +269,8 @@ func loadServerFrom(rootCfg ConfigProvider) {
if !filepath.IsAbs(HTTPAddr) { if !filepath.IsAbs(HTTPAddr) {
HTTPAddr = filepath.Join(AppWorkPath, HTTPAddr) HTTPAddr = filepath.Join(AppWorkPath, HTTPAddr)
} }
default:
log.Fatal("Invalid PROTOCOL %q", Protocol)
} }
UseProxyProtocol = sec.Key("USE_PROXY_PROTOCOL").MustBool(false) UseProxyProtocol = sec.Key("USE_PROXY_PROTOCOL").MustBool(false)
ProxyProtocolTLSBridging = sec.Key("PROXY_PROTOCOL_TLS_BRIDGING").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) PerWritePerKbTimeout = sec.Key("PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout)
defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort 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 // Check validity of AppURL
appURL, err := url.Parse(AppURL) appURL, err := url.Parse(AppURL)
if err != nil { 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. // Remove default ports from AppURL.
// (scheme-based URL normalization, RFC 3986 section 6.2.3) // (scheme-based URL normalization, RFC 3986 section 6.2.3)
@ -309,13 +328,15 @@ func loadServerFrom(rootCfg ConfigProvider) {
defaultLocalURL = AppURL defaultLocalURL = AppURL
case FCGIUnix: case FCGIUnix:
defaultLocalURL = AppURL defaultLocalURL = AppURL
default: case HTTP, HTTPS:
defaultLocalURL = string(Protocol) + "://" defaultLocalURL = string(Protocol) + "://"
if HTTPAddr == "0.0.0.0" { if HTTPAddr == "0.0.0.0" {
defaultLocalURL += net.JoinHostPort("localhost", HTTPPort) + "/" defaultLocalURL += net.JoinHostPort("localhost", HTTPPort) + "/"
} else { } else {
defaultLocalURL += net.JoinHostPort(HTTPAddr, HTTPPort) + "/" defaultLocalURL += net.JoinHostPort(HTTPAddr, HTTPPort) + "/"
} }
default:
log.Fatal("Invalid PROTOCOL %q", Protocol)
} }
LocalURL = sec.Key("LOCAL_ROOT_URL").MustString(defaultLocalURL) LocalURL = sec.Key("LOCAL_ROOT_URL").MustString(defaultLocalURL)
LocalURL = strings.TrimRight(LocalURL, "/") + "/" LocalURL = strings.TrimRight(LocalURL, "/") + "/"

View File

@ -133,3 +133,26 @@ type ActionWorkflowJob struct {
// swagger:strfmt date-time // swagger:strfmt date-time
CompletedAt time.Time `json:"completed_at,omitempty"` 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"`
}

View File

@ -117,6 +117,7 @@ files=ファイル
error=エラー error=エラー
error404=アクセスしようとしたページは<strong>存在しない</strong>か、閲覧が<strong>許可されていません</strong>。 error404=アクセスしようとしたページは<strong>存在しない</strong>か、閲覧が<strong>許可されていません</strong>。
error503=サーバーはリクエストを完了できませんでした。 後でもう一度お試しください。
go_back=戻る go_back=戻る
invalid_data=無効なデータ: %v invalid_data=無効なデータ: %v
@ -730,6 +731,8 @@ public_profile=公開プロフィール
biography_placeholder=自己紹介してください!(Markdownを使うことができます) biography_placeholder=自己紹介してください!(Markdownを使うことができます)
location_placeholder=おおよその場所を他の人と共有 location_placeholder=おおよその場所を他の人と共有
profile_desc=あなたのプロフィールが他のユーザーにどのように表示されるかを制御します。あなたのプライマリメールアドレスは、通知、パスワードの回復、WebベースのGit操作に使用されます。 profile_desc=あなたのプロフィールが他のユーザーにどのように表示されるかを制御します。あなたのプライマリメールアドレスは、通知、パスワードの回復、WebベースのGit操作に使用されます。
password_username_disabled=ユーザー名の変更は許可されていません。詳細はサイト管理者にお問い合わせください。
password_full_name_disabled=フルネームの変更は許可されていません。詳細はサイト管理者にお問い合わせください。
full_name=フルネーム full_name=フルネーム
website=Webサイト website=Webサイト
location=場所 location=場所
@ -924,6 +927,9 @@ permission_not_set=設定なし
permission_no_access=アクセス不可 permission_no_access=アクセス不可
permission_read=読み取り permission_read=読み取り
permission_write=読み取りと書き込み permission_write=読み取りと書き込み
permission_anonymous_read=匿名の読み込み
permission_everyone_read=全員の読み込み
permission_everyone_write=全員の書き込み
access_token_desc=選択したトークン権限に応じて、関連する<a %s>API</a>ルートのみに許可が制限されます。 詳細は<a %s>ドキュメント</a>を参照してください。 access_token_desc=選択したトークン権限に応じて、関連する<a %s>API</a>ルートのみに許可が制限されます。 詳細は<a %s>ドキュメント</a>を参照してください。
at_least_one_permission=トークンを作成するには、少なくともひとつの許可を選択する必要があります at_least_one_permission=トークンを作成するには、少なくともひとつの許可を選択する必要があります
permissions_list=許可: permissions_list=許可:
@ -1136,6 +1142,7 @@ transfer.no_permission_to_reject=この移転を拒否する権限がありま
desc.private=プライベート desc.private=プライベート
desc.public=公開 desc.public=公開
desc.public_access=公開アクセス
desc.template=テンプレート desc.template=テンプレート
desc.internal=内部 desc.internal=内部
desc.archived=アーカイブ desc.archived=アーカイブ
@ -1544,6 +1551,7 @@ issues.filter_project=プロジェクト
issues.filter_project_all=すべてのプロジェクト issues.filter_project_all=すべてのプロジェクト
issues.filter_project_none=プロジェクトなし issues.filter_project_none=プロジェクトなし
issues.filter_assignee=担当者 issues.filter_assignee=担当者
issues.filter_assignee_no_assignee=担当者なし
issues.filter_assignee_any_assignee=担当者あり issues.filter_assignee_any_assignee=担当者あり
issues.filter_poster=作成者 issues.filter_poster=作成者
issues.filter_user_placeholder=ユーザーを検索 issues.filter_user_placeholder=ユーザーを検索
@ -1647,6 +1655,8 @@ issues.label_archived_filter=アーカイブされたラベルを表示
issues.label_archive_tooltip=アーカイブされたラベルは、ラベルによる検索時のサジェストからデフォルトで除外されます。 issues.label_archive_tooltip=アーカイブされたラベルは、ラベルによる検索時のサジェストからデフォルトで除外されます。
issues.label_exclusive_desc=ラベル名を <code>スコープ/アイテム</code> の形にすることで、他の <code>スコープ/</code> ラベルと排他的になります。 issues.label_exclusive_desc=ラベル名を <code>スコープ/アイテム</code> の形にすることで、他の <code>スコープ/</code> ラベルと排他的になります。
issues.label_exclusive_warning=イシューやプルリクエストのラベル編集では、競合するスコープ付きラベルは解除されます。 issues.label_exclusive_warning=イシューやプルリクエストのラベル編集では、競合するスコープ付きラベルは解除されます。
issues.label_exclusive_order=ソート順
issues.label_exclusive_order_tooltip=同じスコープ内の排他的なラベルは、この数値順にソートされます。
issues.label_count=ラベル %d件 issues.label_count=ラベル %d件
issues.label_open_issues=オープン中のイシュー %d件 issues.label_open_issues=オープン中のイシュー %d件
issues.label_edit=編集 issues.label_edit=編集
@ -2129,6 +2139,12 @@ contributors.contribution_type.deletions=削除
settings=設定 settings=設定
settings.desc=設定では、リポジトリの設定を管理することができます。 settings.desc=設定では、リポジトリの設定を管理することができます。
settings.options=リポジトリ 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=共同作業者
settings.collaboration.admin=管理者 settings.collaboration.admin=管理者
settings.collaboration.write=書き込み settings.collaboration.write=書き込み
@ -2719,6 +2735,7 @@ branch.restore_success=ブランチ "%s" を復元しました。
branch.restore_failed=ブランチ "%s" の復元に失敗しました。 branch.restore_failed=ブランチ "%s" の復元に失敗しました。
branch.protected_deletion_failed=ブランチ "%s" は保護されています。 削除できません。 branch.protected_deletion_failed=ブランチ "%s" は保護されています。 削除できません。
branch.default_deletion_failed=ブランチ "%s" はデフォルトブランチです。 削除できません。 branch.default_deletion_failed=ブランチ "%s" はデフォルトブランチです。 削除できません。
branch.default_branch_not_exist=デフォルトブランチ "%s" がありません。
branch.restore=ブランチ "%s" の復元 branch.restore=ブランチ "%s" の復元
branch.download=ブランチ "%s" をダウンロード branch.download=ブランチ "%s" をダウンロード
branch.rename=ブランチ名 "%s" を変更 branch.rename=ブランチ名 "%s" を変更

View File

@ -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) { func UploadPackageFile(ctx *context.Context) {
packageScope := ctx.PathParam("scope") packageScope := ctx.PathParam("scope")
packageName := ctx.PathParam("name") packageName := ctx.PathParam("name")
@ -304,9 +321,9 @@ func UploadPackageFile(ctx *context.Context) {
packageVersion := v.Core().String() packageVersion := v.Core().String()
file, _, err := ctx.Req.FormFile("source-archive") file, err := formFileOptionalReadCloser(ctx, "source-archive")
if err != nil { if file == nil || err != nil {
apiError(ctx, http.StatusBadRequest, err) apiError(ctx, http.StatusBadRequest, "unable to read source-archive file")
return return
} }
defer file.Close() defer file.Close()
@ -318,10 +335,13 @@ func UploadPackageFile(ctx *context.Context) {
} }
defer buf.Close() defer buf.Close()
var mr io.Reader mr, err := formFileOptionalReadCloser(ctx, "metadata")
metadata := ctx.Req.FormValue("metadata") if err != nil {
if metadata != "" { apiError(ctx, http.StatusBadRequest, "unable to read metadata file")
mr = strings.NewReader(metadata) return
}
if mr != nil {
defer mr.Close()
} }
pck, err := swift_module.ParsePackage(buf, buf.Size(), mr) pck, err := swift_module.ParsePackage(buf, buf.Size(), mr)

View File

@ -24,3 +24,81 @@ func GetRegistrationToken(ctx *context.APIContext) {
shared.GetRegistrationToken(ctx, 0, 0) 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"))
}

View File

@ -912,7 +912,11 @@ func Routes() *web.Router {
}) })
m.Group("/runners", func() { m.Group("/runners", func() {
m.Get("", reqToken(), reqChecker, act.ListRunners)
m.Get("/registration-token", reqToken(), reqChecker, act.GetRegistrationToken) 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.Group("/runners", func() {
m.Get("", reqToken(), user.ListRunners)
m.Get("/registration-token", reqToken(), user.GetRegistrationToken) 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). Patch(bind(api.EditHookOption{}), admin.EditHook).
Delete(admin.DeleteHook) 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.Group("/runners", func() {
m.Get("/registration-token", admin.GetRegistrationToken) m.Get("/registration-token", admin.GetRegistrationToken)
}) })

View File

@ -190,6 +190,27 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) {
shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0) 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 // ListVariables list org-level variables
func (Action) ListVariables(ctx *context.APIContext) { func (Action) ListVariables(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList // swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList
@ -470,6 +491,85 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent) 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) var _ actions_service.API = new(Action)
// Action implements actions_service.API // Action implements actions_service.API

View File

@ -183,7 +183,7 @@ func (Action) DeleteSecret(ctx *context.APIContext) {
// required: true // required: true
// responses: // responses:
// "204": // "204":
// description: delete one secret of the organization // description: delete one secret of the repository
// "400": // "400":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "404": // "404":
@ -531,6 +531,125 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) {
shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID) 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) var _ actions_service.API = new(Action)
// Action implements actions_service.API // Action implements actions_service.API

View File

@ -193,7 +193,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi
} }
// get commit count - wiki revisions // get commit count - wiki revisions
commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
// Get last change information. // Get last change information.
lastCommit, err := wikiRepo.GetCommitByPath(pageFilename) lastCommit, err := wikiRepo.GetCommitByPath(pageFilename)
@ -432,7 +432,7 @@ func ListPageRevisions(ctx *context.APIContext) {
} }
// get commit count - wiki revisions // get commit count - wiki revisions
commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
page := ctx.FormInt("page") page := ctx.FormInt("page")
if page <= 1 { if page <= 1 {
@ -442,7 +442,7 @@ func ListPageRevisions(ctx *context.APIContext) {
// get Commit Count // get Commit Count
commitsHistory, err := wikiRepo.CommitsByFileAndRange( commitsHistory, err := wikiRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{ git.CommitsByFileAndRangeOptions{
Revision: "master", Revision: ctx.Repo.Repository.DefaultWikiBranch,
File: pageFilename, File: pageFilename,
Page: page, Page: page,
}) })
@ -486,7 +486,7 @@ func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit)
return nil, nil return nil, nil
} }
commit, err := wikiRepo.GetBranchCommit("master") commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
if err != nil { if err != nil {
if git.IsErrNotExist(err) { if git.IsErrNotExist(err) {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)

View File

@ -8,8 +8,13 @@ import (
"net/http" "net/http"
actions_model "code.gitea.io/gitea/models/actions" 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/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
) )
// RegistrationToken is response related to registration token // 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}) 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)
}

View File

@ -457,6 +457,20 @@ type swaggerRepoArtifact struct {
Body api.ActionArtifact `json:"body"` 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 // swagger:response Compare
type swaggerCompare struct { type swaggerCompare struct {
// in:body // in:body

View File

@ -24,3 +24,81 @@ func GetRegistrationToken(ctx *context.APIContext) {
shared.GetRegistrationToken(ctx, ctx.Doer.ID, 0) 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"))
}

View File

@ -76,6 +76,7 @@ func TestShadowPassword(t *testing.T) {
func TestSelfCheckPost(t *testing.T) { func TestSelfCheckPost(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")() defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")()
defer test.MockVariableValue(&setting.AppSubURL, "/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") ctx, resp := contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend")
SelfCheckPost(ctx) SelfCheckPost(ctx)

View File

@ -181,6 +181,7 @@ func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) {
// GetPullDiffStats get Pull Requests diff stats // GetPullDiffStats get Pull Requests diff stats
func GetPullDiffStats(ctx *context.Context) { func GetPullDiffStats(ctx *context.Context) {
// FIXME: this getPullInfo seems to be a duplicate call with other route handlers
issue, ok := getPullInfo(ctx) issue, ok := getPullInfo(ctx)
if !ok { if !ok {
return return
@ -188,21 +189,19 @@ func GetPullDiffStats(ctx *context.Context) {
pull := issue.PullRequest pull := issue.PullRequest
mergeBaseCommitID := GetMergedBaseCommitID(ctx, issue) mergeBaseCommitID := GetMergedBaseCommitID(ctx, issue)
if mergeBaseCommitID == "" { if mergeBaseCommitID == "" {
ctx.NotFound(nil) return // no merge base, do nothing, do not stop the route handler, see below
return
} }
// 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()) headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitRefName())
if err != nil { if err != nil {
ctx.ServerError("GetRefCommitID", err) log.Error("Failed to GetRefCommitID: %v, repo: %v", err, ctx.Repo.Repository.FullName())
return return
} }
diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, mergeBaseCommitID, headCommitID) diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, mergeBaseCommitID, headCommitID)
if err != nil { if err != nil {
ctx.ServerError("GetDiffShortStat", err) log.Error("Failed to GetDiffShortStat: %v, repo: %v", err, ctx.Repo.Repository.FullName())
return return
} }

View File

@ -197,7 +197,7 @@ func RunnersEdit(ctx *context.Context) {
ctx.ServerError("LoadAttributes", err) ctx.ServerError("LoadAttributes", err)
return return
} }
if !runner.Editable(ownerID, repoID) { if !runner.EditableInContext(ownerID, repoID) {
err = errors.New("no permission to edit this runner") err = errors.New("no permission to edit this runner")
ctx.NotFound(err) ctx.NotFound(err)
return return
@ -250,7 +250,7 @@ func RunnersEditPost(ctx *context.Context) {
ctx.ServerError("RunnerDetailsEditPost.GetRunnerByID", err) ctx.ServerError("RunnerDetailsEditPost.GetRunnerByID", err)
return return
} }
if !runner.Editable(ownerID, repoID) { if !runner.EditableInContext(ownerID, repoID) {
ctx.NotFound(util.NewPermissionDeniedErrorf("no permission to edit this runner")) ctx.NotFound(util.NewPermissionDeniedErrorf("no permission to edit this runner"))
return return
} }
@ -304,7 +304,7 @@ func RunnerDeletePost(ctx *context.Context) {
return 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")) ctx.NotFound(util.NewPermissionDeniedErrorf("no permission to delete this runner"))
return return
} }

View File

@ -25,4 +25,12 @@ type API interface {
UpdateVariable(*context.APIContext) UpdateVariable(*context.APIContext)
// GetRegistrationToken get registration token // GetRegistrationToken get registration token
GetRegistrationToken(*context.APIContext) 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)
} }

View File

@ -30,6 +30,8 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey" asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/gitdiff" "code.gitea.io/gitea/services/gitdiff"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
) )
// ToEmail convert models.EmailAddress to api.Email // ToEmail convert models.EmailAddress to api.Email
@ -252,6 +254,30 @@ func ToActionArtifact(repo *repo_model.Repository, art *actions_model.ActionArti
}, nil }, 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 // ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification { func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification {
verif := asymkey_service.ParseCommitWithSignature(ctx, c) verif := asymkey_service.ParseCommitWithSignature(ctx, c)

View File

@ -6,7 +6,7 @@ package feed
import ( import (
"context" "context"
"fmt" "fmt"
"strconv" "strings"
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -14,15 +14,10 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" 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/setting"
"code.gitea.io/gitea/modules/util" "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) { func GetFeedsForDashboard(ctx context.Context, opts activities_model.GetFeedsOptions) (activities_model.ActionList, int, error) {
opts.DontCount = opts.RequestedTeam == nil && opts.Date == "" opts.DontCount = opts.RequestedTeam == nil && opts.Date == ""
results, cnt, err := activities_model.GetFeeds(ctx, opts) 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 // * Organization action: UserID=100 (the repo's org), ActUserID=1
// * Watcher action: UserID=20 (a user who is watching a repo), 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 { 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 act.UserID = act.ActUserID
if err := db.Insert(ctx, act); err != nil { if err := db.Insert(ctx, act); err != nil {
return fmt.Errorf("insert new actioner: %w", err) 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] { if !permPR[i] {
continue continue
} }
default:
} }
if err := db.Insert(ctx, act); err != nil { if err := db.Insert(ctx, act); err != nil {
return fmt.Errorf("insert new action: %w", err) 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 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 { func NotifyWatchers(ctx context.Context, acts ...*activities_model.Action) error {
return db.WithTx(ctx, func(ctx context.Context) error { return db.WithTx(ctx, func(ctx context.Context) error {
if len(acts) == 0 { if len(acts) == 0 {

View File

@ -9,16 +9,16 @@
<a class="silenced" href="#">silenced</a> <a class="silenced" href="#">silenced</a>
</div> </div>
<h1>Button</h1> <h1>Button</h1>
<div> ".ui.button" styles:
Style: <div class="flex-text-block tw-gap-4">
<label><input type="checkbox" name="button-style-compact" value="compact">compact</label> <label class="gt-checkbox"><input type="radio" name="button-style-size" value="">(normal)</label>
<label><input type="radio" name="button-style-size" value="">(normal)</label> <label class="gt-checkbox"><input type="radio" name="button-style-size" value="small">small</label>
<label><input type="radio" name="button-style-size" value="tiny">tiny</label> <label class="gt-checkbox"><input type="radio" name="button-style-size" value="tiny">tiny</label>
<label><input type="radio" name="button-style-size" value="mini">mini</label> <label class="gt-checkbox"><input type="radio" name="button-style-size" value="mini">mini</label>
</div> </div>
<div> <div class="flex-text-block tw-gap-4">
State: <label class="gt-checkbox"><input type="checkbox" name="button-style-compact" value="compact">compact</label>
<label><input type="checkbox" name="button-state-disabled" value="disabled">disabled</label> <label class="gt-checkbox"><input type="checkbox" name="button-state-disabled" value="disabled">disabled</label>
</div> </div>
<div id="devtest-button-samples"> <div id="devtest-button-samples">
<ul class="button-sample-groups"> <ul class="button-sample-groups">

View File

@ -0,0 +1,71 @@
{{template "devtest/devtest-header"}}
<div class="page-content devtest ui container">
{{$longCode := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}
<div class="tw-flex">
<div class="tw-w-[50%] tw-p-4">
<div class="markup render-content">
Inline <code>code</code> content
</div>
<div class="divider"></div>
<div class="markup render-content">
<p>content before</p>
<pre><code>Very long line with no code block or container: {{$longCode}}</code></pre>
<p>content after</p>
</div>
<div class="divider"></div>
<div class="markup render-content">
<p>content before</p>
<div class="code-block-container code-overflow-wrap">
<pre class="code-block"><code>Very long line with wrap: {{$longCode}}</code></pre>
</div>
<p>content after</p>
</div>
<div class="divider"></div>
<div class="markup render-content">
<p>content before</p>
<div class="code-block-container code-overflow-scroll">
<pre class="code-block"><code>Short line in scroll container</code></pre>
</div>
<div class="code-block-container code-overflow-scroll">
<pre class="code-block"><code>Very long line with scroll: {{$longCode}}</code></pre>
</div>
<p>content after</p>
</div>
</div>
<div class="tw-w-[50%] tw-p-4">
<div class="markup render-content">
<p>content before</p>
<div class="code-block-container">
<pre class="code-block"><code class="language-math">
\lim\limits_{n\rightarrow\infty}{\left(1+\frac{1}{n}\right)^n}
</code></pre>
</div>
<p>content after</p>
</div>
<div class="divider"></div>
<div class="markup render-content">
<p>content before</p>
<div class="code-block-container">
<pre class="code-block"><code class="language-mermaid is-loading">
graph LR
A[Square Rect] -- Link text --> B((Circle))
A --> C(Round Rect)
B --> D{Rhombus}
C --> D
</code></pre>
</div>
<p>content after</p>
</div>
</div>
</div>
</div>
{{template "devtest/devtest-footer"}}

View File

@ -4,7 +4,7 @@
<div class="ui container"> <div class="ui container">
{{template "base/alert" .}} {{template "base/alert" .}}
{{if .IsOrganizationOwner}} {{if .IsOrganizationOwner}}
<div class="tw-text-right"> <div class="flex-text-block tw-justify-end">
<a class="ui primary button" href="{{.OrgLink}}/teams/new">{{svg "octicon-plus"}} {{ctx.Locale.Tr "org.create_new_team"}}</a> <a class="ui primary button" href="{{.OrgLink}}/teams/new">{{svg "octicon-plus"}} {{ctx.Locale.Tr "org.create_new_team"}}</a>
</div> </div>
<div class="divider"></div> <div class="divider"></div>

View File

@ -1,5 +1,5 @@
{{if and $.CanWriteProjects (not $.Repository.IsArchived)}} {{if and $.CanWriteProjects (not $.Repository.IsArchived)}}
<div class="tw-flex tw-justify-between tw-mb-4"> <div class="flex-text-block tw-justify-between tw-mb-4">
<div class="small-menu-items ui compact tiny menu list-header-toggle"> <div class="small-menu-items ui compact tiny menu list-header-toggle">
<a class="item{{if not .IsShowClosed}} active{{end}}" href="?state=open&q={{$.Keyword}}"> <a class="item{{if not .IsShowClosed}} active{{end}}" href="?state=open&q={{$.Keyword}}">
{{svg "octicon-project-symlink" 16 "tw-mr-2"}} {{svg "octicon-project-symlink" 16 "tw-mr-2"}}
@ -10,9 +10,7 @@
{{ctx.Locale.PrettyNumber .ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}} {{ctx.Locale.PrettyNumber .ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
</a> </a>
</div> </div>
<div class="tw-text-right"> <a class="ui small primary button" href="{{$.Link}}/new">{{ctx.Locale.Tr "repo.projects.new"}}</a>
<a class="ui small primary button" href="{{$.Link}}/new">{{ctx.Locale.Tr "repo.projects.new"}}</a>
</div>
</div> </div>
{{end}} {{end}}

View File

@ -64,7 +64,7 @@
</div> </div>
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<div class="tw-text-right"> <div class="flex-text-block tw-justify-end">
<a class="ui cancel button" href="{{$.CancelLink}}"> <a class="ui cancel button" href="{{$.CancelLink}}">
{{ctx.Locale.Tr "repo.milestones.cancel"}} {{ctx.Locale.Tr "repo.milestones.cancel"}}
</a> </a>

View File

@ -1,4 +1,4 @@
<button class="ui primary button js-btn-clone-panel"> <button class="ui compact primary button js-btn-clone-panel">
{{svg "octicon-code" 16}} {{svg "octicon-code" 16}}
<span>{{ctx.Locale.Tr "repo.code"}}</span> <span>{{ctx.Locale.Tr "repo.code"}}</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}

View File

@ -37,7 +37,7 @@
{{if .PageIsPullFiles}} {{if .PageIsPullFiles}}
<div id="diff-commit-select" data-issuelink="{{$.Issue.Link}}" data-queryparams="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated={{$.ShowOutdatedComments}}" data-filter_changes_by_commit="{{ctx.Locale.Tr "repo.pulls.filter_changes_by_commit"}}"> <div id="diff-commit-select" data-issuelink="{{$.Issue.Link}}" data-queryparams="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated={{$.ShowOutdatedComments}}" data-filter_changes_by_commit="{{ctx.Locale.Tr "repo.pulls.filter_changes_by_commit"}}">
{{/* the following will be replaced by vue component, but this avoids any loading artifacts till the vue component is initialized */}} {{/* the following will be replaced by vue component, but this avoids any loading artifacts till the vue component is initialized */}}
<div class="ui jump dropdown basic button custom"> <div class="ui jump dropdown tiny basic button custom">
{{svg "octicon-git-commit"}} {{svg "octicon-git-commit"}}
</div> </div>
</div> </div>
@ -223,6 +223,7 @@
{{if and (not $.Repository.IsArchived) (not .DiffNotAvailable)}} {{if and (not $.Repository.IsArchived) (not .DiffNotAvailable)}}
<template id="issue-comment-editor-template"> <template id="issue-comment-editor-template">
<form class="ui form comment"> <form class="ui form comment">
<div class="field">
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"CustomInit" true "CustomInit" true
"MarkdownPreviewInRepo" $.Repository "MarkdownPreviewInRepo" $.Repository
@ -230,12 +231,13 @@
"TextareaName" "content" "TextareaName" "content"
"DropzoneParentContainer" ".ui.form" "DropzoneParentContainer" ".ui.form"
)}} )}}
</div>
{{if .IsAttachmentEnabled}} {{if .IsAttachmentEnabled}}
<div class="field"> <div class="field">
{{template "repo/upload" .}} {{template "repo/upload" .}}
</div> </div>
{{end}} {{end}}
<div class="tw-text-right edit buttons"> <div class="field flex-text-block tw-justify-end">
<button class="ui cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button> <button class="ui cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
<button class="ui primary button">{{ctx.Locale.Tr "repo.issues.save"}}</button> <button class="ui primary button">{{ctx.Locale.Tr "repo.issues.save"}}</button>
</div> </div>

View File

@ -27,7 +27,7 @@
{{end}} {{end}}
<div class="field footer"> <div class="field footer">
<div class="tw-text-right"> <div class="flex-text-block tw-justify-end">
{{if $.reply}} {{if $.reply}}
<button class="ui submit primary tiny button btn-reply" type="submit">{{ctx.Locale.Tr "repo.diff.comment.reply"}}</button> <button class="ui submit primary tiny button btn-reply" type="submit">{{ctx.Locale.Tr "repo.diff.comment.reply"}}</button>
<input type="hidden" name="reply" value="{{$.reply}}"> <input type="hidden" name="reply" value="{{$.reply}}">

View File

@ -8,9 +8,9 @@
{{$referenceUrl := printf "%s#%s" $.Issue.Link $comment.HashTag}} {{$referenceUrl := printf "%s#%s" $.Issue.Link $comment.HashTag}}
<div class="conversation-holder" data-path="{{$comment.TreePath}}" data-side="{{if lt $comment.Line 0}}left{{else}}right{{end}}" data-idx="{{$comment.UnsignedLine}}"> <div class="conversation-holder" data-path="{{$comment.TreePath}}" data-side="{{if lt $comment.Line 0}}left{{else}}right{{end}}" data-idx="{{$comment.UnsignedLine}}">
{{if $resolved}} {{if $resolved}}
<div class="ui attached header resolved-placeholder tw-flex tw-items-center tw-justify-between"> <div class="resolved-placeholder">
<div class="ui grey text tw-flex tw-items-center tw-flex-wrap tw-gap-1"> <div class="flex-text-block tw-flex-wrap grey text">
{{svg "octicon-check" 16 "icon tw-mr-1"}} {{svg "octicon-check"}}
<b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}} <b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}}
{{if $invalid}} {{if $invalid}}
<!-- <!--
@ -22,35 +22,33 @@
</a> </a>
{{end}} {{end}}
</div> </div>
<div class="tw-flex tw-items-center tw-gap-2"> <div class="flex-text-block">
<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button show-outdated tw-flex tw-items-center"> <button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="btn tiny show-outdated">
{{svg "octicon-unfold" 16 "tw-mr-2"}} {{svg "octicon-unfold" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
</button> </button>
<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button hide-outdated tw-flex tw-items-center tw-hidden"> <button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="btn tiny hide-outdated tw-hidden">
{{svg "octicon-fold" 16 "tw-mr-2"}} {{svg "octicon-fold" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
</button> </button>
</div> </div>
</div> </div>
{{end}} {{end}}
<div id="code-comments-{{$comment.ID}}" class="field comment-code-cloud {{if $resolved}}tw-hidden{{end}}"> <div id="code-comments-{{$comment.ID}}" class="field comment-code-cloud {{if $resolved}}tw-hidden{{end}}">
<div class="comment-list"> <div class="comment-list">
<ui class="ui comments"> <div class="ui comments">
{{template "repo/diff/comments" dict "root" $ "comments" .comments}} {{template "repo/diff/comments" dict "root" $ "comments" .comments}}
</ui> </div>
</div> </div>
<div class="tw-flex tw-justify-end tw-items-center tw-gap-2 tw-mt-2 tw-flex-wrap"> <div class="flex-text-block tw-mt-2 tw-flex-wrap tw-justify-end">
<div class="ui buttons"> <div class="ui buttons">
<button class="ui icon tiny basic button previous-conversation"> <button class="ui icon tiny basic button previous-conversation">
{{svg "octicon-arrow-up" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.previous"}} {{svg "octicon-arrow-up" 12}} {{ctx.Locale.Tr "repo.issues.previous"}}
</button> </button>
<button class="ui icon tiny basic button next-conversation"> <button class="ui icon tiny basic button next-conversation">
{{svg "octicon-arrow-down" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.next"}} {{svg "octicon-arrow-down" 12}} {{ctx.Locale.Tr "repo.issues.next"}}
</button> </button>
</div> </div>
{{if and $.CanMarkConversation $hasReview (not $isReviewPending)}} {{if and $.CanMarkConversation $hasReview (not $isReviewPending)}}
<button class="ui icon tiny basic button resolve-conversation tw-mr-0" data-origin="diff" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{$comment.ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation"> <button class="ui icon tiny basic button resolve-conversation" data-origin="diff" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{$comment.ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation">
{{if $resolved}} {{if $resolved}}
{{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}} {{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}}
{{else}} {{else}}
@ -59,8 +57,8 @@
</button> </button>
{{end}} {{end}}
{{if and $.SignedUserID (not $.Repository.IsArchived)}} {{if and $.SignedUserID (not $.Repository.IsArchived)}}
<button class="comment-form-reply ui primary tiny labeled icon button tw-mr-0"> <button class="comment-form-reply ui primary icon tiny button">
{{svg "octicon-reply" 16 "reply icon tw-mr-1"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}} {{svg "octicon-reply" 12}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
</button> </button>
{{end}} {{end}}
</div> </div>

View File

@ -45,8 +45,8 @@
{{end}} {{end}}
</div> </div>
</div> </div>
<button id="flow-color-monochrome" class="ui labelled icon button{{if eq .Mode "monochrome"}} active{{end}}" title="{{ctx.Locale.Tr "repo.commit_graph.monochrome"}}">{{svg "material-invert-colors" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.commit_graph.monochrome"}}</button> <button id="flow-color-monochrome" class="ui icon button{{if eq .Mode "monochrome"}} active{{end}}" title="{{ctx.Locale.Tr "repo.commit_graph.monochrome"}}">{{svg "material-invert-colors" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.commit_graph.monochrome"}}</button>
<button id="flow-color-colored" class="ui labelled icon button{{if ne .Mode "monochrome"}} active{{end}}" title="{{ctx.Locale.Tr "repo.commit_graph.color"}}">{{svg "material-palette" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.commit_graph.color"}}</button> <button id="flow-color-colored" class="ui icon button{{if ne .Mode "monochrome"}} active{{end}}" title="{{ctx.Locale.Tr "repo.commit_graph.color"}}">{{svg "material-palette" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.commit_graph.color"}}</button>
</div> </div>
</h2> </h2>
<div class="ui dividing"></div> <div class="ui dividing"></div>

View File

@ -17,18 +17,18 @@
{{if eq $refGroup "pull"}} {{if eq $refGroup "pull"}}
{{if or (not $.HidePRRefs) (SliceUtils.Contains $.SelectedBranches .Name)}} {{if or (not $.HidePRRefs) (SliceUtils.Contains $.SelectedBranches .Name)}}
<!-- it's intended to use issues not pulls, if it's a pull you will get redirected --> <!-- it's intended to use issues not pulls, if it's a pull you will get redirected -->
<a class="ui labelled basic tiny button" href="{{$.RepoLink}}/{{if $.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypePullRequests}}pulls{{else}}issues{{end}}/{{.ShortName|PathEscape}}"> <a class="ui basic tiny button" href="{{$.RepoLink}}/{{if $.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypePullRequests}}pulls{{else}}issues{{end}}/{{.ShortName|PathEscape}}">
{{svg "octicon-git-pull-request"}} #{{.ShortName}} {{svg "octicon-git-pull-request"}} #{{.ShortName}}
</a> </a>
{{end}} {{end}}
{{else if eq $refGroup "tags"}} {{else if eq $refGroup "tags"}}
{{- template "repo/tag/name" dict "RepoLink" $.Repository.Link "TagName" .ShortName -}} {{- template "repo/tag/name" dict "RepoLink" $.Repository.Link "TagName" .ShortName -}}
{{else if eq $refGroup "remotes"}} {{else if eq $refGroup "remotes"}}
<a class="ui labelled basic tiny button" href="{{$.RepoLink}}/src/commit/{{$commit.Rev|PathEscape}}"> <a class="ui basic tiny button" href="{{$.RepoLink}}/src/commit/{{$commit.Rev|PathEscape}}">
{{svg "octicon-cross-reference"}} {{.ShortName}} {{svg "octicon-cross-reference"}} {{.ShortName}}
</a> </a>
{{else if eq $refGroup "heads"}} {{else if eq $refGroup "heads"}}
<a class="ui labelled basic tiny button" href="{{$.RepoLink}}/src/branch/{{.ShortName|PathEscape}}"> <a class="ui basic tiny button" href="{{$.RepoLink}}/src/branch/{{.ShortName|PathEscape}}">
{{svg "octicon-git-branch"}} {{.ShortName}} {{svg "octicon-git-branch"}} {{.ShortName}}
</a> </a>
{{else}} {{else}}

View File

@ -38,20 +38,20 @@
</div> </div>
</div> </div>
{{if not (or .IsBeingCreated .IsBroken)}} {{if not (or .IsBeingCreated .IsBroken)}}
<div class="repo-buttons"> <div class="flex-text-block tw-flex-wrap">
{{if $.RepoTransfer}} {{if $.RepoTransfer}}
<form method="post" action="{{$.RepoLink}}/action/accept_transfer?redirect_to={{$.RepoLink}}"> <form method="post" action="{{$.RepoLink}}/action/accept_transfer?redirect_to={{$.RepoLink}}">
{{$.CsrfTokenHtml}} {{$.CsrfTokenHtml}}
<div data-tooltip-content="{{if $.CanUserAcceptOrRejectTransfer}}{{ctx.Locale.Tr "repo.transfer.accept_desc" $.RepoTransfer.Recipient.DisplayName}}{{else}}{{ctx.Locale.Tr "repo.transfer.no_permission_to_accept"}}{{end}}"> <div class="flex-text-inline" data-tooltip-content="{{if $.CanUserAcceptOrRejectTransfer}}{{ctx.Locale.Tr "repo.transfer.accept_desc" $.RepoTransfer.Recipient.DisplayName}}{{else}}{{ctx.Locale.Tr "repo.transfer.no_permission_to_accept"}}{{end}}">
<button type="submit" class="ui basic button {{if $.CanUserAcceptOrRejectTransfer}}primary {{end}} ok small"{{if not $.CanUserAcceptOrRejectTransfer}} disabled{{end}}> <button type="submit" class="ui compact small basic button {{if $.CanUserAcceptOrRejectTransfer}}primary {{end}} ok small"{{if not $.CanUserAcceptOrRejectTransfer}} disabled{{end}}>
{{ctx.Locale.Tr "repo.transfer.accept"}} {{ctx.Locale.Tr "repo.transfer.accept"}}
</button> </button>
</div> </div>
</form> </form>
<form method="post" action="{{$.RepoLink}}/action/reject_transfer?redirect_to={{$.RepoLink}}"> <form method="post" action="{{$.RepoLink}}/action/reject_transfer?redirect_to={{$.RepoLink}}">
{{$.CsrfTokenHtml}} {{$.CsrfTokenHtml}}
<div data-tooltip-content="{{if $.CanUserAcceptOrRejectTransfer}}{{ctx.Locale.Tr "repo.transfer.reject_desc" $.RepoTransfer.Recipient.DisplayName}}{{else}}{{ctx.Locale.Tr "repo.transfer.no_permission_to_reject"}}{{end}}"> <div class="flex-text-inline" data-tooltip-content="{{if $.CanUserAcceptOrRejectTransfer}}{{ctx.Locale.Tr "repo.transfer.reject_desc" $.RepoTransfer.Recipient.DisplayName}}{{else}}{{ctx.Locale.Tr "repo.transfer.no_permission_to_reject"}}{{end}}">
<button type="submit" class="ui basic button {{if $.CanUserAcceptOrRejectTransfer}}red {{end}}ok small"{{if not $.CanUserAcceptOrRejectTransfer}} disabled{{end}}> <button type="submit" class="ui compact small basic button {{if $.CanUserAcceptOrRejectTransfer}}red {{end}}ok small"{{if not $.CanUserAcceptOrRejectTransfer}} disabled{{end}}>
{{ctx.Locale.Tr "repo.transfer.reject"}} {{ctx.Locale.Tr "repo.transfer.reject"}}
</button> </button>
</div> </div>

View File

@ -3,10 +3,10 @@
{{template "repo/header" .}} {{template "repo/header" .}}
<div class="ui container"> <div class="ui container">
{{template "base/alert" .}} {{template "base/alert" .}}
<div class="tw-flex"> <div class="flex-text-block tw-flex-wrap tw-mb-2">
<h1 class="tw-mb-2">{{.Milestone.Name}}</h1> <h1 class="tw-flex-1 tw-m-0">{{.Milestone.Name}}</h1>
{{if not .Repository.IsArchived}} {{if not .Repository.IsArchived}}
<div class="tw-text-right tw-flex-1"> <div>
{{if or .CanWriteIssues .CanWritePulls}} {{if or .CanWriteIssues .CanWritePulls}}
{{if .Milestone.IsClosed}} {{if .Milestone.IsClosed}}
<a class="ui primary basic button link-action" href data-url="{{$.RepoLink}}/milestones/{{.MilestoneID}}/open">{{ctx.Locale.Tr "repo.milestones.open"}} <a class="ui primary basic button link-action" href data-url="{{$.RepoLink}}/milestones/{{.MilestoneID}}/open">{{ctx.Locale.Tr "repo.milestones.open"}}

View File

@ -44,7 +44,7 @@
"TextareaPlaceholder" (ctx.Locale.Tr "repo.milestones.desc") "TextareaPlaceholder" (ctx.Locale.Tr "repo.milestones.desc")
)}} )}}
</div> </div>
<div class="tw-text-right"> <div class="flex-text-block tw-justify-end">
{{if .PageIsEditMilestone}} {{if .PageIsEditMilestone}}
<a class="ui primary basic button" href="{{.RepoLink}}/milestones"> <a class="ui primary basic button" href="{{.RepoLink}}/milestones">
{{ctx.Locale.Tr "repo.milestones.cancel"}} {{ctx.Locale.Tr "repo.milestones.cancel"}}

View File

@ -33,7 +33,7 @@
{{else}} {{else}}
{{template "repo/issue/comment_tab" .}} {{template "repo/issue/comment_tab" .}}
{{end}} {{end}}
<div class="tw-text-right"> <div class="flex-text-block tw-justify-end">
<button class="ui primary button"> <button class="ui primary button">
{{if .PageIsComparePull}} {{if .PageIsComparePull}}
{{ctx.Locale.Tr "repo.pulls.create"}} {{ctx.Locale.Tr "repo.pulls.create"}}

View File

@ -83,7 +83,7 @@
{{template "repo/issue/comment_tab" .}} {{template "repo/issue/comment_tab" .}}
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<div class="field footer"> <div class="field footer">
<div class="tw-text-right"> <div class="flex-text-block tw-justify-end">
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .DisableStatusChange)}} {{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .DisableStatusChange)}}
{{if .Issue.IsClosed}} {{if .Issue.IsClosed}}
<button id="status-button" class="ui primary basic button" data-status="{{ctx.Locale.Tr "repo.issues.reopen_issue"}}" data-status-and-comment="{{ctx.Locale.Tr "repo.issues.reopen_comment_issue"}}" name="status" value="reopen"> <button id="status-button" class="ui primary basic button" data-status="{{ctx.Locale.Tr "repo.issues.reopen_issue"}}" data-status-and-comment="{{ctx.Locale.Tr "repo.issues.reopen_comment_issue"}}" name="status" value="reopen">
@ -157,7 +157,7 @@
{{end}} {{end}}
<div class="field"> <div class="field">
<div class="tw-text-right edit"> <div class="flex-text-block tw-justify-end">
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button> <button type="button" class="ui cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
<button type="submit" class="ui primary button">{{ctx.Locale.Tr "repo.issues.save"}}</button> <button type="submit" class="ui primary button">{{ctx.Locale.Tr "repo.issues.save"}}</button>
</div> </div>

View File

@ -17,7 +17,7 @@
</div> </div>
<div> <div>
{{if or $invalid $resolved}} {{if or $invalid $resolved}}
<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if not $resolved}}tw-hidden {{end}}ui compact labeled button show-outdated tw-flex tw-items-center"> <button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if not $resolved}}tw-hidden{{end}} btn tiny show-outdated">
{{svg "octicon-unfold" 16 "tw-mr-2"}} {{svg "octicon-unfold" 16 "tw-mr-2"}}
{{if $resolved}} {{if $resolved}}
{{ctx.Locale.Tr "repo.issues.review.show_resolved"}} {{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
@ -25,7 +25,7 @@
{{ctx.Locale.Tr "repo.issues.review.show_outdated"}} {{ctx.Locale.Tr "repo.issues.review.show_outdated"}}
{{end}} {{end}}
</button> </button>
<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if $resolved}}tw-hidden {{end}}ui compact labeled button hide-outdated tw-flex tw-items-center"> <button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if $resolved}}tw-hidden {{end}} btn tiny hide-outdated">
{{svg "octicon-fold" 16 "tw-mr-2"}} {{svg "octicon-fold" 16 "tw-mr-2"}}
{{if $resolved}} {{if $resolved}}
{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}} {{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
@ -109,7 +109,7 @@
</div> </div>
{{end}} {{end}}
</div> </div>
<div class="code-comment-buttons tw-flex tw-items-center tw-flex-wrap tw-mt-2 tw-mb-1 tw-mx-2"> <div class="flex-text-block tw-flex-wrap tw-my-2">
<div class="tw-flex-1"> <div class="tw-flex-1">
{{if $resolved}} {{if $resolved}}
<div class="ui grey text"> <div class="ui grey text">
@ -118,7 +118,7 @@
</div> </div>
{{end}} {{end}}
</div> </div>
<div class="code-comment-buttons-buttons"> <div class="flex-text-block">
{{if and $.CanMarkConversation $hasReview (not $isReviewPending)}} {{if and $.CanMarkConversation $hasReview (not $isReviewPending)}}
<button class="ui tiny basic button resolve-conversation" data-origin="timeline" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{$comment.ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation"> <button class="ui tiny basic button resolve-conversation" data-origin="timeline" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{$comment.ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation">
{{if $resolved}} {{if $resolved}}
@ -129,8 +129,8 @@
</button> </button>
{{end}} {{end}}
{{if and $.SignedUserID (not $.Repository.IsArchived)}} {{if and $.SignedUserID (not $.Repository.IsArchived)}}
<button class="comment-form-reply ui primary tiny labeled icon button tw-ml-1 tw-mr-0"> <button class="comment-form-reply ui primary icon tiny button">
{{svg "octicon-reply" 16 "reply icon tw-mr-1"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}} {{svg "octicon-reply" 12}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
</button> </button>
{{end}} {{end}}
</div> </div>

View File

@ -20,7 +20,7 @@
<label><strong>{{ctx.Locale.Tr "repo.issues.reference_issue.body"}}</strong></label> <label><strong>{{ctx.Locale.Tr "repo.issues.reference_issue.body"}}</strong></label>
<textarea name="content"></textarea> <textarea name="content"></textarea>
</div> </div>
<div class="tw-text-right"> <div class="flex-text-block tw-justify-end">
<button class="ui primary button">{{ctx.Locale.Tr "repo.issues.create"}}</button> <button class="ui primary button">{{ctx.Locale.Tr "repo.issues.create"}}</button>
</div> </div>
</form> </form>

View File

@ -1,12 +1,12 @@
<h2 class="ui header activity-header"> <h2 class="ui header activity-header">
<span>{{DateUtils.AbsoluteLong .DateFrom}} - {{DateUtils.AbsoluteLong .DateUntil}}</span> <span>{{DateUtils.AbsoluteLong .DateFrom}} - {{DateUtils.AbsoluteLong .DateUntil}}</span>
<!-- Period --> <!-- Period -->
<div class="ui floating dropdown jump filter"> <div class="ui floating dropdown jump">
<div class="ui basic compact button"> <div class="ui basic compact button">
{{ctx.Locale.Tr "repo.activity.period.filter_label"}} <strong>{{.PeriodText}}</strong> {{ctx.Locale.Tr "repo.activity.period.filter_label"}} <strong>{{.PeriodText}}</strong>
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
</div> </div>
<div class="menu"> <div class="left menu">
<a class="{{if eq .Period "daily"}}active {{end}}item" href="{{$.RepoLink}}/activity/daily">{{ctx.Locale.Tr "repo.activity.period.daily"}}</a> <a class="{{if eq .Period "daily"}}active {{end}}item" href="{{$.RepoLink}}/activity/daily">{{ctx.Locale.Tr "repo.activity.period.daily"}}</a>
<a class="{{if eq .Period "halfweekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/halfweekly">{{ctx.Locale.Tr "repo.activity.period.halfweekly"}}</a> <a class="{{if eq .Period "halfweekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/halfweekly">{{ctx.Locale.Tr "repo.activity.period.halfweekly"}}</a>
<a class="{{if eq .Period "weekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/weekly">{{ctx.Locale.Tr "repo.activity.period.weekly"}}</a> <a class="{{if eq .Period "weekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/weekly">{{ctx.Locale.Tr "repo.activity.period.weekly"}}</a>

View File

@ -100,7 +100,7 @@
</div> </div>
<span class="help">{{ctx.Locale.Tr "repo.release.prerelease_helper"}}</span> <span class="help">{{ctx.Locale.Tr "repo.release.prerelease_helper"}}</span>
<div class="divider tw-mt-0"></div> <div class="divider tw-mt-0"></div>
<div class="tw-flex tw-justify-end"> <div class="flex-text-block tw-justify-end">
{{if .PageIsEditRelease}} {{if .PageIsEditRelease}}
<a class="ui small button" href="{{.RepoLink}}/releases"> <a class="ui small button" href="{{.RepoLink}}/releases">
{{ctx.Locale.Tr "repo.release.cancel"}} {{ctx.Locale.Tr "repo.release.cancel"}}

View File

@ -2,7 +2,7 @@
{{$canReadCode := $.Permission.CanRead ctx.Consts.RepoUnitTypeCode}} {{$canReadCode := $.Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
{{if $canReadReleases}} {{if $canReadReleases}}
<div class="tw-flex"> <div class="flex-text-block">
<div class="tw-flex-1 tw-flex tw-items-center"> <div class="tw-flex-1 tw-flex tw-items-center">
<h2 class="ui compact small menu small-menu-items"> <h2 class="ui compact small menu small-menu-items">
<a class="{{if and .PageIsReleaseList (not .PageIsSingleTag)}}active {{end}}item" href="{{.RepoLink}}/releases">{{ctx.Locale.PrettyNumber .NumReleases}} {{ctx.Locale.TrN .NumReleases "repo.release" "repo.releases"}}</a> <a class="{{if and .PageIsReleaseList (not .PageIsSingleTag)}}active {{end}}item" href="{{.RepoLink}}/releases">{{ctx.Locale.PrettyNumber .NumReleases}} {{ctx.Locale.TrN .NumReleases "repo.release" "repo.releases"}}</a>

View File

@ -1,4 +1,4 @@
<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if $.IsStaringRepo}}unstar{{else}}star{{end}}"> <form class="flex-text-inline" hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if $.IsStaringRepo}}unstar{{else}}star{{end}}">
<div class="ui labeled button" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.star_guest_user"}}"{{end}}> <div class="ui labeled button" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.star_guest_user"}}"{{end}}>
{{$buttonText := ctx.Locale.Tr "repo.star"}} {{$buttonText := ctx.Locale.Tr "repo.star"}}
{{if $.IsStaringRepo}}{{$buttonText = ctx.Locale.Tr "repo.unstar"}}{{end}} {{if $.IsStaringRepo}}{{$buttonText = ctx.Locale.Tr "repo.unstar"}}{{end}}

View File

@ -1,3 +1,3 @@
<a class="ui label basic tiny button{{if .IsRelease}} primary{{end}}" href="{{.RepoLink}}/src/tag/{{.TagName|PathEscape}}"> <a class="ui basic label tw-p-1 {{if .IsRelease}}primary{{end}}" href="{{.RepoLink}}/src/tag/{{.TagName|PathEscape}}">
{{svg "octicon-tag"}} {{.TagName}} {{svg "octicon-tag"}} {{.TagName}}
</a> </a>

View File

@ -1,4 +1,4 @@
<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}unwatch{{else}}watch{{end}}"> <form class="flex-text-inline" hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}unwatch{{else}}watch{{end}}">
<div class="ui labeled button" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.watch_guest_user"}}"{{end}}> <div class="ui labeled button" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.watch_guest_user"}}"{{end}}>
{{$buttonText := ctx.Locale.Tr "repo.watch"}} {{$buttonText := ctx.Locale.Tr "repo.watch"}}
{{if $.IsWatchingRepo}}{{$buttonText = ctx.Locale.Tr "repo.unwatch"}}{{end}} {{if $.IsWatchingRepo}}{{$buttonText = ctx.Locale.Tr "repo.unwatch"}}{{end}}

View File

@ -35,7 +35,7 @@
<input name="message" aria-label="{{ctx.Locale.Tr "repo.wiki.default_commit_message"}}" placeholder="{{ctx.Locale.Tr "repo.wiki.default_commit_message"}}"> <input name="message" aria-label="{{ctx.Locale.Tr "repo.wiki.default_commit_message"}}" placeholder="{{ctx.Locale.Tr "repo.wiki.default_commit_message"}}">
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<div class="tw-text-right"> <div class="flex-text-block tw-justify-end">
<a class="ui basic cancel button" href="{{.Link}}">{{ctx.Locale.Tr "cancel"}}</a> <a class="ui basic cancel button" href="{{.Link}}">{{ctx.Locale.Tr "cancel"}}</a>
<button class="ui primary button">{{ctx.Locale.Tr "repo.wiki.save_page"}}</button> <button class="ui primary button">{{ctx.Locale.Tr "repo.wiki.save_page"}}</button>
</div> </div>

View File

@ -3,18 +3,18 @@
{{template "repo/header" .}} {{template "repo/header" .}}
{{$title := .title}} {{$title := .title}}
<div class="ui container"> <div class="ui container">
<div class="ui stackable grid"> <div class="ui dividing header flex-text-block tw-flex-wrap tw-justify-between">
<div class="ui eight wide column"> <div class="flex-text-block">
<div class="ui header"> <a class="ui basic button tw-px-3" title="{{ctx.Locale.Tr "repo.wiki.back_to_wiki"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}">{{svg "octicon-home"}}</a>
<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.back_to_wiki"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}">{{if .revision}}<span>{{.revision}}</span> {{end}}{{svg "octicon-home"}}</a> <div class="tw-flex-1 gt-ellipsis">
{{$title}} {{$title}}
<div class="ui sub header tw-break-anywhere"> <div class="ui sub header gt-ellipsis">
{{$timeSince := DateUtils.TimeSince .Author.When}} {{$timeSince := DateUtils.TimeSince .Author.When}}
{{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}} {{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}}
</div> </div>
</div> </div>
</div> </div>
<div class="ui eight wide column tw-text-right"> <div>
{{template "repo/clone_panel" .}} {{template "repo/clone_panel" .}}
</div> </div>
</div> </div>

View File

@ -33,7 +33,7 @@
<div class="ui dividing header"> <div class="ui dividing header">
<div class="flex-text-block tw-flex-wrap tw-justify-end"> <div class="flex-text-block tw-flex-wrap tw-justify-end">
<div class="flex-text-block tw-flex-1 tw-min-w-[300px]"> <div class="flex-text-block tw-flex-1 tw-min-w-[300px]">
<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" >{{if .CommitCount}}<span>{{.CommitCount}}</span> {{end}}{{svg "octicon-history"}}</a> <a class="ui basic button tw-px-3 tw-gap-3" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" >{{if .CommitCount}}<span>{{.CommitCount}}</span> {{end}}{{svg "octicon-history"}}</a>
<div class="tw-flex-1 gt-ellipsis"> <div class="tw-flex-1 gt-ellipsis">
{{$title}} {{$title}}
<div class="ui sub header gt-ellipsis"> <div class="ui sub header gt-ellipsis">

View File

@ -16,7 +16,7 @@
<div class="header"> <div class="header">
Registration Token Registration Token
</div> </div>
<div class="ui input"> <div class="ui action input">
<input type="text" value="{{.RegistrationToken}}" readonly> <input type="text" value="{{.RegistrationToken}}" readonly>
<button class="ui basic label button" aria-label="{{ctx.Locale.Tr "copy"}}" data-clipboard-text="{{.RegistrationToken}}"> <button class="ui basic label button" aria-label="{{ctx.Locale.Tr "copy"}}" data-clipboard-text="{{.RegistrationToken}}">
{{svg "octicon-copy" 14}} {{svg "octicon-copy" 14}}

View File

@ -1,6 +1,6 @@
{{- /* we do not need to set for/id here, global aria init code will add them automatically */ -}} {{- /* we do not need to set for/id here, global aria init code will add them automatically */ -}}
<label>{{.LabelText}}</label> <label>{{.LabelText}}</label>
<input class="avatar-file-with-cropper" name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp"> <input class="avatar-file-with-cropper" name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp" data-global-init="initAvatarUploader">
{{- /* the cropper-panel must be next sibling of the input "avatar" */ -}} {{- /* the cropper-panel must be next sibling of the input "avatar" */ -}}
<div class="cropper-panel tw-hidden"> <div class="cropper-panel tw-hidden">
<div class="tw-my-2">{{ctx.Locale.Tr "settings.cropper_prompt"}}</div> <div class="tw-my-2">{{ctx.Locale.Tr "settings.cropper_prompt"}}</div>

View File

@ -75,6 +75,108 @@
} }
} }
}, },
"/admin/actions/runners": {
"get": {
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Get all runners",
"operationId": "getAdminRunners",
"responses": {
"200": {
"$ref": "#/definitions/ActionRunnersResponse"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/admin/actions/runners/registration-token": {
"post": {
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Get an global actions runner registration token",
"operationId": "adminCreateRunnerRegistrationToken",
"responses": {
"200": {
"$ref": "#/responses/RegistrationToken"
}
}
}
},
"/admin/actions/runners/{runner_id}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Get an global runner",
"operationId": "getAdminRunner",
"parameters": [
{
"type": "string",
"description": "id of the runner",
"name": "runner_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/definitions/ActionRunner"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Delete an global runner",
"operationId": "deleteAdminRunner",
"parameters": [
{
"type": "string",
"description": "id of the runner",
"name": "runner_id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "runner has been deleted"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/admin/cron": { "/admin/cron": {
"get": { "get": {
"produces": [ "produces": [
@ -1697,6 +1799,38 @@
} }
} }
}, },
"/orgs/{org}/actions/runners": {
"get": {
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "Get org-level runners",
"operationId": "getOrgRunners",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/definitions/ActionRunnersResponse"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/orgs/{org}/actions/runners/registration-token": { "/orgs/{org}/actions/runners/registration-token": {
"get": { "get": {
"produces": [ "produces": [
@ -1721,6 +1855,106 @@
"$ref": "#/responses/RegistrationToken" "$ref": "#/responses/RegistrationToken"
} }
} }
},
"post": {
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "Get an organization's actions runner registration token",
"operationId": "orgCreateRunnerRegistrationToken",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/RegistrationToken"
}
}
}
},
"/orgs/{org}/actions/runners/{runner_id}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "Get an org-level runner",
"operationId": "getOrgRunner",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"required": true
},
{
"type": "string",
"description": "id of the runner",
"name": "runner_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/definitions/ActionRunner"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "Delete an org-level runner",
"operationId": "deleteOrgRunner",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"required": true
},
{
"type": "string",
"description": "id of the runner",
"name": "runner_id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "runner has been deleted"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
} }
}, },
"/orgs/{org}/actions/secrets": { "/orgs/{org}/actions/secrets": {
@ -4331,6 +4565,45 @@
} }
} }
}, },
"/repos/{owner}/{repo}/actions/runners": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Get repo-level runners",
"operationId": "getRepoRunners",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/definitions/ActionRunnersResponse"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/actions/runners/registration-token": { "/repos/{owner}/{repo}/actions/runners/registration-token": {
"get": { "get": {
"produces": [ "produces": [
@ -4362,6 +4635,127 @@
"$ref": "#/responses/RegistrationToken" "$ref": "#/responses/RegistrationToken"
} }
} }
},
"post": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Get a repository's actions runner registration token",
"operationId": "repoCreateRunnerRegistrationToken",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/RegistrationToken"
}
}
}
},
"/repos/{owner}/{repo}/actions/runners/{runner_id}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Get an repo-level runner",
"operationId": "getRepoRunner",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "id of the runner",
"name": "runner_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/definitions/ActionRunner"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Delete an repo-level runner",
"operationId": "deleteRepoRunner",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "id of the runner",
"name": "runner_id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "runner has been deleted"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
} }
}, },
"/repos/{owner}/{repo}/actions/runs/{run}/artifacts": { "/repos/{owner}/{repo}/actions/runs/{run}/artifacts": {
@ -4559,7 +4953,7 @@
], ],
"responses": { "responses": {
"204": { "204": {
"description": "delete one secret of the organization" "description": "delete one secret of the repository"
}, },
"400": { "400": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
@ -16869,6 +17263,29 @@
} }
} }
}, },
"/user/actions/runners": {
"get": {
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Get user-level runners",
"operationId": "getUserRunners",
"responses": {
"200": {
"$ref": "#/definitions/ActionRunnersResponse"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/user/actions/runners/registration-token": { "/user/actions/runners/registration-token": {
"get": { "get": {
"produces": [ "produces": [
@ -16884,6 +17301,83 @@
"$ref": "#/responses/RegistrationToken" "$ref": "#/responses/RegistrationToken"
} }
} }
},
"post": {
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Get an user's actions runner registration token",
"operationId": "userCreateRunnerRegistrationToken",
"responses": {
"200": {
"$ref": "#/responses/RegistrationToken"
}
}
}
},
"/user/actions/runners/{runner_id}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Get an user-level runner",
"operationId": "getUserRunner",
"parameters": [
{
"type": "string",
"description": "id of the runner",
"name": "runner_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/definitions/ActionRunner"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Delete an user-level runner",
"operationId": "deleteUserRunner",
"parameters": [
{
"type": "string",
"description": "id of the runner",
"name": "runner_id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "runner has been deleted"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
} }
}, },
"/user/actions/secrets/{secretname}": { "/user/actions/secrets/{secretname}": {
@ -19377,6 +19871,80 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"ActionRunner": {
"description": "ActionRunner represents a Runner",
"type": "object",
"properties": {
"busy": {
"type": "boolean",
"x-go-name": "Busy"
},
"ephemeral": {
"type": "boolean",
"x-go-name": "Ephemeral"
},
"id": {
"type": "integer",
"format": "int64",
"x-go-name": "ID"
},
"labels": {
"type": "array",
"items": {
"$ref": "#/definitions/ActionRunnerLabel"
},
"x-go-name": "Labels"
},
"name": {
"type": "string",
"x-go-name": "Name"
},
"status": {
"type": "string",
"x-go-name": "Status"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"ActionRunnerLabel": {
"description": "ActionRunnerLabel represents a Runner Label",
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64",
"x-go-name": "ID"
},
"name": {
"type": "string",
"x-go-name": "Name"
},
"type": {
"type": "string",
"x-go-name": "Type"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"ActionRunnersResponse": {
"description": "ActionRunnersResponse returns Runners",
"type": "object",
"properties": {
"runners": {
"type": "array",
"items": {
"$ref": "#/definitions/ActionRunner"
},
"x-go-name": "Entries"
},
"total_count": {
"type": "integer",
"format": "int64",
"x-go-name": "TotalCount"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"ActionTask": { "ActionTask": {
"description": "ActionTask represents a ActionTask", "description": "ActionTask represents a ActionTask",
"type": "object", "type": "object",
@ -27409,6 +27977,18 @@
} }
} }
}, },
"Runner": {
"description": "Runner",
"schema": {
"$ref": "#/definitions/ActionRunner"
}
},
"RunnerList": {
"description": "RunnerList",
"schema": {
"$ref": "#/definitions/ActionRunnersResponse"
}
},
"SearchResults": { "SearchResults": {
"description": "SearchResults", "description": "SearchResults",
"schema": { "schema": {

View File

@ -37,7 +37,7 @@
</div> </div>
{{if .SignedUser.CanCreateOrganization}} {{if .SignedUser.CanCreateOrganization}}
<a class="item" href="{{AppSubUrl}}/org/create"> <a class="item" href="{{AppSubUrl}}/org/create">
{{svg "octicon-plus"}}&nbsp;&nbsp;&nbsp;{{ctx.Locale.Tr "new_org"}} {{svg "octicon-plus" 16 "tw-ml-1 tw-mr-5"}}{{ctx.Locale.Tr "new_org"}}
</a> </a>
{{end}} {{end}}
</div> </div>
@ -77,7 +77,7 @@
{{end}} {{end}}
{{if .ContextUser.IsOrganization}} {{if .ContextUser.IsOrganization}}
<div class="right menu"> <div class="right menu tw-flex-wrap tw-justify-end">
<a class="{{if .PageIsNews}}active {{end}}item tw-ml-auto" href="{{.ContextUser.DashboardLink}}{{if .Team}}/{{PathEscape .Team.Name}}{{end}}"> <a class="{{if .PageIsNews}}active {{end}}item tw-ml-auto" href="{{.ContextUser.DashboardLink}}{{if .Team}}/{{PathEscape .Team.Name}}{{end}}">
{{svg "octicon-rss"}}&nbsp;{{ctx.Locale.Tr "activities"}} {{svg "octicon-rss"}}&nbsp;{{ctx.Locale.Tr "activities"}}
</a> </a>
@ -98,7 +98,7 @@
{{end}} {{end}}
<div class="item"> <div class="item">
<a class="ui primary basic button" href="{{.ContextUser.HomeLink}}" title="{{ctx.Locale.Tr "home.view_home" .ContextUser.Name}}"> <a class="ui primary basic button" href="{{.ContextUser.HomeLink}}" title="{{ctx.Locale.Tr "home.view_home" .ContextUser.Name}}">
{{ctx.Locale.Tr "home.view_home" (.ContextUser.ShortName 40)}} {{ctx.Locale.Tr "home.view_home" (.ContextUser.ShortName 20)}}
</a> </a>
</div> </div>
</div> </div>

View File

@ -0,0 +1,332 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"slices"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIActionsRunner(t *testing.T) {
t.Run("AdminRunner", testActionsRunnerAdmin)
t.Run("UserRunner", testActionsRunnerUser)
t.Run("OwnerRunner", testActionsRunnerOwner)
t.Run("RepoRunner", testActionsRunnerRepo)
}
func testActionsRunnerAdmin(t *testing.T) {
defer tests.PrepareTestEnv(t)()
adminUsername := "user1"
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
req := NewRequest(t, "POST", "/api/v1/admin/actions/runners/registration-token").AddTokenAuth(token)
tokenResp := MakeRequest(t, req, http.StatusOK)
var registrationToken struct {
Token string `json:"token"`
}
DecodeJSON(t, tokenResp, &registrationToken)
assert.NotEmpty(t, registrationToken.Token)
req = NewRequest(t, "GET", "/api/v1/admin/actions/runners").AddTokenAuth(token)
runnerListResp := MakeRequest(t, req, http.StatusOK)
runnerList := api.ActionRunnersResponse{}
DecodeJSON(t, runnerListResp, &runnerList)
assert.Len(t, runnerList.Entries, 4)
idx := slices.IndexFunc(runnerList.Entries, func(e *api.ActionRunner) bool { return e.ID == 34349 })
require.NotEqual(t, -1, idx)
expectedRunner := runnerList.Entries[idx]
assert.Equal(t, "runner_to_be_deleted", expectedRunner.Name)
assert.False(t, expectedRunner.Ephemeral)
assert.Len(t, expectedRunner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", expectedRunner.Labels[0].Name)
assert.Equal(t, "linux", expectedRunner.Labels[1].Name)
// Verify all returned runners can be requested and deleted
for _, runnerEntry := range runnerList.Entries {
// Verify get the runner by id
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/actions/runners/%d", runnerEntry.ID)).AddTokenAuth(token)
runnerResp := MakeRequest(t, req, http.StatusOK)
runner := api.ActionRunner{}
DecodeJSON(t, runnerResp, &runner)
assert.Equal(t, runnerEntry.Name, runner.Name)
assert.Equal(t, runnerEntry.ID, runner.ID)
assert.Equal(t, runnerEntry.Ephemeral, runner.Ephemeral)
assert.ElementsMatch(t, runnerEntry.Labels, runner.Labels)
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/admin/actions/runners/%d", runnerEntry.ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
// Verify runner deletion
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/actions/runners/%d", runnerEntry.ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
}
func testActionsRunnerUser(t *testing.T) {
defer tests.PrepareTestEnv(t)()
userUsername := "user1"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteUser)
req := NewRequest(t, "POST", "/api/v1/user/actions/runners/registration-token").AddTokenAuth(token)
tokenResp := MakeRequest(t, req, http.StatusOK)
var registrationToken struct {
Token string `json:"token"`
}
DecodeJSON(t, tokenResp, &registrationToken)
assert.NotEmpty(t, registrationToken.Token)
req = NewRequest(t, "GET", "/api/v1/user/actions/runners").AddTokenAuth(token)
runnerListResp := MakeRequest(t, req, http.StatusOK)
runnerList := api.ActionRunnersResponse{}
DecodeJSON(t, runnerListResp, &runnerList)
assert.Len(t, runnerList.Entries, 1)
assert.Equal(t, "runner_to_be_deleted-user", runnerList.Entries[0].Name)
assert.Equal(t, int64(34346), runnerList.Entries[0].ID)
assert.False(t, runnerList.Entries[0].Ephemeral)
assert.Len(t, runnerList.Entries[0].Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name)
assert.Equal(t, "linux", runnerList.Entries[0].Labels[1].Name)
// Verify get the runner by id
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
runnerResp := MakeRequest(t, req, http.StatusOK)
runner := api.ActionRunner{}
DecodeJSON(t, runnerResp, &runner)
assert.Equal(t, "runner_to_be_deleted-user", runner.Name)
assert.Equal(t, int64(34346), runner.ID)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
assert.Equal(t, "linux", runner.Labels[1].Name)
// Verify delete the runner by id
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
// Verify runner deletion
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func testActionsRunnerOwner(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("GetRunner", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
// Verify get the runner by id with read scope
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347)).AddTokenAuth(token)
runnerResp := MakeRequest(t, req, http.StatusOK)
runner := api.ActionRunner{}
DecodeJSON(t, runnerResp, &runner)
assert.Equal(t, "runner_to_be_deleted-org", runner.Name)
assert.Equal(t, int64(34347), runner.ID)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
assert.Equal(t, "linux", runner.Labels[1].Name)
})
t.Run("Access", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteOrganization)
req := NewRequest(t, "POST", "/api/v1/orgs/org3/actions/runners/registration-token").AddTokenAuth(token)
tokenResp := MakeRequest(t, req, http.StatusOK)
var registrationToken struct {
Token string `json:"token"`
}
DecodeJSON(t, tokenResp, &registrationToken)
assert.NotEmpty(t, registrationToken.Token)
req = NewRequest(t, "GET", "/api/v1/orgs/org3/actions/runners").AddTokenAuth(token)
runnerListResp := MakeRequest(t, req, http.StatusOK)
runnerList := api.ActionRunnersResponse{}
DecodeJSON(t, runnerListResp, &runnerList)
assert.Len(t, runnerList.Entries, 1)
assert.Equal(t, "runner_to_be_deleted-org", runnerList.Entries[0].Name)
assert.Equal(t, int64(34347), runnerList.Entries[0].ID)
assert.False(t, runnerList.Entries[0].Ephemeral)
assert.Len(t, runnerList.Entries[0].Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name)
assert.Equal(t, "linux", runnerList.Entries[0].Labels[1].Name)
// Verify get the runner by id
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
runnerResp := MakeRequest(t, req, http.StatusOK)
runner := api.ActionRunner{}
DecodeJSON(t, runnerResp, &runner)
assert.Equal(t, "runner_to_be_deleted-org", runner.Name)
assert.Equal(t, int64(34347), runner.ID)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
assert.Equal(t, "linux", runner.Labels[1].Name)
// Verify delete the runner by id
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
// Verify runner deletion
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("DeleteReadScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
// Verify delete the runner by id is forbidden with read scope
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("GetRepoScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
// Verify get the runner by id with read scope
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("GetAdminRunner", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
// Verify get a runner by id of different entity is not found
// runner.EditableInContext(ownerID, repoID) false
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34349)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("DeleteAdminRunner", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteOrganization)
// Verify delete a runner by id of different entity is not found
// runner.EditableInContext(ownerID, repoID) false
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34349)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
})
}
func testActionsRunnerRepo(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("GetRunner", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
// Verify get the runner by id with read scope
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348)).AddTokenAuth(token)
runnerResp := MakeRequest(t, req, http.StatusOK)
runner := api.ActionRunner{}
DecodeJSON(t, runnerResp, &runner)
assert.Equal(t, "runner_to_be_deleted-repo1", runner.Name)
assert.Equal(t, int64(34348), runner.ID)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
assert.Equal(t, "linux", runner.Labels[1].Name)
})
t.Run("Access", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteRepository)
req := NewRequest(t, "POST", "/api/v1/repos/user2/repo1/actions/runners/registration-token").AddTokenAuth(token)
tokenResp := MakeRequest(t, req, http.StatusOK)
var registrationToken struct {
Token string `json:"token"`
}
DecodeJSON(t, tokenResp, &registrationToken)
assert.NotEmpty(t, registrationToken.Token)
req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/actions/runners").AddTokenAuth(token)
runnerListResp := MakeRequest(t, req, http.StatusOK)
runnerList := api.ActionRunnersResponse{}
DecodeJSON(t, runnerListResp, &runnerList)
assert.Len(t, runnerList.Entries, 1)
assert.Equal(t, "runner_to_be_deleted-repo1", runnerList.Entries[0].Name)
assert.Equal(t, int64(34348), runnerList.Entries[0].ID)
assert.False(t, runnerList.Entries[0].Ephemeral)
assert.Len(t, runnerList.Entries[0].Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name)
assert.Equal(t, "linux", runnerList.Entries[0].Labels[1].Name)
// Verify get the runner by id
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
runnerResp := MakeRequest(t, req, http.StatusOK)
runner := api.ActionRunner{}
DecodeJSON(t, runnerResp, &runner)
assert.Equal(t, "runner_to_be_deleted-repo1", runner.Name)
assert.Equal(t, int64(34348), runner.ID)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
assert.Equal(t, "linux", runner.Labels[1].Name)
// Verify delete the runner by id
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
// Verify runner deletion
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("DeleteReadScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
// Verify delete the runner by id is forbidden with read scope
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("GetOrganizationScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
// Verify get the runner by id with read scope
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("GetAdminRunnerNotFound", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
// Verify get a runner by id of different entity is not found
// runner.EditableInContext(ownerID, repoID) false
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34349)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("DeleteAdminRunnerNotFound", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteRepository)
// Verify delete a runner by id of different entity is not found
// runner.EditableInContext(ownerID, repoID) false
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34349)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
})
}

View File

@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestPackageSwift(t *testing.T) { func TestPackageSwift(t *testing.T) {
@ -34,6 +35,7 @@ func TestPackageSwift(t *testing.T) {
packageName := "test_package" packageName := "test_package"
packageID := packageScope + "." + packageName packageID := packageScope + "." + packageName
packageVersion := "1.0.3" packageVersion := "1.0.3"
packageVersion2 := "1.0.4"
packageAuthor := "KN4CK3R" packageAuthor := "KN4CK3R"
packageDescription := "Gitea Test Package" packageDescription := "Gitea Test Package"
packageRepositoryURL := "https://gitea.io/gitea/gitea" packageRepositoryURL := "https://gitea.io/gitea/gitea"
@ -183,6 +185,94 @@ func TestPackageSwift(t *testing.T) {
) )
}) })
t.Run("UploadMultipart", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
uploadPackage := func(t *testing.T, url string, expectedStatus int, sr io.Reader, metadata string) {
var body bytes.Buffer
mpw := multipart.NewWriter(&body)
// Read the source archive content
sourceContent, err := io.ReadAll(sr)
assert.NoError(t, err)
mpw.WriteField("source-archive", string(sourceContent))
if metadata != "" {
mpw.WriteField("metadata", metadata)
}
mpw.Close()
req := NewRequestWithBody(t, "PUT", url, &body).
SetHeader("Content-Type", mpw.FormDataContentType()).
SetHeader("Accept", swift_router.AcceptJSON).
AddBasicAuth(user.Name)
MakeRequest(t, req, expectedStatus)
}
createArchive := func(files map[string]string) *bytes.Buffer {
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
for filename, content := range files {
w, _ := zw.Create(filename)
w.Write([]byte(content))
}
zw.Close()
return &buf
}
uploadURL := fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion2)
req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{}))
MakeRequest(t, req, http.StatusUnauthorized)
// Test with metadata as form field
uploadPackage(
t,
uploadURL,
http.StatusCreated,
createArchive(map[string]string{
"Package.swift": contentManifest1,
"Package@swift-5.6.swift": contentManifest2,
}),
`{"name":"`+packageName+`","version":"`+packageVersion2+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
)
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeSwift)
assert.NoError(t, err)
require.Len(t, pvs, 2) // ATTENTION: many subtests are unable to run separately, they depend on the results of previous tests
thisPackageVersion := pvs[0]
pd, err := packages.GetPackageDescriptor(db.DefaultContext, thisPackageVersion)
assert.NoError(t, err)
assert.NotNil(t, pd.SemVer)
assert.Equal(t, packageID, pd.Package.Name)
assert.Equal(t, packageVersion2, pd.Version.Version)
assert.IsType(t, &swift_module.Metadata{}, pd.Metadata)
metadata := pd.Metadata.(*swift_module.Metadata)
assert.Equal(t, packageDescription, metadata.Description)
assert.Len(t, metadata.Manifests, 2)
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
assert.Len(t, pd.VersionProperties, 1)
assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, thisPackageVersion.ID)
assert.NoError(t, err)
assert.Len(t, pfs, 1)
assert.Equal(t, fmt.Sprintf("%s-%s.zip", packageName, packageVersion2), pfs[0].Name)
assert.True(t, pfs[0].IsLead)
uploadPackage(
t,
uploadURL,
http.StatusConflict,
createArchive(map[string]string{
"Package.swift": contentManifest1,
}),
"",
)
})
t.Run("Download", func(t *testing.T) { t.Run("Download", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
@ -211,7 +301,7 @@ func TestPackageSwift(t *testing.T) {
SetHeader("Accept", swift_router.AcceptJSON) SetHeader("Accept", swift_router.AcceptJSON)
resp := MakeRequest(t, req, http.StatusOK) resp := MakeRequest(t, req, http.StatusOK)
versionURL := setting.AppURL + url[1:] + fmt.Sprintf("/%s/%s/%s", packageScope, packageName, packageVersion) versionURL := setting.AppURL + url[1:] + fmt.Sprintf("/%s/%s/%s", packageScope, packageName, packageVersion2)
assert.Equal(t, "1", resp.Header().Get("Content-Version")) assert.Equal(t, "1", resp.Header().Get("Content-Version"))
assert.Equal(t, fmt.Sprintf(`<%s>; rel="latest-version"`, versionURL), resp.Header().Get("Link")) assert.Equal(t, fmt.Sprintf(`<%s>; rel="latest-version"`, versionURL), resp.Header().Get("Link"))
@ -221,9 +311,9 @@ func TestPackageSwift(t *testing.T) {
var result *swift_router.EnumeratePackageVersionsResponse var result *swift_router.EnumeratePackageVersionsResponse
DecodeJSON(t, resp, &result) DecodeJSON(t, resp, &result)
assert.Len(t, result.Releases, 1) assert.Len(t, result.Releases, 2)
assert.Contains(t, result.Releases, packageVersion) assert.Contains(t, result.Releases, packageVersion2)
assert.Equal(t, versionURL, result.Releases[packageVersion].URL) assert.Equal(t, versionURL, result.Releases[packageVersion2].URL)
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.json", url, packageScope, packageName)). req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.json", url, packageScope, packageName)).
AddBasicAuth(user.Name) AddBasicAuth(user.Name)

View File

@ -224,6 +224,7 @@ progress::-moz-progress-bar {
} }
.unselectable, .unselectable,
.btn,
.button, .button,
.lines-num, .lines-num,
.lines-commit, .lines-commit,
@ -1037,12 +1038,13 @@ table th[data-sortt-desc] .svg {
text-align: left; text-align: left;
} }
.ellipsis-button { .ui.button.ellipsis-button {
padding: 0 5px 8px !important; padding: 0 5px 8px;
display: inline-block !important; display: inline-block;
font-weight: var(--font-weight-semibold) !important; font-weight: var(--font-weight-semibold);
line-height: 6px !important; line-height: 8px;
vertical-align: middle !important; vertical-align: middle;
min-height: 0;
} }
.precolors { .precolors {

View File

@ -116,6 +116,7 @@
max-width: 200px; max-width: 200px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
min-height: 0;
} }
#git-graph-container #graph-raw-list { #git-graph-container #graph-raw-list {

View File

@ -1,8 +1,3 @@
.markup .code-block,
.markup .mermaid-block {
position: relative;
}
.markup .code-copy { .markup .code-copy {
position: absolute; position: absolute;
top: 8px; top: 8px;
@ -28,8 +23,8 @@
background: var(--color-secondary-dark-1) !important; background: var(--color-secondary-dark-1) !important;
} }
.markup .code-block:hover .code-copy, .markup .code-block-container:hover .code-copy,
.markup .mermaid-block:hover .code-copy { .markup .code-block:hover .code-copy {
visibility: visible; visibility: visible;
animation: fadein 0.2s both; animation: fadein 0.2s both;
} }

View File

@ -443,13 +443,25 @@
} }
.markup pre > code { .markup pre > code {
padding: 0;
margin: 0;
font-size: 100%; font-size: 100%;
}
.markup .code-block,
.markup .code-block-container {
position: relative;
}
.markup .code-block-container.code-overflow-wrap pre > code {
white-space: pre-wrap; white-space: pre-wrap;
overflow-wrap: anywhere; }
background: transparent;
border: 0; .markup .code-block-container.code-overflow-scroll pre {
overflow-x: auto;
}
.markup .code-block-container.code-overflow-scroll pre > code {
white-space: pre;
overflow-wrap: normal;
} }
.markup .highlight { .markup .highlight {
@ -470,16 +482,11 @@
word-break: normal; word-break: normal;
} }
.markup pre {
word-wrap: normal;
}
.markup pre code, .markup pre code,
.markup pre tt { .markup pre tt {
display: inline; display: inline;
padding: 0; padding: 0;
line-height: inherit; line-height: inherit;
word-wrap: normal;
background-color: transparent; background-color: transparent;
border: 0; border: 0;
} }
@ -522,20 +529,6 @@
margin: 0 0.25em; margin: 0 0.25em;
} }
.file-revisions-btn {
display: block;
float: left;
margin-bottom: 2px !important;
padding: 11px !important;
margin-right: 10px !important;
}
.file-revisions-btn i {
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
.markup-content-iframe { .markup-content-iframe {
display: block; display: block;
border: none; border: none;

View File

@ -1,20 +1,15 @@
/* based on Fomantic UI checkbox module, with just the parts extracted that we use. If you find any
unused rules here after refactoring, please remove them. */
.ui.button { .ui.button {
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-flex;
min-height: 1em;
outline: none; outline: none;
vertical-align: baseline;
font-family: var(--fonts-regular); font-family: var(--fonts-regular);
margin: 0 0.25em 0 0; margin: 0 0.25em 0 0;
padding: 0.78571429em 1.5em;
font-weight: var(--font-weight-normal); font-weight: var(--font-weight-normal);
font-size: 1rem;
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
line-height: 1; line-height: 1;
border-radius: 0.28571429rem; border-radius: var(--border-radius);
user-select: none; user-select: none;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
justify-content: center; justify-content: center;
@ -58,12 +53,13 @@
pointer-events: none !important; pointer-events: none !important;
} }
/* there is no "ui labeled icon button" support" because it is not used */
.ui.labeled.button:not(.icon) { .ui.labeled.button:not(.icon) {
display: inline-flex;
flex-direction: row; flex-direction: row;
background: none; background: none;
padding: 0 !important; padding: 0;
border: none; border: none;
min-height: unset;
} }
.ui.labeled.button > .button { .ui.labeled.button > .button {
margin: 0; margin: 0;
@ -102,47 +98,60 @@
margin: 0 -0.21428571em 0 0.42857143em; margin: 0 -0.21428571em 0 0.42857143em;
} }
/* reference sizes (not exactly at the moment): normal: padding-x=21, height=38 ; compact: padding-x=15, height=32 */
.ui.button { /* stylelint-disable-line no-duplicate-selectors */
min-height: 38px;
padding: 0.57em /* around 8px */ 1.43em /* around 20px */;
}
.ui.compact.buttons .button, .ui.compact.buttons .button,
.ui.compact.button { .ui.compact.button {
padding: 0.58928571em 1.125em; padding: 0.42em /* around 8px */ 1.07em /* around 15px */;
min-height: 32px;
} }
.ui.compact.icon.buttons .button, .ui.compact.icon.buttons .button,
.ui.compact.icon.button { .ui.compact.icon.button {
padding: 0.58928571em; padding: 0.57em /* around 8px */;
}
.ui.compact.labeled.icon.button {
padding: 0.58928571em 3.69642857em;
}
.ui.compact.labeled.icon.button > .icon {
padding: 0.58928571em 0;
} }
.ui.buttons .button, /* reference size: mini: padding-x=16, height=30 ; compact: padding-x=12, height=26 */
.ui.button {
font-size: 1rem;
}
.ui.mini.buttons .dropdown, .ui.mini.buttons .dropdown,
.ui.mini.buttons .dropdown .menu > .item, .ui.mini.buttons .dropdown .menu > .item,
.ui.mini.buttons .button, .ui.mini.buttons .button,
.ui.ui.ui.ui.mini.button { .ui.ui.ui.ui.mini.button {
font-size: 0.78571429rem; font-size: 11px;
min-height: 30px;
} }
.ui.ui.ui.ui.mini.button.compact {
min-height: 26px;
}
/* reference size: tiny: padding-x=18, height=32 ; compact: padding-x=13, height=28 */
.ui.tiny.buttons .dropdown, .ui.tiny.buttons .dropdown,
.ui.tiny.buttons .dropdown .menu > .item, .ui.tiny.buttons .dropdown .menu > .item,
.ui.tiny.buttons .button, .ui.tiny.buttons .button,
.ui.ui.ui.ui.tiny.button { .ui.ui.ui.ui.tiny.button {
font-size: 0.85714286rem; font-size: 12px;
min-height: 32px;
} }
.ui.ui.ui.ui.tiny.button.compact {
min-height: 28px;
}
/* reference size: small: padding-x=19, height=34 ; compact: padding-x=14, height=30 */
.ui.small.buttons .dropdown, .ui.small.buttons .dropdown,
.ui.small.buttons .dropdown .menu > .item, .ui.small.buttons .dropdown .menu > .item,
.ui.small.buttons .button, .ui.small.buttons .button,
.ui.ui.ui.ui.small.button { .ui.ui.ui.ui.small.button {
font-size: 0.92857143rem; font-size: 13px;
min-height: 34px;
}
.ui.ui.ui.ui.small.button.compact {
min-height: 30px;
} }
.ui.icon.buttons .button, .ui.icon.buttons .button,
.ui.icon.button:not(.compact) { .ui.icon.button:not(.compact) {
padding: 0.78571429em; padding: 0.57em;
} }
.ui.icon.buttons .button > .icon, .ui.icon.buttons .button > .icon,
.ui.icon.button > .icon { .ui.icon.button > .icon {
@ -152,12 +161,12 @@
.ui.basic.buttons .button, .ui.basic.buttons .button,
.ui.basic.button { .ui.basic.button {
border-radius: 0.28571429rem; border-radius: var(--border-radius);
background: none; background: none;
} }
.ui.basic.buttons { .ui.basic.buttons {
border: 1px solid var(--color-secondary); border: 1px solid var(--color-secondary);
border-radius: 0.28571429rem; border-radius: var(--border-radius);
} }
.ui.basic.buttons .button { .ui.basic.buttons .button {
border-radius: 0; border-radius: 0;
@ -188,29 +197,6 @@
background: var(--color-active); background: var(--color-active);
} }
.ui.labeled.icon.button {
position: relative;
padding-left: 4.07142857em !important;
padding-right: 1.5em !important;
}
.ui.labeled.icon.button > .icon {
position: absolute;
top: 0;
left: 0;
height: 100%;
line-height: 1;
border-radius: 0;
border-top-left-radius: inherit;
border-bottom-left-radius: inherit;
text-align: center;
animation: none;
padding: 0.78571429em 0;
margin: 0;
width: 2.57142857em;
background: var(--color-hover);
}
.ui.button.toggle.active { .ui.button.toggle.active {
background-color: var(--color-green); background-color: var(--color-green);
color: var(--color-white); color: var(--color-white);
@ -366,6 +352,14 @@ a.btn:hover {
color: inherit; color: inherit;
} }
.btn.tiny {
font-size: 12px;
}
.btn.small {
font-size: 13px;
}
/* By default, Fomantic UI doesn't support "bordered" buttons group, but Gitea would like to use it. /* By default, Fomantic UI doesn't support "bordered" buttons group, but Gitea would like to use it.
And the default buttons always have borders now (not the same as Fomantic UI's default buttons, see above). And the default buttons always have borders now (not the same as Fomantic UI's default buttons, see above).
It needs some tricks to tweak the left/right borders with active state */ It needs some tricks to tweak the left/right borders with active state */
@ -379,12 +373,12 @@ It needs some tricks to tweak the left/right borders with active state */
.ui.buttons .button:first-child { .ui.buttons .button:first-child {
border-left: none; border-left: none;
margin-left: 0; margin-left: 0;
border-top-left-radius: 0.28571429rem; border-top-left-radius: var(--border-radius);
border-bottom-left-radius: 0.28571429rem; border-bottom-left-radius: var(--border-radius);
} }
.ui.buttons .button:last-child { .ui.buttons .button:last-child {
border-top-right-radius: 0.28571429rem; border-top-right-radius: var(--border-radius);
border-bottom-right-radius: 0.28571429rem; border-bottom-right-radius: var(--border-radius);
} }
.ui.buttons .button:hover { .ui.buttons .button:hover {
@ -414,10 +408,3 @@ It needs some tricks to tweak the left/right borders with active state */
.ui.buttons .button.active + .button { .ui.buttons .button.active + .button {
border-left: none; border-left: none;
} }
/* apply the vertical padding of .compact to non-compact buttons when they contain a svg as they
would otherwise appear too large. Seen on "RSS Feed" button on repo releases tab. */
.ui.small.button:not(.compact):has(.svg) {
padding-top: 0.58928571em;
padding-bottom: 0.58928571em;
}

View File

@ -92,6 +92,10 @@
} }
.tippy-box[data-theme="menu"] .item:focus { .tippy-box[data-theme="menu"] .item:focus {
background: var(--color-hover);
}
.tippy-box[data-theme="menu"] .item.active {
background: var(--color-active); background: var(--color-active);
} }

View File

@ -1629,21 +1629,17 @@ td .commit-summary {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
flex-wrap: wrap;
} }
.repo-button-row-left { .repo-button-row-left {
flex-grow: 1; flex-grow: 1;
} }
.repo-button-row .button { .repo-button-row .ui.button {
padding: 6px 10px !important;
height: 30px;
flex-shrink: 0; flex-shrink: 0;
margin: 0; margin: 0;
} min-height: 30px;
.repo-button-row .button.dropdown:not(.icon) {
padding-right: 22px !important; /* normal buttons have !important paddings, so we need to override it for dropdown (Add File) icons */
} }
tbody.commit-list { tbody.commit-list {
@ -1788,12 +1784,12 @@ tbody.commit-list {
.resolved-placeholder { .resolved-placeholder {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 14px !important; justify-content: space-between;
padding: 8px !important; margin: 4px;
font-weight: var(--font-weight-normal) !important; padding: 8px;
border: 1px solid var(--color-secondary) !important; border: 1px solid var(--color-secondary);
border-radius: var(--border-radius) !important; border-radius: var(--border-radius);
margin: 4px !important; background: var(--color-box-header);
} }
.resolved-placeholder + .comment-code-cloud { .resolved-placeholder + .comment-code-cloud {
@ -2221,10 +2217,11 @@ tbody.commit-list {
max-width: min(400px, 90vw); max-width: min(400px, 90vw);
} }
.branch-selector-dropdown .branch-dropdown-button { .branch-selector-dropdown .ui.button.branch-dropdown-button {
margin: 0; margin: 0;
max-width: 340px; max-width: 340px;
line-height: var(--line-height-default); line-height: var(--line-height-default);
padding: 0 0.5em 0 0.75em;
} }
/* FIXME: These media selectors are not ideal (just keep them from old code). /* FIXME: These media selectors are not ideal (just keep them from old code).

View File

@ -1,7 +1,7 @@
/* only used by "repo/empty.tmpl" */ /* only used by "repo/empty.tmpl" */
.clone-buttons-combo { .clone-buttons-combo {
display: flex; display: flex;
align-items: center; align-items: stretch;
flex: 1; flex: 1;
} }
@ -11,7 +11,6 @@
.ui.action.input.clone-buttons-combo input { .ui.action.input.clone-buttons-combo input {
border-radius: 0; /* override fomantic border-radius for ".ui.input > input" */ border-radius: 0; /* override fomantic border-radius for ".ui.input > input" */
height: 30px;
} }
/* used by the clone-panel popup */ /* used by the clone-panel popup */

View File

@ -27,47 +27,3 @@
.repo-header .flex-item-trailing { .repo-header .flex-item-trailing {
flex-wrap: nowrap; flex-wrap: nowrap;
} }
.repo-buttons {
align-items: center;
display: flex;
flex-flow: row wrap;
word-break: keep-all;
gap: 0.25em;
}
.repo-buttons button[disabled] ~ .label {
opacity: var(--opacity-disabled);
color: var(--color-text-dark);
background: var(--color-light-mimic-enabled) !important;
}
.repo-buttons button[disabled] ~ .label:hover {
color: var(--color-primary-dark-1);
}
.repo-buttons .ui.labeled.button.disabled {
pointer-events: inherit !important;
}
.repo-buttons .ui.labeled.button.disabled > .label {
color: var(--color-text-dark);
background: var(--color-light-mimic-enabled) !important;
}
.repo-buttons .ui.labeled.button.disabled > .label:hover {
color: var(--color-primary-dark-1);
}
.repo-buttons .ui.labeled.button.disabled > .button {
pointer-events: none !important;
}
@media (max-width: 767.98px) {
.repo-buttons .ui.button,
.repo-buttons .ui.label {
padding-left: 8px;
padding-right: 8px;
margin: 0;
}
}

View File

@ -2,17 +2,15 @@
color: var(--color-text-dark) !important; color: var(--color-text-dark) !important;
} }
.code-line-button { .ui.button.code-line-button {
border: 1px solid var(--color-secondary); border: 1px solid var(--color-secondary);
border-radius: var(--border-radius); padding: 1px 4px;
padding: 1px 4px !important; margin: 0;
min-height: 0;
position: absolute; position: absolute;
font-family: var(--fonts-regular); left: 6px;
left: 0;
transform: translateX(calc(-50% + 6px));
cursor: pointer;
} }
.code-line-button:hover { .ui.button.code-line-button:hover {
background: var(--color-secondary) !important; background: var(--color-secondary);
} }

View File

@ -1,6 +1,6 @@
.list-header { .list-header {
display: flex; display: flex;
align-items: center; align-items: stretch;
flex-wrap: wrap; flex-wrap: wrap;
gap: .5rem; gap: .5rem;
} }
@ -8,9 +8,8 @@
.list-header-search { .list-header-search {
display: flex; display: flex;
flex: 1; flex: 1;
align-items: center; align-items: stretch;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center;
min-width: 200px; /* to enable flexbox wrapping on mobile */ min-width: 200px; /* to enable flexbox wrapping on mobile */
} }

View File

@ -1,15 +1,8 @@
.show-outdated,
.hide-outdated {
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
margin-right: 0 !important;
}
.ui.button.add-code-comment { .ui.button.add-code-comment {
padding: 2px; padding: 2px;
position: absolute; position: absolute;
margin-left: -22px; margin-left: -22px;
min-height: 0;
z-index: 5; z-index: 5;
opacity: 0; opacity: 0;
transition: transform 0.1s ease-in-out; transition: transform 0.1s ease-in-out;
@ -58,11 +51,6 @@
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
.show-outdated:hover,
.hide-outdated:hover {
text-decoration: underline;
}
.comment-code-cloud { .comment-code-cloud {
padding: 0.5rem 1rem !important; padding: 0.5rem 1rem !important;
position: relative; position: relative;

View File

@ -1173,11 +1173,6 @@ select.ui.dropdown {
border-radius: 0.28571429rem !important; border-radius: 0.28571429rem !important;
} }
/* GITEA-PATCH: gitea also have "right menu" support */
.ui.dropdown > .right.menu {
left: auto;
right: 0;
}
/* Leftward Opening Menu */ /* Leftward Opening Menu */
.ui.dropdown > .left.menu { .ui.dropdown > .left.menu {
left: auto !important; left: auto !important;

View File

@ -212,7 +212,7 @@ export default defineComponent({
<div class="ui scrolling dropdown custom diff-commit-selector"> <div class="ui scrolling dropdown custom diff-commit-selector">
<button <button
ref="expandBtn" ref="expandBtn"
class="ui basic button" class="ui tiny basic button"
@click.stop="toggleMenu()" @click.stop="toggleMenu()"
:data-tooltip-content="locale.filter_changes_by_commit" :data-tooltip-content="locale.filter_changes_by_commit"
aria-haspopup="true" aria-haspopup="true"

View File

@ -217,13 +217,13 @@ export default defineComponent({
</script> </script>
<template> <template>
<div class="ui dropdown custom branch-selector-dropdown ellipsis-text-items"> <div class="ui dropdown custom branch-selector-dropdown ellipsis-text-items">
<div tabindex="0" class="ui button branch-dropdown-button" @click="menuVisible = !menuVisible"> <div tabindex="0" class="ui compact button branch-dropdown-button" @click="menuVisible = !menuVisible">
<span class="flex-text-block gt-ellipsis"> <span class="flex-text-block gt-ellipsis">
<template v-if="dropdownFixedText">{{ dropdownFixedText }}</template> <template v-if="dropdownFixedText">{{ dropdownFixedText }}</template>
<template v-else> <template v-else>
<svg-icon v-if="currentRefType === 'tag'" name="octicon-tag"/> <svg-icon v-if="currentRefType === 'tag'" name="octicon-tag"/>
<svg-icon v-else name="octicon-git-branch"/> <svg-icon v-else name="octicon-git-branch"/>
<strong ref="dropdownRefName" class="tw-ml-2 tw-inline-block gt-ellipsis">{{ currentRefShortName }}</strong> <strong ref="dropdownRefName" class="tw-inline-block gt-ellipsis">{{ currentRefShortName }}</strong>
</template> </template>
</span> </span>
<svg-icon name="octicon-triangle-down" :size="14" class="dropdown icon"/> <svg-icon name="octicon-triangle-down" :size="14" class="dropdown icon"/>

View File

@ -353,12 +353,12 @@ export default defineComponent({
</div> </div>
<div> <div>
<!-- Contribution type --> <!-- Contribution type -->
<div class="ui dropdown jump" id="repo-contributors"> <div class="ui floating dropdown jump" id="repo-contributors">
<div class="ui basic compact button tw-mr-0"> <div class="ui basic compact button tw-mr-0">
<span class="not-mobile">{{ locale.filterLabel }}</span> <strong>{{ locale.contributionType[type] }}</strong> <span class="not-mobile">{{ locale.filterLabel }}</span> <strong>{{ locale.contributionType[type] }}</strong>
<svg-icon name="octicon-triangle-down" :size="14"/> <svg-icon name="octicon-triangle-down" :size="14"/>
</div> </div>
<div class="right menu"> <div class="left menu">
<div :class="['item', {'selected': type === 'commits'}]" data-value="commits"> <div :class="['item', {'selected': type === 'commits'}]" data-value="commits">
{{ locale.contributionType.commits }} {{ locale.contributionType.commits }}
</div> </div>

View File

@ -1,7 +1,6 @@
import {checkAppUrl} from '../common-page.ts'; import {checkAppUrl} from '../common-page.ts';
import {hideElem, queryElems, showElem, toggleElem} from '../../utils/dom.ts'; import {hideElem, queryElems, showElem, toggleElem} from '../../utils/dom.ts';
import {POST} from '../../modules/fetch.ts'; import {POST} from '../../modules/fetch.ts';
import {initAvatarUploaderWithCropper} from '../comp/Cropper.ts';
import {fomanticQuery} from '../../modules/fomantic/base.ts'; import {fomanticQuery} from '../../modules/fomantic/base.ts';
const {appSubUrl} = window.config; const {appSubUrl} = window.config;
@ -23,8 +22,6 @@ export function initAdminCommon(): void {
initAdminUser(); initAdminUser();
initAdminAuthentication(); initAdminAuthentication();
initAdminNotice(); initAdminNotice();
queryElems(document, '.avatar-file-with-cropper', initAvatarUploaderWithCropper);
} }
function initAdminUser() { function initAdminUser() {

View File

@ -1,6 +1,5 @@
import {initCompLabelEdit} from './comp/LabelEdit.ts'; import {initCompLabelEdit} from './comp/LabelEdit.ts';
import {queryElems, toggleElem} from '../utils/dom.ts'; import {toggleElem} from '../utils/dom.ts';
import {initAvatarUploaderWithCropper} from './comp/Cropper.ts';
export function initCommonOrganization() { export function initCommonOrganization() {
if (!document.querySelectorAll('.organization').length) { if (!document.querySelectorAll('.organization').length) {
@ -14,6 +13,4 @@ export function initCommonOrganization() {
// Labels // Labels
initCompLabelEdit('.page-content.organization.settings.labels'); initCompLabelEdit('.page-content.organization.settings.labels');
queryElems(document, '.avatar-file-with-cropper', initAvatarUploaderWithCropper);
} }

View File

@ -3,6 +3,7 @@ import {showGlobalErrorMessage} from '../bootstrap.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts';
import {queryElems} from '../utils/dom.ts'; import {queryElems} from '../utils/dom.ts';
import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts'; import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts';
import {initAvatarUploaderWithCropper} from './comp/Cropper.ts';
const {appUrl} = window.config; const {appUrl} = window.config;
@ -80,6 +81,10 @@ export function initGlobalTabularMenu() {
fomanticQuery('.ui.menu.tabular:not(.custom) .item').tab(); fomanticQuery('.ui.menu.tabular:not(.custom) .item').tab();
} }
export function initGlobalAvatarUploader() {
registerGlobalInitFunc('initAvatarUploader', initAvatarUploaderWithCropper);
}
// for performance considerations, it only uses performant syntax // for performance considerations, it only uses performant syntax
function attachInputDirAuto(el: Partial<HTMLInputElement | HTMLTextAreaElement>) { function attachInputDirAuto(el: Partial<HTMLInputElement | HTMLTextAreaElement>) {
if (el.type !== 'hidden' && if (el.type !== 'hidden' &&

View File

@ -1,6 +1,5 @@
import {svg} from '../svg.ts'; import {svg} from '../svg.ts';
import {createTippy} from '../modules/tippy.ts'; import {createTippy} from '../modules/tippy.ts';
import {clippie} from 'clippie';
import {toAbsoluteUrl} from '../utils.ts'; import {toAbsoluteUrl} from '../utils.ts';
import {addDelegatedEventListener} from '../utils/dom.ts'; import {addDelegatedEventListener} from '../utils/dom.ts';
@ -43,7 +42,8 @@ function selectRange(range: string): Element {
if (!copyPermalink) return; if (!copyPermalink) return;
let link = copyPermalink.getAttribute('data-url'); let link = copyPermalink.getAttribute('data-url');
link = `${link.replace(/#L\d+$|#L\d+-L\d+$/, '')}#${anchor}`; link = `${link.replace(/#L\d+$|#L\d+-L\d+$/, '')}#${anchor}`;
copyPermalink.setAttribute('data-url', link); copyPermalink.setAttribute('data-clipboard-text', link);
copyPermalink.setAttribute('data-clipboard-text-type', 'url');
}; };
const rangeFields = range ? range.split('-') : []; const rangeFields = range ? range.split('-') : [];
@ -138,8 +138,4 @@ export function initRepoCodeView() {
}; };
onHashChange(); onHashChange();
window.addEventListener('hashchange', onHashChange); window.addEventListener('hashchange', onHashChange);
addDelegatedEventListener(document, 'click', '.copy-line-permalink', (el) => {
clippie(toAbsoluteUrl(el.getAttribute('data-url')));
});
} }

View File

@ -2,7 +2,6 @@ import {minimatch} from 'minimatch';
import {createMonaco} from './codeeditor.ts'; import {createMonaco} from './codeeditor.ts';
import {onInputDebounce, queryElems, toggleClass, toggleElem} from '../utils/dom.ts'; import {onInputDebounce, queryElems, toggleClass, toggleElem} from '../utils/dom.ts';
import {POST} from '../modules/fetch.ts'; import {POST} from '../modules/fetch.ts';
import {initAvatarUploaderWithCropper} from './comp/Cropper.ts';
import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts'; import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts';
@ -149,6 +148,4 @@ export function initRepoSettings() {
initRepoSettingsSearchTeamBox(); initRepoSettingsSearchTeamBox();
initRepoSettingsGitHook(); initRepoSettingsGitHook();
initRepoSettingsBranchesDrag(); initRepoSettingsBranchesDrag();
queryElems(document, '.avatar-file-with-cropper', initAvatarUploaderWithCropper);
} }

View File

@ -1,11 +1,8 @@
import {hideElem, queryElems, showElem} from '../utils/dom.ts'; import {hideElem, showElem} from '../utils/dom.ts';
import {initAvatarUploaderWithCropper} from './comp/Cropper.ts';
export function initUserSettings() { export function initUserSettings() {
if (!document.querySelector('.user.settings.profile')) return; if (!document.querySelector('.user.settings.profile')) return;
queryElems(document, '.avatar-file-with-cropper', initAvatarUploaderWithCropper);
const usernameInput = document.querySelector<HTMLInputElement>('#username'); const usernameInput = document.querySelector<HTMLInputElement>('#username');
if (!usernameInput) return; if (!usernameInput) return;
usernameInput.addEventListener('input', function () { usernameInput.addEventListener('input', function () {

View File

@ -60,7 +60,7 @@ import {initColorPickers} from './features/colorpicker.ts';
import {initAdminSelfCheck} from './features/admin/selfcheck.ts'; import {initAdminSelfCheck} from './features/admin/selfcheck.ts';
import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts'; import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts';
import {initGlobalFetchAction} from './features/common-fetch-action.ts'; import {initGlobalFetchAction} from './features/common-fetch-action.ts';
import {initFootLanguageMenu, initGlobalDropdown, initGlobalInput, initGlobalTabularMenu, initHeadNavbarContentToggle} from './features/common-page.ts'; import {initFootLanguageMenu, initGlobalAvatarUploader, initGlobalDropdown, initGlobalInput, initGlobalTabularMenu, initHeadNavbarContentToggle} from './features/common-page.ts';
import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton} from './features/common-button.ts'; import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton} from './features/common-button.ts';
import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts'; import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts';
import {callInitFunctions} from './modules/init.ts'; import {callInitFunctions} from './modules/init.ts';
@ -72,6 +72,7 @@ initSubmitEventPolyfill();
onDomReady(() => { onDomReady(() => {
const initStartTime = performance.now(); const initStartTime = performance.now();
const initPerformanceTracer = callInitFunctions([ const initPerformanceTracer = callInitFunctions([
initGlobalAvatarUploader,
initGlobalDropdown, initGlobalDropdown,
initGlobalTabularMenu, initGlobalTabularMenu,
initGlobalFetchAction, initGlobalFetchAction,

View File

@ -15,6 +15,8 @@ export function initMarkupCodeCopy(elMarkup: HTMLElement): void {
const btn = makeCodeCopyButton(); const btn = makeCodeCopyButton();
// remove final trailing newline introduced during HTML rendering // remove final trailing newline introduced during HTML rendering
btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, '')); btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, ''));
el.after(btn); // we only want to use `.code-block-container` if it exists, no matter `.code-block` exists or not.
const btnContainer = el.closest('.code-block-container') ?? el.closest('.code-block');
btnContainer.append(btn);
}); });
} }

View File

@ -89,7 +89,7 @@ export function queryElemChildren<T extends Element>(parent: Element | ParentNod
} }
// it works like parent.querySelectorAll: all descendants are selected // it works like parent.querySelectorAll: all descendants are selected
// in the future, all "queryElems(document, ...)" should be refactored to use a more specific parent // in the future, all "queryElems(document, ...)" should be refactored to use a more specific parent if the targets are not for page-level components.
export function queryElems<T extends HTMLElement>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback<T>): ArrayLikeIterable<T> { export function queryElems<T extends HTMLElement>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
return applyElemsCallback<T>(parent.querySelectorAll(selector), fn); return applyElemsCallback<T>(parent.querySelectorAll(selector), fn);
} }
@ -360,7 +360,11 @@ export function querySingleVisibleElem<T extends HTMLElement>(parent: Element, s
export function addDelegatedEventListener<T extends HTMLElement, E extends Event>(parent: Node, type: string, selector: string, listener: (elem: T, e: E) => Promisable<void>, options?: boolean | AddEventListenerOptions) { export function addDelegatedEventListener<T extends HTMLElement, E extends Event>(parent: Node, type: string, selector: string, listener: (elem: T, e: E) => Promisable<void>, options?: boolean | AddEventListenerOptions) {
parent.addEventListener(type, (e: Event) => { parent.addEventListener(type, (e: Event) => {
const elem = (e.target as HTMLElement).closest(selector); const elem = (e.target as HTMLElement).closest(selector);
if (!elem || !parent.contains(elem)) return; // It strictly checks "parent contains the target elem" to avoid side effects of selector running on outside the parent.
// Keep in mind that the elem could have been removed from parent by other event handlers before this event handler is called.
// For example: tippy popup item, the tippy popup could be hidden and removed from DOM before this.
// It is caller's responsibility make sure the elem is still in parent's DOM when this event handler is called.
if (!elem || (parent !== document && !parent.contains(elem))) return;
listener(elem as T, e as E); listener(elem as T, e as E);
}, options); }, options);
} }