0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-06 23:38:48 +02:00

Merge branch 'main' into fix/project-board-api-review-feedback

This commit is contained in:
Lunny Xiao 2026-03-24 20:46:45 -07:00 committed by GitHub
commit 2f27c75f65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 604 additions and 670 deletions

View File

@ -13,7 +13,7 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: DeterminateSystems/determinate-nix-action@v3
- uses: DeterminateSystems/update-flake-lock@main
with:

View File

@ -40,7 +40,7 @@ jobs:
json: ${{ steps.changes.outputs.json }}
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
- uses: dorny/paths-filter@v4
id: changes
with:
filters: |

View File

@ -40,7 +40,7 @@ jobs:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v7
- run: uv python install 3.14
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
node-version: 24
@ -71,7 +71,7 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v5
with:
node-version: 24
@ -86,7 +86,7 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
node-version: 24
@ -168,7 +168,7 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
node-version: 24
@ -222,7 +222,7 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
node-version: 24

View File

@ -21,17 +21,17 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v6
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- name: Build regular container image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: false
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
- name: Build rootless container image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
push: false

View File

@ -25,7 +25,7 @@ jobs:
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
node-version: 24

View File

@ -22,7 +22,7 @@ jobs:
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
node-version: 24
@ -35,7 +35,7 @@ jobs:
TAGS: bindata sqlite sqlite_unlock_notify
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v6
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
@ -52,7 +52,7 @@ jobs:
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@v5
uses: aws-actions/configure-aws-credentials@v6
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
@ -71,14 +71,14 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- name: Get cleaned branch name
id: clean_name
run: |
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- uses: docker/metadata-action@v5
- uses: docker/metadata-action@v6
id: meta
with:
images: |-
@ -88,7 +88,7 @@ jobs:
type=raw,value=${{ steps.clean_name.outputs.branch }}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@v5
- uses: docker/metadata-action@v6
id: meta_rootless
with:
images: |-
@ -102,18 +102,18 @@ jobs:
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
@ -123,7 +123,7 @@ jobs:
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max
- name: build rootless docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64

View File

@ -23,7 +23,7 @@ jobs:
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
node-version: 24
@ -36,7 +36,7 @@ jobs:
TAGS: bindata sqlite sqlite_unlock_notify
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v6
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
@ -53,7 +53,7 @@ jobs:
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@v5
uses: aws-actions/configure-aws-credentials@v6
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
@ -81,9 +81,9 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/metadata-action@v5
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- uses: docker/metadata-action@v6
id: meta
with:
images: |-
@ -96,7 +96,7 @@ jobs:
type=semver,pattern={{version}}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@v5
- uses: docker/metadata-action@v6
id: meta_rootless
with:
images: |-
@ -112,18 +112,18 @@ jobs:
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular container image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
@ -131,7 +131,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
- name: build rootless container image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64

View File

@ -26,7 +26,7 @@ jobs:
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
node-version: 24
@ -39,7 +39,7 @@ jobs:
TAGS: bindata sqlite sqlite_unlock_notify
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v6
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
@ -56,7 +56,7 @@ jobs:
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@v5
uses: aws-actions/configure-aws-credentials@v6
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
@ -84,9 +84,9 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/metadata-action@v5
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- uses: docker/metadata-action@v6
id: meta
with:
images: |-
@ -103,7 +103,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@v5
- uses: docker/metadata-action@v6
id: meta_rootless
with:
images: |-
@ -124,18 +124,18 @@ jobs:
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular container image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
@ -143,7 +143,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
- name: build rootless container image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64

View File

@ -1,10 +1,8 @@
# Instructions for agents
- Use `make help` to find available development targets
- Before committing `.go` changes, run `make fmt` to format, and run `make lint-go` to lint
- Before committing `.ts` changes, run `make lint-js` to lint
- Before committing `go.mod` changes, run `make tidy`
- Before committing new `.go` files, add the current year into the copyright header
- Before committing any files, remove all trailing whitespace from source code lines
- Run `make fmt` to format `.go` files, and run `make lint-go` to lint them
- Run `make lint-js` to lint `.ts` files
- Run `make tidy` after any `go.mod` changes
- Add the current year into the copyright header of new `.go` files
- Ensure no trailing whitespace in edited files
- Never force-push to pull request branches
- Always start issue and pull request comments with an authorship attribution

View File

