Compare commits

...

10 Commits

Author SHA1 Message Date
silverwind
bb1e22bba4 Allow text selection on checkbox labels (#36970)
Some checks failed
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
cron-translations / crowdin-pull (push) Has been cancelled
cron-flake-updater / nix-flake-update (push) Has been cancelled
Remove `user-select: none` from checkbox labels to allow text selection
which is sometimes useful.

---------

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com>
2026-03-25 07:40:46 +00:00
Nicolas
e24c3f7a40 Fix org contact email not clearable once set (#36975)
When the email field was submitted as empty in org settings (web and
API), the previous guard `if form.Email != ""` silently skipped the
update, making it impossible to remove a contact email after it was set.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-03-25 15:23:11 +08:00
techknowlogick
943ff75233 Require additional user confirmation for making repo private (#36959)
To align with how GitHub requires additional explicit user interaction
to make a repo private, including informing them of implications on what
happens if they do.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-03-24 20:13:08 +00:00
TheFox0x7
cfd9008891 add valid github scopes (#36977)
test for github supported scopes with test to ensure all of them work
and don't panic

fixes: https://github.com/go-gitea/gitea/issues/36967
2026-03-24 19:16:23 +00:00
Tyrone Yeh
c96cc70144 Add class "list-header-filters" to the div for projects (#36889)
closes #36886
2026-03-24 17:23:13 +00:00
silverwind
66b8178e59 Improve AGENTS.md (#36974)
1. Remove header line, useless context bloat
2. Reword all "before commiting" lines because some people may not be
using the agent to commit, only to write changes.
2026-03-24 17:49:29 +01:00
Lunny Xiao
c453d09c36 Catch scanner error when possible to avoid bypass (#36963) 2026-03-23 21:08:48 -07:00
GiteaBot
c5e196dedb [skip ci] Updated translations via Crowdin 2026-03-24 00:45:32 +00:00
wxiaoguang
63c2b69259 Make PUBLIC_URL_DETECTION default to "auto" (#36955)
Related issues including: #36939 , #35619, #34950 , #34253 , #32554

For users who use reverse-proxy, we have documented the requirements
clearly since long time ago :
https://docs.gitea.com/administration/reverse-proxies
2026-03-23 23:19:08 +00:00
Nicolas
86401fd5fd Fix user settings sidebar showing disabled features on some pages (#36958)
Move UserDisabledFeatures context data into a shared SettingsCtxData
middleware for the /user/settings route group, so it is set consistently
on all pages (including Notifications, Actions, etc.) instead of only on
the handlers that remembered to set it individually.

Fixes #36954
2026-03-23 22:30:48 +00:00
43 changed files with 604 additions and 410 deletions

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!!!

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

@@ -66,23 +66,21 @@ type CreateOrgOption struct {
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
}
// TODO: make EditOrgOption fields optional after https://gitea.com/go-chi/binding/pulls/5 got merged
// EditOrgOption options for editing an organization
type EditOrgOption struct {
// The full display name of the organization
FullName string `json:"full_name" binding:"MaxSize(100)"`
// The email address of the organization
Email string `json:"email" binding:"MaxSize(255)"`
FullName *string `json:"full_name" binding:"MaxSize(100)"`
// The email address of the organization; use empty string to clear
Email *string `json:"email" binding:"MaxSize(255)"`
// The description of the organization
Description string `json:"description" binding:"MaxSize(255)"`
Description *string `json:"description" binding:"MaxSize(255)"`
// The website URL of the organization
Website string `json:"website" binding:"ValidUrl;MaxSize(255)"`
Website *string `json:"website" binding:"ValidUrl;MaxSize(255)"`
// The location of the organization
Location string `json:"location" binding:"MaxSize(50)"`
Location *string `json:"location" binding:"MaxSize(50)"`
// possible values are `public`, `limited` or `private`
// enum: public,limited,private
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
Visibility *string `json:"visibility" binding:"In(,public,limited,private)"`
// Whether repository administrators can change team access
RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"`
}

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

@@ -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

@@ -5,6 +5,7 @@
package org
import (
"errors"
"net/http"
activities_model "code.gitea.io/gitea/models/activities"
@@ -14,6 +15,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
@@ -379,19 +381,21 @@ func Edit(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditOrgOption)
if form.Email != "" {
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), form.Email); err != nil {
ctx.APIErrorInternal(err)
if err := org.UpdateOrgEmailAddress(ctx, ctx.Org.Organization, form.Email); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
ctx.APIErrorInternal(err)
return
}
opts := &user_service.UpdateOptions{
FullName: optional.Some(form.FullName),
Description: optional.Some(form.Description),
Website: optional.Some(form.Website),
Location: optional.Some(form.Location),
Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
FullName: optional.FromPtr(form.FullName),
Description: optional.FromPtr(form.Description),
Website: optional.FromPtr(form.Website),
Location: optional.FromPtr(form.Location),
Visibility: optional.FromMapLookup(api.VisibilityModes, optional.FromPtr(form.Visibility).Value()),
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
}
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {

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

@@ -5,6 +5,7 @@
package org
import (
"errors"
"net/http"
"net/url"
@@ -69,24 +70,25 @@ func SettingsPost(ctx *context.Context) {
}
org := ctx.Org.Organization
if form.Email != "" {
if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil {
if err := org_service.UpdateOrgEmailAddress(ctx, org, form.Email); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Data["Err_Email"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form)
return
}
ctx.ServerError("UpdateOrgEmailAddress", err)
return
}
opts := &user_service.UpdateOptions{
FullName: optional.Some(form.FullName),
Description: optional.Some(form.Description),
Website: optional.Some(form.Website),
Location: optional.Some(form.Location),
RepoAdminChangeTeamAccess: optional.Some(form.RepoAdminChangeTeamAccess),
FullName: optional.FromPtr(form.FullName),
Description: optional.FromPtr(form.Description),
Website: optional.FromPtr(form.Website),
Location: optional.FromPtr(form.Location),
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
}
if ctx.Doer.IsAdmin {
opts.MaxRepoCreation = optional.Some(form.MaxRepoCreation)
opts.MaxRepoCreation = optional.FromPtr(form.MaxRepoCreation)
}
if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {

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

@@ -36,13 +36,13 @@ func (f *CreateOrgForm) Validate(req *http.Request, errs binding.Errors) binding
// UpdateOrgSettingForm form for updating organization settings
type UpdateOrgSettingForm struct {
FullName string `binding:"MaxSize(100)"`
Email string `binding:"MaxSize(255)"`
Description string `binding:"MaxSize(255)"`
Website string `binding:"ValidUrl;MaxSize(255)"`
Location string `binding:"MaxSize(50)"`
MaxRepoCreation int
RepoAdminChangeTeamAccess bool
FullName *string `binding:"MaxSize(100)"`
Email *string `binding:"MaxSize(255)"`
Description *string `binding:"MaxSize(255)"`
Website *string `binding:"ValidUrl;MaxSize(255)"`
Location *string `binding:"MaxSize(50)"`
MaxRepoCreation *int
RepoAdminChangeTeamAccess *bool
}
// Validate validates the fields

View File

@@ -168,3 +168,20 @@ func ChangeOrganizationVisibility(ctx context.Context, org *org_model.Organizati
return nil
})
}
// UpdateOrgEmailAddress validates and updates the organization's contact email.
// A nil email means no change.
func UpdateOrgEmailAddress(ctx context.Context, org *org_model.Organization, email *string) error {
if email == nil {
return nil
}
if *email != "" {
if err := user_model.ValidateEmail(*email); err != nil {
return err
}
}
org.Email = *email
return user_model.UpdateUserCols(ctx, org.AsUser(), "email")
}

View File

@@ -10,28 +10,54 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
unittest.MainTest(m)
}
func TestDeleteOrganization(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 6})
assert.NoError(t, DeleteOrganization(t.Context(), org, false))
unittest.AssertNotExistsBean(t, &organization.Organization{ID: 6})
unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: 6})
unittest.AssertNotExistsBean(t, &organization.Team{OrgID: 6})
func TestOrg(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
err := DeleteOrganization(t.Context(), org, false)
assert.Error(t, err)
assert.True(t, repo_model.IsErrUserOwnRepos(err))
t.Run("UpdateOrgEmailAddress", func(t *testing.T) {
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
originalEmail := org.Email
user := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 5})
assert.Error(t, DeleteOrganization(t.Context(), user, false))
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
require.NoError(t, UpdateOrgEmailAddress(t.Context(), org, nil))
unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: originalEmail})
newEmail := "contact@org3.example.com"
require.NoError(t, UpdateOrgEmailAddress(t.Context(), org, &newEmail))
unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: newEmail})
invalidEmail := "invalid email"
err := UpdateOrgEmailAddress(t.Context(), org, &invalidEmail)
require.ErrorIs(t, err, util.ErrInvalidArgument)
unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: newEmail})
require.NoError(t, UpdateOrgEmailAddress(t.Context(), org, new("")))
org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: ""})
assert.Empty(t, org.Email)
})
t.Run("DeleteOrganization", func(t *testing.T) {
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 6})
assert.NoError(t, DeleteOrganization(t.Context(), org, false))
unittest.AssertNotExistsBean(t, &organization.Organization{ID: 6})
unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: 6})
unittest.AssertNotExistsBean(t, &organization.Team{OrgID: 6})
org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
err := DeleteOrganization(t.Context(), org, false)
assert.Error(t, err)
assert.True(t, repo_model.IsErrUserOwnRepos(err))
user := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 5})
assert.Error(t, DeleteOrganization(t.Context(), user, false))
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
})
}

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

