0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-07-21 08:04:54 +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",
Usage: "Make a restricted user account",
},
&cli.StringFlag{
Name: "fullname",
Usage: `The full, human-readable name of the user`,
},
},
}
@ -191,6 +195,7 @@ func runCreateUser(c *cli.Context) error {
Passwd: password,
MustChangePassword: mustChangePassword,
Visibility: visibility,
FullName: c.String("fullname"),
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{

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"))
})
createUser := func(name, args string) error {
return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args)))
createUser := func(name string, args ...string) error {
return app.Run(append([]string{"./gitea", "admin", "user", "create", "--username", name, "--email", name + "@gitea.local"}, args...))
}
t.Run("UserType", func(t *testing.T) {
reset()
assert.ErrorContains(t, createUser("u", "--user-type invalid"), "invalid user type")
assert.ErrorContains(t, createUser("u", "--user-type bot --password 123"), "can only be set for individual users")
assert.ErrorContains(t, createUser("u", "--user-type bot --must-change-password"), "can only be set for individual users")
assert.ErrorContains(t, createUser("u", "--user-type", "invalid"), "invalid user type")
assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--password", "123"), "can only be set for individual users")
assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--must-change-password"), "can only be set for individual users")
assert.NoError(t, createUser("u", "--user-type bot"))
assert.NoError(t, createUser("u", "--user-type", "bot"))
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
assert.Equal(t, user_model.UserTypeBot, u.Type)
assert.Empty(t, u.Passwd)
@ -75,7 +75,7 @@ func TestAdminUserCreate(t *testing.T) {
// using "--access-token" only means "all" access
reset()
assert.NoError(t, createUser("u", "--random-password --access-token"))
assert.NoError(t, createUser("u", "--random-password", "--access-token"))
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"})
@ -85,7 +85,7 @@ func TestAdminUserCreate(t *testing.T) {
// using "--access-token" with name & scopes
reset()
assert.NoError(t, createUser("u", "--random-password --access-token --access-token-name new-token-name --access-token-scopes read:issue,read:user"))
assert.NoError(t, createUser("u", "--random-password", "--access-token", "--access-token-name", "new-token-name", "--access-token-scopes", "read:issue,read:user"))
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"})
@ -98,23 +98,38 @@ func TestAdminUserCreate(t *testing.T) {
// using "--access-token-name" without "--access-token"
reset()
err = createUser("u", "--random-password --access-token-name new-token-name")
err = createUser("u", "--random-password", "--access-token-name", "new-token-name")
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
// using "--access-token-scopes" without "--access-token"
reset()
err = createUser("u", "--random-password --access-token-scopes read:issue")
err = createUser("u", "--random-password", "--access-token-scopes", "read:issue")
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
// empty permission
reset()
err = createUser("u", "--random-password --access-token --access-token-scopes public-only")
err = createUser("u", "--random-password", "--access-token", "--access-token-scopes", "public-only")
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.ErrorContains(t, err, "access token does not have any permission")
})
t.Run("UserFields", func(t *testing.T) {
reset()
assert.NoError(t, createUser("u-FullNameWithSpace", "--random-password", "--fullname", "First O'Middle Last"))
unittest.AssertExistsAndLoadBean(t, &user_model.User{
Name: "u-FullNameWithSpace",
LowerName: "u-fullnamewithspace",
FullName: "First O'Middle Last",
Email: "u-FullNameWithSpace@gitea.local",
})
assert.NoError(t, createUser("u-FullNameEmpty", "--random-password", "--fullname", ""))
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u-fullnameempty"})
assert.Empty(t, u.FullName)
})
}

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'
;; Note: Value must be lowercase.
;; The protocol the server listens on. One of "http", "https", "http+unix", "fcgi" or "fcgi+unix".
;PROTOCOL = http
;;
;; Expect PROXY protocol headers on connections
;USE_PROXY_PROTOCOL = false
;;
;; Use PROXY protocol in TLS Bridging mode
;PROXY_PROTOCOL_TLS_BRIDGING = false
;;
; Timeout to wait for PROXY protocol header (set to 0 to have no timeout)
;PROXY_PROTOCOL_HEADER_TIMEOUT=5s
;;
; Accept PROXY protocol headers with UNKNOWN type
;PROXY_PROTOCOL_ACCEPT_UNKNOWN=false
;;
;; Set the domain for the server
;; Set the domain for the server.
;; Most users should set it to the real website domain of their Gitea instance.
;DOMAIN = localhost
;;
;; The AppURL used by Gitea to generate absolute links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/".
;; Most users should set it to the real website URL of their Gitea instance.
;; Most users should set it to the real website URL of their Gitea instance when there is a reverse proxy.
;; When it is empty, Gitea will use HTTP "Host" header to generate ROOT_URL, and fall back to the default one if no "Host" header.
;ROOT_URL =
;;
;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy.
@ -90,13 +79,25 @@ RUN_USER = ; git
;STATIC_URL_PREFIX =
;;
;; The address to listen on. Either a IPv4/IPv6 address or the path to a unix socket.
;; If PROTOCOL is set to `http+unix` or `fcgi+unix`, this should be the name of the Unix socket file to use.
;; If PROTOCOL is set to "http+unix" or "fcgi+unix", this should be the name of the Unix socket file to use.
;; Relative paths will be made absolute against the _`AppWorkPath`_.
;HTTP_ADDR = 0.0.0.0
;;
;; The port to listen on. Leave empty when using a unix socket.
;; The port to listen on for "http" or "https" protocol. Leave empty when using a unix socket.
;HTTP_PORT = 3000
;;
;; Expect PROXY protocol headers on connections
;USE_PROXY_PROTOCOL = false
;;
;; Use PROXY protocol in TLS Bridging mode
;PROXY_PROTOCOL_TLS_BRIDGING = false
;;
;; Timeout to wait for PROXY protocol header (set to 0 to have no timeout)
;PROXY_PROTOCOL_HEADER_TIMEOUT = 5s
;;
;; Accept PROXY protocol headers with UNKNOWN type
;PROXY_PROTOCOL_ACCEPT_UNKNOWN = false
;;
;; If REDIRECT_OTHER_PORT is true, and PROTOCOL is set to https an http server
;; will be started on PORT_TO_REDIRECT and it will redirect plain, non-secure http requests to the main
;; ROOT_URL. Defaults are false for REDIRECT_OTHER_PORT and 80 for

2
go.mod
View File

@ -120,7 +120,7 @@ require (
gitlab.com/gitlab-org/api/client-go v0.126.0
golang.org/x/crypto v0.36.0
golang.org/x/image v0.25.0
golang.org/x/net v0.37.0
golang.org/x/net v0.38.0
golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.12.0
golang.org/x/sys v0.31.0

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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/models/shared/types"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
@ -123,8 +124,15 @@ func (r *ActionRunner) IsOnline() bool {
return false
}
// Editable checks if the runner is editable by the user
func (r *ActionRunner) Editable(ownerID, repoID int64) bool {
// EditableInContext checks if the runner is editable by the "context" owner/repo
// ownerID == 0 and repoID == 0 means "admin" context, any runner including global runners could be edited
// ownerID == 0 and repoID != 0 means "repo" context, any runner belonging to the given repo could be edited
// ownerID != 0 and repoID == 0 means "owner(org/user)" context, any runner belonging to the given user/org could be edited
// ownerID != 0 and repoID != 0 means "owner" OR "repo" context, legacy behavior, but we should forbid using it
func (r *ActionRunner) EditableInContext(ownerID, repoID int64) bool {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
if ownerID == 0 && repoID == 0 {
return true
}
@ -168,6 +176,12 @@ func init() {
db.RegisterModel(&ActionRunner{})
}
// FindRunnerOptions
// ownerID == 0 and repoID == 0 means any runner including global runners
// repoID != 0 and WithAvailable == false means any runner for the given repo
// repoID != 0 and WithAvailable == true means any runner for the given repo, parent user/org, and global runners
// ownerID != 0 and repoID == 0 and WithAvailable == false means any runner for the given user/org
// ownerID != 0 and repoID == 0 and WithAvailable == true means any runner for the given user/org and global runners
type FindRunnerOptions struct {
db.ListOptions
IDs []int64

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:
e.Desc("package_version.created_unix")
}
// Sort by id for stable order with duplicates in the other field
e.Asc("package_version.id")
e.Desc("package_version.id") // Sort by id for stable order with duplicates in the other field
}
// SearchVersions gets all versions of packages matching the search options

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.
// 2. The reverse proxy is not configured correctly, doesn't pass "X-Forwarded-Proto/Host" headers, eg: only one "proxy_pass http://gitea:3000" in Nginx.
// 3. There is no reverse proxy.
// Without an extra config option, Gitea is impossible to distinguish between case 2 and case 3,
// then case 2 would result in wrong guess like guessed AppURL becomes "http://gitea:3000/", which is not accessible by end users.
// So in the future maybe it should introduce a new config option, to let site admin decide how to guess the AppURL.
// Without more information, Gitea is impossible to distinguish between case 2 and case 3, then case 2 would result in
// wrong guess like guessed AppURL becomes "http://gitea:3000/" behind a "https" reverse proxy, which is not accessible by end users.
// So we introduced "UseHostHeader" option, it could be enabled by setting "ROOT_URL" to empty
reqScheme := getRequestScheme(req)
if reqScheme == "" {
// if no reverse proxy header, try to use "Host" header for absolute URL
if setting.UseHostHeader && req.Host != "" {
return util.Iif(req.TLS == nil, "http://", "https://") + req.Host
}
// fall back to default AppURL
return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
}
// X-Forwarded-Host has many problems: non-standard, not well-defined (X-Forwarded-Port or not), conflicts with Host header.

View File

@ -5,6 +5,7 @@ package httplib
import (
"context"
"crypto/tls"
"net/http"
"testing"
@ -39,6 +40,25 @@ func TestIsRelativeURL(t *testing.T) {
}
}
func TestGuessCurrentHostURL(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")()
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
defer test.MockVariableValue(&setting.UseHostHeader, false)()
ctx := t.Context()
assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx))
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "localhost:3000"})
assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx))
defer test.MockVariableValue(&setting.UseHostHeader, true)()
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "http-host:3000"})
assert.Equal(t, "http://http-host:3000", GuessCurrentHostURL(ctx))
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "http-host", TLS: &tls.ConnectionState{}})
assert.Equal(t, "https://http-host", GuessCurrentHostURL(ctx))
}
func TestMakeAbsoluteURL(t *testing.T) {
defer test.MockVariableValue(&setting.Protocol, "http")()
defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")()

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:
// http://spec.commonmark.org/0.28/#email-address
// https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
v.emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))")
// At the moment, we use stricter rule for rendering purpose: only allow the "name" part starting after the word boundary
v.emailRegex = regexp.MustCompile(`\b([-\w.!#$%&'*+/=?^{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)\b`)
// emojiShortCodeRegex find emoji by alias like :smile:
v.emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`)

View File

@ -3,7 +3,11 @@
package markup
import "golang.org/x/net/html"
import (
"strings"
"golang.org/x/net/html"
)
// emailAddressProcessor replaces raw email addresses with a mailto: link.
func emailAddressProcessor(ctx *RenderContext, node *html.Node) {
@ -14,6 +18,14 @@ func emailAddressProcessor(ctx *RenderContext, node *html.Node) {
return
}
var nextByte byte
if len(node.Data) > m[3] {
nextByte = node.Data[m[3]]
}
if strings.IndexByte(":/", nextByte) != -1 {
// for cases: "git@gitea.com:owner/repo.git", "https://git@gitea.com/owner/repo.git"
return
}
mail := node.Data[m[2]:m[3]]
replaceContent(node, m[2], m[3], createLink(ctx, "mailto:"+mail, mail, "" /*mailto*/))
node = node.NextSibling.NextSibling

View File

@ -225,10 +225,10 @@ func TestRender_email(t *testing.T) {
test := func(input, expected string) {
res, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res))
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res), "input: %s", input)
}
// Text that should be turned into email link
// Text that should be turned into email link
test(
"info@gitea.com",
`<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>!</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(
"\"info@gitea.com\"",
`<p>&#34;info@gitea.com&#34;</p>`)
test(
"/home/gitea/mailstore/info@gitea/com",
`<p>/home/gitea/mailstore/info@gitea/com</p>`)
test(
"git@try.gitea.io:go-gitea/gitea.git",
`<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(
"gitea@3",
`<p>gitea@3</p>`)
test(
"gitea@gmail.c",
`<p>gitea@gmail.c</p>`)
test(
"email@domain@domain.com",
`<p>email@domain@domain.com</p>`)
test(
"email@domain..com",
`<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) {

View File

@ -86,20 +86,15 @@ func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.C
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
// 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>
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 {
return
}
} else {
_, err := w.WriteString("</code></pre>")
_, err := w.WriteString("</code></pre></div>")
if err != nil {
return
}

View File

@ -46,25 +46,37 @@ var (
// AppURL is the Application ROOT_URL. It always has a '/' suffix
// It maps to ini:"ROOT_URL"
AppURL string
// AppSubURL represents the sub-url mounting point for gitea. It is either "" or starts with '/' and ends without '/', such as '/{subpath}'.
// AppSubURL represents the sub-url mounting point for gitea, parsed from "ROOT_URL"
// It is either "" or starts with '/' and ends without '/', such as '/{sub-path}'.
// This value is empty if site does not have sub-url.
AppSubURL string
// UseSubURLPath makes Gitea handle requests with sub-path like "/sub-path/owner/repo/...", to make it easier to debug sub-path related problems without a reverse proxy.
// UseSubURLPath makes Gitea handle requests with sub-path like "/sub-path/owner/repo/...",
// to make it easier to debug sub-path related problems without a reverse proxy.
UseSubURLPath bool
// UseHostHeader makes Gitea prefer to use the "Host" request header for construction of absolute URLs.
UseHostHeader bool
// AppDataPath is the default path for storing data.
// It maps to ini:"APP_DATA_PATH" in [server] and defaults to AppWorkPath + "/data"
AppDataPath string
// LocalURL is the url for locally running applications to contact Gitea. It always has a '/' suffix
// It maps to ini:"LOCAL_ROOT_URL" in [server]
LocalURL string
// AssetVersion holds a opaque value that is used for cache-busting assets
// AssetVersion holds an opaque value that is used for cache-busting assets
AssetVersion string
appTempPathInternal string // the temporary path for the app, it is only an internal variable, do not use it, always use AppDataTempDir
// appTempPathInternal is the temporary path for the app, it is only an internal variable
// DO NOT use it directly, always use AppDataTempDir
appTempPathInternal string
Protocol Scheme
UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"`
ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"`
UseProxyProtocol bool
ProxyProtocolTLSBridging bool
ProxyProtocolHeaderTimeout time.Duration
ProxyProtocolAcceptUnknown bool
Domain string
@ -181,13 +193,14 @@ func loadServerFrom(rootCfg ConfigProvider) {
EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false)
}
Protocol = HTTP
protocolCfg := sec.Key("PROTOCOL").String()
if protocolCfg != "https" && EnableAcme {
log.Fatal("ACME could only be used with HTTPS protocol")
}
switch protocolCfg {
case "", "http":
Protocol = HTTP
case "https":
Protocol = HTTPS
if EnableAcme {
@ -243,7 +256,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
case "unix":
log.Warn("unix PROTOCOL value is deprecated, please use http+unix")
fallthrough
case "http+unix":
default: // "http+unix"
Protocol = HTTPUnix
}
UnixSocketPermissionRaw := sec.Key("UNIX_SOCKET_PERMISSION").MustString("666")
@ -256,6 +269,8 @@ func loadServerFrom(rootCfg ConfigProvider) {
if !filepath.IsAbs(HTTPAddr) {
HTTPAddr = filepath.Join(AppWorkPath, HTTPAddr)
}
default:
log.Fatal("Invalid PROTOCOL %q", Protocol)
}
UseProxyProtocol = sec.Key("USE_PROXY_PROTOCOL").MustBool(false)
ProxyProtocolTLSBridging = sec.Key("PROXY_PROTOCOL_TLS_BRIDGING").MustBool(false)
@ -268,12 +283,16 @@ func loadServerFrom(rootCfg ConfigProvider) {
PerWritePerKbTimeout = sec.Key("PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout)
defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort
AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL)
AppURL = sec.Key("ROOT_URL").String()
if AppURL == "" {
UseHostHeader = true
AppURL = defaultAppURL
}
// Check validity of AppURL
appURL, err := url.Parse(AppURL)
if err != nil {
log.Fatal("Invalid ROOT_URL '%s': %s", AppURL, err)
log.Fatal("Invalid ROOT_URL %q: %s", AppURL, err)
}
// Remove default ports from AppURL.
// (scheme-based URL normalization, RFC 3986 section 6.2.3)
@ -309,13 +328,15 @@ func loadServerFrom(rootCfg ConfigProvider) {
defaultLocalURL = AppURL
case FCGIUnix:
defaultLocalURL = AppURL
default:
case HTTP, HTTPS:
defaultLocalURL = string(Protocol) + "://"
if HTTPAddr == "0.0.0.0" {
defaultLocalURL += net.JoinHostPort("localhost", HTTPPort) + "/"
} else {
defaultLocalURL += net.JoinHostPort(HTTPAddr, HTTPPort) + "/"
}
default:
log.Fatal("Invalid PROTOCOL %q", Protocol)
}
LocalURL = sec.Key("LOCAL_ROOT_URL").MustString(defaultLocalURL)
LocalURL = strings.TrimRight(LocalURL, "/") + "/"

View File

@ -133,3 +133,26 @@ type ActionWorkflowJob struct {
// swagger:strfmt date-time
CompletedAt time.Time `json:"completed_at,omitempty"`
}
// ActionRunnerLabel represents a Runner Label
type ActionRunnerLabel struct {
ID int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
}
// ActionRunner represents a Runner
type ActionRunner struct {
ID int64 `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Busy bool `json:"busy"`
Ephemeral bool `json:"ephemeral"`
Labels []*ActionRunnerLabel `json:"labels"`
}
// ActionRunnersResponse returns Runners
type ActionRunnersResponse struct {
Entries []*ActionRunner `json:"runners"`
TotalCount int64 `json:"total_count"`
}

View File

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

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

View File

@ -24,3 +24,81 @@ func GetRegistrationToken(ctx *context.APIContext) {
shared.GetRegistrationToken(ctx, 0, 0)
}
// CreateRegistrationToken returns the token to register global runners
func CreateRegistrationToken(ctx *context.APIContext) {
// swagger:operation POST /admin/actions/runners/registration-token admin adminCreateRunnerRegistrationToken
// ---
// summary: Get an global actions runner registration token
// produces:
// - application/json
// parameters:
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, 0, 0)
}
// ListRunners get all runners
func ListRunners(ctx *context.APIContext) {
// swagger:operation GET /admin/actions/runners admin getAdminRunners
// ---
// summary: Get all runners
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/definitions/ActionRunnersResponse"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListRunners(ctx, 0, 0)
}
// GetRunner get an global runner
func GetRunner(ctx *context.APIContext) {
// swagger:operation GET /admin/actions/runners/{runner_id} admin getAdminRunner
// ---
// summary: Get an global runner
// produces:
// - application/json
// parameters:
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/definitions/ActionRunner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.GetRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id"))
}
// DeleteRunner delete an global runner
func DeleteRunner(ctx *context.APIContext) {
// swagger:operation DELETE /admin/actions/runners/{runner_id} admin deleteAdminRunner
// ---
// summary: Delete an global runner
// produces:
// - application/json
// parameters:
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// responses:
// "204":
// description: runner has been deleted
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.DeleteRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id"))
}

View File

@ -912,7 +912,11 @@ func Routes() *web.Router {
})
m.Group("/runners", func() {
m.Get("", reqToken(), reqChecker, act.ListRunners)
m.Get("/registration-token", reqToken(), reqChecker, act.GetRegistrationToken)
m.Post("/registration-token", reqToken(), reqChecker, act.CreateRegistrationToken)
m.Get("/{runner_id}", reqToken(), reqChecker, act.GetRunner)
m.Delete("/{runner_id}", reqToken(), reqChecker, act.DeleteRunner)
})
})
}
@ -1043,7 +1047,11 @@ func Routes() *web.Router {
})
m.Group("/runners", func() {
m.Get("", reqToken(), user.ListRunners)
m.Get("/registration-token", reqToken(), user.GetRegistrationToken)
m.Post("/registration-token", reqToken(), user.CreateRegistrationToken)
m.Get("/{runner_id}", reqToken(), user.GetRunner)
m.Delete("/{runner_id}", reqToken(), user.DeleteRunner)
})
})
@ -1689,6 +1697,12 @@ func Routes() *web.Router {
Patch(bind(api.EditHookOption{}), admin.EditHook).
Delete(admin.DeleteHook)
})
m.Group("/actions/runners", func() {
m.Get("", admin.ListRunners)
m.Post("/registration-token", admin.CreateRegistrationToken)
m.Get("/{runner_id}", admin.GetRunner)
m.Delete("/{runner_id}", admin.DeleteRunner)
})
m.Group("/runners", func() {
m.Get("/registration-token", admin.GetRegistrationToken)
})

View File

@ -190,6 +190,27 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) {
shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0)
}
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
// CreateRegistrationToken returns the token to register org runners
func (Action) CreateRegistrationToken(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/actions/runners/registration-token organization orgCreateRunnerRegistrationToken
// ---
// summary: Get an organization's actions runner registration token
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0)
}
// ListVariables list org-level variables
func (Action) ListVariables(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList
@ -470,6 +491,85 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
// ListRunners get org-level runners
func (Action) ListRunners(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/runners organization getOrgRunners
// ---
// summary: Get org-level runners
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/definitions/ActionRunnersResponse"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListRunners(ctx, ctx.Org.Organization.ID, 0)
}
// GetRunner get an org-level runner
func (Action) GetRunner(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/runners/{runner_id} organization getOrgRunner
// ---
// summary: Get an org-level runner
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/definitions/ActionRunner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.GetRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id"))
}
// DeleteRunner delete an org-level runner
func (Action) DeleteRunner(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/actions/runners/{runner_id} organization deleteOrgRunner
// ---
// summary: Delete an org-level runner
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// responses:
// "204":
// description: runner has been deleted
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.DeleteRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id"))
}
var _ actions_service.API = new(Action)
// Action implements actions_service.API

View File

@ -183,7 +183,7 @@ func (Action) DeleteSecret(ctx *context.APIContext) {
// required: true
// responses:
// "204":
// description: delete one secret of the organization
// description: delete one secret of the repository
// "400":
// "$ref": "#/responses/error"
// "404":
@ -531,6 +531,125 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) {
shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID)
}
// CreateRegistrationToken returns the token to register repo runners
func (Action) CreateRegistrationToken(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/runners/registration-token repository repoCreateRunnerRegistrationToken
// ---
// summary: Get a repository's actions runner registration token
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID)
}
// ListRunners get repo-level runners
func (Action) ListRunners(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runners repository getRepoRunners
// ---
// summary: Get repo-level runners
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/definitions/ActionRunnersResponse"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListRunners(ctx, 0, ctx.Repo.Repository.ID)
}
// GetRunner get an repo-level runner
func (Action) GetRunner(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runners/{runner_id} repository getRepoRunner
// ---
// summary: Get an repo-level runner
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/definitions/ActionRunner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.GetRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
}
// DeleteRunner delete an repo-level runner
func (Action) DeleteRunner(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/actions/runners/{runner_id} repository deleteRepoRunner
// ---
// summary: Delete an repo-level runner
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// responses:
// "204":
// description: runner has been deleted
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.DeleteRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
}
var _ actions_service.API = new(Action)
// Action implements actions_service.API

View File

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

View File

@ -8,8 +8,13 @@ import (
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
// RegistrationToken is response related to registration token
@ -30,3 +35,84 @@ func GetRegistrationToken(ctx *context.APIContext, ownerID, repoID int64) {
ctx.JSON(http.StatusOK, RegistrationToken{Token: token.Token})
}
// ListRunners lists runners for api route validated ownerID and repoID
// ownerID == 0 and repoID == 0 means all runners including global runners, does not appear in sql where clause
// ownerID == 0 and repoID != 0 means all runners for the given repo
// ownerID != 0 and repoID == 0 means all runners for the given user/org
// ownerID != 0 and repoID != 0 undefined behavior
// Access rights are checked at the API route level
func ListRunners(ctx *context.APIContext, ownerID, repoID int64) {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
runners, total, err := db.FindAndCount[actions_model.ActionRunner](ctx, &actions_model.FindRunnerOptions{
OwnerID: ownerID,
RepoID: repoID,
ListOptions: utils.GetListOptions(ctx),
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
res := new(api.ActionRunnersResponse)
res.TotalCount = total
res.Entries = make([]*api.ActionRunner, len(runners))
for i, runner := range runners {
res.Entries[i] = convert.ToActionRunner(ctx, runner)
}
ctx.JSON(http.StatusOK, &res)
}
// GetRunner get the runner for api route validated ownerID and repoID
// ownerID == 0 and repoID == 0 means any runner including global runners
// ownerID == 0 and repoID != 0 means any runner for the given repo
// ownerID != 0 and repoID == 0 means any runner for the given user/org
// ownerID != 0 and repoID != 0 undefined behavior
// Access rights are checked at the API route level
func GetRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
if err != nil {
ctx.APIErrorNotFound(err)
return
}
if !runner.EditableInContext(ownerID, repoID) {
ctx.APIErrorNotFound("No permission to get this runner")
return
}
ctx.JSON(http.StatusOK, convert.ToActionRunner(ctx, runner))
}
// DeleteRunner deletes the runner for api route validated ownerID and repoID
// ownerID == 0 and repoID == 0 means any runner including global runners
// ownerID == 0 and repoID != 0 means any runner for the given repo
// ownerID != 0 and repoID == 0 means any runner for the given user/org
// ownerID != 0 and repoID != 0 undefined behavior
// Access rights are checked at the API route level
func DeleteRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !runner.EditableInContext(ownerID, repoID) {
ctx.APIErrorNotFound("No permission to delete this runner")
return
}
err = actions_model.DeleteRunner(ctx, runner.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}

View File

@ -457,6 +457,20 @@ type swaggerRepoArtifact struct {
Body api.ActionArtifact `json:"body"`
}
// RunnerList
// swagger:response RunnerList
type swaggerRunnerList struct {
// in:body
Body api.ActionRunnersResponse `json:"body"`
}
// Runner
// swagger:response Runner
type swaggerRunner struct {
// in:body
Body api.ActionRunner `json:"body"`
}
// swagger:response Compare
type swaggerCompare struct {
// in:body

View File

@ -24,3 +24,81 @@ func GetRegistrationToken(ctx *context.APIContext) {
shared.GetRegistrationToken(ctx, ctx.Doer.ID, 0)
}
// CreateRegistrationToken returns the token to register user runners
func CreateRegistrationToken(ctx *context.APIContext) {
// swagger:operation POST /user/actions/runners/registration-token user userCreateRunnerRegistrationToken
// ---
// summary: Get an user's actions runner registration token
// produces:
// - application/json
// parameters:
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, ctx.Doer.ID, 0)
}
// ListRunners get user-level runners
func ListRunners(ctx *context.APIContext) {
// swagger:operation GET /user/actions/runners user getUserRunners
// ---
// summary: Get user-level runners
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/definitions/ActionRunnersResponse"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListRunners(ctx, ctx.Doer.ID, 0)
}
// GetRunner get an user-level runner
func GetRunner(ctx *context.APIContext) {
// swagger:operation GET /user/actions/runners/{runner_id} user getUserRunner
// ---
// summary: Get an user-level runner
// produces:
// - application/json
// parameters:
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/definitions/ActionRunner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.GetRunner(ctx, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id"))
}
// DeleteRunner delete an user-level runner
func DeleteRunner(ctx *context.APIContext) {
// swagger:operation DELETE /user/actions/runners/{runner_id} user deleteUserRunner
// ---
// summary: Delete an user-level runner
// produces:
// - application/json
// parameters:
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// responses:
// "204":
// description: runner has been deleted
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.DeleteRunner(ctx, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id"))
}

View File

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

View File

@ -181,6 +181,7 @@ func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) {
// GetPullDiffStats get Pull Requests diff stats
func GetPullDiffStats(ctx *context.Context) {
// FIXME: this getPullInfo seems to be a duplicate call with other route handlers
issue, ok := getPullInfo(ctx)
if !ok {
return
@ -188,21 +189,19 @@ func GetPullDiffStats(ctx *context.Context) {
pull := issue.PullRequest
mergeBaseCommitID := GetMergedBaseCommitID(ctx, issue)
if mergeBaseCommitID == "" {
ctx.NotFound(nil)
return
return // no merge base, do nothing, do not stop the route handler, see below
}
// do not report 500 server error to end users if error occurs, otherwise a PR missing ref won't be able to view.
headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitRefName())
if err != nil {
ctx.ServerError("GetRefCommitID", err)
log.Error("Failed to GetRefCommitID: %v, repo: %v", err, ctx.Repo.Repository.FullName())
return
}
diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, mergeBaseCommitID, headCommitID)
if err != nil {
ctx.ServerError("GetDiffShortStat", err)
log.Error("Failed to GetDiffShortStat: %v, repo: %v", err, ctx.Repo.Repository.FullName())
return
}

View File

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

View File

@ -25,4 +25,12 @@ type API interface {
UpdateVariable(*context.APIContext)
// GetRegistrationToken get registration token
GetRegistrationToken(*context.APIContext)
// CreateRegistrationToken get registration token
CreateRegistrationToken(*context.APIContext)
// ListRunners list runners
ListRunners(*context.APIContext)
// GetRunner get a runner
GetRunner(*context.APIContext)
// DeleteRunner delete runner
DeleteRunner(*context.APIContext)
}

View File

@ -30,6 +30,8 @@ import (
"code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/gitdiff"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
)
// ToEmail convert models.EmailAddress to api.Email
@ -252,6 +254,30 @@ func ToActionArtifact(repo *repo_model.Repository, art *actions_model.ActionArti
}, nil
}
func ToActionRunner(ctx context.Context, runner *actions_model.ActionRunner) *api.ActionRunner {
status := runner.Status()
apiStatus := "offline"
if runner.IsOnline() {
apiStatus = "online"
}
labels := make([]*api.ActionRunnerLabel, len(runner.AgentLabels))
for i, label := range runner.AgentLabels {
labels[i] = &api.ActionRunnerLabel{
ID: int64(i),
Name: label,
Type: "custom",
}
}
return &api.ActionRunner{
ID: runner.ID,
Name: runner.Name,
Status: apiStatus,
Busy: status == runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE,
Ephemeral: runner.Ephemeral,
Labels: labels,
}
}
// ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification {
verif := asymkey_service.ParseCommitWithSignature(ctx, c)

View File

@ -6,7 +6,7 @@ package feed
import (
"context"
"fmt"
"strconv"
"strings"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
@ -14,15 +14,10 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
func userFeedCacheKey(userID int64) string {
return fmt.Sprintf("user_feed_%d", userID)
}
func GetFeedsForDashboard(ctx context.Context, opts activities_model.GetFeedsOptions) (activities_model.ActionList, int, error) {
opts.DontCount = opts.RequestedTeam == nil && opts.Date == ""
results, cnt, err := activities_model.GetFeeds(ctx, opts)
@ -40,7 +35,18 @@ func GetFeeds(ctx context.Context, opts activities_model.GetFeedsOptions) (activ
// * Organization action: UserID=100 (the repo's org), ActUserID=1
// * Watcher action: UserID=20 (a user who is watching a repo), ActUserID=1
func notifyWatchers(ctx context.Context, act *activities_model.Action, watchers []*repo_model.Watch, permCode, permIssue, permPR []bool) error {
// Add feed for actioner.
// MySQL has TEXT length limit 65535.
// Sometimes the content is "field1|field2|field3", sometimes the content is JSON (ActionMirrorSyncPush, ActionCommitRepo, ActionPushTag, etc...)
if left, right := util.EllipsisDisplayStringX(act.Content, 65535); right != "" {
if strings.HasPrefix(act.Content, `{"`) && strings.HasSuffix(act.Content, `}`) {
// FIXME: at the moment we can do nothing if the content is JSON and it is too long
act.Content = "{}"
} else {
act.Content = left
}
}
// Add feed for actor.
act.UserID = act.ActUserID
if err := db.Insert(ctx, act); err != nil {
return fmt.Errorf("insert new actioner: %w", err)
@ -76,24 +82,18 @@ func notifyWatchers(ctx context.Context, act *activities_model.Action, watchers
if !permPR[i] {
continue
}
default:
}
if err := db.Insert(ctx, act); err != nil {
return fmt.Errorf("insert new action: %w", err)
}
total, err := activities_model.CountUserFeeds(ctx, act.UserID)
if err != nil {
return fmt.Errorf("count user feeds: %w", err)
}
_ = cache.GetCache().Put(userFeedCacheKey(act.UserID), strconv.FormatInt(total, 10), setting.CacheService.TTLSeconds())
}
return nil
}
// NotifyWatchersActions creates batch of actions for every watcher.
// NotifyWatchers creates batch of actions for every watcher.
func NotifyWatchers(ctx context.Context, acts ...*activities_model.Action) error {
return db.WithTx(ctx, func(ctx context.Context) error {
if len(acts) == 0 {

View File

@ -9,16 +9,16 @@
<a class="silenced" href="#">silenced</a>
</div>
<h1>Button</h1>
<div>
Style:
<label><input type="checkbox" name="button-style-compact" value="compact">compact</label>
<label><input type="radio" name="button-style-size" value="">(normal)</label>
<label><input type="radio" name="button-style-size" value="tiny">tiny</label>
<label><input type="radio" name="button-style-size" value="mini">mini</label>
".ui.button" styles:
<div class="flex-text-block tw-gap-4">
<label class="gt-checkbox"><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 class="gt-checkbox"><input type="radio" name="button-style-size" value="tiny">tiny</label>
<label class="gt-checkbox"><input type="radio" name="button-style-size" value="mini">mini</label>
</div>
<div>
State:
<label><input type="checkbox" name="button-state-disabled" value="disabled">disabled</label>
<div class="flex-text-block tw-gap-4">
<label class="gt-checkbox"><input type="checkbox" name="button-style-compact" value="compact">compact</label>
<label class="gt-checkbox"><input type="checkbox" name="button-state-disabled" value="disabled">disabled</label>
</div>
<div id="devtest-button-samples">
<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">
{{template "base/alert" .}}
{{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>
</div>
<div class="divider"></div>

View File

@ -1,5 +1,5 @@
{{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">
<a class="item{{if not .IsShowClosed}} active{{end}}" href="?state=open&q={{$.Keyword}}">
{{svg "octicon-project-symlink" 16 "tw-mr-2"}}
@ -10,9 +10,7 @@
{{ctx.Locale.PrettyNumber .ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
</a>
</div>
<div class="tw-text-right">
<a class="ui small primary button" href="{{$.Link}}/new">{{ctx.Locale.Tr "repo.projects.new"}}</a>
</div>
<a class="ui small primary button" href="{{$.Link}}/new">{{ctx.Locale.Tr "repo.projects.new"}}</a>
</div>
{{end}}

View File

@ -64,7 +64,7 @@
</div>
</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}}">
{{ctx.Locale.Tr "repo.milestones.cancel"}}
</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}}
<span>{{ctx.Locale.Tr "repo.code"}}</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}

View File

@ -37,7 +37,7 @@
{{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"}}">
{{/* 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"}}
</div>
</div>
@ -223,6 +223,7 @@
{{if and (not $.Repository.IsArchived) (not .DiffNotAvailable)}}
<template id="issue-comment-editor-template">
<form class="ui form comment">
<div class="field">
{{template "shared/combomarkdowneditor" (dict
"CustomInit" true
"MarkdownPreviewInRepo" $.Repository
@ -230,12 +231,13 @@
"TextareaName" "content"
"DropzoneParentContainer" ".ui.form"
)}}
</div>
{{if .IsAttachmentEnabled}}
<div class="field">
{{template "repo/upload" .}}
</div>
{{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 primary button">{{ctx.Locale.Tr "repo.issues.save"}}</button>
</div>

View File

@ -27,7 +27,7 @@
{{end}}
<div class="field footer">
<div class="tw-text-right">
<div class="flex-text-block tw-justify-end">
{{if $.reply}}
<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}}">

View File

@ -8,9 +8,9 @@
{{$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}}">
{{if $resolved}}
<div class="ui attached header resolved-placeholder tw-flex tw-items-center tw-justify-between">
<div class="ui grey text tw-flex tw-items-center tw-flex-wrap tw-gap-1">
{{svg "octicon-check" 16 "icon tw-mr-1"}}
<div class="resolved-placeholder">
<div class="flex-text-block tw-flex-wrap grey text">
{{svg "octicon-check"}}
<b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}}
{{if $invalid}}
<!--
@ -22,35 +22,33 @@
</a>
{{end}}
</div>
<div class="tw-flex tw-items-center tw-gap-2">
<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button show-outdated tw-flex tw-items-center">
{{svg "octicon-unfold" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
<div class="flex-text-block">
<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="btn tiny show-outdated">
{{svg "octicon-unfold" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
</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">
{{svg "octicon-fold" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="btn tiny hide-outdated tw-hidden">
{{svg "octicon-fold" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
</button>
</div>
</div>
{{end}}
<div id="code-comments-{{$comment.ID}}" class="field comment-code-cloud {{if $resolved}}tw-hidden{{end}}">
<div class="comment-list">
<ui class="ui comments">
<div class="ui comments">
{{template "repo/diff/comments" dict "root" $ "comments" .comments}}
</ui>
</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">
<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 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>
</div>
{{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}}
{{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}}
{{else}}
@ -59,8 +57,8 @@
</button>
{{end}}
{{if and $.SignedUserID (not $.Repository.IsArchived)}}
<button class="comment-form-reply ui primary tiny labeled icon button tw-mr-0">
{{svg "octicon-reply" 16 "reply icon tw-mr-1"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
<button class="comment-form-reply ui primary icon tiny button">
{{svg "octicon-reply" 12}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
</button>
{{end}}
</div>

View File

@ -45,8 +45,8 @@
{{end}}
</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-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-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 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>
</h2>
<div class="ui dividing"></div>

View File

@ -17,18 +17,18 @@
{{if eq $refGroup "pull"}}
{{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 -->
<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}}
</a>
{{end}}
{{else if eq $refGroup "tags"}}
{{- template "repo/tag/name" dict "RepoLink" $.Repository.Link "TagName" .ShortName -}}
{{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}}
</a>
{{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}}
</a>
{{else}}

View File

@ -38,20 +38,20 @@
</div>
</div>
{{if not (or .IsBeingCreated .IsBroken)}}
<div class="repo-buttons">
<div class="flex-text-block tw-flex-wrap">
{{if $.RepoTransfer}}
<form method="post" action="{{$.RepoLink}}/action/accept_transfer?redirect_to={{$.RepoLink}}">
{{$.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}}">
<button type="submit" class="ui basic button {{if $.CanUserAcceptOrRejectTransfer}}primary {{end}} ok small"{{if not $.CanUserAcceptOrRejectTransfer}} disabled{{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 compact small basic button {{if $.CanUserAcceptOrRejectTransfer}}primary {{end}} ok small"{{if not $.CanUserAcceptOrRejectTransfer}} disabled{{end}}>
{{ctx.Locale.Tr "repo.transfer.accept"}}
</button>
</div>
</form>
<form method="post" action="{{$.RepoLink}}/action/reject_transfer?redirect_to={{$.RepoLink}}">
{{$.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}}">
<button type="submit" class="ui basic button {{if $.CanUserAcceptOrRejectTransfer}}red {{end}}ok small"{{if not $.CanUserAcceptOrRejectTransfer}} disabled{{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 compact small basic button {{if $.CanUserAcceptOrRejectTransfer}}red {{end}}ok small"{{if not $.CanUserAcceptOrRejectTransfer}} disabled{{end}}>
{{ctx.Locale.Tr "repo.transfer.reject"}}
</button>
</div>

View File

@ -3,10 +3,10 @@
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
<div class="tw-flex">
<h1 class="tw-mb-2">{{.Milestone.Name}}</h1>
<div class="flex-text-block tw-flex-wrap tw-mb-2">
<h1 class="tw-flex-1 tw-m-0">{{.Milestone.Name}}</h1>
{{if not .Repository.IsArchived}}
<div class="tw-text-right tw-flex-1">
<div>
{{if or .CanWriteIssues .CanWritePulls}}
{{if .Milestone.IsClosed}}
<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")
)}}
</div>
<div class="tw-text-right">
<div class="flex-text-block tw-justify-end">
{{if .PageIsEditMilestone}}
<a class="ui primary basic button" href="{{.RepoLink}}/milestones">
{{ctx.Locale.Tr "repo.milestones.cancel"}}

View File

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

View File

@ -83,7 +83,7 @@
{{template "repo/issue/comment_tab" .}}
{{.CsrfTokenHtml}}
<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 .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">
@ -157,7 +157,7 @@
{{end}}
<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="submit" class="ui primary button">{{ctx.Locale.Tr "repo.issues.save"}}</button>
</div>

View File

@ -17,7 +17,7 @@
</div>
<div>
{{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"}}
{{if $resolved}}
{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
@ -25,7 +25,7 @@
{{ctx.Locale.Tr "repo.issues.review.show_outdated"}}
{{end}}
</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"}}
{{if $resolved}}
{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
@ -109,7 +109,7 @@
</div>
{{end}}
</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">
{{if $resolved}}
<div class="ui grey text">
@ -118,7 +118,7 @@
</div>
{{end}}
</div>
<div class="code-comment-buttons-buttons">
<div class="flex-text-block">
{{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">
{{if $resolved}}
@ -129,8 +129,8 @@
</button>
{{end}}
{{if and $.SignedUserID (not $.Repository.IsArchived)}}
<button class="comment-form-reply ui primary tiny labeled icon button tw-ml-1 tw-mr-0">
{{svg "octicon-reply" 16 "reply icon tw-mr-1"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
<button class="comment-form-reply ui primary icon tiny button">
{{svg "octicon-reply" 12}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
</button>
{{end}}
</div>

View File

@ -20,7 +20,7 @@
<label><strong>{{ctx.Locale.Tr "repo.issues.reference_issue.body"}}</strong></label>
<textarea name="content"></textarea>
</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>
</div>
</form>

View File

@ -1,12 +1,12 @@
<h2 class="ui header activity-header">
<span>{{DateUtils.AbsoluteLong .DateFrom}} - {{DateUtils.AbsoluteLong .DateUntil}}</span>
<!-- Period -->
<div class="ui floating dropdown jump filter">
<div class="ui floating dropdown jump">
<div class="ui basic compact button">
{{ctx.Locale.Tr "repo.activity.period.filter_label"}} <strong>{{.PeriodText}}</strong>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
</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 "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>

View File

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

View File

@ -2,7 +2,7 @@
{{$canReadCode := $.Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
{{if $canReadReleases}}
<div class="tw-flex">
<div class="flex-text-block">
<div class="tw-flex-1 tw-flex tw-items-center">
<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>

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}}>
{{$buttonText := ctx.Locale.Tr "repo.star"}}
{{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}}
</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}}>
{{$buttonText := ctx.Locale.Tr "repo.watch"}}
{{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"}}">
</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>
<button class="ui primary button">{{ctx.Locale.Tr "repo.wiki.save_page"}}</button>
</div>

View File

@ -3,18 +3,18 @@
{{template "repo/header" .}}
{{$title := .title}}
<div class="ui container">
<div class="ui stackable grid">
<div class="ui eight wide column">
<div class="ui header">
<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="ui dividing header flex-text-block tw-flex-wrap tw-justify-between">
<div class="flex-text-block">
<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>
<div class="tw-flex-1 gt-ellipsis">
{{$title}}
<div class="ui sub header tw-break-anywhere">
<div class="ui sub header gt-ellipsis">
{{$timeSince := DateUtils.TimeSince .Author.When}}
{{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}}
</div>
</div>
</div>
<div class="ui eight wide column tw-text-right">
<div>
{{template "repo/clone_panel" .}}
</div>
</div>

View File

@ -33,7 +33,7 @@
<div class="ui dividing header">
<div class="flex-text-block tw-flex-wrap tw-justify-end">
<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">
{{$title}}
<div class="ui sub header gt-ellipsis">

View File

@ -16,7 +16,7 @@
<div class="header">
Registration Token
</div>
<div class="ui input">
<div class="ui action input">
<input type="text" value="{{.RegistrationToken}}" readonly>
<button class="ui basic label button" aria-label="{{ctx.Locale.Tr "copy"}}" data-clipboard-text="{{.RegistrationToken}}">
{{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 */ -}}
<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" */ -}}
<div class="cropper-panel tw-hidden">
<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": {
"get": {
"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": {
"get": {
"produces": [
@ -1721,6 +1855,106 @@
"$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": {
@ -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": {
"get": {
"produces": [
@ -4362,6 +4635,127 @@
"$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": {
@ -4559,7 +4953,7 @@
],
"responses": {
"204": {
"description": "delete one secret of the organization"
"description": "delete one secret of the repository"
},
"400": {
"$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": {
"get": {
"produces": [
@ -16884,6 +17301,83 @@
"$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}": {
@ -19377,6 +19871,80 @@
},
"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": {
"description": "ActionTask represents a ActionTask",
"type": "object",
@ -27409,6 +27977,18 @@
}
}
},
"Runner": {
"description": "Runner",
"schema": {
"$ref": "#/definitions/ActionRunner"
}
},
"RunnerList": {
"description": "RunnerList",
"schema": {
"$ref": "#/definitions/ActionRunnersResponse"
}
},
"SearchResults": {
"description": "SearchResults",
"schema": {

View File

@ -37,7 +37,7 @@
</div>
{{if .SignedUser.CanCreateOrganization}}
<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>
{{end}}
</div>
@ -77,7 +77,7 @@
{{end}}
{{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}}">
{{svg "octicon-rss"}}&nbsp;{{ctx.Locale.Tr "activities"}}
</a>
@ -98,7 +98,7 @@
{{end}}
<div class="item">
<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>
</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"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPackageSwift(t *testing.T) {
@ -34,6 +35,7 @@ func TestPackageSwift(t *testing.T) {
packageName := "test_package"
packageID := packageScope + "." + packageName
packageVersion := "1.0.3"
packageVersion2 := "1.0.4"
packageAuthor := "KN4CK3R"
packageDescription := "Gitea Test Package"
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) {
defer tests.PrintCurrentTest(t)()
@ -211,7 +301,7 @@ func TestPackageSwift(t *testing.T) {
SetHeader("Accept", swift_router.AcceptJSON)
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, 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
DecodeJSON(t, resp, &result)
assert.Len(t, result.Releases, 1)
assert.Contains(t, result.Releases, packageVersion)
assert.Equal(t, versionURL, result.Releases[packageVersion].URL)
assert.Len(t, result.Releases, 2)
assert.Contains(t, result.Releases, packageVersion2)
assert.Equal(t, versionURL, result.Releases[packageVersion2].URL)
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.json", url, packageScope, packageName)).
AddBasicAuth(user.Name)

View File

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

View File

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

View File

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

View File

@ -443,13 +443,25 @@
}
.markup pre > code {
padding: 0;
margin: 0;
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;
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 {
@ -470,16 +482,11 @@
word-break: normal;
}
.markup pre {
word-wrap: normal;
}
.markup pre code,
.markup pre tt {
display: inline;
padding: 0;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
@ -522,20 +529,6 @@
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 {
display: block;
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 {
cursor: pointer;
display: inline-block;
min-height: 1em;
display: inline-flex;
outline: none;
vertical-align: baseline;
font-family: var(--fonts-regular);
margin: 0 0.25em 0 0;
padding: 0.78571429em 1.5em;
font-weight: var(--font-weight-normal);
font-size: 1rem;
text-align: center;
text-decoration: none;
line-height: 1;
border-radius: 0.28571429rem;
border-radius: var(--border-radius);
user-select: none;
-webkit-tap-highlight-color: transparent;
justify-content: center;
@ -58,12 +53,13 @@
pointer-events: none !important;
}
/* there is no "ui labeled icon button" support" because it is not used */
.ui.labeled.button:not(.icon) {
display: inline-flex;
flex-direction: row;
background: none;
padding: 0 !important;
padding: 0;
border: none;
min-height: unset;
}
.ui.labeled.button > .button {
margin: 0;
@ -102,47 +98,60 @@
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.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.button {
padding: 0.58928571em;
}
.ui.compact.labeled.icon.button {
padding: 0.58928571em 3.69642857em;
}
.ui.compact.labeled.icon.button > .icon {
padding: 0.58928571em 0;
padding: 0.57em /* around 8px */;
}
.ui.buttons .button,
.ui.button {
font-size: 1rem;
}
/* reference size: mini: padding-x=16, height=30 ; compact: padding-x=12, height=26 */
.ui.mini.buttons .dropdown,
.ui.mini.buttons .dropdown .menu > .item,
.ui.mini.buttons .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 .menu > .item,
.ui.tiny.buttons .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 .menu > .item,
.ui.small.buttons .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.button:not(.compact) {
padding: 0.78571429em;
padding: 0.57em;
}
.ui.icon.buttons .button > .icon,
.ui.icon.button > .icon {
@ -152,12 +161,12 @@
.ui.basic.buttons .button,
.ui.basic.button {
border-radius: 0.28571429rem;
border-radius: var(--border-radius);
background: none;
}
.ui.basic.buttons {
border: 1px solid var(--color-secondary);
border-radius: 0.28571429rem;
border-radius: var(--border-radius);
}
.ui.basic.buttons .button {
border-radius: 0;
@ -188,29 +197,6 @@
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 {
background-color: var(--color-green);
color: var(--color-white);
@ -366,6 +352,14 @@ a.btn:hover {
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.
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 */
@ -379,12 +373,12 @@ It needs some tricks to tweak the left/right borders with active state */
.ui.buttons .button:first-child {
border-left: none;
margin-left: 0;
border-top-left-radius: 0.28571429rem;
border-bottom-left-radius: 0.28571429rem;
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
}
.ui.buttons .button:last-child {
border-top-right-radius: 0.28571429rem;
border-bottom-right-radius: 0.28571429rem;
border-top-right-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
.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 {
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 {
background: var(--color-hover);
}
.tippy-box[data-theme="menu"] .item.active {
background: var(--color-active);
}

View File

@ -1629,21 +1629,17 @@ td .commit-summary {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.repo-button-row-left {
flex-grow: 1;
}
.repo-button-row .button {
padding: 6px 10px !important;
height: 30px;
.repo-button-row .ui.button {
flex-shrink: 0;
margin: 0;
}
.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 */
min-height: 30px;
}
tbody.commit-list {
@ -1788,12 +1784,12 @@ tbody.commit-list {
.resolved-placeholder {
display: flex;
align-items: center;
font-size: 14px !important;
padding: 8px !important;
font-weight: var(--font-weight-normal) !important;
border: 1px solid var(--color-secondary) !important;
border-radius: var(--border-radius) !important;
margin: 4px !important;
justify-content: space-between;
margin: 4px;
padding: 8px;
border: 1px solid var(--color-secondary);
border-radius: var(--border-radius);
background: var(--color-box-header);
}
.resolved-placeholder + .comment-code-cloud {
@ -2221,10 +2217,11 @@ tbody.commit-list {
max-width: min(400px, 90vw);
}
.branch-selector-dropdown .branch-dropdown-button {
.branch-selector-dropdown .ui.button.branch-dropdown-button {
margin: 0;
max-width: 340px;
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).

View File

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

View File

@ -27,47 +27,3 @@
.repo-header .flex-item-trailing {
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;
}
.code-line-button {
.ui.button.code-line-button {
border: 1px solid var(--color-secondary);
border-radius: var(--border-radius);
padding: 1px 4px !important;
padding: 1px 4px;
margin: 0;
min-height: 0;
position: absolute;
font-family: var(--fonts-regular);
left: 0;
transform: translateX(calc(-50% + 6px));
cursor: pointer;
left: 6px;
}
.code-line-button:hover {
background: var(--color-secondary) !important;
.ui.button.code-line-button:hover {
background: var(--color-secondary);
}

View File

@ -1,6 +1,6 @@
.list-header {
display: flex;
align-items: center;
align-items: stretch;
flex-wrap: wrap;
gap: .5rem;
}
@ -8,9 +8,8 @@
.list-header-search {
display: flex;
flex: 1;
align-items: center;
align-items: stretch;
flex-wrap: wrap;
justify-content: center;
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 {
padding: 2px;
position: absolute;
margin-left: -22px;
min-height: 0;
z-index: 5;
opacity: 0;
transition: transform 0.1s ease-in-out;
@ -58,11 +51,6 @@
margin-bottom: 0.5em;
}
.show-outdated:hover,
.hide-outdated:hover {
text-decoration: underline;
}
.comment-code-cloud {
padding: 0.5rem 1rem !important;
position: relative;

View File

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

View File

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

View File

@ -217,13 +217,13 @@ export default defineComponent({
</script>
<template>
<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">
<template v-if="dropdownFixedText">{{ dropdownFixedText }}</template>
<template v-else>
<svg-icon v-if="currentRefType === 'tag'" name="octicon-tag"/>
<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>
</span>
<svg-icon name="octicon-triangle-down" :size="14" class="dropdown icon"/>

View File

@ -353,12 +353,12 @@ export default defineComponent({
</div>
<div>
<!-- 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">
<span class="not-mobile">{{ locale.filterLabel }}</span> <strong>{{ locale.contributionType[type] }}</strong>
<svg-icon name="octicon-triangle-down" :size="14"/>
</div>
<div class="right menu">
<div class="left menu">
<div :class="['item', {'selected': type === 'commits'}]" data-value="commits">
{{ locale.contributionType.commits }}
</div>

View File

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

View File

@ -1,6 +1,5 @@
import {initCompLabelEdit} from './comp/LabelEdit.ts';
import {queryElems, toggleElem} from '../utils/dom.ts';
import {initAvatarUploaderWithCropper} from './comp/Cropper.ts';
import {toggleElem} from '../utils/dom.ts';
export function initCommonOrganization() {
if (!document.querySelectorAll('.organization').length) {
@ -14,6 +13,4 @@ export function initCommonOrganization() {
// 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 {queryElems} from '../utils/dom.ts';
import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts';
import {initAvatarUploaderWithCropper} from './comp/Cropper.ts';
const {appUrl} = window.config;
@ -80,6 +81,10 @@ export function initGlobalTabularMenu() {
fomanticQuery('.ui.menu.tabular:not(.custom) .item').tab();
}
export function initGlobalAvatarUploader() {
registerGlobalInitFunc('initAvatarUploader', initAvatarUploaderWithCropper);
}
// for performance considerations, it only uses performant syntax
function attachInputDirAuto(el: Partial<HTMLInputElement | HTMLTextAreaElement>) {
if (el.type !== 'hidden' &&

View File

@ -1,6 +1,5 @@
import {svg} from '../svg.ts';
import {createTippy} from '../modules/tippy.ts';
import {clippie} from 'clippie';
import {toAbsoluteUrl} from '../utils.ts';
import {addDelegatedEventListener} from '../utils/dom.ts';
@ -43,7 +42,8 @@ function selectRange(range: string): Element {
if (!copyPermalink) return;
let link = copyPermalink.getAttribute('data-url');
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('-') : [];
@ -138,8 +138,4 @@ export function initRepoCodeView() {
};
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 {onInputDebounce, queryElems, toggleClass, toggleElem} from '../utils/dom.ts';
import {POST} from '../modules/fetch.ts';
import {initAvatarUploaderWithCropper} from './comp/Cropper.ts';
import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
@ -149,6 +148,4 @@ export function initRepoSettings() {
initRepoSettingsSearchTeamBox();
initRepoSettingsGitHook();
initRepoSettingsBranchesDrag();
queryElems(document, '.avatar-file-with-cropper', initAvatarUploaderWithCropper);
}

View File

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

View File

@ -60,7 +60,7 @@ import {initColorPickers} from './features/colorpicker.ts';
import {initAdminSelfCheck} from './features/admin/selfcheck.ts';
import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.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 {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts';
import {callInitFunctions} from './modules/init.ts';
@ -72,6 +72,7 @@ initSubmitEventPolyfill();
onDomReady(() => {
const initStartTime = performance.now();
const initPerformanceTracer = callInitFunctions([
initGlobalAvatarUploader,
initGlobalDropdown,
initGlobalTabularMenu,
initGlobalFetchAction,

View File

@ -15,6 +15,8 @@ export function initMarkupCodeCopy(elMarkup: HTMLElement): void {
const btn = makeCodeCopyButton();
// remove final trailing newline introduced during HTML rendering
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
// 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> {
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) {
parent.addEventListener(type, (e: Event) => {
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);
}, options);
}