@ -276,6 +276,9 @@ Gitea or set your environment appropriately.`, "")
lastline = 0
}
}
if err := scanner.Err(); err != nil {
return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err)
}
if count > 0 {
hookOptions.OldCommitIDs = oldCommitIDs[:count]
@ -415,6 +418,11 @@ Gitea or set your environment appropriately.`, "")
count = 0
}
}
if err := scanner.Err(); err != nil {
_ = dWriter.Close()
hookPrintResults(results)
return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err)
}
if count == 0 {
if wasEmpty && masterPushed {

View File

@ -69,13 +69,12 @@ RUN_USER = ; git
;; Most users should set it to the real website URL of their Gitea instance when there is a reverse proxy.
;ROOT_URL =
;;
;; Controls how to detect the public URL.
;; Although it defaults to "legacy" (to avoid breaking existing users), most instances should use the "auto" behavior,
;; Controls how to detect the public URL. Most instances should use the "auto" behavior,
;; especially when the Gitea instance needs to be accessed in a container network.
;; * legacy: detect the public URL from "Host" header if "X-Forwarded-Proto" header exists, otherwise use "ROOT_URL".
;; * auto: always use "Host" header, and also use "X-Forwarded-Proto" header if it exists. If no "Host" header, use "ROOT_URL".
;; * legacy: (default <= 1.25) detect the public URL from "Host" header if "X-Forwarded-Proto" header exists, otherwise use "ROOT_URL".
;; * auto: (default >= 1.26) always use "Host" header, and also use "X-Forwarded-Proto" header if it exists. If no "Host" header, use "ROOT_URL".
;; * never: always use "ROOT_URL", never detect from request headers.
;PUBLIC_URL_DETECTION = legacy
;PUBLIC_URL_DETECTION = auto
;;
;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy.
;; DO NOT USE IT IN PRODUCTION!!!
@ -1153,15 +1152,15 @@ LEVEL = Info
;; Add co-authored-by and co-committed-by trailers if committer does not match author
;ADD_CO_COMMITTER_TRAILERS = true
;;
;; In addition to testing patches using the three-way merge method, re-test conflicting patches with git apply
;TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY = false
;;
;; Retarget child pull requests to the parent pull request branch target on merge of parent pull request. It only works on merged PRs where the head and base branch target the same repo.
;RETARGET_CHILDREN_ON_MERGE = true
;;
;; Delay mergeable check until page view or API access, for pull requests that have not been updated in the specified days when their base branches get updated.
;; Use "-1" to always check all pull requests (old behavior). Use "0" to always delay the checks.
;DELAY_CHECK_FOR_INACTIVE_DAYS = 7
;;
;; Set the default value for "Delete pull request branch after merge by default" for new repositories
;DEFAULT_DELETE_BRANCH_AFTER_MERGE = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@ -139,7 +140,10 @@ func init() {
// BeforeSet is invoked from XORM before setting the value of a field of this object.
func (source *Source) BeforeSet(colName string, val xorm.Cell) {
if colName == "type" {
typ := Type(db.Cell2Int64(val))
typ, _, err := db.CellToInt(val, NoType)
if err != nil {
setting.PanicInDevOrTesting("Unable to convert login source (id=%d) type: %v", source.ID, err)
}
constructor, ok := registeredConfigs[typ]
if !ok {
return

View File

@ -17,13 +17,9 @@ import (
)
type TestSource struct {
auth_model.ConfigBase
auth_model.ConfigBase `json:"-"`
Provider string
ClientID string
ClientSecret string
OpenIDConnectAutoDiscoveryURL string
IconURL string
TestField string
}
// FromDB fills up a LDAPConfig from serialized format.
@ -37,27 +33,23 @@ func (source *TestSource) ToDB() ([]byte, error) {
}
func TestDumpAuthSource(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
require.NoError(t, unittest.PrepareTestDatabase())
authSourceSchema, err := unittest.GetXORMEngine().TableInfo(new(auth_model.Source))
assert.NoError(t, err)
require.NoError(t, err)
auth_model.RegisterTypeConfig(auth_model.OAuth2, new(TestSource))
source := &auth_model.Source{
Type: auth_model.OAuth2,
Name: "TestSource",
Cfg: &TestSource{TestField: "TestValue"},
}
require.NoError(t, auth_model.CreateSource(t.Context(), source))
auth_model.CreateSource(t.Context(), &auth_model.Source{
Type: auth_model.OAuth2,
Name: "TestSource",
IsActive: false,
Cfg: &TestSource{
Provider: "ConvertibleSourceName",
ClientID: "42",
},
})
sb := new(strings.Builder)
// TODO: this test is quite hacky, it should use a low-level "select" (without model processors) but not a database dump
engine := unittest.GetXORMEngine()
require.NoError(t, engine.DumpTables([]*schemas.Table{authSourceSchema}, sb))
assert.Contains(t, sb.String(), `"Provider":"ConvertibleSourceName"`)
// intentionally test the "dump" to make sure the dumped JSON is correct: https://github.com/go-gitea/gitea/pull/16847
sb := &strings.Builder{}
require.NoError(t, unittest.GetXORMEngine().DumpTables([]*schemas.Table{authSourceSchema}, sb))
// the dumped SQL is something like:
// INSERT INTO `login_source` (`id`, `type`, `name`, `is_active`, `is_sync_enabled`, `two_factor_policy`, `cfg`, `created_unix`, `updated_unix`) VALUES (1,6,'TestSource',0,0,'','{"TestField":"TestValue"}',1774179784,1774179784);
assert.Contains(t, sb.String(), `'{"TestField":"TestValue"}'`)
}

View File

@ -5,12 +5,11 @@ package db
import (
"fmt"
"strconv"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"xorm.io/xorm"
"xorm.io/xorm/convert"
"xorm.io/xorm/schemas"
)
@ -74,15 +73,14 @@ WHERE ST.name ='varchar'`)
return err
}
// Cell2Int64 converts a xorm.Cell type to int64,
// and handles possible irregular cases.
func Cell2Int64(val xorm.Cell) int64 {
switch (*val).(type) {
case []uint8:
log.Trace("Cell2Int64 ([]uint8): %v", *val)
v, _ := strconv.ParseInt(string((*val).([]uint8)), 10, 64)
return v
// CellToInt converts a xorm.Cell field value to an int value
func CellToInt[T ~int | int64](cell xorm.Cell, def T) (ret T, has bool, err error) {
if *cell == nil {
return def, false, nil
}
return (*val).(int64)
val, err := convert.AsInt64(*cell)
if err != nil {
return def, false, err
}
return T(val), true, err
}

View File

@ -142,6 +142,7 @@ func DefaultPullRequestsConfig() *PullRequestsConfig {
AllowRebaseUpdate: true,
DefaultAllowMaintainerEdit: true,
}
cfg.DefaultDeleteBranchAfterMerge = setting.Repository.PullRequest.DefaultDeleteBranchAfterMerge
cfg.DefaultMergeStyle = MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle)
cfg.DefaultMergeStyle = util.IfZero(cfg.DefaultMergeStyle, MergeStyleMerge)
return cfg
@ -226,7 +227,11 @@ func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool {
func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
switch colName {
case "type":
r.Type = unit.Type(db.Cell2Int64(val))
var err error
r.Type, _, err = db.CellToInt(val, unit.TypeInvalid)
if err != nil {
setting.PanicInDevOrTesting("Unable to convert repo unit (id=%d) type: %v", r.ID, err)
}
switch r.Type {
case unit.TypeExternalWiki:
r.Config = new(ExternalWikiConfig)

View File

@ -86,9 +86,9 @@ var (
DefaultMergeMessageOfficialApproversOnly bool
PopulateSquashCommentWithCommitMessages bool
AddCoCommitterTrailers bool
TestConflictingPatchesWithGitApply bool
RetargetChildrenOnMerge bool
DelayCheckForInactiveDays int
DefaultDeleteBranchAfterMerge bool
} `ini:"repository.pull-request"`
// Issue Setting
@ -210,9 +210,9 @@ var (
DefaultMergeMessageOfficialApproversOnly bool
PopulateSquashCommentWithCommitMessages bool
AddCoCommitterTrailers bool
TestConflictingPatchesWithGitApply bool
RetargetChildrenOnMerge bool
DelayCheckForInactiveDays int
DefaultDeleteBranchAfterMerge bool
}{
WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
// Same as GitHub. See

View File

@ -286,7 +286,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort
AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL)
PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLLegacy)
PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLAuto)
if PublicURLDetection != PublicURLAuto && PublicURLDetection != PublicURLLegacy && PublicURLDetection != PublicURLNever {
log.Fatal("Invalid PUBLIC_URL_DETECTION value: %s", PublicURLDetection)
}

View File

@ -45,6 +45,14 @@ func ParseJSONError(buf []byte) (ret struct {
return ret
}
func ParseJSONRedirect(buf []byte) (ret struct {
Redirect string `json:"redirect"`
},
) {
_ = json.Unmarshal(buf, &ret)
return ret
}
func IsNormalPageCompleted(s string) bool {
return strings.Contains(s, `<footer class="page-footer"`) && strings.Contains(s, `</html>`)
}

View File

@ -5,8 +5,8 @@ package translation
import (
"fmt"
"html"
"html/template"
"strings"
)
// MockLocale provides a mocked locale without any translations
@ -20,25 +20,40 @@ func (l MockLocale) Language() string {
return "en"
}
func (l MockLocale) TrString(s string, args ...any) string {
return sprintAny(s, args...)
func (l MockLocale) TrString(format string, args ...any) (ret string) {
ret = format + ":"
for _, arg := range args {
// usually there is no arg or at most 1-2 args, so a simple string concatenation is more efficient
switch v := arg.(type) {
case string:
ret += v + ","
default:
ret += fmt.Sprint(v) + ","
}
}
return ret[:len(ret)-1]
}
func (l MockLocale) Tr(s string, args ...any) template.HTML {
return template.HTML(sprintAny(s, args...))
func (l MockLocale) Tr(format string, args ...any) (ret template.HTML) {
ret = template.HTML(html.EscapeString(format)) + ":"
for _, arg := range args {
// usually there is no arg or at most 1-2 args, so a simple string concatenation is more efficient
switch v := arg.(type) {
case template.HTML:
ret += v + ","
case string:
ret += template.HTML(html.EscapeString(v)) + ","
default:
ret += template.HTML(html.EscapeString(fmt.Sprint(v))) + ","
}
}
return ret[:len(ret)-1]
}
func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
return template.HTML(sprintAny(key1, args...))
return l.Tr(key1, args...)
}
func (l MockLocale) PrettyNumber(v any) string {
return fmt.Sprint(v)
}
func sprintAny(s string, args ...any) string {
if len(args) == 0 {
return s
}
return s + ":" + fmt.Sprintf(strings.Repeat(",%v", len(args))[1:], args...)
}

View File

@ -969,7 +969,6 @@
"repo.visibility_description": "Only the owner or the organization members if they have rights, will be able to see it.",
"repo.visibility_helper": "Make repository private",
"repo.visibility_helper_forced": "Your site administrator forces new repositories to be private.",
"repo.visibility_fork_helper": "(Changing this will affect all forks.)",
"repo.clone_helper": "Need help cloning? Visit <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">Help</a>.",
"repo.fork_repo": "Fork Repository",
"repo.fork_from": "Fork From",
@ -2174,7 +2173,8 @@
"repo.settings.transfer_abort_invalid": "You cannot cancel a non existent repository transfer.",
"repo.settings.transfer_abort_success": "The repository transfer to %s was successfully canceled.",
"repo.settings.transfer_desc": "Transfer this repository to a user or to an organization for which you have administrator rights.",
"repo.settings.transfer_form_title": "Enter the repository name as confirmation:",
"repo.settings.enter_repo_name_to_confirm": "Enter the repository name as confirmation:",
"repo.settings.enter_repo_full_name_to_confirm": "Enter the full repository name (owner/name) as confirmation:",
"repo.settings.transfer_in_progress": "There is currently an ongoing transfer. Please cancel it if you would like to transfer this repository to another user.",
"repo.settings.transfer_notices_1": "- You will lose access to the repository if you transfer it to an individual user.",
"repo.settings.transfer_notices_2": "- You will keep access to the repository if you transfer it to an organization that you (co-)own.",
@ -2477,7 +2477,10 @@
"repo.settings.visibility.private.text": "Changing the visibility to private will make the repo visible only to allowed members and may remove the relationship between it and existing forks, watchers, and stars.",
"repo.settings.visibility.private.bullet_title": "<strong>Changing the visibility to private will:</strong>",
"repo.settings.visibility.private.bullet_one": "Make the repo visible only to allowed members.",
"repo.settings.visibility.private.bullet_two": "May remove the relationship between it and <strong>forks</strong>, <strong>watchers</strong>, and <strong>stars</strong>.",
"repo.settings.visibility.private.bullet_two": "Apply the visibility to its forks, and remove the <strong>watchers</strong> and <strong>stars</strong>.",
"repo.settings.visibility.private.stats_stars": "This repository has <strong>%d</strong> star(s) that may be lost.",
"repo.settings.visibility.private.stats_watchers": "This repository has <strong>%d</strong> watcher(s) that may be lost.",
"repo.settings.visibility.private.stats_forks": "This repository has <strong>%d</strong> fork(s) that are associated.",
"repo.settings.visibility.public.button": "Make Public",
"repo.settings.visibility.public.text": "Changing the visibility to public will make the repo visible to anyone.",
"repo.settings.visibility.public.bullet_title": "<strong>Changing the visibility to public will:</strong>",

View File

@ -81,6 +81,7 @@
"retry": "Atriail",
"rerun": "Ath-rith",
"rerun_all": "Ath-rith na poist go léir",
"rerun_failed": "Athrith poist theipithe",
"save": "Sábháil",
"add": "Cuir",
"add_all": "Cuir Gach",
@ -168,6 +169,7 @@
"search.exact_tooltip": "Ní chuir san áireamh ach torthaí a mheaitseálann leis an téarma",
"search.repo_kind": "Cuardaigh stórais…",
"search.user_kind": "Cuardaigh úsáideoirí…",
"search.badge_kind": "Cuardaigh suaitheantais…",
"search.org_kind": "Cuardaigh eagraíochtaí…",
"search.team_kind": "Cuardaigh foirne…",
"search.code_kind": "Cuardaigh cód…",
@ -542,6 +544,7 @@
"form.glob_pattern_error": " tá patrún glob neamhbhailí: %s.",
"form.regex_pattern_error": "tá patrún regex neamhbhailí: %s.",
"form.username_error": "Ní féidir ach carachtair alfa-uimhriúla ('0-9','a-z','A-Z'), fleasc ('-'), fo-líne ('_') agus ponc ('.') a bheith i `. Ní féidir é a thosú ná a chríochnú le carachtair neamh-alfa-uimhriúla, agus tá cosc ar charachtair neamh-alfa-uimhriúla as a chéile ach an oiread.`",
"form.invalid_slug_error": " neamhbhailí.",
"form.invalid_group_team_map_error": " tá mapáil neamhbhailí: %s",
"form.unknown_error": "Earráid anaithnid:",
"form.captcha_incorrect": "Tá an cód CAPTCHA mícheart.",
@ -645,6 +648,7 @@
"user.block.note.edit": "Cuir nóta in eagar",
"user.block.list": "Úsáideoirí blocáilte",
"user.block.list.none": "Níor chuir tú bac ar aon úsáideoirí.",
"settings.general": "Ginearálta",
"settings.profile": "Próifíl",
"settings.account": "Cuntas",
"settings.appearance": "Dealramh",
@ -2856,6 +2860,30 @@
"admin.hooks": "Crúcaí Gréasán",
"admin.integrations": "Comhtháthaithe",
"admin.authentication": "Foinsí Fíordheimhnithe",
"admin.badges": "Suaitheantais",
"admin.badges.badges_manage_panel": "Bainistíocht Suaitheantais",
"admin.badges.details": "Sonraí Suaitheantais",
"admin.badges.new_badge": "Cruthaigh Suaitheantas Nua",
"admin.badges.slug": "Sluga",
"admin.badges.slug_been_taken": "Tá an sluga tógtha cheana féin.",
"admin.badges.description": "Cur síos",
"admin.badges.image_url": "URL na híomhá",
"admin.badges.new_success": "Tá an suaitheantas \"%s\" cruthaithe.",
"admin.badges.update_success": "Tá an suaitheantas nuashonraithe.",
"admin.badges.deletion_success": "Tá an suaitheantas scriosta.",
"admin.badges.edit_badge": "Cuir Suaitheantas in Eagar",
"admin.badges.update_badge": "Nuashonraigh Suaitheantas",
"admin.badges.delete_badge": "Scrios Suaitheantas",
"admin.badges.delete_badge_desc": "An bhfuil tú cinnte gur mian leat an suaitheantas seo a scriosadh go buan?",
"admin.badges.users_with_badge": "Úsáideoirí a bhfuil suaitheantas acu: %s",
"admin.badges.not_found": "Níor aimsíodh an suaitheantas.",
"admin.badges.user_already_has": "Tá an suaitheantas seo ag an úsáideoir cheana féin.",
"admin.badges.user_add_success": "Sannadh suaitheantas don úsáideoir go rathúil.",
"admin.badges.user_remove_success": "Baineadh an suaitheantas den úsáideoir go rathúil.",
"admin.badges.manage_users": "Bainistigh Úsáideoirí",
"admin.badges.add_user": "Cuir Úsáideoir leis",
"admin.badges.remove_user": "Bain Úsáideoir",
"admin.badges.delete_user_desc": "An bhfuil tú cinnte gur mian leat an t-úsáideoir seo a bhaint den suaitheantas?",
"admin.emails": "Seoltaí Ríomhphoist Úsáideoirí",
"admin.config": "Cumraíocht",
"admin.config_summary": "Achoimre",
@ -3707,6 +3735,10 @@
"actions.runs.not_done": "Níl an rith sreabha oibre seo críochnaithe.",
"actions.runs.view_workflow_file": "Féach ar chomhad sreabha oibre",
"actions.runs.workflow_graph": "Graf Sreabhadh Oibre",
"actions.runs.summary": "Achoimre",
"actions.runs.all_jobs": "Gach post",
"actions.runs.triggered_via": "Spreagtha trí %s",
"actions.runs.total_duration": "Fad iomlán:",
"actions.workflow.disable": "Díchumasaigh sreabhadh oibre",
"actions.workflow.disable_success": "D'éirigh le sreabhadh oibre '%s' a dhíchumasú.",
"actions.workflow.enable": "Cumasaigh sreabhadh oibre",
@ -3756,5 +3788,24 @@
"git.filemode.normal_file": "Rialta",
"git.filemode.executable_file": "Inrite",
"git.filemode.symbolic_link": "Nasc siombalach",
"git.filemode.submodule": "Fo-mhodúl"
"git.filemode.submodule": "Fo-mhodúl",
"org.repos.none": "Gan aon stórtha.",
"actions.general.permissions": "Ceadanna Comhartha Gníomhartha",
"actions.general.token_permissions.mode": "Ceadanna Réamhshocraithe Comharthaí",
"actions.general.token_permissions.mode.desc": "Úsáidfidh post Gníomhartha na ceadanna réamhshocraithe mura ndearbhaíonn sé a cheadanna sa chomhad sreabha oibre.",
"actions.general.token_permissions.mode.permissive": "Ceadaitheach",
"actions.general.token_permissions.mode.permissive.desc": "Ceadanna léigh agus scríbhneoireachta do stórlann an phoist.",
"actions.general.token_permissions.mode.restricted": "Srianta",
"actions.general.token_permissions.mode.restricted.desc": "Ceadanna léite amháin d'aonaid ábhair (cód, eisiúintí) stórlann an phoist.",
"actions.general.token_permissions.override_owner": "Sáraigh cumraíocht leibhéal an úinéara",
"actions.general.token_permissions.override_owner_desc": "Má tá sé cumasaithe, úsáidfidh an stór seo a chumraíocht Gníomhartha féin in ionad an chumraíocht ar leibhéal an úinéara (úsáideoir nó eagraíocht) a leanúint.",
"actions.general.token_permissions.maximum": "Uasmhéid Ceadanna Comharthaí",
"actions.general.token_permissions.maximum.description": "Beidh ceadanna éifeachtacha an phoist gníomhartha teoranta ag na ceadanna uasta.",
"actions.general.token_permissions.fork_pr_note": "Mura gcuirtear tús le post trí iarratas tarraingthe ó fhorc, ní rachaidh a cheadanna éifeachtacha thar na ceadanna léite amháin.",
"actions.general.token_permissions.customize_max_permissions": "Saincheap na ceadanna uasta",
"actions.general.cross_repo": "Rochtain Tras-Stórtha",
"actions.general.cross_repo_desc": "Ceadaigh rochtain (léamh amháin) a bheith ag na stórtha uile san úinéir seo ar na stórtha roghnaithe le GITEA_TOKEN agus poist Gníomhartha á reáchtáil.",
"actions.general.cross_repo_selected": "Stórtha roghnaithe",
"actions.general.cross_repo_target_repos": "Stórtha Spriocdhírithe",
"actions.general.cross_repo_add": "Cuir Stór Sprioc leis"
}

View File

@ -81,6 +81,7 @@
"retry": "재시도",
"rerun": "다시 실행",
"rerun_all": "모든 작업 다시 실행",
"rerun_failed": "실패한 작업 다시 실행",
"save": "저장",
"add": "추가",
"add_all": "모두 추가",
@ -168,6 +169,7 @@
"search.exact_tooltip": "검색어와 정확하게 일치하는 결과만 포함합니다",
"search.repo_kind": "리포지토리 검색…",
"search.user_kind": "사용자 검색…",
"search.badge_kind": "배지 검색…",
"search.org_kind": "조직 검색…",
"search.team_kind": "팀 검색…",
"search.code_kind": "코드 검색…",
@ -542,6 +544,7 @@
"form.glob_pattern_error": " 와일드카드 패턴이 잘못됨: %s.",
"form.regex_pattern_error": " 정규 표현식 패턴이 잘못됨: %s.",
"form.username_error": " `영숫자 ('0-9','a-z','A-Z'), 대시('-'), 밑줄('_'), 마침점 ('.') 만 포함할 수 있습니다. 시작과 끝문자는 반드시 영숫자이어야 하고, 영숫자가 아닌 문자를 연속해서 사용할 수 없습니다.`.",
"form.invalid_slug_error": " 유효하지 않음.",
"form.invalid_group_team_map_error": " 매핑이 잘못되었습니다: %s",
"form.unknown_error": "알 수 없는 오류:",
"form.captcha_incorrect": "CAPTCHA 코드가 올바르지 않습니다.",
@ -645,6 +648,7 @@
"user.block.note.edit": "노트 편집",
"user.block.list": "차단된 사용자",
"user.block.list.none": "차단한 사용자가 없습니다.",
"settings.general": "일반",
"settings.profile": "프로필",
"settings.account": "계정",
"settings.appearance": "외관",
@ -1154,7 +1158,7 @@
"repo.unstar": "별점취소",
"repo.star": "별점",
"repo.fork": "포크",
"repo.action.blocked_user": "리포지토리 소유자에게 차단되어 작업을 수행할 수 없습니다.",
"repo.action.blocked_user": "리포지토리 소유자에게 차단되어 액션을 수행할 수 없습니다.",
"repo.download_archive": "리포지토리 다운로드",
"repo.more_operations": "추가 작업",
"repo.quick_guide": "퀵 가이드",
@ -1178,7 +1182,7 @@
"repo.pulls": "풀 리퀘스트",
"repo.projects": "프로젝트",
"repo.packages": "패키지",
"repo.actions": "동작",
"repo.actions": "액션",
"repo.labels": "레이블",
"repo.org_labels_desc": "이 조직의 <strong>모든 리포지토리</strong>에서 사용할 수 있는 조직 수준 레이블",
"repo.org_labels_desc_manage": "관리",
@ -2140,7 +2144,7 @@
"repo.settings.projects_mode_repo": "리포지토리 프로젝트만",
"repo.settings.projects_mode_owner": "사용자 또는 조직 프로젝트만",
"repo.settings.projects_mode_all": "모든 프로젝트",
"repo.settings.actions_desc": "리포지토리 동작 활성화",
"repo.settings.actions_desc": "리포지토리 액션 활성화",
"repo.settings.admin_settings": "운영자 설정",
"repo.settings.admin_enable_health_check": "리포지토리 헬스 체크 활성화 (git fsck)",
"repo.settings.admin_code_indexer": "코드 인덱서",
@ -2174,7 +2178,7 @@
"repo.settings.transfer_in_progress": "현재 진행 중인 이전이 있습니다. 이 저장소를 다른 사용자에게 이전하려면 먼저 취소하십시오.",
"repo.settings.transfer_notices_1": "- 개별 사용자에게 리포지토리를 이전하면 리포지토리에 대한 액세스 권한을 잃게 됩니다.",
"repo.settings.transfer_notices_2": "- 당신이 (공동)소유하고 있는 조직으로 리포지토리를 이전하면 리포지토리에 대한 액세스 권한을 유지합니다.",
"repo.settings.transfer_notices_3": "- 리포지토리가 비공개이고 개별 사용자에게 이전되는 경우, 이 작업은 해당 사용자가 최소한 읽기 권한을 가지도록 보장합니다 (필요하다면 권한을 변경함).",
"repo.settings.transfer_notices_3": "- 저장소가 비공개이고 개별 사용자에게 이전되는 경우, 이 액션은 해당 사용자가 최소한 읽기 권한을 가지도록 보장합니다(필요하다면 권한을 변경함).",
"repo.settings.transfer_notices_4": "- 리포지토리가 조직에 속하고 다른 조직 또는 개인에게 이전하는 경우, 저장소의 이슈와 조직의 프로젝트 보드 간의 연결이 끊어집니다.",
"repo.settings.transfer_owner": "새 소유자",
"repo.settings.transfer_perform": "이전 수행",
@ -2789,7 +2793,7 @@
"org.teams.can_create_org_repo": "리포지토리 생성",
"org.teams.can_create_org_repo_helper": "멤버는 조직에서 새 리포지토리를 만들 수 있습니다. 생성자는 새 리포지토리에 대한 운영자 액세스 권한을 얻습니다.",
"org.teams.none_access": "액세스 없음",
"org.teams.none_access_helper": "멤버는 이 단위에서 어떤 작업도 보거나 수행할 수 없습니다. 공개 리포지토리에는 영향을 미치지 않습니다.",
"org.teams.none_access_helper": "멤버는 이 단위에서 어떤 액션도 보거나 수행할 수 없습니다. 공개 리포지토리에는 영향을 미치지 않습니다.",
"org.teams.general_access": "일반 액세스",
"org.teams.general_access_helper": "멤버 권한은 아래 권한 테이블에 따라 결정됩니다.",
"org.teams.read_access": "읽음",
@ -2856,6 +2860,30 @@
"admin.hooks": "Webhook",
"admin.integrations": "통합",
"admin.authentication": "인증 소스",
"admin.badges": "배지",
"admin.badges.badges_manage_panel": "배지 관리",
"admin.badges.details": "배지 상세정보",
"admin.badges.new_badge": "새 배지 만들기",
"admin.badges.slug": "슬러그",
"admin.badges.slug_been_taken": "이미 사용 중인 슬러그입니다.",
"admin.badges.description": "설명",
"admin.badges.image_url": "이미지 URL",
"admin.badges.new_success": "배지 \"%s\"가 생성되었습니다.",
"admin.badges.update_success": "배지가 업데이트 되었습니다.",
"admin.badges.deletion_success": "배지가 삭제되었습니다.",
"admin.badges.edit_badge": "배지 수정",
"admin.badges.update_badge": "배지 업데이트",
"admin.badges.delete_badge": "배지 삭제",
"admin.badges.delete_badge_desc": "이 배지를 영구적으로 삭제하겠습니까?",
"admin.badges.users_with_badge": "배지를 가진 사용자: %s",
"admin.badges.not_found": "배지를 찾을 수 없음.",
"admin.badges.user_already_has": "사용자는 이 배지를 이미 갖고 있습니다.",
"admin.badges.user_add_success": "사용자에게 배지가 성공적으로 지정되었습니다.",
"admin.badges.user_remove_success": "사용자에게 배지를 제거하는데 성공하였습니다.",
"admin.badges.manage_users": "사용자 관리",
"admin.badges.add_user": "사용자 추가",
"admin.badges.remove_user": "사용자 삭제",
"admin.badges.delete_user_desc": "이 사용자를 배지에서 제거하는 것이 확실한가요?",
"admin.emails": "사용자 이메일 주소",
"admin.config": "구성",
"admin.config_summary": "요약",
@ -2909,7 +2937,7 @@
"admin.dashboard.sync_external_users": "외부 사용자 데이터 동기화",
"admin.dashboard.cleanup_hook_task_table": "hook_task 테이블 정리",
"admin.dashboard.cleanup_packages": "만료된 패키지 정리",
"admin.dashboard.cleanup_actions": "만료된 작업 리소스 정리",
"admin.dashboard.cleanup_actions": "만료된 액션 리소스 정리",
"admin.dashboard.server_uptime": "서버를 켠 시간",
"admin.dashboard.current_goroutine": "현재 Go루틴",
"admin.dashboard.current_memory_usage": "현재 메모리 사용율",
@ -2944,10 +2972,10 @@
"admin.dashboard.update_checker": "업데이트 확인",
"admin.dashboard.delete_old_system_notices": "데이터베이스에서 모든 오래된 시스템 알림 삭제",
"admin.dashboard.gc_lfs": "LFS 메타 객체 가비지 컬렉션",
"admin.dashboard.stop_zombie_tasks": "좀비 작업 동작 중지",
"admin.dashboard.stop_zombie_tasks": "좀비 작업 액션 중지",
"admin.dashboard.stop_endless_tasks": "끝나지 않는 작업 중지",
"admin.dashboard.cancel_abandoned_jobs": "포기한 작업 동작 취소",
"admin.dashboard.start_schedule_tasks": "예약된 작업 동작 시작",
"admin.dashboard.cancel_abandoned_jobs": "포기한 작업 액션 취소",
"admin.dashboard.start_schedule_tasks": "예약된 작업 액션 시작",
"admin.dashboard.sync_branch.started": "브랜치 동기화 시작됨",
"admin.dashboard.sync_tag.started": "태그 동기화 시작됨",
"admin.dashboard.rebuild_issue_indexer": "이슈 인덱서 재구축",
@ -3611,7 +3639,7 @@
"packages.owner.settings.chef.keypair": "키 쌍 생성",
"packages.owner.settings.chef.keypair.description": "Chef 레지스트리에 인증하려면 키 쌍이 필요합니다. 이전에 키 쌍을 생성한 경우, 새 키 쌍를 생성하면 이전 키 쌍은 폐기됩니다.",
"secrets.secrets": "비밀",
"secrets.description": "비밀 키는 특정 동작에 전달되며 다른 방법으로는 읽을 수 없습니다.",
"secrets.description": "비밀 키는 특정 액션에 전달되며 다른 방법으로는 읽을 수 없습니다.",
"secrets.none": "아직 비밀이 없습니다.",
"secrets.creation.description": "설명",
"secrets.creation.name_placeholder": "대소문자를 구분하지 않으며, 영숫자 또는 밑줄 문자만, GITEA_ 또는 GITHUB_로 시작할 수 없음",
@ -3626,8 +3654,8 @@
"secrets.deletion.success": "비밀이 삭제되었습니다.",
"secrets.deletion.failed": "비밀 제거에 실패했습니다.",
"secrets.management": "비밀 관리",
"actions.actions": "동작",
"actions.unit.desc": "동작 관리",
"actions.actions": "액션",
"actions.unit.desc": "액션 관리",
"actions.status.unknown": "알수없음",
"actions.status.waiting": "대기 중",
"actions.status.running": "실행중",
@ -3703,10 +3731,14 @@
"actions.runs.expire_log_message": "로그가 너무 오래되어 제거되었습니다.",
"actions.runs.delete": "워크플로우 실행 삭제",
"actions.runs.cancel": "워크플로우 실행 취소",
"actions.runs.delete.description": "이 워크플로우 실행을 영구적으로 삭제하시겠습니까? 이 은 되돌릴 수 없습니다.",
"actions.runs.delete.description": "이 워크플로우 실행을 영구적으로 삭제하시겠습니까? 이 작은 되돌릴 수 없습니다.",
"actions.runs.not_done": "이 워크플로우 실행은 완료되지 않았습니다.",
"actions.runs.view_workflow_file": "워크플로우 파일 표시",
"actions.runs.workflow_graph": "워크플로우 그래프",
"actions.runs.summary": "요약",
"actions.runs.all_jobs": "모든 작업",
"actions.runs.triggered_via": "%s를 통해 트리거됨",
"actions.runs.total_duration": "총기간:",
"actions.workflow.disable": "워크플로 비활성화",
"actions.workflow.disable_success": "워크플로 '%s'가 성공적으로 비활성화되었습니다.",
"actions.workflow.enable": "워크플로 활성화",
@ -3726,7 +3758,7 @@
"actions.variables.none": "아직 변수가 없습니다.",
"actions.variables.deletion": "변수 제거",
"actions.variables.deletion.description": "변수 제거는 영구적이며 되돌릴 수 없습니다. 계속하시겠습니까?",
"actions.variables.description": "변수는 특정 동작에 전달되며 다른 방법으로는 읽을 수 없습니다.",
"actions.variables.description": "변수는 특정 액션에 전달되며 다른 방법으로는 읽을 수 없습니다.",
"actions.variables.id_not_exist": "ID %d인 변수가 존재하지 않습니다.",
"actions.variables.edit": "변수 수정",
"actions.variables.deletion.failed": "변수 제거에 실패했습니다.",
@ -3738,7 +3770,7 @@
"actions.logs.always_auto_scroll": "로그 자동 스크롤 항상 켜기",
"actions.logs.always_expand_running": "실행 중인 로그 항상 펼침",
"actions.general": "일반",
"actions.general.enable_actions": "동작 활성화",
"actions.general.enable_actions": "액션 활성화",
"actions.general.collaborative_owners_management": "보조 소유자 관리",
"actions.general.collaborative_owners_management_help": "보조 소유자는 이 리포지토리의 액션 및 워크플로에 접근할 수 있는 개인 리포지토리를 가진 사용자 또는 조직입니다.",
"actions.general.add_collaborative_owner": "보조 소유자 추가",
@ -3756,5 +3788,24 @@
"git.filemode.normal_file": "일반",
"git.filemode.executable_file": "실행파일",
"git.filemode.symbolic_link": "Symlink",
"git.filemode.submodule": "서브모듈"
"git.filemode.submodule": "서브모듈",
"org.repos.none": "리포지토리 없음.",
"actions.general.permissions": "액션 토큰 권한",
"actions.general.token_permissions.mode": "기본 토큰 권한",
"actions.general.token_permissions.mode.desc": "워크플로 파일에서 권한을 선언하지 않으면 액션 작업은 기본 권한을 사용합니다.",
"actions.general.token_permissions.mode.permissive": "허용적",
"actions.general.token_permissions.mode.permissive.desc": "작업 리포지토리에 대한 읽기 쓰기 권한.",
"actions.general.token_permissions.mode.restricted": "제한됨",
"actions.general.token_permissions.mode.restricted.desc": "작업 리포지토리의 콘텐츠 단위(코드, 릴리스)에 대한 읽기 전용 권한.",
"actions.general.token_permissions.override_owner": "소유자 수준의 구성 오버라이드",
"actions.general.token_permissions.override_owner_desc": "활성화하면, 이 리포지토리는 다음 소유자-수준(사용자 혹은 조직) 구성 대신에 자신의 액션 구성을 사용합니다.",
"actions.general.token_permissions.maximum": "최대 토큰 권한",
"actions.general.token_permissions.maximum.description": "액션 작업의 효과 권한은 최대 권한으로 제한됩니다.",
"actions.general.token_permissions.fork_pr_note": "포크로부터 생성된 풀 리퀘스트로 작업이 시작된 경우, 해당 작업의 유효 권한은 읽기 전용 권한을 초과하지 않습니다.",
"actions.general.token_permissions.customize_max_permissions": "최대 권한을 커스터마이징",
"actions.general.cross_repo": "크로스 리포지토리 억세스",
"actions.general.cross_repo_desc": "선택된 저장소가 이 소유자의 모든 저장소에서 액션 작업을 실행할 때 GITEA_TOKEN을 사용하여 (읽기 전용으로) 액세스할 수 있도록 허용합니다.",
"actions.general.cross_repo_selected": "선택된 리포지토리",
"actions.general.cross_repo_target_repos": "대상 리포지토리",
"actions.general.cross_repo_add": "대상 리포지토리 추가"
}

View File

@ -7,29 +7,39 @@ import (
"net/http"
"strings"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/setting"
)
func isMaintenanceModeAllowedRequest(req *http.Request) bool {
if strings.HasPrefix(req.URL.Path, "/-/") {
// URLs like "/-/admin", "/-/fetch-redirect" and "/-/markup" are still accessible in maintenance mode
return true
}
if strings.HasPrefix(req.URL.Path, "/api/internal/") {
// internal APIs should be allowed
return true
}
if strings.HasPrefix(req.URL.Path, "/user/") {
// URLs like "/user/signin" and "/user/signup" are still accessible in maintenance mode
return true
}
if strings.HasPrefix(req.URL.Path, "/assets/") {
return true
}
return false
}
func MaintenanceModeHandler() func(h http.Handler) http.Handler {
allowedPrefixes := []string{
"/.well-known/",
"/assets/",
"/avatars/",
// admin: "/-/admin"
// general-purpose URLs: "/-/fetch-redirect", "/-/markup", etc.
"/-/",
// internal APIs
"/api/internal/",
// user login (for admin to login): "/user/login", "/user/logout", "/catpcha/..."
"/user/",
"/captcha/",
}
allowedPaths := container.SetOf(
"/api/healthz",
)
isMaintenanceModeAllowedRequest := func(req *http.Request) bool {
for _, prefix := range allowedPrefixes {
if strings.HasPrefix(req.URL.Path, prefix) {
return true
}
}
return allowedPaths.Contains(req.URL.Path)
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
maintenanceMode := setting.Config().Instance.MaintenanceMode.Value(req.Context())

View File

@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestShadowPassword(t *testing.T) {
@ -74,19 +75,29 @@ func TestShadowPassword(t *testing.T) {
}
func TestSelfCheckPost(t *testing.T) {
defer test.MockVariableValue(&setting.PublicURLDetection)()
defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")()
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
ctx, resp := contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend")
SelfCheckPost(ctx)
assert.Equal(t, http.StatusOK, resp.Code)
data := struct {
Problems []string `json:"problems"`
}{}
err := json.Unmarshal(resp.Body.Bytes(), &data)
assert.NoError(t, err)
setting.PublicURLDetection = setting.PublicURLLegacy
ctx, resp := contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend")
SelfCheckPost(ctx)
assert.Equal(t, http.StatusOK, resp.Code)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &data))
assert.Equal(t, []string{
ctx.Locale.TrString("admin.self_check.location_origin_mismatch", "http://frontend/sub/", "http://config/sub/"),
}, data.Problems)
setting.PublicURLDetection = setting.PublicURLAuto
ctx, resp = contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend")
SelfCheckPost(ctx)
assert.Equal(t, http.StatusOK, resp.Code)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &data))
assert.Equal(t, []string{
ctx.Locale.TrString("admin.self_check.location_origin_mismatch", "http://frontend/sub/", "http://host/sub/"),
}, data.Problems)
}

View File

@ -7,7 +7,6 @@ import (
"errors"
"net/http"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
shared "code.gitea.io/gitea/routers/web/shared/secrets"
@ -74,7 +73,6 @@ func Secrets(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("actions.actions")
ctx.Data["PageType"] = "secrets"
ctx.Data["PageIsSharedSettingsSecrets"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
sCtx, err := getSecretsCtx(ctx)
if err != nil {

View File

@ -999,39 +999,33 @@ func handleSettingsPostUnarchive(ctx *context.Context) {
}
func handleSettingsPostVisibility(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if repo.IsFork {
ctx.Flash.Error(ctx.Tr("repo.settings.visibility.fork_error"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
ctx.JSONError(ctx.Tr("repo.settings.visibility.fork_error"))
return
}
var err error
private := ctx.FormOptionalBool("private").ValueOrDefault(true) // default to true for privacy & safety
// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
if setting.Repository.ForcePrivate && repo.IsPrivate && !ctx.Doer.IsAdmin {
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form)
if !private && setting.Repository.ForcePrivate && !ctx.Doer.IsAdmin {
ctx.JSONError(ctx.Tr("form.repository_force_private"))
return
}
if private && repo.FullName() != ctx.FormString("confirm_repo_name") {
ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
return
}
if repo.IsPrivate {
err = repo_service.MakeRepoPublic(ctx, repo)
} else {
err = repo_service.MakeRepoPrivate(ctx, repo)
}
err := repo_service.MakeRepoPrivate(ctx, repo, private)
if err != nil {
log.Error("Tried to change the visibility of the repo: %s", err)
ctx.Flash.Error(ctx.Tr("repo.settings.visibility.error"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
ctx.JSONError(ctx.Tr("repo.settings.visibility.error"))
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.visibility.success"))
log.Trace("Repository visibility changed: %s/%s", ctx.Repo.Owner.Name, repo.Name)
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings")
}
func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.RepoSettingForm) {

View File

@ -321,7 +321,6 @@ func loadAccountData(ctx *context.Context) {
ctx.Data["Emails"] = emails
ctx.Data["ActivationsPending"] = pendingActivation
ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
if setting.Service.UserDeleteWithCommentsMaxTime != 0 {
ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String()

View File

@ -10,7 +10,6 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
@ -27,7 +26,6 @@ const (
func Applications(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.applications")
ctx.Data["PageIsSettingsApplications"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
loadApplicationsData(ctx)
@ -39,7 +37,6 @@ func ApplicationsPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.NewAccessTokenForm)
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsApplications"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
_ = ctx.Req.ParseForm()
var scopeNames []string

View File

@ -6,7 +6,6 @@ package setting
import (
"net/http"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
@ -20,7 +19,6 @@ const (
func BlockedUsers(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("user.block.list")
ctx.Data["PageIsSettingsBlockedUsers"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
shared_user.BlockedUsers(ctx, ctx.Doer)
if ctx.Written() {

View File

@ -35,7 +35,6 @@ func Keys(ctx *context.Context) {
ctx.Data["DisableSSH"] = setting.SSH.Disabled
ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
loadKeysData(ctx)
@ -50,7 +49,6 @@ func KeysPost(ctx *context.Context) {
ctx.Data["DisableSSH"] = setting.SSH.Disabled
ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
if ctx.HasError() {
loadKeysData(ctx)
@ -341,5 +339,4 @@ func loadKeysData(ctx *context.Context) {
ctx.Data["VerifyingID"] = ctx.FormString("verify_gpg")
ctx.Data["VerifyingFingerprint"] = ctx.FormString("verify_ssh")
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
}

View File

@ -25,7 +25,6 @@ const (
func Packages(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
shared.SetPackagesContext(ctx, ctx.Doer)
@ -35,7 +34,6 @@ func Packages(ctx *context.Context) {
func PackagesRuleAdd(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
shared.SetRuleAddContext(ctx)
@ -45,7 +43,6 @@ func PackagesRuleAdd(ctx *context.Context) {
func PackagesRuleEdit(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
shared.SetRuleEditContext(ctx, ctx.Doer)
@ -55,7 +52,6 @@ func PackagesRuleEdit(ctx *context.Context) {
func PackagesRuleAddPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsPackages"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
shared.PerformRuleAddPost(
ctx,
@ -68,7 +64,6 @@ func PackagesRuleAddPost(ctx *context.Context) {
func PackagesRuleEditPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
shared.PerformRuleEditPost(
ctx,
@ -81,7 +76,6 @@ func PackagesRuleEditPost(ctx *context.Context) {
func PackagesRulePreview(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
shared.SetRulePreviewContext(ctx, ctx.Doer)

View File

@ -49,8 +49,6 @@ func Profile(ctx *context.Context) {
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
ctx.HTML(http.StatusOK, tplSettingsProfile)
}
@ -60,7 +58,6 @@ func ProfilePost(ctx *context.Context) {
ctx.Data["PageIsSettingsProfile"] = true
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplSettingsProfile)
@ -200,7 +197,6 @@ func DeleteAvatar(ctx *context.Context) {
func Organization(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.organization")
ctx.Data["PageIsSettingsOrganization"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
opts := organization.FindOrgOptions{
ListOptions: db.ListOptions{
@ -232,7 +228,6 @@ func Organization(ctx *context.Context) {
func Repos(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.repos")
ctx.Data["PageIsSettingsRepos"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories
@ -340,7 +335,6 @@ func Appearance(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.appearance")
ctx.Data["PageIsSettingsAppearance"] = true
ctx.Data["AllThemes"] = webtheme.GetAvailableThemes()
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
var hiddenCommentTypes *big.Int
val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes)

View File

@ -156,5 +156,4 @@ func loadSecurityData(ctx *context.Context) {
return
}
ctx.Data["OpenIDs"] = openid
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
}

View File

@ -9,9 +9,17 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context"
)
func SettingsCtxData(ctx *context.Context) {
ctx.Data["PageIsUserSettings"] = true
ctx.Data["EnablePackages"] = setting.Packages.Enabled
ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
}
func UpdatePreferences(ctx *context.Context) {
type preferencesForm struct {
CodeViewShowFileTree bool `json:"codeViewShowFileTree"`

View File

@ -7,7 +7,6 @@ import (
"net/http"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
@ -25,7 +24,6 @@ func Webhooks(ctx *context.Context) {
ctx.Data["BaseLink"] = setting.AppSubURL + "/user/settings/hooks"
ctx.Data["BaseLinkNew"] = setting.AppSubURL + "/user/settings/hooks"
ctx.Data["Description"] = ctx.Tr("settings.hooks.desc")
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
ws, err := db.Find[webhook.Webhook](ctx, webhook.ListWebhookOptions{OwnerID: ctx.Doer.ID})
if err != nil {

View File

@ -721,7 +721,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Get("", user_setting.BlockedUsers)
m.Post("", web.Bind(forms.BlockUserForm{}), user_setting.BlockedUsersPost)
})
}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "EnablePackages", setting.Packages.Enabled, "EnableNotifyMail", setting.Service.EnableNotifyMail))
}, reqSignIn, user_setting.SettingsCtxData)
m.Group("/user", func() {
m.Get("/activate", auth.Activate)

View File

@ -117,6 +117,11 @@ func parseRawPermissionsExplicit(rawPerms *yaml.Node) *repo_model.ActionsTokenPe
result.UnitAccessModes[unit.TypeReleases] = mode
case "projects":
result.UnitAccessModes[unit.TypeProjects] = mode
// Scopes github supports but gitea does not, see url for details
// https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax
case "artifact-metadata", "attestations", "checks", "deployments",
"id-token", "models", "discussions", "pages", "security-events", "statuses":
// not supported
default:
setting.PanicInDevOrTesting("Unrecognized permission scope: %s", scope)
}

View File

@ -33,6 +33,36 @@ func TestParseRawPermissions_ReadAll(t *testing.T) {
assert.Equal(t, perm.AccessModeRead, result.UnitAccessModes[unit.TypeProjects])
}
// TestParseRawPermissions_GithubScopes verifies that all scopes that github supports are accounted for
func TestParseRawPermissions_GithubScopes(t *testing.T) {
var rawPerms yaml.Node
// Taken and stripped down from:
// https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#defining-access-for-the-github_token-scopes
yamlContent := `
actions: read
artifact-metadata: read
attestations: read
checks: read
contents: read
deployments: read
id-token: write
issues: read
models: read
discussions: read
packages: read
pages: read
pull-requests: read
security-events: read
statuses: read`
err := yaml.Unmarshal([]byte(yamlContent), &rawPerms)
require.NoError(t, err)
result := parseRawPermissionsExplicit(&rawPerms)
require.NotNil(t, result)
// No asserts for permissions set on purpose
}
func TestParseRawPermissions_WriteAll(t *testing.T) {
var rawPerms yaml.Node
err := yaml.Unmarshal([]byte(`write-all`), &rawPerms)

View File

@ -5,7 +5,6 @@
package pull
import (
"bufio"
"context"
"fmt"
"io"
@ -15,15 +14,12 @@ import (
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/glob"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
@ -57,15 +53,6 @@ func DownloadDiffOrPatch(ctx context.Context, pr *issues_model.PullRequest, w io
return nil
}
var patchErrorSuffices = []string{
": already exists in index",
": patch does not apply",
": already exists in working directory",
"unrecognized input",
": No such file or directory",
": does not exist in index",
}
func checkPullRequestBranchMergeable(ctx context.Context, pr *issues_model.PullRequest) error {
ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("checkPullRequestBranchMergeable: %s", pr))
defer finished()
@ -349,151 +336,10 @@ func checkConflictsByTmpRepo(ctx context.Context, pr *issues_model.PullRequest,
}
// 3. OK the three-way merge method has detected conflicts
// 3a. Are still testing with GitApply? If not set the conflict status and move on
if !setting.Repository.PullRequest.TestConflictingPatchesWithGitApply {
pr.Status = issues_model.PullRequestStatusConflict
pr.ConflictedFiles = conflictFiles
log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
return true, nil
}
// 3b. Create a plain patch from head to base
tmpPatchFile, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("patch")
if err != nil {
log.Error("Unable to create temporary patch file! Error: %v", err)
return false, fmt.Errorf("unable to create temporary patch file! Error: %w", err)
}
defer cleanup()
if err := gitRepo.GetDiffBinary(pr.MergeBase+"...tracking", tmpPatchFile); err != nil {
log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
return false, fmt.Errorf("unable to get patch file from %s to %s in %s Error: %w", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
}
stat, err := tmpPatchFile.Stat()
if err != nil {
return false, fmt.Errorf("unable to stat patch file: %w", err)
}
patchPath := tmpPatchFile.Name()
tmpPatchFile.Close()
// 3c. if the size of that patch is 0 - there can be no conflicts!
if stat.Size() == 0 {
log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID)
pr.Status = issues_model.PullRequestStatusEmpty
return false, nil
}
log.Trace("PullRequest[%d].checkPullRequestMergeableByTmpRepo (patchPath): %s", pr.ID, patchPath)
// 4. Read the base branch in to the index of the temporary repository
_, _, err = gitcmd.NewCommand("read-tree", tmpRepoBaseBranch).WithDir(tmpBasePath).RunStdString(ctx)
if err != nil {
return false, fmt.Errorf("git read-tree %s: %w", pr.BaseBranch, err)
}
// 5. Now get the pull request configuration to check if we need to ignore whitespace
prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
if err != nil {
return false, err
}
prConfig := prUnit.PullRequestsConfig()
// 6. Prepare the arguments to apply the patch against the index
cmdApply := gitcmd.NewCommand("apply", "--check", "--cached")
if prConfig.IgnoreWhitespaceConflicts {
cmdApply.AddArguments("--ignore-whitespace")
}
is3way := false
if git.DefaultFeatures().CheckVersionAtLeast("2.32.0") {
cmdApply.AddArguments("--3way")
is3way = true
}
cmdApply.AddDynamicArguments(patchPath)
// 7. Prep the pipe:
// - Here we could do the equivalent of:
// `git apply --check --cached patch_file > conflicts`
// Then iterate through the conflicts. However, that means storing all the conflicts
// in memory - which is very wasteful.
// - alternatively we can do the equivalent of:
// `git apply --check ... | grep ...`
// meaning we don't store all the conflicts unnecessarily.
stderrReader, stderrReaderClose := cmdApply.MakeStderrPipe()
defer stderrReaderClose()
// 8. Run the check command
conflict = false
err = cmdApply.
WithDir(tmpBasePath).
WithPipelineFunc(func(ctx gitcmd.Context) error {
const prefix = "error: patch failed:"
const errorPrefix = "error: "
const threewayFailed = "Failed to perform three-way merge..."
const appliedPatchPrefix = "Applied patch to '"
const withConflicts = "' with conflicts."
conflicts := make(container.Set[string])
// Now scan the output from the command
scanner := bufio.NewScanner(stderrReader)
for scanner.Scan() {
line := scanner.Text()
log.Trace("PullRequest[%d].checkPullRequestMergeableByTmpRepo: stderr: %s", pr.ID, line)
if strings.HasPrefix(line, prefix) {
conflict = true
filepath := strings.TrimSpace(strings.Split(line[len(prefix):], ":")[0])
conflicts.Add(filepath)
} else if is3way && line == threewayFailed {
conflict = true
} else if strings.HasPrefix(line, errorPrefix) {
conflict = true
for _, suffix := range patchErrorSuffices {
if strings.HasSuffix(line, suffix) {
filepath := strings.TrimSpace(strings.TrimSuffix(line[len(errorPrefix):], suffix))
if filepath != "" {
conflicts.Add(filepath)
}
break
}
}
} else if is3way && strings.HasPrefix(line, appliedPatchPrefix) && strings.HasSuffix(line, withConflicts) {
conflict = true
filepath := strings.TrimPrefix(strings.TrimSuffix(line, withConflicts), appliedPatchPrefix)
if filepath != "" {
conflicts.Add(filepath)
}
}
// only list part of conflicted files
if len(conflicts) >= gitrepo.MaxConflictedDetectFiles {
break
}
}
if len(conflicts) > 0 {
pr.ConflictedFiles = make([]string, 0, len(conflicts))
for key := range conflicts {
pr.ConflictedFiles = append(pr.ConflictedFiles, key)
}
}
return nil
}).
Run(gitRepo.Ctx)
// 9. Check if the found conflicted files is non-zero, "err" could be non-nil, so we should ignore it if we found conflicts.
// Note: `"err" could be non-nil` is due that if enable 3-way merge, it doesn't return any error on found conflicts.
if len(pr.ConflictedFiles) > 0 {
if conflict {
pr.Status = issues_model.PullRequestStatusConflict
log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
return true, nil
}
} else if err != nil {
return false, fmt.Errorf("git apply --check: %w", err)
}
return false, nil
pr.Status = issues_model.PullRequestStatusConflict
pr.ConflictedFiles = conflictFiles
log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
return true, nil
}
// ErrFilePathProtected represents a "FilePathProtected" kind of error.

View File

@ -122,9 +122,9 @@ func UpdateRepository(ctx context.Context, repo *repo_model.Repository, visibili
})
}
func MakeRepoPublic(ctx context.Context, repo *repo_model.Repository) (err error) {
func MakeRepoPrivate(ctx context.Context, repo *repo_model.Repository, private bool) (err error) {
return db.WithTx(ctx, func(ctx context.Context) error {
repo.IsPrivate = false
repo.IsPrivate = private
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
return err
}
@ -144,15 +144,33 @@ func MakeRepoPublic(ctx context.Context, repo *repo_model.Repository) (err error
return err
}
forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID)
if err != nil {
return fmt.Errorf("getRepositoriesByForkID: %w", err)
// If repo has become private, we need to set its actions to private, and clear stars and watches.
if private {
_, err = db.GetEngine(ctx).
Where("repo_id = ?", repo.ID).Cols("is_private").Update(&activities_model.Action{IsPrivate: true})
if err != nil {
return err
}
if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil {
return err
}
if err = repo_model.ClearRepoWatches(ctx, repo.ID); err != nil {
return err
}
}
if repo.Owner.Visibility != structs.VisibleTypePrivate {
for i := range forkRepos {
if err = MakeRepoPublic(ctx, forkRepos[i]); err != nil {
return fmt.Errorf("MakeRepoPublic[%d]: %w", forkRepos[i].ID, err)
shouldUpdateForks := private
if !private && repo.Owner.Visibility != structs.VisibleTypePrivate {
shouldUpdateForks = true
}
if shouldUpdateForks {
forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID)
if err != nil {
return fmt.Errorf("getRepositoriesByForkID: %w", err)
}
for _, forkRepo := range forkRepos {
if err = MakeRepoPrivate(ctx, forkRepo, private); err != nil {
return fmt.Errorf("MakeRepoPrivate[%d]: %w", forkRepo.ID, err)
}
}
}
@ -160,63 +178,6 @@ func MakeRepoPublic(ctx context.Context, repo *repo_model.Repository) (err error
// If visibility is changed, we need to update the issue indexer.
// Since the data in the issue indexer have field to indicate if the repo is public or not.
issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
return nil
})
}
func MakeRepoPrivate(ctx context.Context, repo *repo_model.Repository) (err error) {
return db.WithTx(ctx, func(ctx context.Context) error {
repo.IsPrivate = true
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
return err
}
if err = repo.LoadOwner(ctx); err != nil {
return fmt.Errorf("LoadOwner: %w", err)
}
if repo.Owner.IsOrganization() {
// Organization repository need to recalculate access table when visibility is changed.
if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil {
return fmt.Errorf("recalculateTeamAccesses: %w", err)
}
}
// If repo has become private, we need to set its actions to private.
_, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).Cols("is_private").Update(&activities_model.Action{
IsPrivate: true,
})
if err != nil {
return err
}
if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil {
return err
}
if err = repo_model.ClearRepoWatches(ctx, repo.ID); err != nil {
return err
}
// Create/Remove git-daemon-export-ok for git-daemon...
if err := CheckDaemonExportOK(ctx, repo); err != nil {
return err
}
forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID)
if err != nil {
return fmt.Errorf("getRepositoriesByForkID: %w", err)
}
for i := range forkRepos {
if err = MakeRepoPrivate(ctx, forkRepos[i]); err != nil {
return fmt.Errorf("MakeRepoPrivate[%d]: %w", forkRepos[i].ID, err)
}
}
// If visibility is changed, we need to update the issue indexer.
// Since the data in the issue indexer have field to indicate if the repo is public or not.
issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
return nil
})
}

View File

@ -74,13 +74,13 @@ func TestMakeRepoPrivateClearsWatches(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
repo.IsPrivate = false
assert.False(t, repo.IsPrivate)
watchers, err := repo_model.GetRepoWatchersIDs(t.Context(), repo.ID)
require.NoError(t, err)
require.NotEmpty(t, watchers)
assert.NoError(t, MakeRepoPrivate(t.Context(), repo))
assert.NoError(t, MakeRepoPrivate(t.Context(), repo, true))
watchers, err = repo_model.GetRepoWatchersIDs(t.Context(), repo.ID)
assert.NoError(t, err)

View File

@ -4,7 +4,7 @@
<div class="ui container flex-text-block project-header">
<h2>{{.Project.Title}}</h2>
<div class="tw-flex-1"></div>
<div class="ui secondary menu tw-m-0">
<div class="list-header-filters ui secondary menu tw-m-0">
{{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "milestone" $.MilestoneID "archived_labels" (Iif $.ShowArchivedLabels "true")}}
{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}}
{{template "repo/issue/filter_item_user_assign" dict

View File

@ -67,13 +67,13 @@
<input type="hidden" name="action" value="delete">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.transfer_form_title"}}
{{ctx.Locale.Tr "repo.settings.enter_repo_name_to_confirm"}}
<span class="tw-text-red">{{.Repository.Name}}</span>
</label>
</div>
<div class="required field">
<label for="repo_name_to_delete">{{ctx.Locale.Tr "repo.repo_name"}}</label>
<input id="repo_name_to_delete" name="repo_name" required>
<label>{{ctx.Locale.Tr "repo.repo_name"}}</label>
<input name="repo_name" required>
</div>
<div class="actions">

View File

@ -887,21 +887,8 @@
</div>
<form class="ui form" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="convert">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.transfer_form_title"}}
<span class="tw-text-red">{{.Repository.Name}}</span>
</label>
</div>
<div class="required field">
<label>{{ctx.Locale.Tr "repo.repo_name"}}</label>
<input name="repo_name" required maxlength="100">
</div>
<div class="actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">{{ctx.Locale.Tr "repo.settings.convert_confirm"}}</button>
</div>
{{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.convert_confirm"))}}
</form>
</div>
</div>
@ -917,21 +904,8 @@
</div>
<form class="ui form" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="convert_fork">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.transfer_form_title"}}
<span class="tw-text-red">{{.Repository.Name}}</span>
</label>
</div>
<div class="required field">
<label>{{ctx.Locale.Tr "repo.repo_name"}}</label>
<input name="repo_name" required>
</div>
<div class="actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">{{ctx.Locale.Tr "repo.settings.convert_fork_confirm"}}</button>
</div>
{{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.convert_fork_confirm"))}}
</form>
</div>
</div>
@ -949,25 +923,13 @@
</div>
<form class="ui form" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="transfer">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.transfer_form_title"}}
<span class="tw-text-red">{{.Repository.Name}}</span>
</label>
</div>
<div class="required field">
<label>{{ctx.Locale.Tr "repo.repo_name"}}</label>
<input name="repo_name" required>
</div>
{{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
<div class="required field">
<label for="new_owner_name">{{ctx.Locale.Tr "repo.settings.transfer_owner"}}</label>
<input id="new_owner_name" name="new_owner_name" required>
</div>
<div class="actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">{{ctx.Locale.Tr "repo.settings.transfer_perform"}}</button>
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.transfer_perform"))}}
</form>
</div>
</div>
@ -986,49 +948,57 @@
</div>
<form class="ui form" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="delete">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.transfer_form_title"}}
<span class="tw-text-red">{{.Repository.Name}}</span>
</label>
</div>
<div class="required field">
<label for="repo_name_to_delete">{{ctx.Locale.Tr "repo.repo_name"}}</label>
<input id="repo_name_to_delete" name="repo_name" required>
</div>
<div class="actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">{{ctx.Locale.Tr "repo.settings.confirm_delete"}}</button>
</div>
{{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.confirm_delete"))}}
</form>
</div>
</div>
{{if not .Repository.IsFork}}
<div class="ui g-modal-confirm modal" id="visibility-repo-modal">
<div class="ui small modal" id="visibility-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.visibility"}}
</div>
<div class="content">
{{if .Repository.IsPrivate}}
<p>{{ctx.Locale.Tr "repo.settings.visibility.public.bullet_title"}}</p>
<ul>
<li>{{ctx.Locale.Tr "repo.settings.visibility.public.bullet_one"}}</li>
</ul>
{{else}}
<p>{{ctx.Locale.Tr "repo.settings.visibility.private.bullet_title"}}</p>
<ul>
<li>{{ctx.Locale.Tr "repo.settings.visibility.private.bullet_one"}}</li>
<li>{{ctx.Locale.Tr "repo.settings.visibility.private.bullet_two"}}{{if .Repository.NumForks}}<span class="tw-text-red">{{ctx.Locale.Tr "repo.visibility_fork_helper"}}</span>{{end}}</li>
{{if .Repository.IsPrivate}}
<p>{{ctx.Locale.Tr "repo.settings.visibility.public.bullet_title"}}</p>
<ul>
<li>{{ctx.Locale.Tr "repo.settings.visibility.public.bullet_one"}}</li>
</ul>
{{else}}
<p>{{ctx.Locale.Tr "repo.settings.visibility.private.bullet_title"}}</p>
<ul>
<li>{{ctx.Locale.Tr "repo.settings.visibility.private.bullet_one"}}</li>
<li>
{{ctx.Locale.Tr "repo.settings.visibility.private.bullet_two"}}
</li>
{{if or .Repository.NumStars .Repository.NumWatches .Repository.NumForks}}
<ul class="tw-my-0 tw-pl-4">
{{if .Repository.NumStars}}<li>{{ctx.Locale.Tr "repo.settings.visibility.private.stats_stars" .Repository.NumStars}}</li>{{end}}
{{if .Repository.NumWatches}}<li>{{ctx.Locale.Tr "repo.settings.visibility.private.stats_watchers" .Repository.NumWatches}}</li>{{end}}
{{if .Repository.NumForks}}<li>{{ctx.Locale.Tr "repo.settings.visibility.private.stats_forks" .Repository.NumForks}}</li>{{end}}
</ul>
{{end}}
</ul>
{{end}}
<form class="ui form tw-mt-5 form-fetch-action" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="visibility">
<input type="hidden" name="private" value="{{not .Repository.IsPrivate}}">
{{if not .Repository.IsPrivate}}
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.enter_repo_full_name_to_confirm"}}
<span class="tw-text-red">{{.Repository.FullName}}</span>
</label>
</div>
<div class="required field">
<label>{{ctx.Locale.Tr "repo.repo_name"}}</label>
<input name="confirm_repo_name" required maxlength="200">
</div>
{{end}}
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (Iif .Repository.IsPrivate (ctx.Locale.Tr "repo.settings.visibility.public.button") (ctx.Locale.Tr "repo.settings.visibility.private.button")))}}
</form>
</div>
<form action="{{.Link}}" method="post">
<input type="hidden" name="action" value="visibility">
<input type="hidden" name="repo_id" value="{{.Repository.ID}}">
{{template "base/modal_actions_confirm" .}}
</form>
</div>
{{end}}
@ -1044,21 +1014,8 @@
</div>
<form class="ui form" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="delete-wiki">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.transfer_form_title"}}
<span class="tw-text-red">{{.Repository.Name}}</span>
</label>
</div>
<div class="required field">
<label>{{ctx.Locale.Tr "repo.repo_name"}}</label>
<input name="repo_name" required>
</div>
<div class="actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">{{ctx.Locale.Tr "repo.settings.confirm_wiki_delete"}}</button>
</div>
{{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.confirm_wiki_delete"))}}
</form>
</div>
</div>

View File

@ -0,0 +1,10 @@
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.enter_repo_name_to_confirm"}}
<span class="tw-text-red">{{.RepoName}}</span>
</label>
</div>
<div class="required field">
<label>{{ctx.Locale.Tr "repo.repo_name"}}</label>
<input name="repo_name" required maxlength="100">
</div>

View File

@ -39,7 +39,6 @@ import (
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
files_service "code.gitea.io/gitea/services/repository/files"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -549,86 +548,6 @@ func TestCantFastForwardOnlyMergeDiverging(t *testing.T) {
})
}
func TestConflictChecking(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Create new clean repo to test conflict checking.
baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{
Name: "conflict-checking",
Description: "Tempo repo",
AutoInit: true,
Readme: "Default",
DefaultBranch: "main",
})
assert.NoError(t, err)
assert.NotEmpty(t, baseRepo)
// create a commit on new branch.
_, err = files_service.ChangeRepoFiles(t.Context(), baseRepo, user, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: "important_file",
ContentReader: strings.NewReader("Just a non-important file"),
},
},
Message: "Add a important file",
OldBranch: "main",
NewBranch: "important-secrets",
})
assert.NoError(t, err)
// create a commit on main branch.
_, err = files_service.ChangeRepoFiles(t.Context(), baseRepo, user, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: "important_file",
ContentReader: strings.NewReader("Not the same content :P"),
},
},
Message: "Add a important file",
OldBranch: "main",
NewBranch: "main",
})
assert.NoError(t, err)
// create Pull to merge the important-secrets branch into main branch.
pullIssue := &issues_model.Issue{
RepoID: baseRepo.ID,
Title: "PR with conflict!",
PosterID: user.ID,
Poster: user,
IsPull: true,
}
pullRequest := &issues_model.PullRequest{
HeadRepoID: baseRepo.ID,
BaseRepoID: baseRepo.ID,
HeadBranch: "important-secrets",
BaseBranch: "main",
HeadRepo: baseRepo,
BaseRepo: baseRepo,
Type: issues_model.PullRequestGitea,
}
prOpts := &pull_service.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest}
err = pull_service.NewPullRequest(t.Context(), prOpts)
assert.NoError(t, err)
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
assert.NoError(t, issue.LoadPullRequest(t.Context()))
conflictingPR := issue.PullRequest
// Ensure conflictedFiles is populated.
assert.Len(t, conflictingPR.ConflictedFiles, 1)
// Check if status is correct.
assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status)
// Ensure that mergeable returns false
assert.False(t, conflictingPR.Mergeable(t.Context()))
})
}
func TestPullRetargetChildOnBranchDelete(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
session := loginUser(t, "user1") // FIXME: don't use admin user for testing

View File

@ -0,0 +1,59 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestRepositoryVisibilityChange(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
t.Run("MakePrivateRequiresCorrectName", func(t *testing.T) {
// Wrong name should be rejected with a JSON error
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings", map[string]string{
"action": "visibility",
"private": "true",
"confirm_repo_name": "wrong-name",
})
resp := session.MakeRequest(t, req, http.StatusBadRequest)
assert.NotEmpty(t, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.False(t, repo1.IsPrivate)
// Correct full name (owner/repo) should succeed with a JSON redirect
req = NewRequestWithValues(t, "POST", "/user2/repo1/settings", map[string]string{
"action": "visibility",
"private": "true",
"confirm_repo_name": "user2/repo1",
})
resp = session.MakeRequest(t, req, http.StatusOK)
assert.NotEmpty(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect)
repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.True(t, repo1.IsPrivate)
})
t.Run("MakePublicDoesNotRequireName", func(t *testing.T) {
req := NewRequestWithValues(t, "POST", "/user2/repo2/settings", map[string]string{
"action": "visibility",
"private": "false",
})
resp := session.MakeRequest(t, req, http.StatusOK)
assert.NotEmpty(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect)
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
assert.False(t, repo2.IsPrivate)
})
}

View File

@ -15,8 +15,8 @@
/* line-height: use the default value as "modules/normalize.css" */
--line-height-default: normal;
/* images */
--checkbox-mask-checked: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 18 18" width="16" height="16"><path fill-rule="evenodd" d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"></path></svg>');
--checkbox-mask-indeterminate: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2 7.75A.75.75 0 012.75 7h10a.75.75 0 010 1.5h-10A.75.75 0 012 7.75z"></path></svg>');
--checkbox-mask-checked: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="9" viewBox="0 0 12 9"><path fill-rule="evenodd" d="M11.78.22a.75.75 0 0 1 0 1.061L4.52 8.541a.75.75 0 0 1-1.062 0L.202 5.285a.75.75 0 0 1 1.061-1.061l2.725 2.723L10.718.22a.75.75 0 0 1 1.062 0"/></svg>');
--checkbox-mask-indeterminate: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="10" height="2" viewBox="0 0 10 2"><path fill-rule="evenodd" d="M0 1a1 1 0 0 1 1-1h8a1 1 0 1 1 0 2H1a1 1 0 0 1-1-1" clip-rule="evenodd"/></svg>');
--octicon-chevron-right: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06Z"></path></svg>');
--select-arrows: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="m4.074 9.427 3.396 3.396a.25.25 0 0 0 .354 0l3.396-3.396A.25.25 0 0 0 11.043 9H4.251a.25.25 0 0 0-.177.427m0-1.957L7.47 4.073a.25.25 0 0 1 .354 0L11.22 7.47a.25.25 0 0 1-.177.426H4.251a.25.25 0 0 1-.177-.426"/></svg>');
/* other variables */
@ -27,7 +27,7 @@
--height-loading: 16rem;
--min-height-textarea: 132px; /* padding + 6 lines + border = calc(1.57142em + 6lh + 2px), but lh is not fully supported */
--tab-size: 4;
--checkbox-size: 15px; /* height and width of checkbox and radio inputs */
--checkbox-size: 14px; /* height and width of checkbox and radio inputs */
--page-spacing: 16px; /* space between page elements */
--page-margin-x: 32px; /* minimum space on left and right side of page */
--page-space-bottom: 64px; /* space between last page element and footer */

View File

@ -164,12 +164,16 @@ In markup content, we always use bottom margin for all elements */
list-style-type: none;
}
.markup .task-list-item > ul {
margin-top: 4px;
}
.markup .task-list-item p + ul {
margin-top: 16px;
}
.markup .task-list-item input[type="checkbox"] {
margin: 0 .3em .25em -1.4em;
margin: 0 .6em .25em -1.4em;
vertical-align: middle;
padding: 0;
}
@ -188,48 +192,12 @@ In markup content, we always use bottom margin for all elements */
}
.markup input[type="checkbox"] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
position: relative;
border: 1px solid var(--color-secondary);
border-radius: var(--border-radius);
background: var(--color-input-background);
height: 14px;
width: 14px;
margin-right: .25em;
margin-bottom: .25em;
cursor: default;
opacity: 1 !important; /* override fomantic on edit preview */
pointer-events: auto !important; /* override fomantic on edit preview */
vertical-align: middle !important; /* override fomantic on edit preview */
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
.markup input[type="checkbox"]:not([disabled]):hover,
.markup input[type="checkbox"]:not([disabled]):active {
border-color: var(--color-primary);
}
.markup input[type="checkbox"]::after {
position: absolute;
inset: 0;
pointer-events: none;
background: var(--color-text);
mask-size: cover;
-webkit-mask-size: cover;
}
.markup input[type="checkbox"]:checked::after {
content: "";
mask-image: var(--checkbox-mask-checked);
-webkit-mask-image: var(--checkbox-mask-checked);
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
.markup input[type="checkbox"]:indeterminate::after {
content: "";
mask-image: var(--checkbox-mask-indeterminate);
-webkit-mask-image: var(--checkbox-mask-indeterminate);
}
.markup ul ul,

View File

@ -1,10 +1,75 @@
/* 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. */
input[type="checkbox"],
input[type="radio"] {
appearance: none;
width: var(--checkbox-size);
height: var(--checkbox-size);
border: 1px solid var(--color-input-border);
border-radius: 50%;
background: var(--color-input-background);
}
input[type="radio"]:checked {
background: var(--color-white);
border: 4px solid var(--color-primary);
}
input[type="checkbox"] {
appearance: none;
display: inline-grid;
place-content: center;
width: var(--checkbox-size);
height: var(--checkbox-size);
border: 1px solid var(--color-input-border);
border-radius: 3px;
background: var(--color-input-background);
overflow: hidden;
print-color-adjust: exact;
}
input[type="checkbox"]::before {
content: "";
background: var(--color-white);
width: var(--checkbox-size);
height: var(--checkbox-size);
clip-path: inset(var(--checkbox-size) 0 0 0);
mask-image: var(--checkbox-mask-checked);
-webkit-mask-image: var(--checkbox-mask-checked);
mask-size: 75%;
-webkit-mask-size: 75%;
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-position: center;
-webkit-mask-position: center;
}
input[type="checkbox"]:checked,
input[type="checkbox"]:indeterminate {
background: var(--color-primary);
border-color: var(--color-primary);
}
input[type="checkbox"]:checked::before {
clip-path: inset(0);
}
input[type="checkbox"]:disabled:checked,
input[type="checkbox"]:disabled:indeterminate {
background: var(--color-secondary-dark-4);
border-color: var(--color-secondary-dark-4);
}
input[type="radio"]:disabled:checked {
border-color: var(--color-secondary-dark-4);
}
input[type="checkbox"]:indeterminate::before {
clip-path: inset(0);
mask-image: var(--checkbox-mask-indeterminate);
-webkit-mask-image: var(--checkbox-mask-indeterminate);
mask-size: 75%;
-webkit-mask-size: 75%;
}
.ui.checkbox {

View File

@ -268,7 +268,6 @@ select.ui.dropdown {
}
.ui.selection.dropdown:hover {
border-color: var(--color-input-border-hover);
box-shadow: none;
}

View File

@ -43,7 +43,7 @@
height: 1.21428571em;
}
.ui.form input,
.ui.form input:not([type="checkbox"], [type="radio"]),
.ui.search > .prompt {
font-family: var(--fonts-regular);
margin: 0;
@ -74,10 +74,10 @@
max-height: 24em;
}
input,
input:not([type="checkbox"], [type="radio"]),
textarea,
.ui.input > input,
.ui.form input,
.ui.form input:not([type="checkbox"], [type="radio"]),
.ui.form select,
.ui.form textarea,
.ui.selection.dropdown,
@ -87,22 +87,10 @@ textarea,
color: var(--color-input-text);
}
input:hover,
textarea:hover,
.ui.input input:hover,
.ui.form input:hover,
.ui.form select:hover,
.ui.form textarea:hover,
.ui.search > .prompt:hover {
background: var(--color-input-background);
border: 1px solid var(--color-input-border-hover);
color: var(--color-input-text);
}
input:focus,
input:not([type="checkbox"], [type="radio"]):focus,
textarea:focus,
.ui.input input:focus,
.ui.form input:focus,
.ui.form input:not([type="checkbox"], [type="radio"]):focus,
.ui.form select:focus,
.ui.form textarea:focus,
.ui.search > .prompt:focus {

View File

@ -205,8 +205,7 @@ gitea-theme-meta-info {
--color-input-text: var(--color-text-dark);
--color-input-background: #171a1e;
--color-input-toggle-background: #2e353c;
--color-input-border: var(--color-secondary);
--color-input-border-hover: var(--color-secondary-dark-1);
--color-input-border: var(--color-secondary-dark-1);
--color-light: #00001728;
--color-light-mimic-enabled: rgba(0, 0, 0, calc(40 / 255 * 222 / 255 / var(--opacity-disabled)));
--color-light-border: #e8f3ff28;

View File

@ -205,8 +205,7 @@ gitea-theme-meta-info {
--color-input-text: var(--color-text-dark);
--color-input-background: #fff;
--color-input-toggle-background: #d0d7de;
--color-input-border: var(--color-secondary);
--color-input-border-hover: var(--color-secondary-dark-1);
--color-input-border: var(--color-secondary-dark-1);
--color-light: #00001706;
--color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled)));
--color-light-border: #0000171d;