@@ -14,7 +14,14 @@ import (
"code.gitea.io/gitea/modules/util"
)
// ReplacePrimaryEmailAddress replaces the user's primary email address with the given email address.
// It also updates the user's email field to match the new primary email address.
func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error {
// FIXME: this check is from old logic, but it is not right, there are far more user types, not only "organization"
if u.IsOrganization() {
return util.NewInvalidArgumentErrorf("user %s is an organization", u.Name)
}
if strings.EqualFold(u.Email, emailStr) {
return nil
}
@@ -24,41 +31,38 @@ func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailSt
}
return db.WithTx(ctx, func(ctx context.Context) error {
if !u.IsOrganization() {
// Check if address exists already
email, err := user_model.GetEmailAddressByEmail(ctx, emailStr)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return err
}
if email != nil {
if email.IsPrimary && email.UID == u.ID {
return nil
}
return user_model.ErrEmailAlreadyUsed{Email: emailStr}
// Check if address exists already
email, err := user_model.GetEmailAddressByEmail(ctx, emailStr)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return err
}
if email != nil {
if email.IsPrimary && email.UID == u.ID {
return nil
}
return user_model.ErrEmailAlreadyUsed{Email: emailStr}
}
// Remove old primary address
primary, err := user_model.GetPrimaryEmailAddressOfUser(ctx, u.ID)
if err != nil {
return err
}
if _, err := db.DeleteByID[user_model.EmailAddress](ctx, primary.ID); err != nil {
return err
}
// Remove old primary address
primary, err := user_model.GetPrimaryEmailAddressOfUser(ctx, u.ID)
if err != nil {
return err
}
if _, err := db.DeleteByID[user_model.EmailAddress](ctx, primary.ID); err != nil {
return err
}
// Insert new primary address
if _, err := user_model.InsertEmailAddress(ctx, &user_model.EmailAddress{
UID: u.ID,
Email: emailStr,
IsActivated: true,
IsPrimary: true,
}); err != nil {
return err
}
// Insert new primary address
if _, err := user_model.InsertEmailAddress(ctx, &user_model.EmailAddress{
UID: u.ID,
Email: emailStr,
IsActivated: true,
IsPrimary: true,
}); err != nil {
return err
}
u.Email = emailStr
return user_model.UpdateUserCols(ctx, u, "email")
})
}

View File

@@ -6,17 +6,16 @@ package user
import (
"testing"
organization_model "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestReplacePrimaryEmailAddress(t *testing.T) {
func TestUserEmail(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
t.Run("User", func(t *testing.T) {
t.Run("PrimaryEmailAddress", func(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13})
emails, err := user_model.GetEmailAddresses(t.Context(), user.ID)
@@ -42,50 +41,36 @@ func TestReplacePrimaryEmailAddress(t *testing.T) {
assert.NoError(t, ReplacePrimaryEmailAddress(t.Context(), user, "primary-13@example.com"))
})
t.Run("Organization", func(t *testing.T) {
org := unittest.AssertExistsAndLoadBean(t, &organization_model.Organization{ID: 3})
t.Run("AddEmailAddresses", func(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
assert.Equal(t, "org3@example.com", org.Email)
assert.Error(t, AddEmailAddresses(t.Context(), user, []string{" invalid email "}))
assert.NoError(t, ReplacePrimaryEmailAddress(t.Context(), org.AsUser(), "primary-org@example.com"))
emails := []string{"user1234@example.com", "user5678@example.com"}
assert.Equal(t, "primary-org@example.com", org.Email)
assert.NoError(t, AddEmailAddresses(t.Context(), user, emails))
err := AddEmailAddresses(t.Context(), user, emails)
assert.Error(t, err)
assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
})
t.Run("DeleteEmailAddresses", func(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
emails := []string{"user2-2@example.com"}
err := DeleteEmailAddresses(t.Context(), user, emails)
assert.NoError(t, err)
err = DeleteEmailAddresses(t.Context(), user, emails)
assert.Error(t, err)
assert.True(t, user_model.IsErrEmailAddressNotExist(err))
emails = []string{"user2@example.com"}
err = DeleteEmailAddresses(t.Context(), user, emails)
assert.Error(t, err)
assert.True(t, user_model.IsErrPrimaryEmailCannotDelete(err))
})
}
func TestAddEmailAddresses(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
assert.Error(t, AddEmailAddresses(t.Context(), user, []string{" invalid email "}))
emails := []string{"user1234@example.com", "user5678@example.com"}
assert.NoError(t, AddEmailAddresses(t.Context(), user, emails))
err := AddEmailAddresses(t.Context(), user, emails)
assert.Error(t, err)
assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
}
func TestDeleteEmailAddresses(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
emails := []string{"user2-2@example.com"}
err := DeleteEmailAddresses(t.Context(), user, emails)
assert.NoError(t, err)
err = DeleteEmailAddresses(t.Context(), user, emails)
assert.Error(t, err)
assert.True(t, user_model.IsErrEmailAddressNotExist(err))
emails = []string{"user2@example.com"}
err = DeleteEmailAddresses(t.Context(), user, emails)
assert.Error(t, err)
assert.True(t, user_model.IsErrPrimaryEmailCannotDelete(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

@@ -24855,7 +24855,7 @@
"x-go-name": "Description"
},
"email": {
"description": "The email address of the organization",
"description": "The email address of the organization; use empty string to clear",
"type": "string",
"x-go-name": "Email"
},

View File

@@ -137,34 +137,45 @@ func TestAPIOrgGeneral(t *testing.T) {
})
t.Run("OrgEdit", func(t *testing.T) {
org := api.EditOrgOption{
FullName: "Org3 organization new full name",
Description: "A new description",
Website: "https://try.gitea.io/new",
Location: "Beijing",
Visibility: "private",
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
resp := MakeRequest(t, req, http.StatusOK)
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
assert.NotEqual(t, api.VisibleTypeLimited, org3.Visibility)
var apiOrg api.Organization
DecodeJSON(t, resp, &apiOrg)
org3Edit := api.EditOrgOption{
FullName: new("new full name"),
Description: new("new description"),
Website: new("https://org3-new-website.example.com"),
Location: new("new location"),
Visibility: new("limited"),
Email: new("org3-new-email@example.com"),
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org3Edit).AddTokenAuth(user1Token)
resp := MakeRequest(t, req, http.StatusOK)
apiOrg := DecodeJSON(t, resp, &api.Organization{})
assert.Equal(t, "org3", apiOrg.Name)
assert.Equal(t, org.FullName, apiOrg.FullName)
assert.Equal(t, org.Description, apiOrg.Description)
assert.Equal(t, org.Website, apiOrg.Website)
assert.Equal(t, org.Location, apiOrg.Location)
assert.Equal(t, org.Visibility, apiOrg.Visibility)
assert.Equal(t, *org3Edit.FullName, apiOrg.FullName)
assert.Equal(t, *org3Edit.Description, apiOrg.Description)
assert.Equal(t, *org3Edit.Website, apiOrg.Website)
assert.Equal(t, *org3Edit.Location, apiOrg.Location)
assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility)
assert.Equal(t, *org3Edit.Email, apiOrg.Email)
org3 = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
assert.Equal(t, api.VisibleTypeLimited, org3.Visibility)
// empty email can clear the email, nil fields won't change the settings
req = NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &api.EditOrgOption{
Email: new(""),
}).AddTokenAuth(user1Token)
resp = MakeRequest(t, req, http.StatusOK)
apiOrg = DecodeJSON(t, resp, &api.Organization{})
assert.Equal(t, *org3Edit.FullName, apiOrg.FullName)
assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility)
assert.Empty(t, apiOrg.Email)
})
t.Run("OrgEditBadVisibility", func(t *testing.T) {
t.Run("OrgEditInvalidVisibility", func(t *testing.T) {
org := api.EditOrgOption{
FullName: "Org3 organization new full name",
Description: "A new description",
Website: "https://try.gitea.io/new",
Location: "Beijing",
Visibility: "badvisibility",
Visibility: new("invalid-visibility"),
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
MakeRequest(t, req, http.StatusUnprocessableEntity)

View File

@@ -410,12 +410,13 @@ func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) {
}
}
func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v any) {
func DecodeJSON[T any](t testing.TB, resp *httptest.ResponseRecorder, v T) (ret T) {
t.Helper()
// FIXME: JSON-KEY-CASE: for testing purpose only, because many structs don't provide `json` tags, they just use capitalized field names
decoder := json.NewDecoderCaseInsensitive(resp.Body)
require.NoError(t, decoder.Decode(v))
return v
}
func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile string) {

View File

@@ -23,9 +23,18 @@ import (
"github.com/stretchr/testify/require"
)
func TestOrgRepos(t *testing.T) {
func TestOrg(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("OrgRepos", testOrgRepos)
t.Run("PrivateOrg", testPrivateOrg)
t.Run("LimitedOrg", testLimitedOrg)
t.Run("OrgMembers", testOrgMembers)
t.Run("OrgRestrictedUser", testOrgRestrictedUser)
t.Run("TeamSearch", testTeamSearch)
t.Run("OrgSettings", testOrgSettings)
}
func testOrgRepos(t *testing.T) {
var (
users = []string{"user1", "user2"}
cases = map[string][]string{
@@ -53,10 +62,8 @@ func TestOrgRepos(t *testing.T) {
}
}
func TestLimitedOrg(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// not logged in user
func testLimitedOrg(t *testing.T) {
// not logged-in user
req := NewRequest(t, "GET", "/limited_org")
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", "/limited_org/public_repo_on_limited_org")
@@ -83,10 +90,8 @@ func TestLimitedOrg(t *testing.T) {
session.MakeRequest(t, req, http.StatusOK)
}
func TestPrivateOrg(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// not logged in user
func testPrivateOrg(t *testing.T) {
// not logged-in user
req := NewRequest(t, "GET", "/privated_org")
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", "/privated_org/public_repo_on_private_org")
@@ -122,10 +127,8 @@ func TestPrivateOrg(t *testing.T) {
session.MakeRequest(t, req, http.StatusOK)
}
func TestOrgMembers(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// not logged in user
func testOrgMembers(t *testing.T) {
// not logged-in user
req := NewRequest(t, "GET", "/org/org25/members")
MakeRequest(t, req, http.StatusOK)
@@ -140,9 +143,7 @@ func TestOrgMembers(t *testing.T) {
session.MakeRequest(t, req, http.StatusOK)
}
func TestOrgRestrictedUser(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testOrgRestrictedUser(t *testing.T) {
// privated_org is a private org who has id 23
orgName := "privated_org"
@@ -200,9 +201,7 @@ func TestOrgRestrictedUser(t *testing.T) {
restrictedSession.MakeRequest(t, req, http.StatusOK)
}
func TestTeamSearch(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testTeamSearch(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})
@@ -251,3 +250,24 @@ func TestTeamSearch(t *testing.T) {
assert.Len(t, teams, 1) // team permission is "write", so can write "code"
})
}
func testOrgSettings(t *testing.T) {
session := loginUser(t, "user2")
req := NewRequestWithValues(t, "POST", "/org/org3/settings", map[string]string{
"full_name": "org3 new full name",
"email": "org3-new-email@example.com",
})
session.MakeRequest(t, req, http.StatusSeeOther)
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
assert.Equal(t, "org3 new full name", org.FullName)
assert.Equal(t, "org3-new-email@example.com", org.Email)
req = NewRequestWithValues(t, "POST", "/org/org3/settings", map[string]string{
"email": "", // empty email means "clear email"
})
session.MakeRequest(t, req, http.StatusSeeOther)
org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
assert.Equal(t, "org3 new full name", org.FullName)
assert.Empty(t, org.Email)
}

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

@@ -101,7 +101,6 @@ input[type="checkbox"]:indeterminate::before {
cursor: auto;
position: relative;
display: block;
user-select: none;
}
.ui.checkbox label,