+ {{$statusUnread := 1}}{{$statusRead := 2}}{{$statusPinned := 3}} {{$notificationUnreadCount := call .PageGlobalData.GetNotificationUnreadCount}} -
+ {{$pageTypeIsRead := eq $.PageType "read"}} +
- {{if and (eq .Status 1)}} + {{if and (not $pageTypeIsRead) $notificationUnreadCount}}
{{$.CsrfTokenHtml}} -
- -
+
{{end}}
-
-
- {{if not .Notifications}} -
- {{svg "octicon-inbox" 56 "tw-mb-4"}} - {{if eq .Status 1}} - {{ctx.Locale.Tr "notification.no_unread"}} +
+ {{range $one := .Notifications}} +
+
+ {{if $one.Issue}} + {{template "shared/issueicon" $one.Issue}} {{else}} - {{ctx.Locale.Tr "notification.no_read"}} + {{svg "octicon-repo" 16 "text grey"}} {{end}}
- {{else}} - {{range $notification := .Notifications}} -
-
- {{if .Issue}} - {{template "shared/issueicon" .Issue}} - {{else}} - {{svg "octicon-repo" 16 "text grey"}} - {{end}} -
- -
- {{.Repository.FullName}} {{if .Issue}}#{{.Issue.Index}}{{end}} - {{if eq .Status 3}} - {{svg "octicon-pin" 13 "text blue tw-mt-0.5 tw-ml-1"}} - {{end}} -
-
- - {{if .Issue}} - {{.Issue.Title | ctx.RenderUtils.RenderIssueSimpleTitle}} - {{else}} - {{.Repository.FullName}} - {{end}} - -
-
-
- {{if .Issue}} - {{DateUtils.TimeSince .Issue.UpdatedUnix}} - {{else}} - {{DateUtils.TimeSince .UpdatedUnix}} - {{end}} -
-
- {{if ne .Status 3}} -
- {{$.CsrfTokenHtml}} - - - -
- {{end}} - {{if or (eq .Status 1) (eq .Status 3)}} -
- {{$.CsrfTokenHtml}} - - - - -
- {{else if eq .Status 2}} -
- {{$.CsrfTokenHtml}} - - - - -
- {{end}} -
+ +
+ {{$one.Repository.FullName}} {{if $one.Issue}}#{{$one.Issue.Index}}{{end}} + {{if eq $one.Status $statusPinned}} + {{svg "octicon-pin" 13 "text blue"}} + {{end}}
+
+ {{if $one.Issue}} + {{$one.Issue.Title | ctx.RenderUtils.RenderIssueSimpleTitle}} + {{else}} + {{$one.Repository.FullName}} + {{end}} +
+
+
+ {{if $one.Issue}} + {{DateUtils.TimeSince $one.Issue.UpdatedUnix}} + {{else}} + {{DateUtils.TimeSince $one.UpdatedUnix}} + {{end}} +
+
+ {{$.CsrfTokenHtml}} + + {{if ne $one.Status $statusPinned}} + + {{end}} + {{if or (eq $one.Status $statusUnread) (eq $one.Status $statusPinned)}} + + {{else if eq $one.Status $statusRead}} + + {{end}} +
+
+ {{else}} +
+ {{svg "octicon-inbox" 56 "tw-mb-4"}} + {{if $pageTypeIsRead}} + {{ctx.Locale.Tr "notification.no_read"}} + {{else}} + {{ctx.Locale.Tr "notification.no_unread"}} {{end}} - {{end}} -
+
+ {{end}}
{{template "base/paginate" .}}
diff --git a/web_src/css/user.css b/web_src/css/user.css index caabf1834cb..d42e8688fbc 100644 --- a/web_src/css/user.css +++ b/web_src/css/user.css @@ -114,6 +114,14 @@ border-radius: var(--border-radius); } +.notifications-item { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5em; + padding: 0.5em 1em; +} + .notifications-item:hover { background: var(--color-hover); } @@ -129,6 +137,9 @@ .notifications-item:hover .notifications-buttons { display: flex; + align-items: center; + justify-content: end; + gap: 0.25em; } .notifications-item:hover .notifications-updated { diff --git a/web_src/js/features/notification.ts b/web_src/js/features/notification.ts index dc0acb0244e..4a1aa3ede94 100644 --- a/web_src/js/features/notification.ts +++ b/web_src/js/features/notification.ts @@ -1,40 +1,13 @@ import {GET} from '../modules/fetch.ts'; -import {toggleElem, type DOMEvent, createElementFromHTML} from '../utils/dom.ts'; +import {toggleElem, createElementFromHTML} from '../utils/dom.ts'; import {logoutFromWorker} from '../modules/worker.ts'; const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config; let notificationSequenceNumber = 0; -export function initNotificationsTable() { - const table = document.querySelector('#notification_table'); - if (!table) return; - - // when page restores from bfcache, delete previously clicked items - window.addEventListener('pageshow', (e) => { - if (e.persisted) { // page was restored from bfcache - const table = document.querySelector('#notification_table'); - const unreadCountEl = document.querySelector('.notifications-unread-count'); - let unreadCount = parseInt(unreadCountEl.textContent); - for (const item of table.querySelectorAll('.notifications-item[data-remove="true"]')) { - item.remove(); - unreadCount -= 1; - } - unreadCountEl.textContent = String(unreadCount); - } - }); - - // mark clicked unread links for deletion on bfcache restore - for (const link of table.querySelectorAll('.notifications-item[data-status="1"] .notifications-link')) { - link.addEventListener('click', (e: DOMEvent) => { - e.target.closest('.notifications-item').setAttribute('data-remove', 'true'); - }); - } -} - -async function receiveUpdateCount(event: MessageEvent) { +async function receiveUpdateCount(event: MessageEvent<{type: string, data: string}>) { try { - const data = JSON.parse(event.data); - + const data = JSON.parse(event.data.data); for (const count of document.querySelectorAll('.notification_count')) { count.classList.toggle('tw-hidden', data.Count === 0); count.textContent = `${data.Count}`; @@ -71,7 +44,7 @@ export function initNotificationCount() { type: 'start', url: `${window.location.origin}${appSubUrl}/user/events`, }); - worker.port.addEventListener('message', (event: MessageEvent) => { + worker.port.addEventListener('message', (event: MessageEvent<{type: string, data: string}>) => { if (!event.data || !event.data.type) { console.error('unknown worker message event', event); return; @@ -144,11 +117,11 @@ async function updateNotificationCountWithCallback(callback: (timeout: number, n } async function updateNotificationTable() { - const notificationDiv = document.querySelector('#notification_div'); + let notificationDiv = document.querySelector('#notification_div'); if (notificationDiv) { try { const params = new URLSearchParams(window.location.search); - params.set('div-only', String(true)); + params.set('div-only', 'true'); params.set('sequence-number', String(++notificationSequenceNumber)); const response = await GET(`${appSubUrl}/notifications?${params.toString()}`); @@ -160,7 +133,8 @@ async function updateNotificationTable() { const el = createElementFromHTML(data); if (parseInt(el.getAttribute('data-sequence-number')) === notificationSequenceNumber) { notificationDiv.outerHTML = data; - initNotificationsTable(); + notificationDiv = document.querySelector('#notification_div'); + window.htmx.process(notificationDiv); // when using htmx, we must always remember to process the new content changed by us } } catch (error) { console.error(error); diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts index ca18d1e8281..770c7fc00c6 100644 --- a/web_src/js/index-domready.ts +++ b/web_src/js/index-domready.ts @@ -15,7 +15,7 @@ import {initTableSort} from './features/tablesort.ts'; import {initAdminUserListSearchForm} from './features/admin/users.ts'; import {initAdminConfigs} from './features/admin/config.ts'; import {initMarkupAnchors} from './markup/anchors.ts'; -import {initNotificationCount, initNotificationsTable} from './features/notification.ts'; +import {initNotificationCount} from './features/notification.ts'; import {initRepoIssueContentHistory} from './features/repo-issue-content.ts'; import {initStopwatch} from './features/stopwatch.ts'; import {initFindFileInRepo} from './features/repo-findfile.ts'; @@ -117,7 +117,6 @@ const initPerformanceTracer = callInitFunctions([ initDashboardRepoList, initNotificationCount, - initNotificationsTable, initOrgTeam, From af0196c1452404e1055693a0683d61e69d6b7ad4 Mon Sep 17 00:00:00 2001 From: Scion Date: Wed, 9 Jul 2025 22:58:07 -0700 Subject: [PATCH 09/51] Fix ListWorkflowRuns OpenAPI response model. (#35026) Change the OpenAPI response of `ListWorkflowRuns` to `WorkflowRunsList` like it is supposed to be. --------- Signed-off-by: wxiaoguang Co-authored-by: wxiaoguang --- modules/structs/repo.go | 4 ++-- routers/api/v1/repo/action.go | 2 +- services/convert/repository.go | 2 +- templates/swagger/v1_json.tmpl | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/structs/repo.go b/modules/structs/repo.go index abc80763878..aca5d9c3f43 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -57,7 +57,7 @@ type Repository struct { Private bool `json:"private"` Fork bool `json:"fork"` Template bool `json:"template"` - Parent *Repository `json:"parent"` + Parent *Repository `json:"parent,omitempty"` Mirror bool `json:"mirror"` Size int `json:"size"` Language string `json:"language"` @@ -114,7 +114,7 @@ type Repository struct { ObjectFormatName string `json:"object_format_name"` // swagger:strfmt date-time MirrorUpdated time.Time `json:"mirror_updated"` - RepoTransfer *RepoTransfer `json:"repo_transfer"` + RepoTransfer *RepoTransfer `json:"repo_transfer,omitempty"` Topics []string `json:"topics"` Licenses []string `json:"licenses"` } diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index a57db015f02..ef0c5cc1995 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -747,7 +747,7 @@ func (Action) ListWorkflowRuns(ctx *context.APIContext) { // type: integer // responses: // "200": - // "$ref": "#/responses/ArtifactsList" + // "$ref": "#/responses/WorkflowRunsList" // "400": // "$ref": "#/responses/error" // "404": diff --git a/services/convert/repository.go b/services/convert/repository.go index 614eb58a883..a364591bb8f 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -251,7 +251,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR RepoTransfer: transfer, Topics: util.SliceNilAsEmpty(repo.Topics), ObjectFormatName: repo.ObjectFormatName, - Licenses: repoLicenses.StringList(), + Licenses: util.SliceNilAsEmpty(repoLicenses.StringList()), } } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 6cf2810baf8..323e0d64ac5 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -5165,7 +5165,7 @@ ], "responses": { "200": { - "$ref": "#/responses/ArtifactsList" + "$ref": "#/responses/WorkflowRunsList" }, "400": { "$ref": "#/responses/error" From 091b3e696daedc9e8a7531f71329ba57b41cb8f0 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 10 Jul 2025 10:28:25 +0200 Subject: [PATCH 10/51] Tweak eslint config, fix new issues (#35019) 1. Enable [`@typescript-eslint/no-unnecessary-type-conversion`](https://typescript-eslint.io/rules/no-unnecessary-type-conversion/), I think the two cases that were hit are safe cases. 2. Disable `no-new-func`, `@typescript-eslint/no-implied-eval` does the same but better. --- .eslintrc.cjs | 3 ++- web_src/js/modules/toast.ts | 2 +- web_src/js/utils/dom.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 57c6b19600b..cf3ff53b30d 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -326,6 +326,7 @@ module.exports = { '@typescript-eslint/no-unnecessary-type-arguments': [0], '@typescript-eslint/no-unnecessary-type-assertion': [2], '@typescript-eslint/no-unnecessary-type-constraint': [2], + '@typescript-eslint/no-unnecessary-type-conversion': [2], '@typescript-eslint/no-unsafe-argument': [0], '@typescript-eslint/no-unsafe-assignment': [0], '@typescript-eslint/no-unsafe-call': [0], @@ -645,7 +646,7 @@ module.exports = { 'no-multi-str': [2], 'no-negated-condition': [0], 'no-nested-ternary': [0], - 'no-new-func': [2], + 'no-new-func': [0], // handled by @typescript-eslint/no-implied-eval 'no-new-native-nonconstructor': [2], 'no-new-object': [2], 'no-new-symbol': [2], diff --git a/web_src/js/modules/toast.ts b/web_src/js/modules/toast.ts index ed807a49772..087103cbd8e 100644 --- a/web_src/js/modules/toast.ts +++ b/web_src/js/modules/toast.ts @@ -44,7 +44,7 @@ type ToastifyElement = HTMLElement & {_giteaToastifyInstance?: Toast }; // See https://github.com/apvarun/toastify-js#api for options function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast { - const body = useHtmlBody ? String(message) : htmlEscape(message); + const body = useHtmlBody ? message : htmlEscape(message); const parent = document.querySelector('.ui.dimmer.active') ?? document.body; const duplicateKey = preventDuplicates ? (preventDuplicates === true ? `${level}-${body}` : preventDuplicates) : ''; diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index 9cdf6f5005c..6d6a3735dab 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -283,7 +283,7 @@ export function isElemVisible(el: HTMLElement): boolean { // This function DOESN'T account for all possible visibility scenarios, its behavior is covered by the tests of "querySingleVisibleElem" if (!el) return false; // checking el.style.display is not necessary for browsers, but it is required by some tests with happy-dom because happy-dom doesn't really do layout - return !el.classList.contains('tw-hidden') && Boolean((el.offsetWidth || el.offsetHeight || el.getClientRects().length) && el.style.display !== 'none'); + return !el.classList.contains('tw-hidden') && (el.offsetWidth || el.offsetHeight || el.getClientRects().length) && el.style.display !== 'none'; } // replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this From 4b174e44a817cf6b11fdf439e3c60dfc5c55dfe2 Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Thu, 10 Jul 2025 13:36:55 +0200 Subject: [PATCH 11/51] Improve CLI commands (#34973) Improve help related commands and flags and add tests Co-authored-by: wxiaoguang --- cmd/hook.go | 1 + cmd/keys.go | 1 + cmd/main.go | 158 ++++++++++------------- cmd/main_test.go | 46 ++++++- cmd/serv.go | 1 + contrib/autocompletion/README | 17 --- contrib/autocompletion/bash_autocomplete | 30 ----- contrib/autocompletion/zsh_autocomplete | 30 ----- 8 files changed, 116 insertions(+), 168 deletions(-) delete mode 100644 contrib/autocompletion/README delete mode 100755 contrib/autocompletion/bash_autocomplete delete mode 100644 contrib/autocompletion/zsh_autocomplete diff --git a/cmd/hook.go b/cmd/hook.go index 2ce272b411e..b741127ca3c 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -32,6 +32,7 @@ var ( CmdHook = &cli.Command{ Name: "hook", Usage: "(internal) Should only be called by Git", + Hidden: true, // internal commands shouldn't be visible Description: "Delegate commands to corresponding Git hooks", Before: PrepareConsoleLoggerLevel(log.FATAL), Commands: []*cli.Command{ diff --git a/cmd/keys.go b/cmd/keys.go index 8710756a813..5ca3b91e15e 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -19,6 +19,7 @@ import ( var CmdKeys = &cli.Command{ Name: "keys", Usage: "(internal) Should only be called by SSH server", + Hidden: true, // internal commands shouldn't not be visible Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint", Before: PrepareConsoleLoggerLevel(log.FATAL), Action: runKeys, diff --git a/cmd/main.go b/cmd/main.go index 3b8a8a93116..3fdaf48ed96 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,6 +6,7 @@ package cmd import ( "context" "fmt" + "io" "os" "strings" @@ -15,26 +16,28 @@ import ( "github.com/urfave/cli/v3" ) -// cmdHelp is our own help subcommand with more information -// Keep in mind that the "./gitea help"(subcommand) is different from "./gitea --help"(flag), the flag doesn't parse the config or output "DEFAULT CONFIGURATION:" information -func cmdHelp() *cli.Command { - c := &cli.Command{ - Name: "help", - Aliases: []string{"h"}, - Usage: "Shows a list of commands or help for one command", - ArgsUsage: "[command]", - Action: func(ctx context.Context, c *cli.Command) (err error) { - lineage := c.Lineage() // The order is from child to parent: help, doctor, Gitea - targetCmdIdx := 0 - if c.Name == "help" { - targetCmdIdx = 1 - } - if lineage[targetCmdIdx] != lineage[targetCmdIdx].Root() { - err = cli.ShowCommandHelp(ctx, lineage[targetCmdIdx+1] /* parent cmd */, lineage[targetCmdIdx].Name /* sub cmd */) - } else { - err = cli.ShowAppHelp(c) - } - _, _ = fmt.Fprintf(c.Root().Writer, ` +var cliHelpPrinterOld = cli.HelpPrinter + +func init() { + cli.HelpPrinter = cliHelpPrinterNew +} + +// cliHelpPrinterNew helps to print "DEFAULT CONFIGURATION" for the following cases ( "-c" can apper in any position): +// * ./gitea -c /dev/null -h +// * ./gitea -c help /dev/null help +// * ./gitea help -c /dev/null +// * ./gitea help -c /dev/null web +// * ./gitea help web -c /dev/null +// * ./gitea web help -c /dev/null +// * ./gitea web -h -c /dev/null +func cliHelpPrinterNew(out io.Writer, templ string, data any) { + cmd, _ := data.(*cli.Command) + if cmd != nil { + prepareWorkPathAndCustomConf(cmd) + } + cliHelpPrinterOld(out, templ, data) + if setting.CustomConf != "" { + _, _ = fmt.Fprintf(out, ` DEFAULT CONFIGURATION: AppPath: %s WorkPath: %s @@ -42,77 +45,36 @@ DEFAULT CONFIGURATION: ConfigFile: %s `, setting.AppPath, setting.AppWorkPath, setting.CustomPath, setting.CustomConf) - return err - }, - } - return c -} - -func appGlobalFlags() []cli.Flag { - return []cli.Flag{ - // make the builtin flags at the top - cli.HelpFlag, - - // shared configuration flags, they are for global and for each sub-command at the same time - // eg: such command is valid: "./gitea --config /tmp/app.ini web --config /tmp/app.ini", while it's discouraged indeed - // keep in mind that the short flags like "-C", "-c" and "-w" are globally polluted, they can't be used for sub-commands anymore. - &cli.StringFlag{ - Name: "custom-path", - Aliases: []string{"C"}, - Usage: "Set custom path (defaults to '{WorkPath}/custom')", - }, - &cli.StringFlag{ - Name: "config", - Aliases: []string{"c"}, - Value: setting.CustomConf, - Usage: "Set custom config file (defaults to '{WorkPath}/custom/conf/app.ini')", - }, - &cli.StringFlag{ - Name: "work-path", - Aliases: []string{"w"}, - Usage: "Set Gitea's working path (defaults to the Gitea's binary directory)", - }, } } -func prepareSubcommandWithGlobalFlags(command *cli.Command) { - command.Flags = append(append([]cli.Flag{}, appGlobalFlags()...), command.Flags...) - command.Action = prepareWorkPathAndCustomConf(command.Action) - command.HideHelp = true - if command.Name != "help" { - command.Commands = append(command.Commands, cmdHelp()) - } - for i := range command.Commands { - prepareSubcommandWithGlobalFlags(command.Commands[i]) - } -} - -// prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config -// It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times -func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(context.Context, *cli.Command) error { - return func(ctx context.Context, cmd *cli.Command) error { - var args setting.ArgWorkPathAndCustomConf - // from children to parent, check the global flags - for _, curCtx := range cmd.Lineage() { - if curCtx.IsSet("work-path") && args.WorkPath == "" { - args.WorkPath = curCtx.String("work-path") - } - if curCtx.IsSet("custom-path") && args.CustomPath == "" { - args.CustomPath = curCtx.String("custom-path") - } - if curCtx.IsSet("config") && args.CustomConf == "" { - args.CustomConf = curCtx.String("config") - } +func prepareSubcommandWithGlobalFlags(originCmd *cli.Command) { + originBefore := originCmd.Before + originCmd.Before = func(ctx context.Context, cmd *cli.Command) (context.Context, error) { + prepareWorkPathAndCustomConf(cmd) + if originBefore != nil { + return originBefore(ctx, cmd) } - setting.InitWorkPathAndCommonConfig(os.Getenv, args) - if cmd.Bool("help") || action == nil { - // the default behavior of "urfave/cli": "nil action" means "show help" - return cmdHelp().Action(ctx, cmd) - } - return action(ctx, cmd) + return ctx, nil } } +// prepareWorkPathAndCustomConf tries to prepare the work path, custom path and custom config from various inputs: +// command line flags, environment variables, config file +func prepareWorkPathAndCustomConf(cmd *cli.Command) { + var args setting.ArgWorkPathAndCustomConf + if cmd.IsSet("work-path") { + args.WorkPath = cmd.String("work-path") + } + if cmd.IsSet("custom-path") { + args.CustomPath = cmd.String("custom-path") + } + if cmd.IsSet("config") { + args.CustomConf = cmd.String("config") + } + setting.InitWorkPathAndCommonConfig(os.Getenv, args) +} + type AppVersion struct { Version string Extra string @@ -125,10 +87,29 @@ func NewMainApp(appVer AppVersion) *cli.Command { app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.` app.Version = appVer.Version + appVer.Extra app.EnableShellCompletion = true - - // these sub-commands need to use config file + app.Flags = []cli.Flag{ + &cli.StringFlag{ + Name: "work-path", + Aliases: []string{"w"}, + TakesFile: true, + Usage: "Set Gitea's working path (defaults to the Gitea's binary directory)", + }, + &cli.StringFlag{ + Name: "config", + Aliases: []string{"c"}, + TakesFile: true, + Value: setting.CustomConf, + Usage: "Set custom config file (defaults to '{WorkPath}/custom/conf/app.ini')", + }, + &cli.StringFlag{ + Name: "custom-path", + Aliases: []string{"C"}, + TakesFile: true, + Usage: "Set custom path (defaults to '{WorkPath}/custom')", + }, + } + // these sub-commands need to use a config file subCmdWithConfig := []*cli.Command{ - cmdHelp(), // the "help" sub-command was used to show the more information for "work path" and "custom config" CmdWeb, CmdServ, CmdHook, @@ -156,9 +137,6 @@ func NewMainApp(appVer AppVersion) *cli.Command { // but not sure whether it would break Windows users who used to double-click the EXE to run. app.DefaultCommand = CmdWeb.Name - app.Flags = append(app.Flags, cli.VersionFlag) - app.Flags = append(app.Flags, appGlobalFlags()...) - app.HideHelp = true // use our own help action to show helps (with more information like default config) app.Before = PrepareConsoleLoggerLevel(log.INFO) for i := range subCmdWithConfig { prepareSubcommandWithGlobalFlags(subCmdWithConfig[i]) diff --git a/cmd/main_test.go b/cmd/main_test.go index 7dfa87a0ef0..d49ebfd4df4 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -74,12 +74,56 @@ func TestCliCmd(t *testing.T) { cmd string exp string }{ - // main command help + // help commands + { + cmd: "./gitea -h", + exp: "DEFAULT CONFIGURATION:", + }, { cmd: "./gitea help", exp: "DEFAULT CONFIGURATION:", }, + { + cmd: "./gitea -c /dev/null -h", + exp: "ConfigFile: /dev/null", + }, + + { + cmd: "./gitea -c /dev/null help", + exp: "ConfigFile: /dev/null", + }, + { + cmd: "./gitea help -c /dev/null", + exp: "ConfigFile: /dev/null", + }, + + { + cmd: "./gitea -c /dev/null test-cmd -h", + exp: "ConfigFile: /dev/null", + }, + { + cmd: "./gitea test-cmd -c /dev/null -h", + exp: "ConfigFile: /dev/null", + }, + { + cmd: "./gitea test-cmd -h -c /dev/null", + exp: "ConfigFile: /dev/null", + }, + + { + cmd: "./gitea -c /dev/null test-cmd help", + exp: "ConfigFile: /dev/null", + }, + { + cmd: "./gitea test-cmd -c /dev/null help", + exp: "ConfigFile: /dev/null", + }, + { + cmd: "./gitea test-cmd help -c /dev/null", + exp: "ConfigFile: /dev/null", + }, + // parse paths { cmd: "./gitea test-cmd", diff --git a/cmd/serv.go b/cmd/serv.go index 8c6001e7274..38c79f68cd2 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -41,6 +41,7 @@ var CmdServ = &cli.Command{ Name: "serv", Usage: "(internal) Should only be called by SSH shell", Description: "Serv provides access auth for repositories", + Hidden: true, // Internal commands shouldn't be visible in help Before: PrepareConsoleLoggerLevel(log.FATAL), Action: runServ, Flags: []cli.Flag{ diff --git a/contrib/autocompletion/README b/contrib/autocompletion/README deleted file mode 100644 index 1defd219d8a..00000000000 --- a/contrib/autocompletion/README +++ /dev/null @@ -1,17 +0,0 @@ -Bash and Zsh completion -======================= - -From within the gitea root run: - -```bash -source contrib/autocompletion/bash_autocomplete -``` - -or for zsh run: - -```bash -source contrib/autocompletion/zsh_autocomplete -``` - -These scripts will check if gitea is on the path and if so add autocompletion for `gitea`. Or if not autocompletion will work for `./gitea`. -If gitea has been installed as a different program pass in the `PROG` environment variable to set the correct program name. diff --git a/contrib/autocompletion/bash_autocomplete b/contrib/autocompletion/bash_autocomplete deleted file mode 100755 index 5cb62f26a71..00000000000 --- a/contrib/autocompletion/bash_autocomplete +++ /dev/null @@ -1,30 +0,0 @@ -#! /bin/bash -# Heavily inspired by https://github.com/urfave/cli - -_cli_bash_autocomplete() { - if [[ "${COMP_WORDS[0]}" != "source" ]]; then - local cur opts base - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - if [[ "$cur" == "-"* ]]; then - opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion ) - else - opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) - fi - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - fi -} - -if [ -z "$PROG" ] && [ ! "$(command -v gitea &> /dev/null)" ] ; then - complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete gitea -elif [ -z "$PROG" ]; then - complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete ./gitea - complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete "$PWD/gitea" -else - complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete "$PROG" - unset PROG -fi - - - diff --git a/contrib/autocompletion/zsh_autocomplete b/contrib/autocompletion/zsh_autocomplete deleted file mode 100644 index b3b40df503f..00000000000 --- a/contrib/autocompletion/zsh_autocomplete +++ /dev/null @@ -1,30 +0,0 @@ -#compdef ${PROG:=gitea} - - -# Heavily inspired by https://github.com/urfave/cli - -_cli_zsh_autocomplete() { - - local -a opts - local cur - cur=${words[-1]} - if [[ "$cur" == "-"* ]]; then - opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}") - else - opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}") - fi - - if [[ "${opts[1]}" != "" ]]; then - _describe 'values' opts - else - _files - fi - - return -} - -if [ -z $PROG ] ; then - compdef _cli_zsh_autocomplete gitea -else - compdef _cli_zsh_autocomplete $(basename $PROG) -fi From 36a19f2569493163d76ea95a5a3060fd1daae6de Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 10 Jul 2025 17:48:36 +0200 Subject: [PATCH 12/51] Update to go 1.24.5 (#35031) https://go.dev/doc/devel/release#go1.24.5 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index afe7c990e40..8b8d61dc657 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module code.gitea.io/gitea -go 1.24.4 +go 1.24.5 // rfc5280 said: "The serial number is an integer assigned by the CA to each certificate." // But some CAs use negative serial number, just relax the check. related: From f35dcfd489490ec9ad552bb38afc237ad94ed5a2 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 11 Jul 2025 00:38:42 +0800 Subject: [PATCH 13/51] Make submodule link work with relative path (#35034) Fix #35033 --- modules/git/commit_submodule_file.go | 36 +++++++++++++-------- modules/git/commit_submodule_file_test.go | 39 ++++++++++++++--------- web_src/css/repo/home-file-list.css | 2 +- 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/modules/git/commit_submodule_file.go b/modules/git/commit_submodule_file.go index 729401f7521..5def80f3bdc 100644 --- a/modules/git/commit_submodule_file.go +++ b/modules/git/commit_submodule_file.go @@ -6,17 +6,18 @@ package git import ( "context" + "strings" giturl "code.gitea.io/gitea/modules/git/url" ) // CommitSubmoduleFile represents a file with submodule type. type CommitSubmoduleFile struct { - refURL string - parsedURL *giturl.RepositoryURL - parsed bool - refID string - repoLink string + refURL string + refID string + + parsed bool + targetRepoLink string } // NewCommitSubmoduleFile create a new submodule file @@ -35,20 +36,27 @@ func (sf *CommitSubmoduleFile) SubmoduleWebLink(ctx context.Context, optCommitID } if !sf.parsed { sf.parsed = true - parsedURL, err := giturl.ParseRepositoryURL(ctx, sf.refURL) - if err != nil { - return nil + if strings.HasPrefix(sf.refURL, "../") { + // FIXME: when handling relative path, this logic is not right. It needs to: + // 1. Remember the submodule's full path and its commit's repo home link + // 2. Resolve the relative path: targetRepoLink = path.Join(repoHomeLink, path.Dir(submoduleFullPath), refURL) + // Not an easy task and need to refactor related code a lot. + sf.targetRepoLink = sf.refURL + } else { + parsedURL, err := giturl.ParseRepositoryURL(ctx, sf.refURL) + if err != nil { + return nil + } + sf.targetRepoLink = giturl.MakeRepositoryWebLink(parsedURL) } - sf.parsedURL = parsedURL - sf.repoLink = giturl.MakeRepositoryWebLink(sf.parsedURL) } var commitLink string if len(optCommitID) == 2 { - commitLink = sf.repoLink + "/compare/" + optCommitID[0] + "..." + optCommitID[1] + commitLink = sf.targetRepoLink + "/compare/" + optCommitID[0] + "..." + optCommitID[1] } else if len(optCommitID) == 1 { - commitLink = sf.repoLink + "/tree/" + optCommitID[0] + commitLink = sf.targetRepoLink + "/tree/" + optCommitID[0] } else { - commitLink = sf.repoLink + "/tree/" + sf.refID + commitLink = sf.targetRepoLink + "/tree/" + sf.refID } - return &SubmoduleWebLink{RepoWebLink: sf.repoLink, CommitWebLink: commitLink} + return &SubmoduleWebLink{RepoWebLink: sf.targetRepoLink, CommitWebLink: commitLink} } diff --git a/modules/git/commit_submodule_file_test.go b/modules/git/commit_submodule_file_test.go index 6581fa87127..103e55e920e 100644 --- a/modules/git/commit_submodule_file_test.go +++ b/modules/git/commit_submodule_file_test.go @@ -10,20 +10,29 @@ import ( ) func TestCommitSubmoduleLink(t *testing.T) { - sf := NewCommitSubmoduleFile("git@github.com:user/repo.git", "aaaa") - - wl := sf.SubmoduleWebLink(t.Context()) - assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) - assert.Equal(t, "https://github.com/user/repo/tree/aaaa", wl.CommitWebLink) - - wl = sf.SubmoduleWebLink(t.Context(), "1111") - assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) - assert.Equal(t, "https://github.com/user/repo/tree/1111", wl.CommitWebLink) - - wl = sf.SubmoduleWebLink(t.Context(), "1111", "2222") - assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) - assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink) - - wl = (*CommitSubmoduleFile)(nil).SubmoduleWebLink(t.Context()) + wl := (*CommitSubmoduleFile)(nil).SubmoduleWebLink(t.Context()) assert.Nil(t, wl) + + t.Run("GitHubRepo", func(t *testing.T) { + sf := NewCommitSubmoduleFile("git@github.com:user/repo.git", "aaaa") + + wl := sf.SubmoduleWebLink(t.Context()) + assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) + assert.Equal(t, "https://github.com/user/repo/tree/aaaa", wl.CommitWebLink) + + wl = sf.SubmoduleWebLink(t.Context(), "1111") + assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) + assert.Equal(t, "https://github.com/user/repo/tree/1111", wl.CommitWebLink) + + wl = sf.SubmoduleWebLink(t.Context(), "1111", "2222") + assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) + assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink) + }) + + t.Run("RelativePath", func(t *testing.T) { + sf := NewCommitSubmoduleFile("../../user/repo", "aaaa") + wl := sf.SubmoduleWebLink(t.Context()) + assert.Equal(t, "../../user/repo", wl.RepoWebLink) + assert.Equal(t, "../../user/repo/tree/aaaa", wl.CommitWebLink) + }) } diff --git a/web_src/css/repo/home-file-list.css b/web_src/css/repo/home-file-list.css index f2ab052a54c..6aa9e4bca3d 100644 --- a/web_src/css/repo/home-file-list.css +++ b/web_src/css/repo/home-file-list.css @@ -71,7 +71,7 @@ #repo-files-table .repo-file-cell.name .entry-name { flex-shrink: 1; - min-width: 3em; + min-width: 1ch; /* leave about one letter space when shrinking, need to fine tune the "shrinks" in this grid in the future */ } @media (max-width: 767.98px) { From 32152a0ac03e912567a9d9a243729a9ed6288255 Mon Sep 17 00:00:00 2001 From: Naxdy Date: Thu, 10 Jul 2025 19:17:56 +0200 Subject: [PATCH 14/51] Also display "recently pushed branch" alert on PR view (#35001) This commit adds the "You recently pushed to branch X" alert also to PR overview, as opposed to only the repository's home page. GitHub also shows this alert on the PR list, as well as the home page. --------- Co-authored-by: wxiaoguang Co-authored-by: Giteabot --- models/git/branch.go | 2 +- models/repo/repo.go | 6 ++ routers/web/repo/common_recentbranches.go | 73 +++++++++++++++++++ routers/web/repo/issue_list.go | 4 + routers/web/repo/view_home.go | 51 ------------- .../code/recently_pushed_new_branches.tmpl | 18 +++-- templates/repo/home.tmpl | 2 +- templates/repo/issue/list.tmpl | 2 + templates/repo/view.tmpl | 2 +- 9 files changed, 100 insertions(+), 60 deletions(-) create mode 100644 routers/web/repo/common_recentbranches.go diff --git a/models/git/branch.go b/models/git/branch.go index 07c94a8ba5b..6021e1101f6 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -472,7 +472,7 @@ type RecentlyPushedNewBranch struct { // if opts.CommitAfterUnix is 0, we will find the branches that were committed to in the last 2 hours // if opts.ListOptions is not set, we will only display top 2 latest branches. // Protected branches will be skipped since they are unlikely to be used to create new PRs. -func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, opts *FindRecentlyPushedNewBranchesOptions) ([]*RecentlyPushedNewBranch, error) { +func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, opts FindRecentlyPushedNewBranchesOptions) ([]*RecentlyPushedNewBranch, error) { if doer == nil { return []*RecentlyPushedNewBranch{}, nil } diff --git a/models/repo/repo.go b/models/repo/repo.go index 34d1bf55f65..2403b3b40ba 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -652,7 +652,13 @@ func (repo *Repository) AllowsPulls(ctx context.Context) bool { } // CanEnableEditor returns true if repository meets the requirements of web editor. +// FIXME: most CanEnableEditor calls should be replaced with CanContentChange +// And all other like CanCreateBranch / CanEnablePulls should also be updated func (repo *Repository) CanEnableEditor() bool { + return repo.CanContentChange() +} + +func (repo *Repository) CanContentChange() bool { return !repo.IsMirror && !repo.IsArchived } diff --git a/routers/web/repo/common_recentbranches.go b/routers/web/repo/common_recentbranches.go new file mode 100644 index 00000000000..c2083dec738 --- /dev/null +++ b/routers/web/repo/common_recentbranches.go @@ -0,0 +1,73 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + git_model "code.gitea.io/gitea/models/git" + access_model "code.gitea.io/gitea/models/perm/access" + unit_model "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/services/context" + repo_service "code.gitea.io/gitea/services/repository" +) + +type RecentBranchesPromptDataStruct struct { + RecentlyPushedNewBranches []*git_model.RecentlyPushedNewBranch +} + +func prepareRecentlyPushedNewBranches(ctx *context.Context) { + if ctx.Doer == nil { + return + } + if err := ctx.Repo.Repository.GetBaseRepo(ctx); err != nil { + log.Error("GetBaseRepo: %v", err) + return + } + + opts := git_model.FindRecentlyPushedNewBranchesOptions{ + Repo: ctx.Repo.Repository, + BaseRepo: ctx.Repo.Repository, + } + if ctx.Repo.Repository.IsFork { + opts.BaseRepo = ctx.Repo.Repository.BaseRepo + } + + baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, opts.BaseRepo, ctx.Doer) + if err != nil { + log.Error("GetUserRepoPermission: %v", err) + return + } + if !opts.Repo.CanContentChange() || !opts.BaseRepo.CanContentChange() { + return + } + if !opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) || !baseRepoPerm.CanRead(unit_model.TypePullRequests) { + return + } + + var finalBranches []*git_model.RecentlyPushedNewBranch + branches, err := git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts) + if err != nil { + log.Error("FindRecentlyPushedNewBranches failed: %v", err) + return + } + + for _, branch := range branches { + divergingInfo, err := repo_service.GetBranchDivergingInfo(ctx, + branch.BranchRepo, branch.BranchName, // "base" repo for diverging info + opts.BaseRepo, opts.BaseRepo.DefaultBranch, // "head" repo for diverging info + ) + if err != nil { + log.Error("GetBranchDivergingInfo failed: %v", err) + continue + } + branchRepoHasNewCommits := divergingInfo.BaseHasNewCommits + baseRepoCommitsBehind := divergingInfo.HeadCommitsBehind + if branchRepoHasNewCommits || baseRepoCommitsBehind > 0 { + finalBranches = append(finalBranches, branch) + } + } + if len(finalBranches) > 0 { + ctx.Data["RecentBranchesPromptData"] = RecentBranchesPromptDataStruct{finalBranches} + } +} diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index b55f4bcc90f..fd34422cfcc 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -767,6 +767,10 @@ func Issues(ctx *context.Context) { } ctx.Data["Title"] = ctx.Tr("repo.pulls") ctx.Data["PageIsPullList"] = true + prepareRecentlyPushedNewBranches(ctx) + if ctx.Written() { + return + } } else { MustEnableIssues(ctx) if ctx.Written() { diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index c7396d44e33..5482780c980 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -15,7 +15,6 @@ import ( "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" - access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -196,56 +195,6 @@ func prepareUpstreamDivergingInfo(ctx *context.Context) { ctx.Data["UpstreamDivergingInfo"] = upstreamDivergingInfo } -func prepareRecentlyPushedNewBranches(ctx *context.Context) { - if ctx.Doer != nil { - if err := ctx.Repo.Repository.GetBaseRepo(ctx); err != nil { - ctx.ServerError("GetBaseRepo", err) - return - } - - opts := &git_model.FindRecentlyPushedNewBranchesOptions{ - Repo: ctx.Repo.Repository, - BaseRepo: ctx.Repo.Repository, - } - if ctx.Repo.Repository.IsFork { - opts.BaseRepo = ctx.Repo.Repository.BaseRepo - } - - baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, opts.BaseRepo, ctx.Doer) - if err != nil { - ctx.ServerError("GetUserRepoPermission", err) - return - } - - if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror && - opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) && - baseRepoPerm.CanRead(unit_model.TypePullRequests) { - var finalBranches []*git_model.RecentlyPushedNewBranch - branches, err := git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts) - if err != nil { - log.Error("FindRecentlyPushedNewBranches failed: %v", err) - } - - for _, branch := range branches { - divergingInfo, err := repo_service.GetBranchDivergingInfo(ctx, - branch.BranchRepo, branch.BranchName, // "base" repo for diverging info - opts.BaseRepo, opts.BaseRepo.DefaultBranch, // "head" repo for diverging info - ) - if err != nil { - log.Error("GetBranchDivergingInfo failed: %v", err) - continue - } - branchRepoHasNewCommits := divergingInfo.BaseHasNewCommits - baseRepoCommitsBehind := divergingInfo.HeadCommitsBehind - if branchRepoHasNewCommits || baseRepoCommitsBehind > 0 { - finalBranches = append(finalBranches, branch) - } - } - ctx.Data["RecentlyPushedNewBranches"] = finalBranches - } - } -} - func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status repo_model.RepositoryStatus) { if ctx.Repo.Repository.IsEmpty == empty && ctx.Repo.Repository.Status == status { return diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl index 4a864ba7567..8569bd6c13d 100644 --- a/templates/repo/code/recently_pushed_new_branches.tmpl +++ b/templates/repo/code/recently_pushed_new_branches.tmpl @@ -1,12 +1,18 @@ -{{range .RecentlyPushedNewBranches}} -
-
- {{$timeSince := DateUtils.TimeSince .CommitTime}} - {{$branchLink := HTMLFormat `%s` .BranchLink .BranchDisplayName}} +{{/* Template Attributes: +* RecentBranchesPromptData +*/}} +{{$data := .RecentBranchesPromptData}} +{{if $data}} + {{range $recentBranch := $data.RecentlyPushedNewBranches}} +
+
+ {{$timeSince := DateUtils.TimeSince $recentBranch.CommitTime}} + {{$branchLink := HTMLFormat `%s` $recentBranch.BranchLink .BranchDisplayName}} {{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" $branchLink $timeSince}}
- + {{ctx.Locale.Tr "repo.pulls.compare_changes"}}
+ {{end}} {{end}} diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index f86b90502df..2a6c0d2fe51 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -15,7 +15,7 @@
{{end}} - {{template "repo/code/recently_pushed_new_branches" .}} + {{template "repo/code/recently_pushed_new_branches" dict "RecentBranchesPromptData" .RecentBranchesPromptData}}
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl index 0ab761e0385..1fe220e1b8b 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -4,6 +4,8 @@
{{template "base/alert" .}} + {{template "repo/code/recently_pushed_new_branches" dict "RecentBranchesPromptData" .RecentBranchesPromptData}} + {{if .PinnedIssues}}
{{range .PinnedIssues}} diff --git a/templates/repo/view.tmpl b/templates/repo/view.tmpl index 85d09d03a18..f99fe2f57ab 100644 --- a/templates/repo/view.tmpl +++ b/templates/repo/view.tmpl @@ -14,7 +14,7 @@
{{end}} - {{template "repo/code/recently_pushed_new_branches" .}} + {{template "repo/code/recently_pushed_new_branches" dict "RecentBranchesPromptData" .RecentBranchesPromptData}}
From 6ab6d4e17ff61b4eaf1a88e3d167f0d504b8390e Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 11 Jul 2025 02:08:40 +0800 Subject: [PATCH 15/51] Support base64-encoded agit push options (#35037) --- services/agit/agit.go | 18 ++++++++++++++++-- services/agit/agit_test.go | 16 ++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 services/agit/agit_test.go diff --git a/services/agit/agit.go b/services/agit/agit.go index b27dfc8ecd3..0ea8bbfa5dc 100644 --- a/services/agit/agit.go +++ b/services/agit/agit.go @@ -5,6 +5,7 @@ package agit import ( "context" + "encoding/base64" "fmt" "os" "strings" @@ -18,17 +19,30 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" pull_service "code.gitea.io/gitea/services/pull" ) +func parseAgitPushOptionValue(s string) string { + if base64Value, ok := strings.CutPrefix(s, "{base64}"); ok { + decoded, err := base64.StdEncoding.DecodeString(base64Value) + return util.Iif(err == nil, string(decoded), s) + } + return s +} + // ProcReceive handle proc receive work func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) { results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs)) forcePush := opts.GitPushOptions.Bool(private.GitPushOptionForcePush) topicBranch := opts.GitPushOptions["topic"] - title := strings.TrimSpace(opts.GitPushOptions["title"]) - description := strings.TrimSpace(opts.GitPushOptions["description"]) + + // some options are base64-encoded with "{base64}" prefix if they contain new lines + // other agit push options like "issue", "reviewer" and "cc" are not supported + title := parseAgitPushOptionValue(opts.GitPushOptions["title"]) + description := parseAgitPushOptionValue(opts.GitPushOptions["description"]) + objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) userName := strings.ToLower(opts.UserName) diff --git a/services/agit/agit_test.go b/services/agit/agit_test.go new file mode 100644 index 00000000000..feaf7dca9ba --- /dev/null +++ b/services/agit/agit_test.go @@ -0,0 +1,16 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package agit + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseAgitPushOptionValue(t *testing.T) { + assert.Equal(t, "a", parseAgitPushOptionValue("a")) + assert.Equal(t, "a", parseAgitPushOptionValue("{base64}YQ==")) + assert.Equal(t, "{base64}invalid value", parseAgitPushOptionValue("{base64}invalid value")) +} From a5a3d9b10177e0266a0877936c517f54102b3f91 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 11 Jul 2025 02:35:59 +0800 Subject: [PATCH 16/51] Refactor OpenIDConnect to support SSH/FullName sync (#34978) * Fix #26585 * Fix #28327 * Fix #34932 --- cmd/admin_auth_oauth.go | 16 +++ cmd/admin_auth_oauth_test.go | 64 +++++----- models/asymkey/ssh_key.go | 4 +- models/auth/oauth2.go | 4 +- models/auth/source.go | 2 +- modules/setting/oauth2.go | 2 +- options/locale/locale_en-US.ini | 2 + routers/web/admin/auths.go | 3 + routers/web/auth/2fa.go | 3 +- routers/web/auth/auth.go | 27 ++--- routers/web/auth/linkaccount.go | 66 +++++------ routers/web/auth/oauth.go | 56 +++++---- routers/web/auth/oauth_signin_sync.go | 88 ++++++++++++++ routers/web/auth/openid.go | 2 +- routers/web/auth/webauthn.go | 5 +- services/auth/source/oauth2/providers.go | 1 + services/auth/source/oauth2/providers_base.go | 7 ++ .../auth/source/oauth2/providers_openid.go | 4 + services/auth/source/oauth2/source.go | 3 + services/externalaccount/link.go | 30 ----- services/externalaccount/user.go | 26 ++--- services/forms/auth_form.go | 101 +++++++++------- templates/admin/auth/edit.tmpl | 15 ++- templates/admin/auth/source/oauth.tmpl | 16 ++- tests/integration/oauth_test.go | 110 ++++++++++++++++++ tests/integration/signin_test.go | 5 +- web_src/js/features/admin/common.ts | 3 + 27 files changed, 459 insertions(+), 206 deletions(-) create mode 100644 routers/web/auth/oauth_signin_sync.go delete mode 100644 services/externalaccount/link.go diff --git a/cmd/admin_auth_oauth.go b/cmd/admin_auth_oauth.go index d1aa753500a..8848c94fc51 100644 --- a/cmd/admin_auth_oauth.go +++ b/cmd/admin_auth_oauth.go @@ -87,6 +87,14 @@ func oauthCLIFlags() []cli.Flag { Value: nil, Usage: "Scopes to request when to authenticate against this OAuth2 source", }, + &cli.StringFlag{ + Name: "ssh-public-key-claim-name", + Usage: "Claim name that provides SSH public keys", + }, + &cli.StringFlag{ + Name: "full-name-claim-name", + Usage: "Claim name that provides user's full name", + }, &cli.StringFlag{ Name: "required-claim-name", Value: "", @@ -177,6 +185,8 @@ func parseOAuth2Config(c *cli.Command) *oauth2.Source { RestrictedGroup: c.String("restricted-group"), GroupTeamMap: c.String("group-team-map"), GroupTeamMapRemoval: c.Bool("group-team-map-removal"), + SSHPublicKeyClaimName: c.String("ssh-public-key-claim-name"), + FullNameClaimName: c.String("full-name-claim-name"), } } @@ -268,6 +278,12 @@ func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error if c.IsSet("group-team-map-removal") { oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal") } + if c.IsSet("ssh-public-key-claim-name") { + oAuth2Config.SSHPublicKeyClaimName = c.String("ssh-public-key-claim-name") + } + if c.IsSet("full-name-claim-name") { + oAuth2Config.FullNameClaimName = c.String("full-name-claim-name") + } // update custom URL mapping customURLMapping := &oauth2.CustomURLMapping{} diff --git a/cmd/admin_auth_oauth_test.go b/cmd/admin_auth_oauth_test.go index df1bd9c1a6d..bb9da667fd1 100644 --- a/cmd/admin_auth_oauth_test.go +++ b/cmd/admin_auth_oauth_test.go @@ -88,6 +88,8 @@ func TestAddOauth(t *testing.T) { "--restricted-group", "restricted", "--group-team-map", `{"group1": [1,2]}`, "--group-team-map-removal=true", + "--ssh-public-key-claim-name", "attr_ssh_pub_key", + "--full-name-claim-name", "attr_full_name", }, source: &auth_model.Source{ Type: auth_model.OAuth2, @@ -104,15 +106,17 @@ func TestAddOauth(t *testing.T) { EmailURL: "https://example.com/email", Tenant: "some_tenant", }, - IconURL: "https://example.com/icon", - Scopes: []string{"scope1", "scope2"}, - RequiredClaimName: "claim_name", - RequiredClaimValue: "claim_value", - GroupClaimName: "group_name", - AdminGroup: "admin", - RestrictedGroup: "restricted", - GroupTeamMap: `{"group1": [1,2]}`, - GroupTeamMapRemoval: true, + IconURL: "https://example.com/icon", + Scopes: []string{"scope1", "scope2"}, + RequiredClaimName: "claim_name", + RequiredClaimValue: "claim_value", + GroupClaimName: "group_name", + AdminGroup: "admin", + RestrictedGroup: "restricted", + GroupTeamMap: `{"group1": [1,2]}`, + GroupTeamMapRemoval: true, + SSHPublicKeyClaimName: "attr_ssh_pub_key", + FullNameClaimName: "attr_full_name", }, TwoFactorPolicy: "skip", }, @@ -223,15 +227,17 @@ func TestUpdateOauth(t *testing.T) { EmailURL: "https://old.example.com/email", Tenant: "old_tenant", }, - IconURL: "https://old.example.com/icon", - Scopes: []string{"old_scope1", "old_scope2"}, - RequiredClaimName: "old_claim_name", - RequiredClaimValue: "old_claim_value", - GroupClaimName: "old_group_name", - AdminGroup: "old_admin", - RestrictedGroup: "old_restricted", - GroupTeamMap: `{"old_group1": [1,2]}`, - GroupTeamMapRemoval: true, + IconURL: "https://old.example.com/icon", + Scopes: []string{"old_scope1", "old_scope2"}, + RequiredClaimName: "old_claim_name", + RequiredClaimValue: "old_claim_value", + GroupClaimName: "old_group_name", + AdminGroup: "old_admin", + RestrictedGroup: "old_restricted", + GroupTeamMap: `{"old_group1": [1,2]}`, + GroupTeamMapRemoval: true, + SSHPublicKeyClaimName: "old_ssh_pub_key", + FullNameClaimName: "old_full_name", }, TwoFactorPolicy: "", }, @@ -257,6 +263,8 @@ func TestUpdateOauth(t *testing.T) { "--restricted-group", "restricted", "--group-team-map", `{"group1": [1,2]}`, "--group-team-map-removal=false", + "--ssh-public-key-claim-name", "new_ssh_pub_key", + "--full-name-claim-name", "new_full_name", }, authSource: &auth_model.Source{ ID: 1, @@ -274,15 +282,17 @@ func TestUpdateOauth(t *testing.T) { EmailURL: "https://example.com/email", Tenant: "new_tenant", }, - IconURL: "https://example.com/icon", - Scopes: []string{"scope1", "scope2"}, - RequiredClaimName: "claim_name", - RequiredClaimValue: "claim_value", - GroupClaimName: "group_name", - AdminGroup: "admin", - RestrictedGroup: "restricted", - GroupTeamMap: `{"group1": [1,2]}`, - GroupTeamMapRemoval: false, + IconURL: "https://example.com/icon", + Scopes: []string{"scope1", "scope2"}, + RequiredClaimName: "claim_name", + RequiredClaimValue: "claim_value", + GroupClaimName: "group_name", + AdminGroup: "admin", + RestrictedGroup: "restricted", + GroupTeamMap: `{"group1": [1,2]}`, + GroupTeamMapRemoval: false, + SSHPublicKeyClaimName: "new_ssh_pub_key", + FullNameClaimName: "new_full_name", }, TwoFactorPolicy: "skip", }, diff --git a/models/asymkey/ssh_key.go b/models/asymkey/ssh_key.go index 7a18732c327..dd94070fb9d 100644 --- a/models/asymkey/ssh_key.go +++ b/models/asymkey/ssh_key.go @@ -355,13 +355,13 @@ func AddPublicKeysBySource(ctx context.Context, usr *user_model.User, s *auth.So return sshKeysNeedUpdate } -// SynchronizePublicKeys updates a users public keys. Returns true if there are changes. +// SynchronizePublicKeys updates a user's public keys. Returns true if there are changes. func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) bool { var sshKeysNeedUpdate bool log.Trace("synchronizePublicKeys[%s]: Handling Public SSH Key synchronization for user %s", s.Name, usr.Name) - // Get Public Keys from DB with current LDAP source + // Get Public Keys from DB with the current auth source var giteaKeys []string keys, err := db.Find[PublicKey](ctx, FindPublicKeyOptions{ OwnerID: usr.ID, diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go index c2b66901164..55af4e9036e 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -612,8 +612,8 @@ func (err ErrOAuthApplicationNotFound) Unwrap() error { return util.ErrNotExist } -// GetActiveOAuth2SourceByName returns a OAuth2 AuthSource based on the given name -func GetActiveOAuth2SourceByName(ctx context.Context, name string) (*Source, error) { +// GetActiveOAuth2SourceByAuthName returns a OAuth2 AuthSource based on the given name +func GetActiveOAuth2SourceByAuthName(ctx context.Context, name string) (*Source, error) { authSource := new(Source) has, err := db.GetEngine(ctx).Where("name = ? and type = ? and is_active = ?", name, OAuth2, true).Get(authSource) if err != nil { diff --git a/models/auth/source.go b/models/auth/source.go index 7d7bc0f03c2..08cfc9615b0 100644 --- a/models/auth/source.go +++ b/models/auth/source.go @@ -334,7 +334,7 @@ func UpdateSource(ctx context.Context, source *Source) error { err = registerableSource.RegisterSource() if err != nil { - // restore original values since we cannot update the provider it self + // restore original values since we cannot update the provider itself if _, err := db.GetEngine(ctx).ID(source.ID).AllCols().Update(originalSource); err != nil { log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", err) } diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 0d3e63e0b4a..1a88f3cb082 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -12,7 +12,7 @@ import ( "code.gitea.io/gitea/modules/log" ) -// OAuth2UsernameType is enum describing the way gitea 'name' should be generated from oauth2 data +// OAuth2UsernameType is enum describing the way gitea generates its 'username' from oauth2 data type OAuth2UsernameType string const ( diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 90cc164a609..ff32c94ff93 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3251,6 +3251,8 @@ auths.oauth2_required_claim_name_helper = Set this name to restrict login from t auths.oauth2_required_claim_value = Required Claim Value auths.oauth2_required_claim_value_helper = Set this value to restrict login from this source to users with a claim with this name and value auths.oauth2_group_claim_name = Claim name providing group names for this source. (Optional) +auths.oauth2_full_name_claim_name = Full Name Claim Name. (Optional, if set, the user's full name will always be synchronized with this claim) +auths.oauth2_ssh_public_key_claim_name = SSH Public Key Claim Name auths.oauth2_admin_group = Group Claim value for administrator users. (Optional - requires claim name above) auths.oauth2_restricted_group = Group Claim value for restricted users. (Optional - requires claim name above) auths.oauth2_map_group_to_team = Map claimed groups to Organization teams. (Optional - requires claim name above) diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 0f6f31b8841..56c384b9709 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -199,6 +199,9 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source { AdminGroup: form.Oauth2AdminGroup, GroupTeamMap: form.Oauth2GroupTeamMap, GroupTeamMapRemoval: form.Oauth2GroupTeamMapRemoval, + + SSHPublicKeyClaimName: form.Oauth2SSHPublicKeyClaimName, + FullNameClaimName: form.Oauth2FullNameClaimName, } } diff --git a/routers/web/auth/2fa.go b/routers/web/auth/2fa.go index d15d33dfd45..1f087a78971 100644 --- a/routers/web/auth/2fa.go +++ b/routers/web/auth/2fa.go @@ -14,7 +14,6 @@ import ( "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" - "code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/forms" ) @@ -75,7 +74,7 @@ func TwoFactorPost(ctx *context.Context) { } if ctx.Session.Get("linkAccount") != nil { - err = externalaccount.LinkAccountFromStore(ctx, ctx.Session, u) + err = linkAccountFromContext(ctx, u) if err != nil { ctx.ServerError("UserSignIn", err) return diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 94f75f69ffe..13cd0837711 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -329,6 +329,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe "twofaUid", "twofaRemember", "linkAccount", + "linkAccountData", }, map[string]any{ session.KeyUID: u.ID, session.KeyUname: u.Name, @@ -519,7 +520,7 @@ func SignUpPost(ctx *context.Context) { Passwd: form.Password, } - if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, nil, false) { + if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, nil) { // error already handled return } @@ -530,22 +531,22 @@ func SignUpPost(ctx *context.Context) { // createAndHandleCreatedUser calls createUserInContext and // then handleUserCreated. -func createAndHandleCreatedUser(ctx *context.Context, tpl templates.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) bool { - if !createUserInContext(ctx, tpl, form, u, overwrites, gothUser, allowLink) { +func createAndHandleCreatedUser(ctx *context.Context, tpl templates.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, possibleLinkAccountData *LinkAccountData) bool { + if !createUserInContext(ctx, tpl, form, u, overwrites, possibleLinkAccountData) { return false } - return handleUserCreated(ctx, u, gothUser) + return handleUserCreated(ctx, u, possibleLinkAccountData) } // createUserInContext creates a user and handles errors within a given context. -// Optionally a template can be specified. -func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) (ok bool) { +// Optionally, a template can be specified. +func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, possibleLinkAccountData *LinkAccountData) (ok bool) { meta := &user_model.Meta{ InitialIP: ctx.RemoteAddr(), InitialUserAgent: ctx.Req.UserAgent(), } if err := user_model.CreateUser(ctx, u, meta, overwrites); err != nil { - if allowLink && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) { + if possibleLinkAccountData != nil && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) { switch setting.OAuth2Client.AccountLinking { case setting.OAuth2AccountLinkingAuto: var user *user_model.User @@ -561,15 +562,15 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, } // TODO: probably we should respect 'remember' user's choice... - linkAccount(ctx, user, *gothUser, true) + oauth2LinkAccount(ctx, user, possibleLinkAccountData, true) return false // user is already created here, all redirects are handled case setting.OAuth2AccountLinkingLogin: - showLinkingLogin(ctx, *gothUser) + showLinkingLogin(ctx, &possibleLinkAccountData.AuthSource, possibleLinkAccountData.GothUser) return false // user will be created only after linking login } } - // handle error without template + // handle error without a template if len(tpl) == 0 { ctx.ServerError("CreateUser", err) return false @@ -610,7 +611,7 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, // handleUserCreated does additional steps after a new user is created. // It auto-sets admin for the only user, updates the optional external user and // sends a confirmation email if required. -func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) { +func handleUserCreated(ctx *context.Context, u *user_model.User, possibleLinkAccountData *LinkAccountData) (ok bool) { // Auto-set admin for the only user. hasUsers, err := user_model.HasUsers(ctx) if err != nil { @@ -631,8 +632,8 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth. } // update external user information - if gothUser != nil { - if err := externalaccount.EnsureLinkExternalToUser(ctx, u, *gothUser); err != nil { + if possibleLinkAccountData != nil { + if err := externalaccount.EnsureLinkExternalToUser(ctx, possibleLinkAccountData.AuthSource.ID, u, possibleLinkAccountData.GothUser); err != nil { log.Error("EnsureLinkExternalToUser failed: %v", err) } } diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index b3c61946b91..cf1aa302c4c 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -5,7 +5,6 @@ package auth import ( "errors" - "fmt" "net/http" "strings" @@ -21,8 +20,6 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/forms" - - "github.com/markbates/goth" ) var tplLinkAccount templates.TplName = "user/auth/link_account" @@ -52,28 +49,28 @@ func LinkAccount(ctx *context.Context) { ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" - gothUser, ok := ctx.Session.Get("linkAccountGothUser").(goth.User) + linkAccountData := oauth2GetLinkAccountData(ctx) // If you'd like to quickly debug the "link account" page layout, just uncomment the blow line // Don't worry, when the below line exists, the lint won't pass: ineffectual assignment to gothUser (ineffassign) - // gothUser, ok = goth.User{Email: "invalid-email", Name: "."}, true // intentionally use invalid data to avoid pass the registration check + // linkAccountData = &LinkAccountData{authSource, gothUser} // intentionally use invalid data to avoid pass the registration check - if !ok { + if linkAccountData == nil { // no account in session, so just redirect to the login page, then the user could restart the process ctx.Redirect(setting.AppSubURL + "/user/login") return } - if missingFields, ok := gothUser.RawData["__giteaAutoRegMissingFields"].([]string); ok { - ctx.Data["AutoRegistrationFailedPrompt"] = ctx.Tr("auth.oauth_callback_unable_auto_reg", gothUser.Provider, strings.Join(missingFields, ",")) + if missingFields, ok := linkAccountData.GothUser.RawData["__giteaAutoRegMissingFields"].([]string); ok { + ctx.Data["AutoRegistrationFailedPrompt"] = ctx.Tr("auth.oauth_callback_unable_auto_reg", linkAccountData.GothUser.Provider, strings.Join(missingFields, ",")) } - uname, err := extractUserNameFromOAuth2(&gothUser) + uname, err := extractUserNameFromOAuth2(&linkAccountData.GothUser) if err != nil { ctx.ServerError("UserSignIn", err) return } - email := gothUser.Email + email := linkAccountData.GothUser.Email ctx.Data["user_name"] = uname ctx.Data["email"] = email @@ -152,8 +149,8 @@ func LinkAccountPostSignIn(ctx *context.Context) { ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" - gothUser := ctx.Session.Get("linkAccountGothUser") - if gothUser == nil { + linkAccountData := oauth2GetLinkAccountData(ctx) + if linkAccountData == nil { ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session")) return } @@ -169,11 +166,14 @@ func LinkAccountPostSignIn(ctx *context.Context) { return } - linkAccount(ctx, u, gothUser.(goth.User), signInForm.Remember) + oauth2LinkAccount(ctx, u, linkAccountData, signInForm.Remember) } -func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, remember bool) { - updateAvatarIfNeed(ctx, gothUser.AvatarURL, u) +func oauth2LinkAccount(ctx *context.Context, u *user_model.User, linkAccountData *LinkAccountData, remember bool) { + oauth2SignInSync(ctx, &linkAccountData.AuthSource, u, linkAccountData.GothUser) + if ctx.Written() { + return + } // If this user is enrolled in 2FA, we can't sign the user in just yet. // Instead, redirect them to the 2FA authentication page. @@ -185,7 +185,7 @@ func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, r return } - err = externalaccount.LinkAccountToUser(ctx, u, gothUser) + err = externalaccount.LinkAccountToUser(ctx, linkAccountData.AuthSource.ID, u, linkAccountData.GothUser) if err != nil { ctx.ServerError("UserLinkAccount", err) return @@ -243,17 +243,11 @@ func LinkAccountPostRegister(ctx *context.Context) { ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" - gothUserInterface := ctx.Session.Get("linkAccountGothUser") - if gothUserInterface == nil { + linkAccountData := oauth2GetLinkAccountData(ctx) + if linkAccountData == nil { ctx.ServerError("UserSignUp", errors.New("not in LinkAccount session")) return } - gothUser, ok := gothUserInterface.(goth.User) - if !ok { - ctx.ServerError("UserSignUp", fmt.Errorf("session linkAccountGothUser type is %t but not goth.User", gothUserInterface)) - return - } - if ctx.HasError() { ctx.HTML(http.StatusOK, tplLinkAccount) return @@ -296,31 +290,33 @@ func LinkAccountPostRegister(ctx *context.Context) { } } - authSource, err := auth.GetActiveOAuth2SourceByName(ctx, gothUser.Provider) - if err != nil { - ctx.ServerError("CreateUser", err) - return - } - u := &user_model.User{ Name: form.UserName, Email: form.Email, Passwd: form.Password, LoginType: auth.OAuth2, - LoginSource: authSource.ID, - LoginName: gothUser.UserID, + LoginSource: linkAccountData.AuthSource.ID, + LoginName: linkAccountData.GothUser.UserID, } - if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, nil, &gothUser, false) { + if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, nil, linkAccountData) { // error already handled return } - source := authSource.Cfg.(*oauth2.Source) - if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil { + source := linkAccountData.AuthSource.Cfg.(*oauth2.Source) + if err := syncGroupsToTeams(ctx, source, &linkAccountData.GothUser, u); err != nil { ctx.ServerError("SyncGroupsToTeams", err) return } handleSignIn(ctx, u, false) } + +func linkAccountFromContext(ctx *context.Context, user *user_model.User) error { + linkAccountData := oauth2GetLinkAccountData(ctx) + if linkAccountData == nil { + return errors.New("not in LinkAccount session") + } + return externalaccount.LinkAccountToUser(ctx, linkAccountData.AuthSource.ID, user, linkAccountData.GothUser) +} diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index a13b987aabe..3df2734bb62 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -20,7 +20,6 @@ import ( "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web/middleware" source_service "code.gitea.io/gitea/services/auth/source" "code.gitea.io/gitea/services/auth/source/oauth2" @@ -35,9 +34,8 @@ import ( // SignInOAuth handles the OAuth2 login buttons func SignInOAuth(ctx *context.Context) { - provider := ctx.PathParam("provider") - - authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider) + authName := ctx.PathParam("provider") + authSource, err := auth.GetActiveOAuth2SourceByAuthName(ctx, authName) if err != nil { ctx.ServerError("SignIn", err) return @@ -74,8 +72,6 @@ func SignInOAuth(ctx *context.Context) { // SignInOAuthCallback handles the callback from the given provider func SignInOAuthCallback(ctx *context.Context) { - provider := ctx.PathParam("provider") - if ctx.Req.FormValue("error") != "" { var errorKeyValues []string for k, vv := range ctx.Req.Form { @@ -88,7 +84,8 @@ func SignInOAuthCallback(ctx *context.Context) { } // first look if the provider is still active - authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider) + authName := ctx.PathParam("provider") + authSource, err := auth.GetActiveOAuth2SourceByAuthName(ctx, authName) if err != nil { ctx.ServerError("SignIn", err) return @@ -133,7 +130,7 @@ func SignInOAuthCallback(ctx *context.Context) { if u == nil { if ctx.Doer != nil { // attach user to the current signed-in user - err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser) + err = externalaccount.LinkAccountToUser(ctx, authSource.ID, ctx.Doer, gothUser) if err != nil { ctx.ServerError("UserLinkAccount", err) return @@ -174,12 +171,11 @@ func SignInOAuthCallback(ctx *context.Context) { gothUser.RawData = make(map[string]any) } gothUser.RawData["__giteaAutoRegMissingFields"] = missingFields - showLinkingLogin(ctx, gothUser) + showLinkingLogin(ctx, authSource, gothUser) return } u = &user_model.User{ Name: uname, - FullName: gothUser.Name, Email: gothUser.Email, LoginType: auth.OAuth2, LoginSource: authSource.ID, @@ -196,7 +192,11 @@ func SignInOAuthCallback(ctx *context.Context) { u.IsAdmin = isAdmin.ValueOrDefault(user_service.UpdateOptionField[bool]{FieldValue: false}).FieldValue u.IsRestricted = isRestricted.ValueOrDefault(setting.Service.DefaultUserIsRestricted) - if !createAndHandleCreatedUser(ctx, templates.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) { + linkAccountData := &LinkAccountData{*authSource, gothUser} + if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingDisabled { + linkAccountData = nil + } + if !createAndHandleCreatedUser(ctx, "", nil, u, overwriteDefault, linkAccountData) { // error already handled return } @@ -207,7 +207,7 @@ func SignInOAuthCallback(ctx *context.Context) { } } else { // no existing user is found, request attach or new account - showLinkingLogin(ctx, gothUser) + showLinkingLogin(ctx, authSource, gothUser) return } } @@ -271,9 +271,22 @@ func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *g return isAdmin, isRestricted } -func showLinkingLogin(ctx *context.Context, gothUser goth.User) { +type LinkAccountData struct { + AuthSource auth.Source + GothUser goth.User +} + +func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData { + v, ok := ctx.Session.Get("linkAccountData").(LinkAccountData) + if !ok { + return nil + } + return &v +} + +func showLinkingLogin(ctx *context.Context, authSource *auth.Source, gothUser goth.User) { if err := updateSession(ctx, nil, map[string]any{ - "linkAccountGothUser": gothUser, + "linkAccountData": LinkAccountData{*authSource, gothUser}, }); err != nil { ctx.ServerError("updateSession", err) return @@ -281,7 +294,7 @@ func showLinkingLogin(ctx *context.Context, gothUser goth.User) { ctx.Redirect(setting.AppSubURL + "/user/link_account") } -func updateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) { +func oauth2UpdateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) { if setting.OAuth2Client.UpdateAvatar && len(url) > 0 { resp, err := http.Get(url) if err == nil { @@ -299,11 +312,14 @@ func updateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) { } } -func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model.User, gothUser goth.User) { - updateAvatarIfNeed(ctx, gothUser.AvatarURL, u) +func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_model.User, gothUser goth.User) { + oauth2SignInSync(ctx, authSource, u, gothUser) + if ctx.Written() { + return + } needs2FA := false - if !source.TwoFactorShouldSkip() { + if !authSource.TwoFactorShouldSkip() { _, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { ctx.ServerError("UserSignIn", err) @@ -312,7 +328,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model needs2FA = err == nil } - oauth2Source := source.Cfg.(*oauth2.Source) + oauth2Source := authSource.Cfg.(*oauth2.Source) groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(oauth2Source.GroupTeamMap) if err != nil { ctx.ServerError("UnmarshalGroupTeamMapping", err) @@ -338,7 +354,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model } } - if err := externalaccount.EnsureLinkExternalToUser(ctx, u, gothUser); err != nil { + if err := externalaccount.EnsureLinkExternalToUser(ctx, authSource.ID, u, gothUser); err != nil { ctx.ServerError("EnsureLinkExternalToUser", err) return } diff --git a/routers/web/auth/oauth_signin_sync.go b/routers/web/auth/oauth_signin_sync.go new file mode 100644 index 00000000000..787ea9223cf --- /dev/null +++ b/routers/web/auth/oauth_signin_sync.go @@ -0,0 +1,88 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "fmt" + + asymkey_model "code.gitea.io/gitea/models/asymkey" + "code.gitea.io/gitea/models/auth" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" + asymkey_service "code.gitea.io/gitea/services/asymkey" + "code.gitea.io/gitea/services/auth/source/oauth2" + "code.gitea.io/gitea/services/context" + + "github.com/markbates/goth" +) + +func oauth2SignInSync(ctx *context.Context, authSource *auth.Source, u *user_model.User, gothUser goth.User) { + oauth2UpdateAvatarIfNeed(ctx, gothUser.AvatarURL, u) + + oauth2Source, _ := authSource.Cfg.(*oauth2.Source) + if !authSource.IsOAuth2() || oauth2Source == nil { + ctx.ServerError("oauth2SignInSync", fmt.Errorf("source %s is not an OAuth2 source", gothUser.Provider)) + return + } + + // sync full name + fullNameKey := util.IfZero(oauth2Source.FullNameClaimName, "name") + fullName, _ := gothUser.RawData[fullNameKey].(string) + fullName = util.IfZero(fullName, gothUser.Name) + + // need to update if the user has no full name set + shouldUpdateFullName := u.FullName == "" + // force to update if the attribute is set + shouldUpdateFullName = shouldUpdateFullName || oauth2Source.FullNameClaimName != "" + // only update if the full name is different + shouldUpdateFullName = shouldUpdateFullName && u.FullName != fullName + if shouldUpdateFullName { + u.FullName = fullName + if err := user_model.UpdateUserCols(ctx, u, "full_name"); err != nil { + log.Error("Unable to sync OAuth2 user full name %s: %v", gothUser.Provider, err) + } + } + + err := oauth2UpdateSSHPubIfNeed(ctx, authSource, &gothUser, u) + if err != nil { + log.Error("Unable to sync OAuth2 SSH public key %s: %v", gothUser.Provider, err) + } +} + +func oauth2SyncGetSSHKeys(source *oauth2.Source, gothUser *goth.User) ([]string, error) { + value, exists := gothUser.RawData[source.SSHPublicKeyClaimName] + if !exists { + return []string{}, nil + } + rawSlice, ok := value.([]any) + if !ok { + return nil, fmt.Errorf("invalid SSH public key value type: %T", value) + } + + sshKeys := make([]string, 0, len(rawSlice)) + for _, v := range rawSlice { + str, ok := v.(string) + if !ok { + return nil, fmt.Errorf("invalid SSH public key value item type: %T", v) + } + sshKeys = append(sshKeys, str) + } + return sshKeys, nil +} + +func oauth2UpdateSSHPubIfNeed(ctx *context.Context, authSource *auth.Source, gothUser *goth.User, user *user_model.User) error { + oauth2Source, _ := authSource.Cfg.(*oauth2.Source) + if oauth2Source == nil || oauth2Source.SSHPublicKeyClaimName == "" { + return nil + } + sshKeys, err := oauth2SyncGetSSHKeys(oauth2Source, gothUser) + if err != nil { + return err + } + if !asymkey_model.SynchronizePublicKeys(ctx, user, authSource, sshKeys) { + return nil + } + return asymkey_service.RewriteAllPublicKeys(ctx) +} diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go index 2ef4a860222..4ef4c96ccc1 100644 --- a/routers/web/auth/openid.go +++ b/routers/web/auth/openid.go @@ -361,7 +361,7 @@ func RegisterOpenIDPost(ctx *context.Context) { Email: form.Email, Passwd: password, } - if !createUserInContext(ctx, tplSignUpOID, form, u, nil, nil, false) { + if !createUserInContext(ctx, tplSignUpOID, form, u, nil, nil) { // error already handled return } diff --git a/routers/web/auth/webauthn.go b/routers/web/auth/webauthn.go index 78f6c3b58e6..dacb6be225e 100644 --- a/routers/web/auth/webauthn.go +++ b/routers/web/auth/webauthn.go @@ -15,7 +15,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" - "code.gitea.io/gitea/services/externalaccount" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" @@ -150,7 +149,7 @@ func WebAuthnPasskeyLogin(ctx *context.Context) { // Now handle account linking if that's requested if ctx.Session.Get("linkAccount") != nil { - if err := externalaccount.LinkAccountFromStore(ctx, ctx.Session, user); err != nil { + if err := linkAccountFromContext(ctx, user); err != nil { ctx.ServerError("LinkAccountFromStore", err) return } @@ -268,7 +267,7 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) { // Now handle account linking if that's requested if ctx.Session.Get("linkAccount") != nil { - if err := externalaccount.LinkAccountFromStore(ctx, ctx.Session, user); err != nil { + if err := linkAccountFromContext(ctx, user); err != nil { ctx.ServerError("LinkAccountFromStore", err) return } diff --git a/services/auth/source/oauth2/providers.go b/services/auth/source/oauth2/providers.go index f2c1bb4894d..75ed41ba668 100644 --- a/services/auth/source/oauth2/providers.go +++ b/services/auth/source/oauth2/providers.go @@ -27,6 +27,7 @@ type Provider interface { DisplayName() string IconHTML(size int) template.HTML CustomURLSettings() *CustomURLSettings + SupportSSHPublicKey() bool } // GothProviderCreator provides a function to create a goth.Provider diff --git a/services/auth/source/oauth2/providers_base.go b/services/auth/source/oauth2/providers_base.go index 9d4ab106e5c..d34597d6d98 100644 --- a/services/auth/source/oauth2/providers_base.go +++ b/services/auth/source/oauth2/providers_base.go @@ -14,6 +14,13 @@ import ( type BaseProvider struct { name string displayName string + + // TODO: maybe some providers also support SSH public keys, then they can set this to true + supportSSHPublicKey bool +} + +func (b *BaseProvider) SupportSSHPublicKey() bool { + return b.supportSSHPublicKey } // Name provides the technical name for this provider diff --git a/services/auth/source/oauth2/providers_openid.go b/services/auth/source/oauth2/providers_openid.go index 285876d5ac3..e86dc48232f 100644 --- a/services/auth/source/oauth2/providers_openid.go +++ b/services/auth/source/oauth2/providers_openid.go @@ -17,6 +17,10 @@ import ( // OpenIDProvider is a GothProvider for OpenID type OpenIDProvider struct{} +func (o *OpenIDProvider) SupportSSHPublicKey() bool { + return true +} + // Name provides the technical name for this provider func (o *OpenIDProvider) Name() string { return "openidConnect" diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go index 08837de3777..00d89b3481b 100644 --- a/services/auth/source/oauth2/source.go +++ b/services/auth/source/oauth2/source.go @@ -27,6 +27,9 @@ type Source struct { GroupTeamMap string GroupTeamMapRemoval bool RestrictedGroup string + + SSHPublicKeyClaimName string + FullNameClaimName string } // FromDB fills up an OAuth2Config from serialized format. diff --git a/services/externalaccount/link.go b/services/externalaccount/link.go deleted file mode 100644 index ab853140cb4..00000000000 --- a/services/externalaccount/link.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package externalaccount - -import ( - "context" - "errors" - - user_model "code.gitea.io/gitea/models/user" - - "github.com/markbates/goth" -) - -// Store represents a thing that stores things -type Store interface { - Get(any) any - Set(any, any) error - Release() error -} - -// LinkAccountFromStore links the provided user with a stored external user -func LinkAccountFromStore(ctx context.Context, store Store, user *user_model.User) error { - gothUser := store.Get("linkAccountGothUser") - if gothUser == nil { - return errors.New("not in LinkAccount session") - } - - return LinkAccountToUser(ctx, user, gothUser.(goth.User)) -} diff --git a/services/externalaccount/user.go b/services/externalaccount/user.go index b53e33654a2..1eddc4a5df3 100644 --- a/services/externalaccount/user.go +++ b/services/externalaccount/user.go @@ -8,7 +8,6 @@ import ( "strconv" "strings" - "code.gitea.io/gitea/models/auth" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -17,15 +16,11 @@ import ( "github.com/markbates/goth" ) -func toExternalLoginUser(ctx context.Context, user *user_model.User, gothUser goth.User) (*user_model.ExternalLoginUser, error) { - authSource, err := auth.GetActiveOAuth2SourceByName(ctx, gothUser.Provider) - if err != nil { - return nil, err - } +func toExternalLoginUser(authSourceID int64, user *user_model.User, gothUser goth.User) *user_model.ExternalLoginUser { return &user_model.ExternalLoginUser{ ExternalID: gothUser.UserID, UserID: user.ID, - LoginSourceID: authSource.ID, + LoginSourceID: authSourceID, RawData: gothUser.RawData, Provider: gothUser.Provider, Email: gothUser.Email, @@ -40,15 +35,12 @@ func toExternalLoginUser(ctx context.Context, user *user_model.User, gothUser go AccessTokenSecret: gothUser.AccessTokenSecret, RefreshToken: gothUser.RefreshToken, ExpiresAt: gothUser.ExpiresAt, - }, nil + } } // LinkAccountToUser link the gothUser to the user -func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth.User) error { - externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser) - if err != nil { - return err - } +func LinkAccountToUser(ctx context.Context, authSourceID int64, user *user_model.User, gothUser goth.User) error { + externalLoginUser := toExternalLoginUser(authSourceID, user, gothUser) if err := user_model.LinkExternalToUser(ctx, user, externalLoginUser); err != nil { return err @@ -72,12 +64,8 @@ func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth } // EnsureLinkExternalToUser link the gothUser to the user -func EnsureLinkExternalToUser(ctx context.Context, user *user_model.User, gothUser goth.User) error { - externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser) - if err != nil { - return err - } - +func EnsureLinkExternalToUser(ctx context.Context, authSourceID int64, user *user_model.User, gothUser goth.User) error { + externalLoginUser := toExternalLoginUser(authSourceID, user, gothUser) return user_model.EnsureLinkExternalToUser(ctx, externalLoginUser) } diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go index a8f97572b10..886110236c2 100644 --- a/services/forms/auth_form.go +++ b/services/forms/auth_form.go @@ -18,45 +18,54 @@ type AuthenticationForm struct { Type int `binding:"Range(2,7)"` Name string `binding:"Required;MaxSize(30)"` TwoFactorPolicy string + IsActive bool + IsSyncEnabled bool - Host string - Port int - BindDN string - BindPassword string - UserBase string - UserDN string - AttributeUsername string - AttributeName string - AttributeSurname string - AttributeMail string - AttributeSSHPublicKey string - AttributeAvatar string - AttributesInBind bool - UsePagedSearch bool - SearchPageSize int - Filter string - AdminFilter string - GroupsEnabled bool - GroupDN string - GroupFilter string - GroupMemberUID string - UserUID string - RestrictedFilter string - AllowDeactivateAll bool - IsActive bool - IsSyncEnabled bool - SMTPAuth string - SMTPHost string - SMTPPort int - AllowedDomains string - SecurityProtocol int `binding:"Range(0,2)"` - TLS bool - SkipVerify bool - HeloHostname string - DisableHelo bool - ForceSMTPS bool - PAMServiceName string - PAMEmailDomain string + // LDAP + Host string + Port int + BindDN string + BindPassword string + UserBase string + UserDN string + AttributeUsername string + AttributeName string + AttributeSurname string + AttributeMail string + AttributeSSHPublicKey string + AttributeAvatar string + AttributesInBind bool + UsePagedSearch bool + SearchPageSize int + Filter string + AdminFilter string + GroupsEnabled bool + GroupDN string + GroupFilter string + GroupMemberUID string + UserUID string + RestrictedFilter string + AllowDeactivateAll bool + GroupTeamMap string `binding:"ValidGroupTeamMap"` + GroupTeamMapRemoval bool + + // SMTP + SMTPAuth string + SMTPHost string + SMTPPort int + AllowedDomains string + SecurityProtocol int `binding:"Range(0,2)"` + TLS bool + SkipVerify bool + HeloHostname string + DisableHelo bool + ForceSMTPS bool + + // PAM + PAMServiceName string + PAMEmailDomain string + + // Oauth2 & OIDC Oauth2Provider string Oauth2Key string Oauth2Secret string @@ -76,13 +85,15 @@ type AuthenticationForm struct { Oauth2RestrictedGroup string Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"` Oauth2GroupTeamMapRemoval bool - SSPIAutoCreateUsers bool - SSPIAutoActivateUsers bool - SSPIStripDomainNames bool - SSPISeparatorReplacement string `binding:"AlphaDashDot;MaxSize(5)"` - SSPIDefaultLanguage string - GroupTeamMap string `binding:"ValidGroupTeamMap"` - GroupTeamMapRemoval bool + Oauth2SSHPublicKeyClaimName string + Oauth2FullNameClaimName string + + // SSPI + SSPIAutoCreateUsers bool + SSPIAutoActivateUsers bool + SSPIStripDomainNames bool + SSPISeparatorReplacement string `binding:"AlphaDashDot;MaxSize(5)"` + SSPIDefaultLanguage string } // Validate validates fields diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index 781f514af45..7b96b4e94fd 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -301,19 +301,30 @@
- {{range .OAuth2Providers}}{{if .CustomURLSettings}} + {{range .OAuth2Providers}} + + {{if .CustomURLSettings}} - {{end}}{{end}} + {{end}} + {{end}}
+
+ + +
+
+ + +
diff --git a/templates/admin/auth/source/oauth.tmpl b/templates/admin/auth/source/oauth.tmpl index f02c5bdf309..69590635e4b 100644 --- a/templates/admin/auth/source/oauth.tmpl +++ b/templates/admin/auth/source/oauth.tmpl @@ -63,19 +63,31 @@
- {{range .OAuth2Providers}}{{if .CustomURLSettings}} + {{range .OAuth2Providers}} + + {{if .CustomURLSettings}} - {{end}}{{end}} + {{end}} + {{end}}
+ +
+ + +
+
+ + +
diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index f8bc33c32aa..a2247801f76 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -9,9 +9,11 @@ import ( "fmt" "io" "net/http" + "net/http/httptest" "strings" "testing" + asymkey_model "code.gitea.io/gitea/models/asymkey" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" @@ -20,9 +22,13 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/oauth2_provider" "code.gitea.io/gitea/tests" + "github.com/markbates/goth" + "github.com/markbates/goth/gothic" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -931,3 +937,107 @@ func testOAuth2WellKnown(t *testing.T) { defer test.MockVariableValue(&setting.OAuth2.Enabled, false)() MakeRequest(t, NewRequest(t, "GET", urlOpenidConfiguration), http.StatusNotFound) } + +func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) { + cfg.Provider = util.IfZero(cfg.Provider, "gitea") + err := auth_model.CreateSource(db.DefaultContext, &auth_model.Source{ + Type: auth_model.OAuth2, + Name: authName, + IsActive: true, + Cfg: &cfg, + }) + require.NoError(t, err) +} + +func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + var mockServer *httptest.Server + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/.well-known/openid-configuration": + _, _ = w.Write([]byte(`{ + "issuer": "` + mockServer.URL + `", + "authorization_endpoint": "` + mockServer.URL + `/authorize", + "token_endpoint": "` + mockServer.URL + `/token", + "userinfo_endpoint": "` + mockServer.URL + `/userinfo" + }`)) + default: + http.NotFound(w, r) + } + })) + defer mockServer.Close() + + ctx := t.Context() + oauth2Source := oauth2.Source{ + Provider: "openidConnect", + ClientID: "test-client-id", + SSHPublicKeyClaimName: "sshpubkey", + FullNameClaimName: "name", + OpenIDConnectAutoDiscoveryURL: mockServer.URL + "/.well-known/openid-configuration", + } + addOAuth2Source(t, "test-oidc-source", oauth2Source) + authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(ctx, "test-oidc-source") + require.NoError(t, err) + + sshKey1 := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf" + sshKey2 := "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIE7kM1R02+4ertDKGKEDcKG0s+2vyDDcIvceJ0Gqv5f1AAAABHNzaDo=" + sshKey3 := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEHjnNEfE88W1pvBLdV3otv28x760gdmPao3lVD5uAt9" + cases := []struct { + testName string + mockFullName string + mockRawData map[string]any + expectedSSHPubKeys []string + }{ + { + testName: "Login1", + mockFullName: "FullName1", + mockRawData: map[string]any{"sshpubkey": []any{sshKey1 + " any-comment"}}, + expectedSSHPubKeys: []string{sshKey1}, + }, + { + testName: "Login2", + mockFullName: "FullName2", + mockRawData: map[string]any{"sshpubkey": []any{sshKey2 + " any-comment", sshKey3}}, + expectedSSHPubKeys: []string{sshKey2, sshKey3}, + }, + { + testName: "Login3", + mockFullName: "FullName3", + mockRawData: map[string]any{}, + expectedSSHPubKeys: []string{}, + }, + } + + session := emptyTestSession(t) + for _, c := range cases { + t.Run(c.testName, func(t *testing.T) { + defer test.MockVariableValue(&setting.OAuth2Client.Username, "")() + defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() + defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) { + return goth.User{ + Provider: authSource.Cfg.(*oauth2.Source).Provider, + UserID: "oidc-userid", + Email: "oidc-email@example.com", + RawData: c.mockRawData, + Name: c.mockFullName, + }, nil + })() + req := NewRequest(t, "GET", "/user/oauth2/test-oidc-source/callback?code=XYZ&state=XYZ") + session.MakeRequest(t, req, http.StatusSeeOther) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "oidc-userid"}) + keys, _, err := db.FindAndCount[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{ + ListOptions: db.ListOptionsAll, + OwnerID: user.ID, + LoginSourceID: authSource.ID, + }) + require.NoError(t, err) + var sshPubKeys []string + for _, key := range keys { + sshPubKeys = append(sshPubKeys, key.Content) + } + assert.ElementsMatch(t, c.expectedSSHPubKeys, sshPubKeys) + assert.Equal(t, c.mockFullName, user.FullName) + }) + } +} diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index 67af5b5877f..aa1571c163f 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -17,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers" + "code.gitea.io/gitea/routers/web/auth" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/tests" @@ -103,8 +105,9 @@ func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) { defer tests.PrepareTestEnv(t)() mockLinkAccount := func(ctx *context.Context) { + authSource := auth_model.Source{ID: 1} gothUser := goth.User{Email: "invalid-email", Name: "."} - _ = ctx.Session.Set("linkAccountGothUser", gothUser) + _ = ctx.Session.Set("linkAccountData", auth.LinkAccountData{AuthSource: authSource, GothUser: gothUser}) } t.Run("EnablePasswordSignInForm=false", func(t *testing.T) { diff --git a/web_src/js/features/admin/common.ts b/web_src/js/features/admin/common.ts index 4ed5d62eeee..dd5b1f464d4 100644 --- a/web_src/js/features/admin/common.ts +++ b/web_src/js/features/admin/common.ts @@ -102,6 +102,9 @@ function initAdminAuthentication() { break; } } + + const supportSshPublicKey = document.querySelector(`#${provider}_SupportSSHPublicKey`)?.value === 'true'; + toggleElem('.field.oauth2_ssh_public_key_claim_name', supportSshPublicKey); onOAuth2UseCustomURLChange(applyDefaultValues); } From 7a15334656cb2b9df0f79a692518ecb38674ba58 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 11 Jul 2025 03:03:36 +0800 Subject: [PATCH 17/51] Fix git commit committer parsing and add some tests (#35007) * Fix #34991 * Fix #34882 --------- Co-authored-by: wxiaoguang --- Dockerfile | 2 - Dockerfile.rootless | 2 - models/asymkey/gpg_key_commit_verification.go | 23 +----- models/user/user.go | 16 ++-- models/user/user_test.go | 5 ++ modules/git/commit.go | 6 +- services/asymkey/commit.go | 62 ++++++++-------- services/asymkey/commit_test.go | 2 +- services/git/commit.go | 7 -- templates/repo/commit_page.tmpl | 2 +- templates/repo/commits_list.tmpl | 2 +- tests/integration/repo_commits_test.go | 74 ++++++++++++------- 12 files changed, 95 insertions(+), 108 deletions(-) diff --git a/Dockerfile b/Dockerfile index c9e6a2d3db8..f852cf42355 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,7 +39,6 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \ /tmp/local/etc/s6/.s6-svscan/* \ /go/src/code.gitea.io/gitea/gitea \ /go/src/code.gitea.io/gitea/environment-to-ini -RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete FROM docker.io/library/alpine:3.22 LABEL maintainer="maintainers@gitea.io" @@ -83,4 +82,3 @@ CMD ["/usr/bin/s6-svscan", "/etc/s6"] COPY --from=build-env /tmp/local / COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea COPY --from=build-env /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini -COPY --from=build-env /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete /etc/profile.d/gitea_bash_autocomplete.sh diff --git a/Dockerfile.rootless b/Dockerfile.rootless index 558e6cf73bc..f955edc6678 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -37,7 +37,6 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \ /tmp/local/usr/local/bin/gitea \ /go/src/code.gitea.io/gitea/gitea \ /go/src/code.gitea.io/gitea/environment-to-ini -RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete FROM docker.io/library/alpine:3.22 LABEL maintainer="maintainers@gitea.io" @@ -72,7 +71,6 @@ RUN chown git:git /var/lib/gitea /etc/gitea COPY --from=build-env /tmp/local / COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini -COPY --from=build-env /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete /etc/profile.d/gitea_bash_autocomplete.sh # git:git USER 1000:1000 diff --git a/models/asymkey/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go index 39ec8936063..b85374e0735 100644 --- a/models/asymkey/gpg_key_commit_verification.go +++ b/models/asymkey/gpg_key_commit_verification.go @@ -15,25 +15,6 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/packet" ) -// __________________ ________ ____ __. -// / _____/\______ \/ _____/ | |/ _|____ ___.__. -// / \ ___ | ___/ \ ___ | <_/ __ < | | -// \ \_\ \| | \ \_\ \ | | \ ___/\___ | -// \______ /|____| \______ / |____|__ \___ > ____| -// \/ \/ \/ \/\/ -// _________ .__ __ -// \_ ___ \ ____ _____ _____ |__|/ |_ -// / \ \/ / _ \ / \ / \| \ __\ -// \ \___( <_> ) Y Y \ Y Y \ || | -// \______ /\____/|__|_| /__|_| /__||__| -// \/ \/ \/ -// ____ ____ .__ _____.__ __ .__ -// \ \ / /___________|__|/ ____\__| ____ _____ _/ |_|__| ____ ____ -// \ Y // __ \_ __ \ \ __\| |/ ___\\__ \\ __\ |/ _ \ / \ -// \ /\ ___/| | \/ || | | \ \___ / __ \| | | ( <_> ) | \ -// \___/ \___ >__| |__||__| |__|\___ >____ /__| |__|\____/|___| / -// \/ \/ \/ \/ - // This file provides functions relating commit verification // CommitVerification represents a commit validation of signature @@ -41,8 +22,8 @@ type CommitVerification struct { Verified bool Warning bool Reason string - SigningUser *user_model.User - CommittingUser *user_model.User + SigningUser *user_model.User // if Verified, then SigningUser is non-nil + CommittingUser *user_model.User // if Verified, then CommittingUser is non-nil SigningEmail string SigningKey *GPGKey SigningSSHKey *PublicKey diff --git a/models/user/user.go b/models/user/user.go index 7c871bf5751..c362cbc6d2b 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1166,12 +1166,6 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([ for _, c := range oldCommits { user := emailUserMap.GetByEmail(c.Author.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"? - if user == nil { - user = &User{ - Name: c.Author.Name, - Email: c.Author.Email, - } - } newCommits = append(newCommits, &UserCommit{ User: user, Commit: c, @@ -1195,12 +1189,14 @@ func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, erro needCheckEmails := make(container.Set[string]) needCheckUserNames := make(container.Set[string]) + noReplyAddressSuffix := "@" + strings.ToLower(setting.Service.NoReplyAddress) for _, email := range emails { - if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) { - username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress) - needCheckUserNames.Add(strings.ToLower(username)) + emailLower := strings.ToLower(email) + if noReplyUserNameLower, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok { + needCheckUserNames.Add(noReplyUserNameLower) + needCheckEmails.Add(emailLower) } else { - needCheckEmails.Add(strings.ToLower(email)) + needCheckEmails.Add(emailLower) } } diff --git a/models/user/user_test.go b/models/user/user_test.go index a2597ba3f5e..7944fc4b734 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -85,6 +85,11 @@ func TestUserEmails(t *testing.T) { testGetUserByEmail(t, c.Email, c.UID) }) } + + t.Run("NoReplyConflict", func(t *testing.T) { + setting.Service.NoReplyAddress = "example.com" + testGetUserByEmail(t, "user1-2@example.COM", 1) + }) }) } diff --git a/modules/git/commit.go b/modules/git/commit.go index ed4876e7b3e..aae40c575bc 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -22,9 +22,9 @@ import ( type Commit struct { Tree // FIXME: bad design, this field can be nil if the commit is from "last commit cache" - ID ObjectID // The ID of this commit object - Author *Signature - Committer *Signature + ID ObjectID + Author *Signature // never nil + Committer *Signature // never nil CommitMessage string Signature *CommitSignature diff --git a/services/asymkey/commit.go b/services/asymkey/commit.go index 148f51fd10e..773e7ca83c8 100644 --- a/services/asymkey/commit.go +++ b/services/asymkey/commit.go @@ -24,47 +24,43 @@ import ( // ParseCommitWithSignature check if signature is good against keystore. func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *asymkey_model.CommitVerification { - var committer *user_model.User - if c.Committer != nil { - var err error - // Find Committer account - committer, err = user_model.GetUserByEmail(ctx, c.Committer.Email) // This finds the user by primary email or activated email so commit will not be valid if email is not - if err != nil { // Skipping not user for committer - committer = &user_model.User{ - Name: c.Committer.Name, - Email: c.Committer.Email, - } - // We can expect this to often be an ErrUserNotExist. in the case - // it is not, however, it is important to log it. - if !user_model.IsErrUserNotExist(err) { - log.Error("GetUserByEmail: %v", err) - return &asymkey_model.CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.no_committer_account", - } - } + committer, err := user_model.GetUserByEmail(ctx, c.Committer.Email) + if err != nil && !user_model.IsErrUserNotExist(err) { + log.Error("GetUserByEmail: %v", err) + return &asymkey_model.CommitVerification{ + Verified: false, + Reason: "gpg.error.no_committer_account", // this error is not right, but such error should seldom happen } } - return ParseCommitWithSignatureCommitter(ctx, c, committer) } +// ParseCommitWithSignatureCommitter parses a commit's GPG or SSH signature. +// If the commit is singed by an instance key, then committer can be nil. +// If the signature exists, even if committer is nil, the returned CommittingUser will be a non-nil fake user. func ParseCommitWithSignatureCommitter(ctx context.Context, c *git.Commit, committer *user_model.User) *asymkey_model.CommitVerification { - // If no signature just report the committer + // If no signature, just report the committer if c.Signature == nil { return &asymkey_model.CommitVerification{ CommittingUser: committer, - Verified: false, // Default value - Reason: "gpg.error.not_signed_commit", // Default value + Verified: false, + Reason: "gpg.error.not_signed_commit", } } - - // If this a SSH signature handle it differently - if strings.HasPrefix(c.Signature.Signature, "-----BEGIN SSH SIGNATURE-----") { - return ParseCommitWithSSHSignature(ctx, c, committer) + // to support instance key, we need a fake committer user (not really needed, but legacy code accesses the committer without nil-check) + if committer == nil { + committer = &user_model.User{ + Name: c.Committer.Name, + Email: c.Committer.Email, + } } + if strings.HasPrefix(c.Signature.Signature, "-----BEGIN SSH SIGNATURE-----") { + return parseCommitWithSSHSignature(ctx, c, committer) + } + return parseCommitWithGPGSignature(ctx, c, committer) +} +func parseCommitWithGPGSignature(ctx context.Context, c *git.Commit, committer *user_model.User) *asymkey_model.CommitVerification { // Parsing signature sig, err := asymkey_model.ExtractSignature(c.Signature.Signature) if err != nil { // Skipping failed to extract sign @@ -165,7 +161,7 @@ func ParseCommitWithSignatureCommitter(ctx context.Context, c *git.Commit, commi } if err := gpgSettings.LoadPublicKeyContent(); err != nil { log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err) - } else if commitVerification := VerifyWithGPGSettings(ctx, &gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { + } else if commitVerification := verifyWithGPGSettings(ctx, &gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { if commitVerification.Reason == asymkey_model.BadSignature { defaultReason = asymkey_model.BadSignature } else { @@ -180,7 +176,7 @@ func ParseCommitWithSignatureCommitter(ctx context.Context, c *git.Commit, commi } else if defaultGPGSettings == nil { log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String()) } else if defaultGPGSettings.Sign { - if commitVerification := VerifyWithGPGSettings(ctx, defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { + if commitVerification := verifyWithGPGSettings(ctx, defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { if commitVerification.Reason == asymkey_model.BadSignature { defaultReason = asymkey_model.BadSignature } else { @@ -295,7 +291,7 @@ func HashAndVerifyForKeyID(ctx context.Context, sig *packet.Signature, payload s } } -func VerifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *user_model.User, keyID string) *asymkey_model.CommitVerification { +func verifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *user_model.User, keyID string) *asymkey_model.CommitVerification { // First try to find the key in the db if commitVerification := HashAndVerifyForKeyID(ctx, sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil { return commitVerification @@ -375,8 +371,8 @@ func verifySSHCommitVerificationByInstanceKey(c *git.Commit, committerUser, sign return verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, sshPubKey, committerUser, signerUser, committerGitEmail) } -// ParseCommitWithSSHSignature check if signature is good against keystore. -func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committerUser *user_model.User) *asymkey_model.CommitVerification { +// parseCommitWithSSHSignature check if signature is good against keystore. +func parseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committerUser *user_model.User) *asymkey_model.CommitVerification { // Now try to associate the signature with the committer, if present if committerUser.ID != 0 { keys, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{ diff --git a/services/asymkey/commit_test.go b/services/asymkey/commit_test.go index 0438209a619..6bcb6997f43 100644 --- a/services/asymkey/commit_test.go +++ b/services/asymkey/commit_test.go @@ -41,7 +41,7 @@ Initial commit with signed file Name: "User Two", Email: "user2@example.com", } - ret := ParseCommitWithSSHSignature(t.Context(), commit, committingUser) + ret := parseCommitWithSSHSignature(t.Context(), commit, committingUser) require.NotNil(t, ret) assert.True(t, ret.Verified) assert.False(t, ret.Warning) diff --git a/services/git/commit.go b/services/git/commit.go index 2e0e8a5096f..e4755ef93d7 100644 --- a/services/git/commit.go +++ b/services/git/commit.go @@ -35,13 +35,6 @@ func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository, for _, c := range oldCommits { committerUser := emailUsers.GetByEmail(c.Committer.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"? - if committerUser == nil { - committerUser = &user_model.User{ - Name: c.Committer.Name, - Email: c.Committer.Email, - } - } - signCommit := &asymkey_model.SignCommit{ UserCommit: c, Verification: asymkey_service.ParseCommitWithSignatureCommitter(ctx, c.Commit, committerUser), diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 46f641824b4..68ccf9d275f 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -147,7 +147,7 @@
{{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}} {{ctx.Locale.Tr "repo.diff.committed_by"}} - {{if ne .Verification.CommittingUser.ID 0}} + {{if and .Verification.CommittingUser .Verification.CommittingUser.ID}} {{ctx.AvatarUtils.Avatar .Verification.CommittingUser 20}} {{.Commit.Committer.Name}} {{else}} diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl index 9dae6594b9d..959f2a93985 100644 --- a/templates/repo/commits_list.tmpl +++ b/templates/repo/commits_list.tmpl @@ -16,7 +16,7 @@
{{$userName := .Author.Name}} - {{if and .User (gt .User.ID 0)}} /* User with id == 0 is a fake user from git author */ + {{if .User}} {{if and .User.FullName DefaultShowFullName}} {{$userName = .User.FullName}} {{end}} diff --git a/tests/integration/repo_commits_test.go b/tests/integration/repo_commits_test.go index 0097a7f62eb..b8f086e2b15 100644 --- a/tests/integration/repo_commits_test.go +++ b/tests/integration/repo_commits_test.go @@ -24,39 +24,59 @@ import ( func TestRepoCommits(t *testing.T) { defer tests.PrepareTestEnv(t)() - session := loginUser(t, "user2") - // Request repository commits page - req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master") - resp := session.MakeRequest(t, req, http.StatusOK) + t.Run("CommitList", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo16/commits/branch/master") + resp := session.MakeRequest(t, req, http.StatusOK) - doc := NewHTMLParser(t, resp.Body) - commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") - assert.True(t, exists) - assert.NotEmpty(t, commitURL) -} - -func Test_ReposGitCommitListNotMaster(t *testing.T) { - defer tests.PrepareTestEnv(t)() - session := loginUser(t, "user2") - req := NewRequest(t, "GET", "/user2/repo16/commits/branch/master") - resp := session.MakeRequest(t, req, http.StatusOK) - - doc := NewHTMLParser(t, resp.Body) - var commits []string - doc.doc.Find("#commits-table .commit-id-short").Each(func(i int, s *goquery.Selection) { - commitURL, _ := s.Attr("href") - commits = append(commits, path.Base(commitURL)) + var commits, userHrefs []string + doc := NewHTMLParser(t, resp.Body) + doc.doc.Find("#commits-table .commit-id-short").Each(func(i int, s *goquery.Selection) { + commits = append(commits, path.Base(s.AttrOr("href", ""))) + }) + doc.doc.Find("#commits-table .author-wrapper").Each(func(i int, s *goquery.Selection) { + userHrefs = append(userHrefs, s.AttrOr("href", "")) + }) + assert.Equal(t, []string{"69554a64c1e6030f051e5c3f94bfbd773cd6a324", "27566bd5738fc8b4e3fef3c5e72cce608537bd95", "5099b81332712fe655e34e8dd63574f503f61811"}, commits) + assert.Equal(t, []string{"/user2", "/user21", "/user2"}, userHrefs) }) - assert.Equal(t, []string{"69554a64c1e6030f051e5c3f94bfbd773cd6a324", "27566bd5738fc8b4e3fef3c5e72cce608537bd95", "5099b81332712fe655e34e8dd63574f503f61811"}, commits) - var userHrefs []string - doc.doc.Find("#commits-table .author-wrapper").Each(func(i int, s *goquery.Selection) { - userHref, _ := s.Attr("href") - userHrefs = append(userHrefs, userHref) + t.Run("LastCommit", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo16") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + commitHref := doc.doc.Find(".latest-commit .commit-id-short").AttrOr("href", "") + authorHref := doc.doc.Find(".latest-commit .author-wrapper").AttrOr("href", "") + assert.Equal(t, "/user2/repo16/commit/69554a64c1e6030f051e5c3f94bfbd773cd6a324", commitHref) + assert.Equal(t, "/user2", authorHref) + }) + + t.Run("CommitListNonExistingCommiter", func(t *testing.T) { + // check the commit list for a repository with no gitea user + // * commit 985f0301dba5e7b34be866819cd15ad3d8f508ee (branch2) + // * Author: 6543 <6543@obermui.de> + req := NewRequest(t, "GET", "/user2/repo1/commits/branch/branch2") + resp := session.MakeRequest(t, req, http.StatusOK) + + doc := NewHTMLParser(t, resp.Body) + commitHref := doc.doc.Find("#commits-table tr:first-child .commit-id-short").AttrOr("href", "") + assert.Equal(t, "/user2/repo1/commit/985f0301dba5e7b34be866819cd15ad3d8f508ee", commitHref) + authorElem := doc.doc.Find("#commits-table tr:first-child .author-wrapper") + assert.Equal(t, "6543", authorElem.Text()) + assert.Equal(t, "span", authorElem.Nodes[0].Data) + }) + + t.Run("LastCommitNonExistingCommiter", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo1/src/branch/branch2") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + commitHref := doc.doc.Find(".latest-commit .commit-id-short").AttrOr("href", "") + assert.Equal(t, "/user2/repo1/commit/985f0301dba5e7b34be866819cd15ad3d8f508ee", commitHref) + authorElem := doc.doc.Find(".latest-commit .author-wrapper") + assert.Equal(t, "6543", authorElem.Text()) + assert.Equal(t, "span", authorElem.Nodes[0].Data) }) - assert.Equal(t, []string{"/user2", "/user21", "/user2"}, userHrefs) } func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { From b46623f6a5aa8beb3613bd89abde8d77b1e66758 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 11 Jul 2025 07:17:28 +0800 Subject: [PATCH 18/51] Fix updating user visibility (#35036) Fix #35030 --------- Co-authored-by: wxiaoguang --- modules/optional/option.go | 7 +++++++ modules/optional/option_test.go | 6 ++++++ routers/api/v1/admin/user.go | 2 +- routers/api/v1/org/org.go | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/modules/optional/option.go b/modules/optional/option.go index 6075c6347e0..cbecf869873 100644 --- a/modules/optional/option.go +++ b/modules/optional/option.go @@ -28,6 +28,13 @@ func FromPtr[T any](v *T) Option[T] { return Some(*v) } +func FromMapLookup[K comparable, V any](m map[K]V, k K) Option[V] { + if v, ok := m[k]; ok { + return Some(v) + } + return None[V]() +} + func FromNonDefault[T comparable](v T) Option[T] { var zero T if v == zero { diff --git a/modules/optional/option_test.go b/modules/optional/option_test.go index f600ff5a2c7..ea80a2e3cb4 100644 --- a/modules/optional/option_test.go +++ b/modules/optional/option_test.go @@ -56,6 +56,12 @@ func TestOption(t *testing.T) { opt3 := optional.FromNonDefault(1) assert.True(t, opt3.Has()) assert.Equal(t, int(1), opt3.Value()) + + opt4 := optional.FromMapLookup(map[string]int{"a": 1}, "a") + assert.True(t, opt4.Has()) + assert.Equal(t, 1, opt4.Value()) + opt4 = optional.FromMapLookup(map[string]int{"a": 1}, "b") + assert.False(t, opt4.Has()) } func Test_ParseBool(t *testing.T) { diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 8a267cc418d..494bace5851 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -240,7 +240,7 @@ func EditUser(ctx *context.APIContext) { Description: optional.FromPtr(form.Description), IsActive: optional.FromPtr(form.Active), IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin), - Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]), + Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility), AllowGitHook: optional.FromPtr(form.AllowGitHook), AllowImportLocal: optional.FromPtr(form.AllowImportLocal), MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation), diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index 05744ba1552..cd676860658 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -391,7 +391,7 @@ func Edit(ctx *context.APIContext) { Description: optional.Some(form.Description), Website: optional.Some(form.Website), Location: optional.Some(form.Location), - Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]), + Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility), RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess), } if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil { From 56eccb49954dbb561f4360481c3e52de92080f20 Mon Sep 17 00:00:00 2001 From: NorthRealm <155140859+NorthRealm@users.noreply.github.com> Date: Fri, 11 Jul 2025 10:17:52 +0800 Subject: [PATCH 19/51] Add Notifications section in User Settings (#35008) Related: #34982 --------- Signed-off-by: NorthRealm <155140859+NorthRealm@users.noreply.github.com> Co-authored-by: wxiaoguang --- routers/web/user/setting/account.go | 31 +---------- routers/web/user/setting/notifications.go | 62 ++++++++++++++++++++++ routers/web/web.go | 6 ++- templates/user/settings/account.tmpl | 27 +--------- templates/user/settings/navbar.tmpl | 7 ++- templates/user/settings/notifications.tmpl | 34 ++++++++++++ 6 files changed, 109 insertions(+), 58 deletions(-) create mode 100644 routers/web/user/setting/notifications.go create mode 100644 templates/user/settings/notifications.tmpl diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index b124d5e1def..6b17da50e5b 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -35,7 +35,7 @@ const ( // Account renders change user's password, user's email and user suicide page func Account(ctx *context.Context) { - if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials, setting.UserFeatureDeletion) && !setting.Service.EnableNotifyMail { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials, setting.UserFeatureDeletion) { ctx.NotFound(errors.New("account setting are not allowed to be changed")) return } @@ -43,7 +43,6 @@ func Account(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings.account") ctx.Data["PageIsSettingsAccount"] = true ctx.Data["Email"] = ctx.Doer.Email - ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail loadAccountData(ctx) @@ -61,7 +60,6 @@ func AccountPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsAccount"] = true ctx.Data["Email"] = ctx.Doer.Email - ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail if ctx.HasError() { loadAccountData(ctx) @@ -112,7 +110,6 @@ func EmailPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsAccount"] = true ctx.Data["Email"] = ctx.Doer.Email - ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail // Make email address primary. if ctx.FormString("_method") == "PRIMARY" { @@ -172,30 +169,6 @@ func EmailPost(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + "/user/settings/account") return } - // Set Email Notification Preference - if ctx.FormString("_method") == "NOTIFICATION" { - preference := ctx.FormString("preference") - if !(preference == user_model.EmailNotificationsEnabled || - preference == user_model.EmailNotificationsOnMention || - preference == user_model.EmailNotificationsDisabled || - preference == user_model.EmailNotificationsAndYourOwn) { - log.Error("Email notifications preference change returned unrecognized option %s: %s", preference, ctx.Doer.Name) - ctx.ServerError("SetEmailPreference", errors.New("option unrecognized")) - return - } - opts := &user.UpdateOptions{ - EmailNotificationsPreference: optional.Some(preference), - } - if err := user.UpdateUser(ctx, ctx.Doer, opts); err != nil { - log.Error("Set Email Notifications failed: %v", err) - ctx.ServerError("UpdateUser", err) - return - } - log.Trace("Email notifications preference made %s: %s", preference, ctx.Doer.Name) - ctx.Flash.Success(ctx.Tr("settings.email_preference_set_success")) - ctx.Redirect(setting.AppSubURL + "/user/settings/account") - return - } if ctx.HasError() { loadAccountData(ctx) @@ -267,7 +240,6 @@ func DeleteAccount(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsAccount"] = true ctx.Data["Email"] = ctx.Doer.Email - ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail if _, _, err := auth.UserSignIn(ctx, ctx.Doer.Name, ctx.FormString("password")); err != nil { switch { @@ -342,7 +314,6 @@ func loadAccountData(ctx *context.Context) { emails[i] = &email } ctx.Data["Emails"] = emails - ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference ctx.Data["ActivationsPending"] = pendingActivation ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) diff --git a/routers/web/user/setting/notifications.go b/routers/web/user/setting/notifications.go new file mode 100644 index 00000000000..16e58a0481a --- /dev/null +++ b/routers/web/user/setting/notifications.go @@ -0,0 +1,62 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "errors" + "net/http" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/user" +) + +const tplSettingsNotifications templates.TplName = "user/settings/notifications" + +// Notifications render user's notifications settings +func Notifications(ctx *context.Context) { + if !setting.Service.EnableNotifyMail { + ctx.NotFound(nil) + return + } + + ctx.Data["Title"] = ctx.Tr("notifications") + ctx.Data["PageIsSettingsNotifications"] = true + ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference + + ctx.HTML(http.StatusOK, tplSettingsNotifications) +} + +// NotificationsEmailPost set user's email notification preference +func NotificationsEmailPost(ctx *context.Context) { + if !setting.Service.EnableNotifyMail { + ctx.NotFound(nil) + return + } + + preference := ctx.FormString("preference") + if !(preference == user_model.EmailNotificationsEnabled || + preference == user_model.EmailNotificationsOnMention || + preference == user_model.EmailNotificationsDisabled || + preference == user_model.EmailNotificationsAndYourOwn) { + log.Error("Email notifications preference change returned unrecognized option %s: %s", preference, ctx.Doer.Name) + ctx.ServerError("SetEmailPreference", errors.New("option unrecognized")) + return + } + opts := &user.UpdateOptions{ + EmailNotificationsPreference: optional.Some(preference), + } + if err := user.UpdateUser(ctx, ctx.Doer, opts); err != nil { + log.Error("Set Email Notifications failed: %v", err) + ctx.ServerError("UpdateUser", err) + return + } + log.Trace("Email notifications preference made %s: %s", preference, ctx.Doer.Name) + ctx.Flash.Success(ctx.Tr("settings.email_preference_set_success")) + ctx.Redirect(setting.AppSubURL + "/user/settings/notifications") +} diff --git a/routers/web/web.go b/routers/web/web.go index ddea468d17f..b9c7013f639 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -595,6 +595,10 @@ func registerWebRoutes(m *web.Router) { m.Post("/hidden_comments", user_setting.UpdateUserHiddenComments) m.Post("/theme", web.Bind(forms.UpdateThemeForm{}), user_setting.UpdateUIThemePost) }) + m.Group("/notifications", func() { + m.Get("", user_setting.Notifications) + m.Post("/email", user_setting.NotificationsEmailPost) + }) m.Group("/security", func() { m.Get("", security.Security) m.Group("/two_factor", func() { @@ -682,7 +686,7 @@ func registerWebRoutes(m *web.Router) { m.Get("", user_setting.BlockedUsers) m.Post("", web.Bind(forms.BlockUserForm{}), user_setting.BlockedUsersPost) }) - }, reqSignIn, ctxDataSet("PageIsUserSettings", true, "EnablePackages", setting.Packages.Enabled)) + }, reqSignIn, ctxDataSet("PageIsUserSettings", true, "EnablePackages", setting.Packages.Enabled, "EnableNotifyMail", setting.Service.EnableNotifyMail)) m.Group("/user", func() { m.Get("/activate", auth.Activate) diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl index 2c01c88d47c..7dbac0ebd45 100644 --- a/templates/user/settings/account.tmpl +++ b/templates/user/settings/account.tmpl @@ -35,37 +35,12 @@ {{end}}
- {{if not (and ($.UserDisabledFeatures.Contains "manage_credentials") (not $.EnableNotifyMail))}} + {{if not ($.UserDisabledFeatures.Contains "manage_credentials")}}

{{ctx.Locale.Tr "settings.manage_emails"}}

- {{if $.EnableNotifyMail}} -
-
- {{$.CsrfTokenHtml}} - -
- - -
-
- -
-
-
- {{end}} {{if not ($.UserDisabledFeatures.Contains "manage_credentials")}} {{range .Emails}}
diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl index c6c15512abd..34e089a68a9 100644 --- a/templates/user/settings/navbar.tmpl +++ b/templates/user/settings/navbar.tmpl @@ -4,11 +4,16 @@ {{ctx.Locale.Tr "settings.profile"}} - {{if not (and ($.UserDisabledFeatures.Contains "manage_credentials" "deletion") (not $.EnableNotifyMail))}} + {{if not ($.UserDisabledFeatures.Contains "manage_credentials" "deletion")}} {{ctx.Locale.Tr "settings.account"}} {{end}} + {{if $.EnableNotifyMail}} + + {{ctx.Locale.Tr "notifications"}} + + {{end}} {{ctx.Locale.Tr "settings.appearance"}} diff --git a/templates/user/settings/notifications.tmpl b/templates/user/settings/notifications.tmpl new file mode 100644 index 00000000000..4694bbb30a7 --- /dev/null +++ b/templates/user/settings/notifications.tmpl @@ -0,0 +1,34 @@ +{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings")}} +
+

+ {{ctx.Locale.Tr "notifications"}} +

+
+
+
+
+ {{$.CsrfTokenHtml}} +
+ + +
+
+ +
+
+
+
+
+
+ +{{template "user/settings/layout_footer" .}} From 1352080ef7ab8da75cba11b2c58d17b3d0e374f3 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 12 Jul 2025 06:18:41 +0800 Subject: [PATCH 20/51] Fix incorrect comment diff hunk parsing, fix github asset ID nil panic (#35046) * Fix missing the first char when parsing diff hunk header * Fix #35040 * Fix #35049 ---- Introduced in https://github.com/go-gitea/gitea/pull/12047/files#diff-de48c2f70e24ff5603180acf8b5ce9d0356ede8a45bfbf2a485707282ace6d6aR268 Before: image After: image --------- Co-authored-by: wxiaoguang --- modules/git/diff.go | 25 ++++++++++++++++++------- services/gitdiff/gitdiff.go | 7 ++++--- services/migrations/github.go | 5 ++++- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/modules/git/diff.go b/modules/git/diff.go index c4df6b80633..35d115be0e5 100644 --- a/modules/git/diff.go +++ b/modules/git/diff.go @@ -99,9 +99,9 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff return nil } -// ParseDiffHunkString parse the diffhunk content and return -func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHunk int) { - ss := strings.Split(diffhunk, "@@") +// ParseDiffHunkString parse the diff hunk content and return +func ParseDiffHunkString(diffHunk string) (leftLine, leftHunk, rightLine, rightHunk int) { + ss := strings.Split(diffHunk, "@@") ranges := strings.Split(ss[1][1:], " ") leftRange := strings.Split(ranges[0], ",") leftLine, _ = strconv.Atoi(leftRange[0][1:]) @@ -112,14 +112,19 @@ func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHu rightRange := strings.Split(ranges[1], ",") rightLine, _ = strconv.Atoi(rightRange[0]) if len(rightRange) > 1 { - righHunk, _ = strconv.Atoi(rightRange[1]) + rightHunk, _ = strconv.Atoi(rightRange[1]) } } else { - log.Debug("Parse line number failed: %v", diffhunk) + log.Debug("Parse line number failed: %v", diffHunk) rightLine = leftLine - righHunk = leftHunk + rightHunk = leftHunk } - return leftLine, leftHunk, rightLine, righHunk + if rightLine == 0 { + // FIXME: GIT-DIFF-CUT-BUG search this tag to see details + // this is only a hacky patch, the rightLine&rightHunk might still be incorrect in some cases. + rightLine++ + } + return leftLine, leftHunk, rightLine, rightHunk } // Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9] @@ -270,6 +275,12 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi oldNumOfLines++ } } + + // "git diff" outputs "@@ -1 +1,3 @@" for "OLD" => "A\nB\nC" + // FIXME: GIT-DIFF-CUT-BUG But there is a bug in CutDiffAroundLine, then the "Patch" stored in the comment model becomes "@@ -1,1 +0,4 @@" + // It may generate incorrect results for difference cases, for example: delete 2 line add 1 line, delete 2 line add 2 line etc, need to double check. + // For example: "L1\nL2" => "A\nB", then the patch shows "L2" as line 1 on the left (deleted part) + // construct the new hunk header newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@", oldBegin, oldNumOfLines, newBegin, newNumOfLines) diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 99643298765..0b6e6be82b7 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -179,7 +179,7 @@ func (d *DiffLine) GetExpandDirection() DiffLineExpandDirection { } func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo { - leftLine, leftHunk, rightLine, righHunk := git.ParseDiffHunkString(line) + leftLine, leftHunk, rightLine, rightHunk := git.ParseDiffHunkString(line) return &DiffLineSectionInfo{ Path: treePath, @@ -188,7 +188,7 @@ func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int LeftIdx: leftLine, RightIdx: rightLine, LeftHunkSize: leftHunk, - RightHunkSize: righHunk, + RightHunkSize: rightHunk, } } @@ -290,7 +290,7 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine, loc // try to find equivalent diff line. ignore, otherwise switch diffLine.Type { case DiffLineSection: - return getLineContent(diffLine.Content[1:], locale) + return getLineContent(diffLine.Content, locale) case DiffLineAdd: compareDiffLine := diffSection.GetLine(diffLine.Match) return diffSection.getDiffLineForRender(DiffLineAdd, compareDiffLine, diffLine, locale) @@ -856,6 +856,7 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact lastLeftIdx = -1 curFile.Sections = append(curFile.Sections, curSection) + // FIXME: the "-1" can't be right, these "line idx" are all 1-based, maybe there are other bugs that covers this bug. lineSectionInfo := getDiffLineSectionInfo(curFile.Name, line, leftLine-1, rightLine-1) diffLine := &DiffLine{ Type: DiffLineSection, diff --git a/services/migrations/github.go b/services/migrations/github.go index 2ce11615c6d..c6cd6ea1733 100644 --- a/services/migrations/github.go +++ b/services/migrations/github.go @@ -322,7 +322,10 @@ func (g *GithubDownloaderV3) convertGithubRelease(ctx context.Context, rel *gith httpClient := NewMigrationHTTPClient() for _, asset := range rel.Assets { - assetID := *asset.ID // Don't optimize this, for closure we need a local variable + assetID := asset.GetID() // Don't optimize this, for closure we need a local variable TODO: no need to do so in new Golang + if assetID == 0 { + continue + } r.Assets = append(r.Assets, &base.ReleaseAsset{ ID: asset.GetID(), Name: asset.GetName(), From 6090d709158a979d2279bc78dc0cef1958a83f24 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sat, 12 Jul 2025 00:38:42 +0000 Subject: [PATCH 21/51] [skip ci] Updated translations via Crowdin --- options/locale/locale_fr-FR.ini | 142 ++++++++++++++++++-------------- options/locale/locale_ga-IE.ini | 12 +++ options/locale/locale_pt-PT.ini | 14 ++++ 3 files changed, 105 insertions(+), 63 deletions(-) diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index a9e09e2f27a..0c1fd95dfdf 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -117,6 +117,7 @@ files=Fichiers error=Erreur error404=La page que vous essayez d'atteindre n'existe pas ou vous n'êtes pas autorisé à la voir. +error503=Le serveur n’a pas pu répondre à votre demande. Veuillez réessayer plus tard. go_back=Retour invalid_data=Données invalides : %v @@ -130,6 +131,7 @@ unpin=Désépingler artifacts=Artefacts expired=Expiré +confirm_delete_artifact=Êtes-vous sûr de vouloir supprimer l’artefact « %s » ? archived=Archivé @@ -169,6 +171,7 @@ internal_error_skipped=Une erreur interne est survenue, mais ignorée : %s search=Rechercher… type_tooltip=Type de recherche fuzzy=Approximative +fuzzy_tooltip=Inclure également les résultats proches de la recherche words=Mots words_tooltip=Inclure uniquement les résultats qui correspondent exactement aux mots recherchés regexp=Regexp @@ -272,7 +275,7 @@ reinstall_confirm_check_3=Vous confirmez : vous êtes absolument certain que ce err_empty_db_path=Le chemin de la base de données SQLite3 ne peut être vide. no_admin_and_disable_registration=Vous ne pouvez pas désactiver la création de nouveaux utilisateurs avant d'avoir créé un compte administrateur. err_empty_admin_password=Le mot de passe administrateur ne peut pas être vide. -err_empty_admin_email=L’adresse courriel de l'administrateur ne peut être vide. +err_empty_admin_email=Le courriel de l’administrateur ne peut être vide. err_admin_name_is_reserved=Le nom d'utilisateur de l'administrateur est invalide, le nom d'utilisateur est réservé err_admin_name_pattern_not_allowed=Le nom d'utilisateur de l'administrateur est invalide, le nom d'utilisateur est réservé err_admin_name_is_invalid=Le nom d'utilisateur de l'administrateur est invalide @@ -306,7 +309,7 @@ smtp_from_invalid=L’adresse « Envoyer le courriel sous » est invalide smtp_from_helper=Adresse courriel utilisée par Gitea. Utilisez directement votre adresse ou la forme « Nom  ». mailer_user=Utilisateur SMTP mailer_password=Mot de passe SMTP -register_confirm=Exiger la confirmation du courriel lors de l'inscription +register_confirm=Exiger la confirmation du courriel lors de l’inscription mail_notify=Activer les notifications par courriel server_service_title=Paramètres Serveur et Tierce Parties offline_mode=Activer le mode hors-ligne @@ -346,7 +349,7 @@ save_config_failed=L'enregistrement de la configuration %v a échoué invalid_admin_setting=Paramètres du compte administrateur invalides : %v invalid_log_root_path=Le répertoire des fichiers de journalisation est invalide : %v default_keep_email_private=Masquer les adresses courriels par défaut -default_keep_email_private_popup=Masquer par défaut les adresses courriels des nouveaux utilisateurs. +default_keep_email_private_popup=Masquer par défaut les courriels des nouveaux utilisateurs. default_allow_create_organization=Autoriser la création d'organisations par défaut default_allow_create_organization_popup=Permettre aux nouveaux comptes utilisateurs de créer des organisations par défaut. default_enable_timetracking=Activer le suivi de temps par défaut @@ -429,10 +432,10 @@ active_your_account=Activer votre compte account_activated=Le compte a été activé prohibit_login=Connexion interdite prohibit_login_desc=Votre compte n'autorise pas la connexion, veuillez contacter l'administrateur de votre site. -resent_limit_prompt=Désolé, vous avez récemment demandé un courriel d'activation. Veuillez réessayer dans 3 minutes. -has_unconfirmed_mail=Bonjour %s, votre adresse courriel (%s) n’a pas été confirmée. Si vous n’avez reçu aucun mail de confirmation ou souhaitez renouveler l’envoi, cliquez sur le bouton ci-dessous. -change_unconfirmed_mail_address=Si votre adresse courriel d’inscription est incorrecte, vous pouvez la modifier ici et renvoyer un nouvel courriel de confirmation. -resend_mail=Cliquez ici pour renvoyer un mail de confirmation +resent_limit_prompt=Désolé, vous avez récemment demandé un courriel d’activation. Veuillez réessayer dans 3 minutes. +has_unconfirmed_mail=Bonjour %s, votre adresse courriel %s n’a pas été confirmée. Si vous n’avez reçu aucun mail de confirmation ou souhaitez renouveler l’envoi, cliquez sur le bouton ci-dessous. +change_unconfirmed_mail_address=Si votre adresse courriel d’inscription est incorrecte, vous pouvez la modifier ici et renvoyer un nouveau courriel de confirmation. +resend_mail=Cliquez ici pour renvoyer un courriel de confirmation email_not_associate=L’adresse courriel n’est associée à aucun compte. send_reset_mail=Envoyer un courriel de récupération du compte reset_password=Récupération du compte @@ -468,8 +471,8 @@ openid_connect_desc=L'URI OpenID choisie est inconnue. Associez-le à un nouveau openid_register_title=Créer un nouveau compte openid_register_desc=L'URI OpenID choisie est inconnue. Associez-le à un nouveau compte ici. openid_signin_desc=Entrez l'URI de votre OpenID. Par exemple : alice.openid.example.org ou https://openid.example.org/alice. -disable_forgot_password_mail=La récupération du compte est désactivée car aucune adresse courriel n’est configurée. Veuillez contacter l'administrateur de votre site. -disable_forgot_password_mail_admin=La récupération du compte est disponible uniquement lorsque l’adresse courriel est configurée. Veuillez configurer l’adresse courriel pour activer la récupération du compte. +disable_forgot_password_mail=La récupération du compte est désactivée car aucun courriel n’est configuré. Veuillez contacter l’administrateur de votre site. +disable_forgot_password_mail_admin=La récupération du compte n’est possible que lorsqu’un courriel est configurée. Veuillez ajouter une adresse courriel à votre compte. email_domain_blacklisted=Vous ne pouvez pas vous enregistrer avec votre adresse courriel. authorize_application=Autoriser l'application authorize_redirect_notice=Vous serez redirigé vers %s si vous autorisez cette application. @@ -499,10 +502,11 @@ activate_account.text_2=Veuillez cliquer sur ce lien pour activer votre compte c activate_email=Veuillez vérifier votre adresse courriel activate_email.title=%s, veuillez vérifier votre adresse courriel -activate_email.text=Veuillez cliquer sur le lien suivant pour vérifier votre adresse courriel dans %s: +activate_email.text=Veuillez cliquer sur le lien suivant pour vérifier votre adresse courriel dans %s : register_notify=Bienvenue sur %s register_notify.title=%[1]s, bienvenue à %[2]s +register_notify.text_1=Voici votre courriel d’inscription pour %s ! register_notify.text_2=Vous pouvez maintenant vous connecter avec le nom d'utilisateur : %s. register_notify.text_3=Si ce compte a été créé pour vous, veuillez définir votre mot de passe d'abord. @@ -616,8 +620,8 @@ visit_rate_limit=Le taux d'appel à distance autorisé a été dépassé. org_name_been_taken=Ce nom d'organisation est déjà pris. team_name_been_taken=Le nom d'équipe est déjà pris. team_no_units_error=Autoriser l’accès à au moins une section du dépôt. -email_been_used=Cette adresse courriel est déjà utilisée. -email_invalid=Cette adresse courriel est invalide. +email_been_used=Ce courriel est déjà utilisé. +email_invalid=Ce courriel est invalide. email_domain_is_not_allowed=Le domaine %s du courriel utilisateur entre en conflit avec EMAIL_DOMAIN_ALLOWLIST ou EMAIL_DOMAIN_BLOCKLIST. Veuillez vous assurer que votre opération est attendue. openid_been_used=Adresse OpenID "%s" déjà utilisée. username_password_incorrect=Identifiant ou mot de passe invalide. @@ -675,7 +679,7 @@ unfollow=Ne plus suivre user_bio=Biographie disabled_public_activity=Cet utilisateur a désactivé la visibilité publique de l'activité. email_visibility.limited=Votre adresse courriel est visible pour tous les utilisateurs authentifiés -email_visibility.private=Votre adresse courriel n'est visible que pour vous et les administrateurs +email_visibility.private=Votre adresse courriel n’est visible que pour vous et les administrateurs show_on_map=Afficher ce lieu sur une carte settings=Paramètres utilisateur @@ -795,36 +799,36 @@ emails=Adresses courriels manage_emails=Gérer les adresses courriels manage_themes=Sélectionner le thème par défaut manage_openid=Gérer les adresses OpenID -email_desc=Votre adresse courriel principale sera utilisée pour les notifications, la récupération de mot de passe et, à condition qu'elle ne soit pas cachée, les opérations Git basées sur le Web. +email_desc=Votre courriel principal sera utilisé pour les notifications, la récupération de mot de passe et, à condition qu’il ne soit pas caché, les opérations Git basées sur le Web. theme_desc=Ce sera votre thème par défaut sur le site. theme_colorblindness_help=Support du thème daltonien theme_colorblindness_prompt=Gitea fournit depuis peu des thèmes daltonien basé sur un spectre coloré réduit. Encore en développement, de futures améliorations devraient enrichir les fichiers de thèmes CSS. primary=Principale activated=Activé requires_activation=Nécessite une activation -primary_email=Faire de cette adresse votre adresse principale +primary_email=Rendre Principal activate_email=Envoyer l’activation activations_pending=Activations en attente -can_not_add_email_activations_pending=Il y a une activation en attente, réessayez dans quelques minutes si vous souhaitez ajouter un nouvel e-mail. +can_not_add_email_activations_pending=Il y a une activation en attente, réessayez dans quelques minutes si vous souhaitez ajouter un nouveau courriel. delete_email=Exclure -email_deletion=Supprimer l'adresse e-mail -email_deletion_desc=L’adresse e-mail et les informations associées seront retirées de votre compte. Les révisions Git effectuées par cette adresse resteront inchangées. Continuer ? -email_deletion_success=L'adresse e-mail a été supprimée. +email_deletion=Supprimer l’adresse courriel +email_deletion_desc=Le courriel et les informations associées seront retirées de votre compte. Les révisions Git effectuées par cette adresse resteront inchangées. Continuer ? +email_deletion_success=L’adresse courriel a été supprimée. theme_update_success=Votre thème a été mis à jour. theme_update_error=Le thème sélectionné n'existe pas. openid_deletion=Supprimer l’adresse OpenID openid_deletion_desc=Supprimer cette adresse OpenID de votre compte vous empêchera de vous connecter avec. Continuer ? openid_deletion_success=L'adresse OpenID a été supprimée. -add_new_email=Ajouter une nouvelle adresse e-mail +add_new_email=Ajouter un nouveau courriel add_new_openid=Ajouter une nouvelle URI OpenID -add_email=Ajouter une adresse e-mail +add_email=Ajouter un courriel add_openid=Ajouter une URI OpenID -add_email_confirmation_sent=Un e-mail de confirmation a été envoyé à "%s". Veuillez vérifier votre boîte de réception dans les %s suivants pour confirmer votre adresse e-mail. -add_email_success=La nouvelle adresse e-mail a été ajoutée. -email_preference_set_success=L'e-mail de préférence a été défini avec succès. +add_email_confirmation_sent=Un courriel de confirmation a été envoyé à « %s ». Veuillez vérifier votre boîte de réception dans les %s suivants pour confirmer votre adresse. +add_email_success=La nouvelle adresse a été ajoutée. +email_preference_set_success=Le courriel de préférence a été défini avec succès. add_openid_success=La nouvelle adresse OpenID a été ajoutée. -keep_email_private=Cacher l'adresse e-mail -keep_email_private_popup=Ceci masquera votre adresse e-mail de votre profil, de vos demandes d’ajout et des fichiers modifiés depuis l'interface Web. Les révisions déjà soumises ne seront pas modifiés. Utilisez %s dans les révisions pour les associer à votre compte. +keep_email_private=Cacher l’adresse e-mail +keep_email_private_popup=Ceci masquera votre adresse courriel de votre profil, de vos demandes d’ajout et des fichiers modifiés depuis l’interface Web. Les révisions déjà soumises ne seront pas modifiés. Utilisez %s dans les révisions pour les associer à votre compte. openid_desc=OpenID vous permet de confier l'authentification à une tierce partie. manage_ssh_keys=Gérer les clés SSH @@ -845,9 +849,9 @@ ssh_key_been_used=Cette clé SSH a déjà été ajoutée au serveur. ssh_key_name_used=Une clé SSH avec le même nom existe déjà sur votre compte. ssh_principal_been_used=Ce principal a déjà été ajouté au serveur. gpg_key_id_used=Une clé publique GPG avec le même ID existe déjà. -gpg_no_key_email_found=Cette clé GPG ne correspond à aucune adresse e-mail activée associée à votre compte. Elle peut toujours être ajoutée si vous signez le jeton fourni. +gpg_no_key_email_found=Cette clé GPG ne correspond à aucun courriel actif associé à votre compte. Elle peut toujours être ajoutée si vous signez le jeton fourni. gpg_key_matched_identities=Identités correspondantes : -gpg_key_matched_identities_long=Les identités intégrées dans cette clé correspondent aux adresses e-mail activées suivantes pour cet utilisateur. Les révisions correspondant à ces adresses e-mail peuvent être vérifiés avec cette clé. +gpg_key_matched_identities_long=Les identités intégrées dans cette clé correspondent aux courriels actifs suivants pour cet utilisateur. Les révisions correspondant à ces courriels peuvent être vérifiés avec cette clé. gpg_key_verified=Clé vérifiée gpg_key_verified_long=Cette clé a été vérifiée à l’aide d’un jeton et peut dorénavant être utilisée pour authentifier vos révisions lorsqu’elles contiennent l’un de vos courriels actifs ou des identités associées à cette clé. gpg_key_verify=Vérifier @@ -859,7 +863,7 @@ gpg_token_signature=Signature GPG renforcée key_signature_gpg_placeholder=Commence par '-----BEGIN PGP SIGNATURE-----' verify_gpg_key_success=La clé GPG "%s" a été vérifiée. ssh_key_verified=Clé vérifiée -ssh_key_verified_long=La clé a été vérifiée avec un jeton et peut dorénavant être utilisée pour vérifier les révisions comportant l'une des adresses e-mails activées de cet utilisateur. +ssh_key_verified_long=La clé a été vérifiée avec un jeton et peut dorénavant être utilisée pour vérifier les révisions comportant l’un des courriels actifs de cet utilisateur. ssh_key_verify=Vérifier ssh_invalid_token_signature=La clé SSH, la signature ou le jeton fournis ne correspondent pas ou le jeton est périmé. ssh_token_required=Vous devez fournir une signature pour le jeton ci-dessous @@ -899,7 +903,7 @@ principal_state_desc=Ce Principal a été utilisé au cours des 7 derniers jours show_openid=Afficher sur le profil hide_openid=Masquer du profil ssh_disabled=SSH désactivé -ssh_signonly=SSH étant désactivé, ces clés ne servent qu'à vérifier la signature des révisions. +ssh_signonly=SSH étant désactivé, ces clés ne servent qu’à vérifier la signature des révisions. ssh_externally_managed=Cette clé SSH est gérée de manière externe pour cet utilisateur manage_social=Gérer les réseaux sociaux associés social_desc=Ces comptes sociaux peuvent être utilisés pour vous connecter à votre compte. Assurez-vous de les reconnaître tous. @@ -994,6 +998,7 @@ webauthn_alternative_tip=Vous devriez configurer une méthode d’authentificati manage_account_links=Gérer les comptes liés manage_account_links_desc=Ces comptes externes sont liés à votre compte Gitea. +account_links_not_available=Il n’y a pour l’instant pas de compte externe connecté à votre compte Gitea. link_account=Lier un Compte remove_account_link=Supprimer un compte lié remove_account_link_desc=La suppression d'un compte lié révoquera son accès à votre compte Gitea. Continuer ? @@ -1011,11 +1016,11 @@ confirm_delete_account=Confirmer la suppression delete_account_title=Supprimer cet utilisateur delete_account_desc=Êtes-vous sûr de vouloir supprimer définitivement ce compte d'utilisateur ? -email_notifications.enable=Activer les notifications par e-mail -email_notifications.onmention=N'envoyer un e-mail que si vous êtes mentionné -email_notifications.disable=Désactiver les notifications par e-mail -email_notifications.submit=Définir les préférences d'e-mail -email_notifications.andyourown=Et vos propres notifications +email_notifications.enable=Notifier par courriel +email_notifications.onmention=Seulement sur Mention +email_notifications.disable=Ne pas notifier +email_notifications.submit=Définir les préférences de courriel +email_notifications.andyourown=Inclure vos propres notifications visibility=Visibilité de l'utilisateur visibility.public=Public @@ -1030,6 +1035,8 @@ new_repo_helper=Un dépôt contient tous les fichiers d’un projet, ainsi que l owner=Propriétaire owner_helper=Certaines organisations peuvent ne pas apparaître dans la liste déroulante en raison d'une limite maximale du nombre de dépôts. repo_name=Nom du dépôt +repo_name_profile_public_hint=.profile est un dépôt spécial que vous pouvez utiliser pour ajouter un README.md à votre profil public d’organisation, visible par tous. Assurez-vous qu’il soit public et initialisez-le avec un README dans le répertoire de profil pour commencer. +repo_name_profile_private_hint=.profile-private est un dépôt spécial que vous pouvez utiliser pour ajouter un README.md à votre profil d’organisation, visible uniquement à ses membres. Assurez-vous qu’il soit privé et initialisez-le avec un README dans le répertoire de profil pour commencer. repo_name_helper=Idéalement, le nom d’un dépôt devrait être court, mémorisable et unique. Vous pouvez personnaliser votre profil ou celui de votre organisation en créant un dépôt nommé « .profile » ou « .profile-private » et contenant un README.md. repo_size=Taille du dépôt template=Modèle @@ -1051,6 +1058,7 @@ fork_branch=Branche à cloner sur la bifurcation all_branches=Toutes les branches view_all_branches=Voir toutes les branches view_all_tags=Voir toutes les étiquettes +fork_no_valid_owners=Ce dépôt ne peut pas être bifurqué car il n'y a pas de propriétaire(s) valide(s). fork.blocked_user=Impossible de bifurquer le dépôt car vous êtes bloqué par son propriétaire. use_template=Utiliser ce modèle open_with_editor=Ouvrir avec %s @@ -1079,13 +1087,13 @@ readme_helper_desc=Le README est l'endroit idéal pour décrire votre projet et auto_init=Initialiser le dépôt (avec un .gitignore, une Licence et un README.md) trust_model_helper=Choisissez, parmi les éléments suivants, les règles de confiance des signatures paraphant les révisions : trust_model_helper_collaborator=Collaborateur : ne se fier qu'aux signatures des collaborateurs du dépôt -trust_model_helper_committer=Auteur : ne se fier qu'aux signatures des auteurs de révisions +trust_model_helper_committer=Auteur : ne se fier qu’aux signatures des auteurs de révisions trust_model_helper_collaborator_committer=Collaborateur et Auteur : ne se fier qu'aux signatures des auteurs collaborant au dépôt trust_model_helper_default=Par défaut : valeur configurée par défaut pour cette instance Gitea create_repo=Créer un dépôt default_branch=Branche par défaut default_branch_label=défaut -default_branch_helper=La branche par défaut est la branche de base pour les demandes d'ajout et les révisions de code. +default_branch_helper=La branche par défaut est la branche de base pour les demandes d’ajout et les révisions de code. mirror_prune=Purger mirror_prune_desc=Supprimer les références externes obsolètes mirror_interval=Intervalle de synchronisation (les unités de temps valides sont 'h', 'm' et 's'). 0 pour désactiver la synchronisation automatique. (Intervalle minimum : %s) @@ -1306,7 +1314,7 @@ vendored=Externe generated=Générée commit_graph=Graphe des révisions commit_graph.select=Sélectionner les branches -commit_graph.hide_pr_refs=Masquer les demandes d'ajout +commit_graph.hide_pr_refs=Masquer les demandes d’ajout commit_graph.monochrome=Monochrome commit_graph.color=Couleur commit.contained_in=Cette révision appartient à : @@ -1747,6 +1755,7 @@ issues.due_date_form=aaaa-mm-jj issues.due_date_form_add=Ajouter une échéance issues.due_date_form_edit=Éditer issues.due_date_form_remove=Supprimer +issues.due_date_not_writer=Vous avez besoin d’un accès en écriture à ce dépôt pour modifier l’échéance de ses tickets. issues.due_date_not_set=Aucune échéance n'a été définie. issues.due_date_added=a ajouté l'échéance %s %s issues.due_date_modified=a modifié l'échéance de %[2]s à %[1]s %[3]s @@ -2114,6 +2123,7 @@ activity.title.releases_1=%d publication activity.title.releases_n=%d publications activity.title.releases_published_by=%s publiée par %s activity.published_release_label=Publiée +activity.no_git_activity=Il n’y a pas de révision durant cette période. activity.git_stats_exclude_merges=En excluant les fusions, activity.git_stats_author_1=%d auteur activity.git_stats_author_n=%d auteurs @@ -2147,8 +2157,8 @@ settings.public_access=Accès public settings.public_access_desc=Configurer les permissions des visiteurs publics remplaçant les valeurs par défaut de ce dépôt. settings.public_access.docs.not_set=Non défini : ne donne aucune permission supplémentaire. Les règles du dépôt et les permissions des utilisateurs font foi. settings.public_access.docs.anonymous_read=Lecture anonyme : les utilisateurs qui ne sont pas connectés peuvent consulter la ressource. -settings.public_access.docs.everyone_read=Consultation publique : tous les utilisateurs connectés peuvent consulter la ressource. Mettre les tickets et demandes d’ajouts en accès public signifie que les utilisateurs connectés peuvent en créer. -settings.public_access.docs.everyone_write=Participation publique : tous les utilisateurs connectés ont la permission d’écrire sur la ressource. Seule le Wiki supporte cette autorisation. +settings.public_access.docs.everyone_read=Consultation collective : tous les utilisateurs connectés peuvent consulter la ressource. Mettre les tickets et demandes d’ajouts en accès public signifie que les utilisateurs connectés peuvent en créer. +settings.public_access.docs.everyone_write=Participation collective : tous les utilisateurs connectés ont la permission d’écrire sur la ressource. Seule le Wiki supporte cette autorisation. settings.collaboration=Collaborateurs settings.collaboration.admin=Administrateur settings.collaboration.write=Écriture @@ -2324,6 +2334,8 @@ settings.hooks_desc=Les Webhooks font automatiquement des requêtes HTTP POST à settings.webhook_deletion=Retirer le Webhook settings.webhook_deletion_desc=Supprimer un webhook supprime ses paramètres et son historique. Continuer ? settings.webhook_deletion_success=Le webhook a été supprimé. +settings.webhook.test_delivery=Tester l’envoi +settings.webhook.test_delivery_desc=Testez ce webhook avec un faux événement. settings.webhook.test_delivery_desc_disabled=Pour tester ce webhook avec un faux événement, activez-le. settings.webhook.request=Requête settings.webhook.response=Réponse @@ -2343,6 +2355,7 @@ settings.payload_url=URL cible settings.http_method=Méthode HTTP settings.content_type=Type de contenu POST settings.secret=Secret +settings.webhook_secret_desc=Si le serveur webhook supporte l’usage de secrets, vous pouvez indiquer un secret ici en vous basant sur leur documentation. settings.slack_username=Nom d'utilisateur settings.slack_icon_url=URL de l'icône settings.slack_color=Couleur @@ -2772,6 +2785,7 @@ topic.done=Terminé topic.count_prompt=Vous ne pouvez pas sélectionner plus de 25 sujets topic.format_prompt=Les sujets doivent commencer par un caractère alphanumérique, peuvent inclure des traits d’union « - » et des points « . », et mesurer jusqu'à 35 caractères. Les lettres doivent être en minuscules. +find_file.follow_symlink=Suivre ce lien symbolique vers sa cible find_file.go_to_file=Aller au fichier find_file.no_matching=Aucun fichier correspondant trouvé @@ -2952,7 +2966,7 @@ repositories=Dépôts hooks=Déclencheurs web integrations=Intégrations authentication=Sources d'authentification -emails=Emails de l'utilisateur +emails=Courriels de l’utilisateur config=Configuration config_summary=Résumé config_settings=Paramètres @@ -3108,22 +3122,22 @@ users.list_status_filter.is_2fa_enabled=2FA Activé users.list_status_filter.not_2fa_enabled=2FA désactivé users.details=Informations de l’utilisateur -emails.email_manage_panel=Gestion des emails des utilisateurs +emails.email_manage_panel=Gestion des courriels des utilisateurs emails.primary=Principale emails.activated=Activée emails.filter_sort.email=Courriel emails.filter_sort.email_reverse=Courriel (inversé) -emails.filter_sort.name=Nom d'utilisateur -emails.filter_sort.name_reverse=Nom d'utilisateur (inverse) +emails.filter_sort.name=Nom d’utilisateur +emails.filter_sort.name_reverse=Nom d’utilisateur (inversé) emails.updated=Courriel mis à jour -emails.not_updated=Impossible de mettre à jour l’adresse courriel demandée : %v -emails.duplicate_active=Cette adresse courriel est déjà active pour un autre utilisateur. +emails.not_updated=Impossible de mettre à jour le courriel demandé : %v +emails.duplicate_active=Ce courriel est déjà attribué à un autre utilisateur. emails.change_email_header=Mettre à jour les propriétés du courriel emails.change_email_text=Êtes-vous sûr de vouloir mettre à jour cette adresse courriel ? -emails.delete=Supprimer l’e-mail -emails.delete_desc=Êtes-vous sûr de vouloir supprimer cette adresse e-mail ? -emails.deletion_success=L’adresse e-mail a été supprimée. -emails.delete_primary_email_error=Vous ne pouvez pas supprimer l’e-mail principal. +emails.delete=Supprimer le courriel +emails.delete_desc=Êtes-vous sûr de vouloir supprimer ce courriel ? +emails.deletion_success=L’adresse courriel a été supprimée. +emails.delete_primary_email_error=Vous ne pouvez pas supprimer le courriel principal. orgs.org_manage_panel=Gestion des organisations orgs.name=Nom @@ -3186,7 +3200,7 @@ auths.attribute_username=Attribut nom d'utilisateur auths.attribute_username_placeholder=Laisser vide afin d'utiliser le nom d'utilisateur spécifié dans Gitea. auths.attribute_name=Attribut prénom auths.attribute_surname=Attribut nom de famille -auths.attribute_mail=Attribut e-mail +auths.attribute_mail=Attribut courriel auths.attribute_ssh_public_key=Attribut clé SSH publique auths.attribute_avatar=Attribut de l'avatar auths.attributes_in_bind=Aller chercher les attributs dans le contexte de liaison DN @@ -3227,7 +3241,7 @@ auths.oauth2_use_custom_url=Utiliser des URLs personnalisées au lieu de l’URL auths.oauth2_tokenURL=URL du jeton auths.oauth2_authURL=URL d'autorisation auths.oauth2_profileURL=URL du profil -auths.oauth2_emailURL=URL de l'e-mail +auths.oauth2_emailURL=URL du courriel auths.skip_local_two_fa=Ignorer l’authentification à deux facteurs locale auths.skip_local_two_fa_helper=Laisser indéfini signifie que les utilisateurs locaux avec l’authentification à deux facteurs activée devront tout de même s’y soumettre pour se connecter auths.oauth2_tenant=Locataire @@ -3237,6 +3251,8 @@ auths.oauth2_required_claim_name_helper=Définissez ce nom pour restreindre la c auths.oauth2_required_claim_value=Valeur de réclamation requise auths.oauth2_required_claim_value_helper=Restreindre la connexion depuis cette source aux utilisateurs ayant réclamé cette valeur. auths.oauth2_group_claim_name=Réclamer le nom fournissant les noms de groupe pour cette source. (facultatif) +auths.oauth2_full_name_claim_name=Nom complet réclamé. (Optionnel. Si défini, le nom complet de l’utilisateur sera toujours synchronisé avec cette réclamation) +auths.oauth2_ssh_public_key_claim_name=Nom réclamé de la clé publique SSH auths.oauth2_admin_group=Valeur de réclamation de groupe pour les administrateurs. (Optionnel, nécessite un nom de réclamation) auths.oauth2_restricted_group=Valeur de réclamation de groupe pour les utilisateurs restreints. (Optionnel, nécessite un nom de réclamation) auths.oauth2_map_group_to_team=Associe les groupes réclamés avec les équipes de l'organisation. (Optionnel, nécessite un nom de réclamation) @@ -3267,7 +3283,7 @@ auths.tip.openid_connect=Utilisez l’URL de découverte OpenID « https://{ser auths.tip.twitter=Rendez-vous sur %s, créez une application et assurez-vous que l’option « Autoriser l’application à être utilisée avec Twitter Connect » est activée. auths.tip.discord=Enregistrer une nouvelle application sur %s auths.tip.gitea=Enregistrez une nouvelle application OAuth2. Le guide peut être trouvé sur %s. -auths.tip.yandex=Créez une nouvelle application sur %s. Sélectionnez les autorisations suivantes dans la section « Yandex.Passport API » : « Accès à l’adresse e-mail », « Accès à l’avatar de l’utilisateur » et « Accès au nom d’utilisateur, prénom, surnom et genre ». +auths.tip.yandex=Créez une nouvelle application sur %s. Sélectionnez les autorisations suivantes dans la section « Yandex.Passport API » : « Accès au courriel », « Accès à l’avatar de l’utilisateur » et « Accès au nom d’utilisateur, prénom, surnom et genre ». auths.tip.mastodon=Entrez une URL d'instance personnalisée pour l'instance mastodon avec laquelle vous voulez vous authentifier (ou utiliser celle par défaut) auths.edit=Mettre à jour la source d'authentification auths.activated=Cette source d'authentification est activée @@ -3328,7 +3344,7 @@ config.db_ssl_mode=SSL config.db_path=Emplacement config.service_config=Configuration du service -config.register_email_confirm=Exiger la confirmation de l'e-mail lors de l'inscription +config.register_email_confirm=Exiger la confirmation du courriel lors de l’inscription config.disable_register=Désactiver le formulaire d'inscription config.allow_only_internal_registration=Autoriser l'inscription uniquement via Gitea lui-même config.allow_only_external_registration=N'autoriser l'inscription qu'à partir des services externes @@ -3336,16 +3352,16 @@ config.enable_openid_signup=Activer l'inscription avec OpenID config.enable_openid_signin=Activer la connexion avec OpenID config.show_registration_button=Afficher le bouton d'enregistrement config.require_sign_in_view=Exiger la connexion pour afficher les pages -config.mail_notify=Activer les notifications par e-mail +config.mail_notify=Activer les notifications par courriel config.enable_captcha=Activer le CAPTCHA config.active_code_lives=Limites de Code Actif config.reset_password_code_lives=Durée d'expiration du code de récupération de compte -config.default_keep_email_private=Masquer les adresses e-mail par défaut +config.default_keep_email_private=Masquer les adresses courriels par défaut config.default_allow_create_organization=Autoriser la création d'organisations par défaut config.enable_timetracking=Activer le suivi du temps config.default_enable_timetracking=Activer le suivi de temps par défaut config.default_allow_only_contributors_to_track_time=Restreindre le suivi de temps aux contributeurs -config.no_reply_address=Domaine pour les e-mails cachés +config.no_reply_address=Domaine pour les courriels cachés config.default_visibility_organization=Visibilité par défaut des nouvelles organisations config.default_enable_dependencies=Activer les dépendances pour les tickets par défaut @@ -3367,11 +3383,11 @@ config.mailer_sendmail_path=Chemin d’accès à Sendmail config.mailer_sendmail_args=Arguments supplémentaires pour Sendmail config.mailer_sendmail_timeout=Délai d’attente de Sendmail config.mailer_use_dummy=Factice -config.test_email_placeholder=E-mail (ex: test@example.com) -config.send_test_mail=Envoyer un e-mail de test +config.test_email_placeholder=Courriel (ex : test@exemple.com) +config.send_test_mail=Envoyer un courriel de test config.send_test_mail_submit=Envoyer -config.test_mail_failed=Impossible d'envoyer un email de test à "%s" : %v -config.test_mail_sent=Un e-mail de test a été envoyé à "%s". +config.test_mail_failed=Impossible d’envoyer un courriel de test à « %s » : %v +config.test_mail_sent=Un courriel de test a été envoyé à « %s ». config.oauth_config=Configuration OAuth config.oauth_enabled=Activé @@ -3565,7 +3581,7 @@ no_subscriptions=Pas d'abonnements default_key=Signé avec la clé par défaut error.extract_sign=Impossible d'extraire la signature error.generate_hash=Impossible de générer la chaine de hachage de la révision -error.no_committer_account=Aucun compte lié à l'adresse e-mail de l'auteur +error.no_committer_account=Aucun compte lié au courriel de l’auteur error.no_gpg_keys_found=Signature inconnue de Gitea error.not_signed_commit=Révision non signée error.failed_retrieval_gpg_keys=Impossible de récupérer la clé liée au compte de l'auteur @@ -3573,7 +3589,7 @@ error.probable_bad_signature=AVERTISSEMENT ! Bien qu'il y ait une clé avec cet error.probable_bad_default_signature=AVERTISSEMENT ! Bien que la clé par défaut ait cet ID, elle ne vérifie pas cette livraison ! Cette livraison est SUSPECTE. [units] -unit=Unité +unit=Ressource error.no_unit_allowed_repo=Vous n'êtes pas autorisé à accéder à n'importe quelle section de ce dépôt. error.unit_not_allowed=Vous n'êtes pas autorisé à accéder à cette section du dépôt. diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index 14fe7fbe8fb..df09b00562b 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -117,6 +117,7 @@ files=Comhaid error=Earráid error404=Níl an leathanach atá tú ag iarraidh a bhaint amach annníl tú údaraithe chun é a fheiceáil. +error503=Níorbh fhéidir leis an bhfreastalaí d’iarratas a chomhlánú. Déan iarracht arís ar ball. go_back=Ar ais invalid_data=Sonraí neamhbhailí: %v @@ -130,6 +131,7 @@ unpin=Díphoráil artifacts=Déantáin expired=Imithe in éag +confirm_delete_artifact=An bhfuil tú cinnte gur mian leat an déantán '%s' a scriosadh? archived=Cartlann @@ -169,6 +171,7 @@ internal_error_skipped=Tharla earráid inmheánach ach éirithe as: %s search=Cuardaigh... type_tooltip=Cineál cuardaigh fuzzy=Doiléir +fuzzy_tooltip=Cuir torthaí san áireamh a mheaitseálann go dlúth leis an téarma cuardaigh words=Focail words_tooltip=Ná cuir san áireamh ach torthaí a mheaitseálann na focail téarma cuardaigh regexp=Nathanna Rialta @@ -503,6 +506,7 @@ activate_email.text=Cliceáil ar an nasc seo a leanas le do sheoladh ríomhphois register_notify=Fáilte go dtí %s register_notify.title=%[1]s, fáilte go %[2]s +register_notify.text_1=Seo do ríomhphost deimhnithe clárúcháin le haghaidh %s! register_notify.text_2=Is féidir leat logáil isteach anois trí ainm úsáideora: %s. register_notify.text_3=Má cruthaíodh an cuntas seo duit, socraigh do phasfhocal ar dtús. @@ -994,6 +998,7 @@ webauthn_alternative_tip=B'fhéidir gur mhaith leat modh fíordheimhnithe breise manage_account_links=Bainistigh Cuntais Nasctha manage_account_links_desc=Tá na cuntais sheachtracha seo nasctha le do chuntas Gitea. +account_links_not_available=Níl aon chuntais sheachtracha nasctha le do chuntas Gitea faoi láthair. link_account=Cuntas Nasc remove_account_link=Bain Cuntas Nasctha remove_account_link_desc=Ag baint cuntas nasctha, cuirfear a rochtain ar do chuntas Gitea a chúlghairm. Lean ar aghaidh? @@ -1030,6 +1035,8 @@ new_repo_helper=Tá gach comhad tionscadail i stór, lena n-áirítear stair ath owner=Úinéir owner_helper=B'fhéidir nach dtaispeánfar roinnt eagraíochtaí sa anuas mar gheall ar theorainn uasta comhaireamh stórais. repo_name=Ainm Stórais +repo_name_profile_public_hint=Is stóras speisialta é .profile ar féidir leat a úsáid chun README.md a chur le do phróifíl eagraíochta poiblí, le feiceáil ag aon duine. Déan cinnte go bhfuil sé poiblí agus cuir README sa chomhadlann próifíle chun tús a chur leis. +repo_name_profile_private_hint=Is stóras speisialta é .profile-private ar féidir leat a úsáid chun README.md a chur le próifíl bhall d'eagraíochta, nach mbeidh le feiceáil ach ag baill na heagraíochta. Déan cinnte go bhfuil sé príobháideach agus cuir README sa chomhadlann próifíle chun tús a chur leis. repo_name_helper=Úsáideann ainmneacha maith stóras focail eochair gairide, áithnid agus uathúla. D'fhéadfaí stóras darbh ainm '.profile' nó '.profile-private' a úsáid chun README.md a chur leis an bpróifíl úsáideora/eagraíochta. repo_size=Méid an Stóras template=Teimpléad @@ -1051,6 +1058,7 @@ fork_branch=Brainse le clónú chuig an bhforc all_branches=Gach brainse view_all_branches=Féach ar gach brainse view_all_tags=Féach ar gach clib +fork_no_valid_owners=Ní féidir an stóras seo a fhorcadh mar nach bhfuil aon úinéirí bailí ann. fork.blocked_user=Ní féidir an stór a fhorcáil toisc go bhfuil úinéir an stórais bac ort. use_template=Úsáid an teimpléad seo open_with_editor=Oscail le %s @@ -1747,6 +1755,7 @@ issues.due_date_form=bbbb-mm-ll issues.due_date_form_add=Cuir dáta dlite leis issues.due_date_form_edit=Cuir in eagar issues.due_date_form_remove=Bain +issues.due_date_not_writer=Ní mór duit rochtain scríbhneoireachta a fháil ar an stóras seo chun dáta dlite saincheiste a nuashonrú. issues.due_date_not_set=Níl aon dáta dlite socraithe. issues.due_date_added=cuireadh an dáta dlite %s %s issues.due_date_modified=d'athraigh an dáta dlite ó %[2]s go %[1]s %[3]s @@ -2114,6 +2123,7 @@ activity.title.releases_1=Scaoileadh %d activity.title.releases_n=Eisiúintí %d activity.title.releases_published_by=%s foilsithe ag %s activity.published_release_label=Foilsithe +activity.no_git_activity=Ní raibh aon ghníomhaíocht tiomantais ann sa tréimhse seo. activity.git_stats_exclude_merges=Gan cumaisc a áireamh, activity.git_stats_author_1=%d údar activity.git_stats_author_n=%d údair @@ -3241,6 +3251,8 @@ auths.oauth2_required_claim_name_helper=Socraigh an t-ainm seo chun logáil iste auths.oauth2_required_claim_value=Luach Éilimh Riachtanach auths.oauth2_required_claim_value_helper=Socraigh an luach seo chun logáil isteach ón bhfoinse seo a shrianadh chuig úsáideoirí a bhfuil éileamh acu leis an ainm agus an luach seo auths.oauth2_group_claim_name=Ainm éileamh ag soláthar ainmneacha grúpa don fhoinse seo (Roghnach) +auths.oauth2_full_name_claim_name=Ainm Iomlán Éilimh. (Roghnach, má shocraítear é, déanfar ainm iomlán an úsáideora a shioncrónú leis an éileamh seo i gcónaí) +auths.oauth2_ssh_public_key_claim_name=Ainm Éilimh Eochrach Phoiblí SSH auths.oauth2_admin_group=Luach Éilimh Grúpa d'úsáideoirí riarthóra. (Roghnach - teastaíonn ainm éilimh thuas) auths.oauth2_restricted_group=Luach Éilimh Grúpa d'úsáideoirí srianta. (Roghnach - teastaíonn ainm éilimh thuas) auths.oauth2_map_group_to_team=Map mhaígh grúpaí chuig foirne Eagraíochta. (Roghnach - éilíonn ainm an éilimh thuas) diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 4e42b8b4b49..c94211d860b 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -117,6 +117,7 @@ files=Ficheiros error=Erro error404=A página que pretende aceder não existe ou não tem autorização para a ver. +error503=O servidor não conseguiu concluir o seu pedido. Tente novamente mais tarde. go_back=Voltar invalid_data=Dados inválidos: %v @@ -130,6 +131,7 @@ unpin=Desafixar artifacts=Artefactos expired=Expirado +confirm_delete_artifact=Tem a certeza que quer eliminar este artefacto "%s"? archived=Arquivado @@ -169,6 +171,7 @@ internal_error_skipped=Ocorreu um erro interno mas foi ignorado: %s search=Pesquisar... type_tooltip=Tipo de pesquisa fuzzy=Aproximada +fuzzy_tooltip=Incluir também os resultados que estejam próximos do termo de pesquisa words=Palavras words_tooltip=Incluir apenas os resultados que correspondam às palavras do termo de pesquisa regexp=Regexp @@ -503,6 +506,7 @@ activate_email.text=Por favor clique na seguinte ligação para validar o seu en register_notify=Bem-vindo(a) a %s register_notify.title=%[1]s, bem-vindo(a) a %[2]s +register_notify.text_1=Este é o seu email de confirmação de registo para %s! register_notify.text_2=Agora pode iniciar a sessão com o nome de utilizador: %s. register_notify.text_3=Se esta conta foi criada para si, defina a sua senha primeiro. @@ -994,6 +998,7 @@ webauthn_alternative_tip=Poderá querer configurar um método de autenticação manage_account_links=Gerir contas vinculadas manage_account_links_desc=Estas contas externas estão vinculadas à sua conta do Gitea. +account_links_not_available=Neste momento não existem contas externas vinculadas à sua conta do Gitea. link_account=Vincular conta remove_account_link=Remover conta vinculada remove_account_link_desc=A remoção de uma conta vinculada revogará o acesso dessa conta à sua conta do Gitea. Quer continuar? @@ -1030,6 +1035,8 @@ new_repo_helper=Um repositório contém todos os ficheiros do trabalho, incluind owner=Proprietário(a) owner_helper=Algumas organizações podem não aparecer na lista suspensa devido a um limite máximo de contagem de repositórios. repo_name=Nome do repositório +repo_name_profile_public_hint=.profile é um repositório especial que pode usar para adicionar README.md ao seu perfil público da organização, visível para qualquer pessoa. Certifique-se que é público e inicialize-o com um README na pasta do perfil para começar. +repo_name_profile_private_hint=.profile-private é um repositório especial que pode usar para adicionar um README.md ao seu perfil de membro da organização, visível apenas para membros da organização. Certifique-se que é privado e inicialize-o com um README na pasta de perfil para começar. repo_name_helper=Bons nomes de repositórios usam palavras-chave curtas, memorizáveis e únicas. Um repositório chamado ".profile" ou ".profile-private" pode ser usado para adicionar um README.md ao perfil do utilizador ou da organização. repo_size=Tamanho do repositório template=Modelo @@ -1051,6 +1058,7 @@ fork_branch=Ramo a ser clonado para a derivação all_branches=Todos os ramos view_all_branches=Ver todos os ramos view_all_tags=Ver todas as etiquetas +fork_no_valid_owners=Não pode fazer uma derivação deste repositório porque não existem proprietários válidos. fork.blocked_user=Não pode derivar o repositório porque foi bloqueado/a pelo/a proprietário/a do repositório. use_template=Usar este modelo open_with_editor=Abrir com %s @@ -1747,6 +1755,7 @@ issues.due_date_form=yyyy-mm-dd issues.due_date_form_add=Adicionar data de vencimento issues.due_date_form_edit=Editar issues.due_date_form_remove=Remover +issues.due_date_not_writer=Tem que ter acesso de escrita neste repositório para poder modificar a data de vencimento de uma questão. issues.due_date_not_set=Sem data de vencimento definida. issues.due_date_added=adicionou a data de vencimento %s %s issues.due_date_modified=modificou a data de vencimento de %[2]s para %[1]s %[3]s @@ -2114,6 +2123,7 @@ activity.title.releases_1=%d lançamento activity.title.releases_n=%d Lançamentos activity.title.releases_published_by=%s publicado por %s activity.published_release_label=Publicado +activity.no_git_activity=Não houve quaisquer cometimentos feitos durante este período. activity.git_stats_exclude_merges=Excluindo integrações, activity.git_stats_author_1=%d autor activity.git_stats_author_n=%d autores @@ -2324,6 +2334,8 @@ settings.hooks_desc=Os automatismos web fazem pedidos HTTP POST automaticamente settings.webhook_deletion=Remover automatismo web settings.webhook_deletion_desc=Remover um automatismo web elimina as configurações e o histórico de entrega desse automatismo. Quer continuar? settings.webhook_deletion_success=O automatismo web foi removido. +settings.webhook.test_delivery=Testar o envio +settings.webhook.test_delivery_desc=Testar este automatismo web com um evento de envio falso. settings.webhook.test_delivery_desc_disabled=Para testar este automatismo web com um evento falso, habilite-o. settings.webhook.request=Pedido settings.webhook.response=Resposta @@ -3239,6 +3251,8 @@ auths.oauth2_required_claim_name_helper=Defina este nome para restringir o iníc auths.oauth2_required_claim_value=Valor de Reivindicação obrigatório auths.oauth2_required_claim_value_helper=Defina este valor para restringir o início de sessão desta fonte a utilizadores que tenham uma reivindicação com este nome e este valor auths.oauth2_group_claim_name=Reivindicar nome que fornece nomes de grupo para esta fonte. (Opcional) +auths.oauth2_full_name_claim_name=Nome completo reivindicado (opcional; se for definido, o nome completo do utilizador será sempre sincronizado com este reivindicado). +auths.oauth2_ssh_public_key_claim_name=Nome reivindicado da chave pública SSH auths.oauth2_admin_group=Valor da Reivindicação de Grupo para utilizadores administradores. (Opcional - exige a reivindicação de nome acima) auths.oauth2_restricted_group=Valor da Reivindicação de Grupo para utilizadores restritos. (Opcional - exige a reivindicação de nome acima) auths.oauth2_map_group_to_team=Mapear grupos reclamados em equipas da organização (opcional — requer nome de reclamação acima). From 6599efb3b1400ac06d06e1c8b68ae6037fbb7952 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 12 Jul 2025 15:13:01 +0800 Subject: [PATCH 22/51] Fix user's sign email check (#35045) Fix #21692 --- models/asymkey/ssh_key_fingerprint.go | 4 +-- models/fixtures/public_key.yml | 1 + services/asymkey/commit.go | 33 ++++--------------- services/asymkey/commit_test.go | 47 ++++++++++++++++++++++++++- 4 files changed, 55 insertions(+), 30 deletions(-) diff --git a/models/asymkey/ssh_key_fingerprint.go b/models/asymkey/ssh_key_fingerprint.go index 4dcfe1f2792..b666469ae87 100644 --- a/models/asymkey/ssh_key_fingerprint.go +++ b/models/asymkey/ssh_key_fingerprint.go @@ -13,9 +13,9 @@ import ( "xorm.io/builder" ) -// The database is used in checkKeyFingerprint however most of these functions probably belong in a module +// The database is used in checkKeyFingerprint. However, most of these functions probably belong in a module -// checkKeyFingerprint only checks if key fingerprint has been used as public key, +// checkKeyFingerprint only checks if key fingerprint has been used as a public key, // it is OK to use same key as deploy key for multiple repositories/users. func checkKeyFingerprint(ctx context.Context, fingerprint string) error { has, err := db.Exist[PublicKey](ctx, builder.Eq{"fingerprint": fingerprint}) diff --git a/models/fixtures/public_key.yml b/models/fixtures/public_key.yml index ae620ee2d19..856b0e3fb29 100644 --- a/models/fixtures/public_key.yml +++ b/models/fixtures/public_key.yml @@ -9,3 +9,4 @@ created_unix: 1559593109 updated_unix: 1565224552 login_source_id: 0 + verified: false diff --git a/services/asymkey/commit.go b/services/asymkey/commit.go index 773e7ca83c8..54ef052a507 100644 --- a/services/asymkey/commit.go +++ b/services/asymkey/commit.go @@ -36,8 +36,9 @@ func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *asymkey_model } // ParseCommitWithSignatureCommitter parses a commit's GPG or SSH signature. +// The caller guarantees that the committer user is related to the commit by checking its activated email addresses or no-reply address. // If the commit is singed by an instance key, then committer can be nil. -// If the signature exists, even if committer is nil, the returned CommittingUser will be a non-nil fake user. +// If the signature exists, even if committer is nil, the returned CommittingUser will be a non-nil fake user (e.g.: instance key) func ParseCommitWithSignatureCommitter(ctx context.Context, c *git.Commit, committer *user_model.User) *asymkey_model.CommitVerification { // If no signature, just report the committer if c.Signature == nil { @@ -114,20 +115,11 @@ func parseCommitWithGPGSignature(ctx context.Context, c *git.Commit, committer * } } - committerEmailAddresses, _ := cache.GetWithContextCache(ctx, cachegroup.UserEmailAddresses, committer.ID, user_model.GetEmailAddresses) - activated := false - for _, e := range committerEmailAddresses { - if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { - activated = true - break - } - } - for _, k := range keys { // Pre-check (& optimization) that emails attached to key can be attached to the committer email and can validate canValidate := false email := "" - if k.Verified && activated { + if k.Verified { canValidate = true email = c.Committer.Email } @@ -217,8 +209,8 @@ func checkKeyEmails(ctx context.Context, email string, keys ...*asymkey_model.GP return true, e.Email } } - if user.KeepEmailPrivate && strings.EqualFold(email, user.GetEmail()) { - return true, user.GetEmail() + if user != nil && strings.EqualFold(email, user.GetPlaceholderEmail()) { + return true, user.GetPlaceholderEmail() } } } @@ -388,21 +380,8 @@ func parseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committerUs } } - committerEmailAddresses, err := cache.GetWithContextCache(ctx, cachegroup.UserEmailAddresses, committerUser.ID, user_model.GetEmailAddresses) - if err != nil { - log.Error("GetEmailAddresses: %v", err) - } - - activated := false - for _, e := range committerEmailAddresses { - if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { - activated = true - break - } - } - for _, k := range keys { - if k.Verified && activated { + if k.Verified { commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committerUser, committerUser, c.Committer.Email) if commitVerification != nil { return commitVerification diff --git a/services/asymkey/commit_test.go b/services/asymkey/commit_test.go index 6bcb6997f43..6edba1e90af 100644 --- a/services/asymkey/commit_test.go +++ b/services/asymkey/commit_test.go @@ -7,6 +7,9 @@ import ( "strings" "testing" + asymkey_model "code.gitea.io/gitea/models/asymkey" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" @@ -17,7 +20,49 @@ import ( ) func TestParseCommitWithSSHSignature(t *testing.T) { - // Here we only test the TrustedSSHKeys. The complete signing test is in tests/integration/gpg_ssh_git_test.go + assert.NoError(t, unittest.PrepareTestDatabase()) + + // Here we only need to do some tests that "tests/integration/gpg_ssh_git_test.go" doesn't cover + + // -----BEGIN OPENSSH PRIVATE KEY----- + // b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW + // QyNTUxOQAAACC6T6zF0oPak8dOIzzT1kXB7LrcsVo04SKc3GjuvMllZwAAAJgy08upMtPL + // qQAAAAtzc2gtZWQyNTUxOQAAACC6T6zF0oPak8dOIzzT1kXB7LrcsVo04SKc3GjuvMllZw + // AAAEDWqPHTH51xb4hy1y1f1VeWL/2A9Q0b6atOyv5fx8x5prpPrMXSg9qTx04jPNPWRcHs + // utyxWjThIpzcaO68yWVnAAAAEXVzZXIyQGV4YW1wbGUuY29tAQIDBA== + // -----END OPENSSH PRIVATE KEY----- + sshPubKey, err := asymkey_model.AddPublicKey(t.Context(), 999, "user-ssh-key-any-name", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILpPrMXSg9qTx04jPNPWRcHsutyxWjThIpzcaO68yWVn", 0) + require.NoError(t, err) + _, err = db.GetEngine(t.Context()).ID(sshPubKey.ID).Cols("verified").Update(&asymkey_model.PublicKey{Verified: true}) + require.NoError(t, err) + + t.Run("UserSSHKey", func(t *testing.T) { + commit, err := git.CommitFromReader(nil, git.Sha1ObjectFormat.EmptyObjectID(), strings.NewReader(`tree a3b1fad553e0f9a2b4a58327bebde36c7da75aa2 +author user2 1752194028 -0700 +committer user2 1752194028 -0700 +gpgsig -----BEGIN SSH SIGNATURE----- + U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAguk+sxdKD2pPHTiM809ZFwey63L + FaNOEinNxo7rzJZWcAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 + AAAAQBfX+6mcKZBnXckwHcBFqRuXMD3vTKi1yv5wgrqIxTyr2LWB97xxmO92cvjsr0POQ2 + 2YA7mQS510Cg2s1uU1XAk= + -----END SSH SIGNATURE----- + +init project +`)) + require.NoError(t, err) + + // the committingUser is guaranteed by the caller, parseCommitWithSSHSignature doesn't do any more checks + committingUser := &user_model.User{ID: 999, Name: "user-x"} + ret := parseCommitWithSSHSignature(t.Context(), commit, committingUser) + require.NotNil(t, ret) + assert.True(t, ret.Verified) + assert.Equal(t, committingUser.Name+" / "+sshPubKey.Fingerprint, ret.Reason) + assert.False(t, ret.Warning) + assert.Equal(t, committingUser, ret.SigningUser) + assert.Equal(t, committingUser, ret.CommittingUser) + assert.Equal(t, sshPubKey.ID, ret.SigningSSHKey.ID) + }) + t.Run("TrustedSSHKey", func(t *testing.T) { defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "gitea")() defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "gitea@fake.local")() From 7cc47da78cef9648a7c930fd68e7436be8f78e57 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 13 Jul 2025 22:52:35 +0800 Subject: [PATCH 23/51] Refactor view issue & comment list styles (#35061) Fix #35060 --- templates/repo/commits_list_small.tmpl | 2 +- templates/repo/diff/comments.tmpl | 14 +- templates/repo/editor/commit_form.tmpl | 2 +- templates/repo/issue/new_form.tmpl | 4 +- templates/repo/issue/view_content.tmpl | 4 +- .../repo/issue/view_content/comments.tmpl | 104 +++++----- .../issue/view_content/pull_merge_box.tmpl | 2 +- templates/repo/issue/view_title.tmpl | 2 +- templates/shared/user/avatarlink.tmpl | 2 +- tests/integration/pull_create_test.go | 4 +- tests/integration/timetracking_test.go | 4 +- web_src/css/base.css | 6 +- web_src/css/modules/comment.css | 4 - web_src/css/repo.css | 196 +++--------------- 14 files changed, 110 insertions(+), 240 deletions(-) diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl index ee94ad7e580..0470d1e9f50 100644 --- a/templates/repo/commits_list_small.tmpl +++ b/templates/repo/commits_list_small.tmpl @@ -22,7 +22,7 @@ {{end}} - + {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} {{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}} diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl index aa483ec6692..22829fbf8a1 100644 --- a/templates/repo/diff/comments.tmpl +++ b/templates/repo/diff/comments.tmpl @@ -2,13 +2,15 @@ {{$createdStr:= DateUtils.TimeSince .CreatedUnix}}
- {{if .OriginalAuthor}} - {{ctx.AvatarUtils.Avatar nil}} - {{else}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - {{end}} +
+ {{if .OriginalAuthor}} + {{ctx.AvatarUtils.Avatar nil}} + {{else}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + {{end}} +
-
+
{{if .OriginalAuthor}} diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl index 70676144447..3e4482f9e2f 100644 --- a/templates/repo/editor/commit_form.tmpl +++ b/templates/repo/editor/commit_form.tmpl @@ -1,6 +1,6 @@
{{ctx.AvatarUtils.Avatar .SignedUser 40 "commit-avatar"}} -
+

{{- if .CommitFormOptions.WillSign}} {{svg "octicon-lock" 24}} {{ctx.Locale.Tr "repo.editor.commit_signed_changes"}} diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index 9413200870e..b63cbcfc0d1 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -4,8 +4,8 @@
- {{ctx.AvatarUtils.Avatar .SignedUser 40}} -
+
{{ctx.AvatarUtils.Avatar .SignedUser 40}}
+
{{end}}
-
+
{{if .Issue.OriginalAuthor}} @@ -78,7 +78,7 @@ {{ctx.AvatarUtils.Avatar .SignedUser 40}}
-
+
{{template "repo/issue/comment_tab" .}} {{.CsrfTokenHtml}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 2bf8837dd51..089cdf2ccdd 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -26,14 +26,14 @@ {{end}}
-
+
{{if .OriginalAuthor}} {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} {{.OriginalAuthor}} - + {{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdStr}} {{if $.Repository.OriginalURL}} @@ -45,7 +45,7 @@ {{ctx.AvatarUtils.Avatar .Poster 24}} {{end}} - + {{template "shared/user/authorlink" .Poster}} {{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdStr}} @@ -85,7 +85,7 @@ {{if not .OriginalAuthor}} {{template "shared/user/avatarlink" dict "user" .Poster}} {{end}} - + {{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}} {{if .Issue.IsPull}} {{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr}} @@ -100,7 +100,7 @@ {{if not .OriginalAuthor}} {{template "shared/user/avatarlink" dict "user" .Poster}} {{end}} - + {{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}} {{if .Issue.IsPull}} {{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr}} @@ -115,7 +115,7 @@ {{if not .OriginalAuthor}} {{template "shared/user/avatarlink" dict "user" .Poster}} {{end}} - + {{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}} {{$link := printf "%s/commit/%s" $.Repository.Link ($.Issue.PullRequest.MergedCommitID|PathEscape)}} {{if eq $.Issue.PullRequest.Status 3}} @@ -143,28 +143,28 @@ {{svg "octicon-bookmark"}} {{template "shared/user/avatarlink" dict "user" .Poster}} {{if eq .RefAction 3}}{{end}} - + {{template "shared/user/authorlink" .Poster}} {{ctx.Locale.Tr $refTr .EventTag $createdStr (.RefCommentLink ctx) $refFrom}} {{if eq .RefAction 3}}{{end}}
{{else if eq .Type 4}}
{{svg "octicon-bookmark"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr}}
{{svg "octicon-git-commit"}} {{/* the content is a link like message title (from CreateRefComment) */}} - {{.Content | SanitizeHTML}} + {{.Content | SanitizeHTML}}
{{else if eq .Type 7}} @@ -172,7 +172,7 @@
{{svg "octicon-tag"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{if and .AddedLabels (not .RemovedLabels)}} {{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (ctx.RenderUtils.RenderLabels .AddedLabels $.RepoLink .Issue) $createdStr}} @@ -188,7 +188,7 @@
{{svg "octicon-milestone"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" .OldMilestone.Name .Milestone.Name $createdStr}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" .OldMilestone.Name $createdStr}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" .Milestone.Name $createdStr}}{{end}} @@ -198,7 +198,7 @@ {{svg "octicon-person"}} {{if .RemovedAssignee}} {{template "shared/user/avatarlink" dict "user" .Assignee}} - + {{template "shared/user/authorlink" .Assignee}} {{if eq .Poster.ID .Assignee.ID}} {{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr}} @@ -208,7 +208,7 @@ {{else}} {{template "shared/user/avatarlink" dict "user" .Assignee}} - + {{template "shared/user/authorlink" .Assignee}} {{if eq .Poster.ID .AssigneeID}} {{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr}} @@ -222,7 +222,7 @@
{{svg "octicon-pencil"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|ctx.RenderUtils.RenderEmoji) (.NewTitle|ctx.RenderUtils.RenderEmoji) $createdStr}} @@ -231,7 +231,7 @@
{{svg "octicon-git-branch"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{$oldRef := HTMLFormat `%s` .OldRef}} {{ctx.Locale.Tr "repo.issues.delete_branch_at" $oldRef $createdStr}} @@ -241,7 +241,7 @@
{{svg "octicon-clock"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{ctx.Locale.Tr "repo.issues.start_tracking_history" $createdStr}} @@ -250,7 +250,7 @@
{{svg "octicon-clock"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{$timeStr := .RenderedContent}} {{/* compatibility with time comments made before v1.21 */}} {{if not $timeStr}}{{$timeStr = .Content|Sec2Hour}}{{end}} @@ -262,7 +262,7 @@
{{svg "octicon-clock"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{$timeStr := .RenderedContent}} {{/* compatibility with time comments made before v1.21 */}} {{if not $timeStr}}{{$timeStr = .Content|Sec2Hour}}{{end}} @@ -274,7 +274,7 @@
{{svg "octicon-clock"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{ctx.Locale.Tr "repo.issues.cancel_tracking_history" $createdStr}} @@ -283,7 +283,7 @@
{{svg "octicon-clock"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{$dueDate := DateUtils.AbsoluteLong (.Content|DateUtils.ParseLegacy)}} {{ctx.Locale.Tr "repo.issues.due_date_added" $dueDate $createdStr}} @@ -293,7 +293,7 @@
{{svg "octicon-clock"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{$parsedDeadline := StringUtils.Split .Content "|"}} {{if eq (len $parsedDeadline) 2}} @@ -307,7 +307,7 @@
{{svg "octicon-clock"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{$dueDate := DateUtils.AbsoluteLong (.Content|DateUtils.ParseLegacy)}} {{ctx.Locale.Tr "repo.issues.due_date_remove" $dueDate $createdStr}} @@ -317,14 +317,14 @@
{{svg "octicon-package-dependents"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr}} {{if .DependentIssue}}
{{svg "octicon-plus"}} - + {{if eq .DependentIssue.RepoID .Issue.RepoID}} #{{.DependentIssue.Index}} {{.DependentIssue.Title}} @@ -340,14 +340,14 @@
{{svg "octicon-package-dependents"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr}} {{if .DependentIssue}}
{{svg "octicon-trash"}} - + {{if eq .DependentIssue.RepoID .Issue.RepoID}} #{{.DependentIssue.Index}} {{.DependentIssue.Title}} @@ -375,7 +375,7 @@ {{if .Review}}{{svg (printf "octicon-%s" .Review.Type.Icon)}}{{end}} - + {{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}} {{if eq $reviewType 1}} {{ctx.Locale.Tr "repo.issues.review.approve" $createdStr}} @@ -394,20 +394,20 @@ {{if or .Content .Attachments}}
-
+
{{if gt .Poster.ID 0}} {{ctx.AvatarUtils.Avatar .Poster 24}} {{end}} - + {{if .OriginalAuthor}} {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} {{.OriginalAuthor}} - {{if $.Repository.OriginalURL}} + {{if $.Repository.OriginalURL}} ({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}}){{end}} {{else}} {{template "shared/user/authorlink" .Poster}} @@ -461,12 +461,12 @@ {{svg "octicon-lock"}} {{template "shared/user/avatarlink" dict "user" .Poster}} {{if .Content}} - + {{template "shared/user/authorlink" .Poster}} {{ctx.Locale.Tr "repo.issues.lock_with_reason" .Content $createdStr}} {{else}} - + {{template "shared/user/authorlink" .Poster}} {{ctx.Locale.Tr "repo.issues.lock_no_reason" $createdStr}} @@ -476,7 +476,7 @@
{{svg "octicon-key"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{ctx.Locale.Tr "repo.issues.unlock_comment" $createdStr}} @@ -487,7 +487,7 @@ {{if not .OriginalAuthor}} {{template "shared/user/avatarlink" dict "user" .Poster}} {{end}} - + {{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}} {{ctx.Locale.Tr "repo.pulls.change_target_branch_at" .OldRef .NewRef $createdStr}} @@ -496,7 +496,7 @@
{{svg "octicon-clock"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr}} @@ -505,9 +505,9 @@ {{svg "octicon-clock"}} {{if .RenderedContent}} {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} + {{.RenderedContent}} {{else}} - - {{.Content|Sec2Hour}} + - {{.Content|Sec2Hour}} {{end}}
@@ -515,7 +515,7 @@
{{svg "octicon-eye"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{if (gt .AssigneeID 0)}} {{if .RemovedAssignee}} @@ -548,7 +548,7 @@ {{end}}
{{svg "octicon-repo-push"}} - + {{template "shared/user/authorlink" .Poster}} {{if .IsForcePush}} {{ctx.Locale.Tr "repo.issues.force_push_codes" $.Issue.PullRequest.HeadBranch (ShortSha .OldCommit) ($.Issue.Repo.CommitLink .OldCommit) (ShortSha .NewCommit) ($.Issue.Repo.CommitLink .NewCommit) $createdStr}} @@ -557,9 +557,7 @@ {{end}} {{if and .IsForcePush $.Issue.PullRequest.BaseRepo.Name}} - - {{ctx.Locale.Tr "repo.issues.force_push_compare"}} - + {{ctx.Locale.Tr "repo.issues.force_push_compare"}} {{end}}
{{if not .IsForcePush}} @@ -569,7 +567,7 @@
{{svg "octicon-project"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{$oldProjectDisplayHtml := "Unknown Project"}} {{if .OldProject}} @@ -601,7 +599,7 @@
{{svg "octicon-project"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{$newProjectDisplay := .CommentMetaData.ProjectTitle}} {{if .Project}} @@ -616,10 +614,10 @@
- + {{ctx.AvatarUtils.Avatar .Poster 40}} {{svg "octicon-x" 16}} - + {{template "shared/user/authorlink" .Poster}} {{$reviewerName := ""}} {{if .Review}} @@ -635,13 +633,13 @@ {{if .Content}}
-
+
{{if gt .Poster.ID 0}} {{ctx.AvatarUtils.Avatar .Poster 24}} {{end}} - + {{ctx.Locale.Tr "action.review_dismissed_reason"}}
@@ -662,7 +660,7 @@
{{svg "octicon-git-branch"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{if and .OldRef .NewRef}} {{ctx.Locale.Tr "repo.issues.change_ref_at" .OldRef .NewRef $createdStr}} @@ -676,7 +674,7 @@ {{else if or (eq .Type 34) (eq .Type 35)}}
{{svg "octicon-git-merge" 16}} - + {{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}} {{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr}} {{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr}}{{end}} @@ -686,7 +684,7 @@
{{svg "octicon-pin" 16}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{if eq .Type 36}}{{ctx.Locale.Tr "repo.issues.pin_comment" $createdStr}} {{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr}}{{end}} @@ -696,7 +694,7 @@
{{svg "octicon-clock"}} {{template "shared/user/avatarlink" dict "user" .Poster}} - + {{template "shared/user/authorlink" .Poster}} {{$timeStr := .Content|TimeEstimateString}} {{if $timeStr}} diff --git a/templates/repo/issue/view_content/pull_merge_box.tmpl b/templates/repo/issue/view_content/pull_merge_box.tmpl index 46bcd3b8b3c..113bfb732ec 100644 --- a/templates/repo/issue/view_content/pull_merge_box.tmpl +++ b/templates/repo/issue/view_content/pull_merge_box.tmpl @@ -38,7 +38,7 @@
{{end}} {{$showGeneralMergeForm := false}} -
+
{{if .Issue.PullRequest.HasMerged}} {{if .IsPullBranchDeletable}}
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl index b8f28dfd9bd..103fa5de530 100644 --- a/templates/repo/issue/view_title.tmpl +++ b/templates/repo/issue/view_title.tmpl @@ -12,7 +12,7 @@
{{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
-

+

{{ctx.RenderUtils.RenderIssueTitle .Issue.Title $.Repository}} #{{.Issue.Index}}

diff --git a/templates/shared/user/avatarlink.tmpl b/templates/shared/user/avatarlink.tmpl index 5e3ed7a68cc..5d56fef430d 100644 --- a/templates/shared/user/avatarlink.tmpl +++ b/templates/shared/user/avatarlink.tmpl @@ -1 +1 @@ -{{ctx.AvatarUtils.Avatar .user}} +{{ctx.AvatarUtils.Avatar .user}} diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go index 179c84e6739..44ef5019611 100644 --- a/tests/integration/pull_create_test.go +++ b/tests/integration/pull_create_test.go @@ -163,10 +163,10 @@ func TestPullCreate_TitleEscape(t *testing.T) { req = NewRequest(t, "GET", url) resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) - titleHTML, err := htmlDoc.doc.Find(".comment-list .timeline-item.event .text b").First().Html() + titleHTML, err := htmlDoc.doc.Find(".comment-list .timeline-item.event .comment-text-line b").First().Html() assert.NoError(t, err) assert.Equal(t, "<i>XSS PR</i>", titleHTML) - titleHTML, err = htmlDoc.doc.Find(".comment-list .timeline-item.event .text b").Next().Html() + titleHTML, err = htmlDoc.doc.Find(".comment-list .timeline-item.event .comment-text-line b").Next().Html() assert.NoError(t, err) assert.Equal(t, "<u>XSS PR</u>", titleHTML) }) diff --git a/tests/integration/timetracking_test.go b/tests/integration/timetracking_test.go index 4e8109be962..ebe084ccdce 100644 --- a/tests/integration/timetracking_test.go +++ b/tests/integration/timetracking_test.go @@ -56,7 +56,7 @@ func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) - events := htmlDoc.doc.Find(".event > span.text") + events := htmlDoc.doc.Find(".event > .comment-text-line") assert.Contains(t, events.Last().Text(), "started working") AssertHTMLElement(t, htmlDoc, ".issue-stop-time", true) @@ -74,7 +74,7 @@ func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) - events = htmlDoc.doc.Find(".event > span.text") + events = htmlDoc.doc.Find(".event > .comment-text-line") assert.Contains(t, events.Last().Text(), "worked for ") } else { session.MakeRequest(t, reqStart, http.StatusNotFound) diff --git a/web_src/css/base.css b/web_src/css/base.css index b415a70cb80..3b819e91bc6 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -38,7 +38,7 @@ --font-size-label: 12px; /* font size of individual labels */ --gap-inline: 0.25rem; /* gap for inline texts and elements, for example: the spaces for sentence with labels, button text, etc */ - --gap-block: 0.25rem; /* gap for element blocks, for example: spaces between buttons, menu image & title, header icon & title etc */ + --gap-block: 0.5rem; /* gap for element blocks, for example: spaces between buttons, menu image & title, header icon & title etc */ } @media (min-width: 768px) and (max-width: 1200px) { @@ -1069,7 +1069,9 @@ table th[data-sortt-desc] .svg { .btn, .ui.ui.button, .ui.ui.dropdown, -.flex-text-inline { +.flex-text-inline, +.flex-text-inline > a, +.flex-text-inline > span { display: inline-flex; align-items: center; gap: var(--gap-inline); diff --git a/web_src/css/modules/comment.css b/web_src/css/modules/comment.css index 9947b15b9ad..1e235a248fc 100644 --- a/web_src/css/modules/comment.css +++ b/web_src/css/modules/comment.css @@ -56,10 +56,6 @@ min-width: 0; } -.ui.comments .comment > .avatar ~ .content { - margin-left: 12px; -} - .ui.comments .comment .author { font-size: 1em; font-weight: var(--font-weight-medium); diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 8729212f10c..0b50d1d1d7a 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -238,8 +238,8 @@ td .commit-summary { border-radius: var(--border-radius); } -.repository.file.editor .commit-form-wrapper .commit-form::before, -.repository.file.editor .commit-form-wrapper .commit-form::after { +.avatar-content-left-arrow::before, +.avatar-content-left-arrow::after { right: 100%; top: 20px; border: solid transparent; @@ -250,18 +250,24 @@ td .commit-summary { pointer-events: none; } -.repository.file.editor .commit-form-wrapper .commit-form::before { +.avatar-content-left-arrow::before { border-right-color: var(--color-secondary); border-width: 9px; margin-top: -9px; } -.repository.file.editor .commit-form-wrapper .commit-form::after { +.avatar-content-left-arrow::after { border-right-color: var(--color-box-body); border-width: 8px; margin-top: -8px; } +@media (max-width: 767.98px) { + .avatar-content-left-arrow::before, + .avatar-content-left-arrow::after { + display: none; + } +} .repository.file.editor .commit-form-wrapper .commit-form .quick-pull-choice .branch-name { display: inline-block; padding: 2px 4px; @@ -294,30 +300,6 @@ td .commit-summary { min-width: 100px; } -.repository.new.issue .comment.form .content::before, -.repository.new.issue .comment.form .content::after { - right: 100%; - top: 20px; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; -} - -.repository.new.issue .comment.form .content::before { - border-right-color: var(--color-secondary); - border-width: 9px; - margin-top: -9px; -} - -.repository.new.issue .comment.form .content::after { - border-right-color: var(--color-box-body); - border-width: 8px; - margin-top: -8px; -} - .repository.new.issue .comment.form .content .markup { font-size: 14px; } @@ -326,21 +308,6 @@ td .commit-summary { display: inline-block; } -@media (max-width: 767.98px) { - .comment.form .issue-content-left .avatar { - display: none; - } - .comment.form .issue-content-left .content { - margin-left: 0 !important; - } - .comment.form .issue-content-left .content::before, - .comment.form .issue-content-left .content::after, - .comment.form .content .form::before, - .comment.form .content .form::after { - display: none; - } -} - /* issue title & meta & edit */ .issue-title-header { width: 100%; @@ -365,7 +332,6 @@ td .commit-summary { .repository.view.issue .issue-title { display: flex; - align-items: center; gap: 0.5em; margin-bottom: 8px; min-height: 40px; /* avoid layout shift on edit */ @@ -379,6 +345,15 @@ td .commit-summary { line-height: 40px; margin: 0; padding-right: 0.25rem; + overflow-wrap: anywhere; +} + +.repository.view.issue .issue-title#issue-title-display .issue-title-buttons { + margin-top: 4px; /* the title's height is 40px, fine tune to align the buttons */ +} + +.repository.view.issue .issue-title#issue-title-editor { + padding-top: 4px; } @media (max-width: 767.98px) { @@ -535,6 +510,7 @@ td .commit-summary { background-color: var(--color-timeline); border-radius: var(--border-radius-full); display: flex; + flex-shrink: 0; float: left; margin-left: -33px; margin-right: 8px; @@ -562,9 +538,18 @@ td .commit-summary { margin-left: -16px; } -.repository.view.issue .comment-list .timeline-item.event > .text { +.repository.view.issue .comment-list .timeline-item .comment-text-line { line-height: 32px; vertical-align: middle; + color: var(--color-text-light); +} + +.repository.view.issue .comment-list .timeline-item .comment-text-line a { + color: inherit; +} + +.repository.view.issue .comment-list .timeline-item .avatar-with-link + .comment-text-line { + margin-left: 0.25em; } .repository.view.issue .comment-list .timeline-item.commits-list { @@ -578,15 +563,11 @@ td .commit-summary { margin-top: 4px; } -.repository.view.issue .comment-list .timeline-item .comparebox { - line-height: 32px; +.repository.view.issue .comment-list .timeline-item .comment-text-label { vertical-align: middle; -} - -.repository.view.issue .comment-list .timeline-item .comparebox .compare.label { - font-size: 1rem; - margin: 0; border: 1px solid var(--color-light-border); + height: 26px; + margin: 4px 0; /* because this label is beside the comment line, which has "line-height: 34px" */ } @media (max-width: 767.98px) { @@ -650,30 +631,6 @@ td .commit-summary { width: calc(100% + 2rem); } -.repository.view.issue .comment-list .comment .merge-section.no-header::before, -.repository.view.issue .comment-list .comment .merge-section.no-header::after { - right: 100%; - top: 20px; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; -} - -.repository.view.issue .comment-list .comment .merge-section.no-header::before { - border-right-color: var(--color-secondary); - border-width: 9px; - margin-top: -9px; -} - -.repository.view.issue .comment-list .comment .merge-section.no-header::after { - border-right-color: var(--color-box-body); - border-width: 8px; - margin-top: -8px; -} - .merge-section-info code { border: 1px solid var(--color-light-border); border-radius: var(--border-radius); @@ -717,19 +674,10 @@ td .commit-summary { padding: 0 !important; } -.repository.view.issue .comment-list .code-comment .comment-header::after, -.repository.view.issue .comment-list .code-comment .comment-header::before { - display: none; -} - .repository.view.issue .comment-list .code-comment .comment-content { margin-left: 24px; } -.repository.view.issue .comment-list .comment > .avatar { - margin-top: 6px; -} - .repository.view.issue .comment-list .comment-code-cloud button.comment-form-reply { margin: 0; } @@ -761,30 +709,6 @@ td .commit-summary { clear: none; } -.repository .comment.form .content .segment::before, -.repository .comment.form .content .segment::after { - right: 100%; - top: 20px; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; -} - -.repository .comment.form .content .segment::before { - border-right-color: var(--color-secondary); - border-width: 9px; - margin-top: -9px; -} - -.repository .comment.form .content .segment::after { - border-right-color: var(--color-box-body); - border-width: 8px; - margin-top: -8px; -} - .repository.new.milestone textarea { height: 200px; } @@ -807,30 +731,6 @@ td .commit-summary { text-align: center; } -.repository.compare.pull .comment.form .content::before, -.repository.compare.pull .comment.form .content::after { - right: 100%; - top: 20px; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; -} - -.repository.compare.pull .comment.form .content::before { - border-right-color: var(--color-secondary); - border-width: 9px; - margin-top: -9px; -} - -.repository.compare.pull .comment.form .content::after { - border-right-color: var(--color-box-body); - border-width: 8px; - margin-top: -8px; -} - .repository.compare.pull .markup { font-size: 14px; } @@ -1431,30 +1331,6 @@ td .commit-summary { gap: 0.25em; } -.comment-header::before, -.comment-header::after { - right: 100%; - top: 20px; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; -} - -.comment-header::before { - border-right-color: var(--color-secondary); - border-width: 9px; - margin-top: -9px; -} - -.comment-header::after { - border-right-color: var(--color-box-header); - border-width: 8px; - margin-top: -8px; -} - .comment-header.arrow-top::before, .comment-header.arrow-top::after { transform: rotate(90deg); @@ -1629,7 +1505,7 @@ tbody.commit-list { overflow-wrap: anywhere; } -.content-history-detail-dialog .header .avatar { +.content-history-detail-dialog .header .ui.avatar { position: relative; top: -2px; } @@ -1978,10 +1854,6 @@ tbody.commit-list { .repository.view.issue .comment-list .timeline .comment-header { padding-left: 4px; } - .repository.view.issue .comment-list .timeline .comment-header::before, - .repository.view.issue .comment-list .timeline .comment-header::after { - content: unset; - } /* Don't show the general avatar, we show the inline avatar on mobile. * And don't show the role labels, there's no place for that. */ .repository.view.issue .comment-list .timeline .timeline-avatar, From ece0ce6854303af7e654d364e375db90cc64bcd5 Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Mon, 14 Jul 2025 20:29:35 +0800 Subject: [PATCH 24/51] UI: add hover background to table rows in user and repo admin page (#35072) --- templates/admin/repo/list.tmpl | 2 +- templates/admin/user/list.tmpl | 2 +- web_src/css/modules/table.css | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index 2ad2de3a1d0..767d00fa741 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -10,7 +10,7 @@ {{template "shared/repo/search" .}}
- +
diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl index eb3f6cd7204..49f62dda748 100644 --- a/templates/admin/user/list.tmpl +++ b/templates/admin/user/list.tmpl @@ -56,7 +56,7 @@
-
ID{{SortArrow "oldest" "newest" $.SortType false}}
+
diff --git a/web_src/css/modules/table.css b/web_src/css/modules/table.css index eabca31a170..6298471d474 100644 --- a/web_src/css/modules/table.css +++ b/web_src/css/modules/table.css @@ -167,6 +167,11 @@ text-overflow: ellipsis; } +.ui.selectable.table > tbody > tr:hover, +.ui.table tbody tr td.selectable:hover { + background: var(--color-hover); +} + .ui.attached.table { top: 0; bottom: 0; @@ -289,6 +294,9 @@ .ui.basic.striped.table > tbody > tr:nth-child(2n) { background: var(--color-light); } +.ui.basic.striped.selectable.table > tbody > tr:nth-child(2n):hover { + background: var(--color-hover); +} .ui[class*="very basic"].table { border: none; From b861d86f80ea7a848efb539b798bbde3c7877d02 Mon Sep 17 00:00:00 2001 From: Joshdike Date: Mon, 14 Jul 2025 16:31:05 +0300 Subject: [PATCH 25/51] Fixed all grammatical errors in locale_en-US.ini (#35053) Co-authored-by: wxiaoguang --- options/locale/locale_en-US.ini | 60 ++++++++++++++++----------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ff32c94ff93..f319b1de3fd 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -358,7 +358,7 @@ no_reply_address = Hidden Email Domain no_reply_address_helper = Domain name for users with a hidden email address. For example, the username 'joe' will be logged in Git as 'joe@noreply.example.org' if the hidden email domain is set to 'noreply.example.org'. password_algorithm = Password Hash Algorithm invalid_password_algorithm = Invalid password hash algorithm -password_algorithm_helper = Set the password hashing algorithm. Algorithms have differing requirements and strength. The argon2 algorithm is rather secure but uses a lot of memory and may be inappropriate for small systems. +password_algorithm_helper = Set the password hashing algorithm. Algorithms have differing requirements and strengths. The argon2 algorithm is rather secure but uses a lot of memory and may be inappropriate for small systems. enable_update_checker = Enable Update Checker enable_update_checker_helper = Checks for new version releases periodically by connecting to gitea.io. env_config_keys = Environment Configuration @@ -452,7 +452,7 @@ use_scratch_code = Use a scratch code twofa_scratch_used = You have used your scratch code. You have been redirected to the two-factor settings page so you may remove your device enrollment or generate a new scratch code. twofa_passcode_incorrect = Your passcode is incorrect. If you misplaced your device, use your scratch code to sign in. twofa_scratch_token_incorrect = Your scratch code is incorrect. -twofa_required = You must setup Two-Factor Authentication to get access to repositories, or try to login again. +twofa_required = You must set up Two-Factor Authentication to get access to repositories, or try to log in again. login_userpass = Sign In login_openid = OpenID oauth_signup_tab = Register New Account @@ -464,7 +464,7 @@ oauth_signin_submit = Link Account oauth.signin.error.general = There was an error processing the authorization request: %s. If this error persists, please contact the site administrator. oauth.signin.error.access_denied = The authorization request was denied. oauth.signin.error.temporarily_unavailable = Authorization failed because the authentication server is temporarily unavailable. Please try again later. -oauth_callback_unable_auto_reg = Auto Registration is enabled, but OAuth2 Provider %[1]s returned missing fields: %[2]s, unable to create an account automatically, please create or link to an account, or contact the site administrator. +oauth_callback_unable_auto_reg = Auto Registration is enabled, but OAuth2 Provider %[1]s returned missing fields: %[2]s, unable to create an account automatically. Please create or link to an account, or contact the site administrator. openid_connect_submit = Connect openid_connect_title = Connect to an existing account openid_connect_desc = The chosen OpenID URI is unknown. Associate it with a new account here. @@ -477,7 +477,7 @@ email_domain_blacklisted = You cannot register with your email address. authorize_application = Authorize Application authorize_redirect_notice = You will be redirected to %s if you authorize this application. authorize_application_created_by = This application was created by %s. -authorize_application_description = If you grant the access, it will be able to access and write to all your account information, including private repos and organisations. +authorize_application_description = If you grant access, it will be able to access and write to all your account information, including private repos and organisations. authorize_application_with_scopes = With scopes: %s authorize_title = Authorize "%s" to access your account? authorization_failed = Authorization failed @@ -507,7 +507,7 @@ activate_email.text = Please click the following link to verify your email addre register_notify = Welcome to %s register_notify.title = %[1]s, welcome to %[2]s register_notify.text_1 = This is your registration confirmation email for %s! -register_notify.text_2 = You can now login via username: %s. +register_notify.text_2 = You can now log in via username: %s. register_notify.text_3 = If this account has been created for you, please set your password first. reset_password = Recover your account @@ -689,7 +689,7 @@ form.name_chars_not_allowed = User name "%s" contains invalid characters. block.block = Block block.block.user = Block user -block.block.org = Block user for organization +block.block.org = Block user from organization block.block.failure = Failed to block user: %s block.unblock = Unblock block.unblock.failure = Failed to unblock user: %s @@ -733,7 +733,7 @@ webauthn = Two-Factor Authentication (Security Keys) public_profile = Public Profile biography_placeholder = Tell us a little bit about yourself! (You can use Markdown) location_placeholder = Share your approximate location with others -profile_desc = Control how your profile is show to other users. Your primary email address will be used for notifications, password recovery and web-based Git operations. +profile_desc = Control how your profile is shown to other users. Your primary email address will be used for notifications, password recovery and web-based Git operations. password_username_disabled = You are not allowed to change your username. Please contact your site administrator for more details. password_full_name_disabled = You are not allowed to change your full name. Please contact your site administrator for more details. full_name = Full Name @@ -812,7 +812,7 @@ activations_pending = Activations Pending can_not_add_email_activations_pending = There is a pending activation, try again in a few minutes if you want to add a new email. delete_email = Remove email_deletion = Remove Email Address -email_deletion_desc = The email address and related information will be removed from your account. Git commits by this email address will remain unchanged. Continue? +email_deletion_desc = This email address and related information will be removed from your account. Git commits by this email address will remain unchanged. Continue? email_deletion_success = The email address has been removed. theme_update_success = Your theme was updated. theme_update_error = The selected theme does not exist. @@ -1102,7 +1102,7 @@ mirror_sync = synced mirror_sync_on_commit = Sync when commits are pushed mirror_address = Clone From URL mirror_address_desc = Put any required credentials in the Authorization section. -mirror_address_url_invalid = The provided URL is invalid. You must escape all components of the url correctly. +mirror_address_url_invalid = The provided URL is invalid. Make sure all components of the URL are escaped correctly. mirror_address_protocol_invalid = The provided URL is invalid. Only http(s):// or git:// locations can be used for mirroring. mirror_lfs = Large File Storage (LFS) mirror_lfs_desc = Activate mirroring of LFS data. @@ -1162,8 +1162,8 @@ template.issue_labels = Issue Labels template.one_item = Must select at least one template item template.invalid = Must select a template repository -archive.title = This repo is archived. You can view files and clone it, but cannot push or open issues or pull requests. -archive.title_date = This repository has been archived on %s. You can view files and clone it, but cannot push or open issues or pull requests. +archive.title = This repo is archived. You can view files and clone it. You cannot open issues, pull requests or push a commit. +archive.title_date = This repository has been archived on %s. You can view files and clone it. You cannot open issues, pull requests or push a commit. archive.issue.nocomment = This repo is archived. You cannot comment on issues. archive.pull.nocomment = This repo is archived. You cannot comment on pull requests. @@ -1192,7 +1192,7 @@ migrate_items_releases = Releases migrate_repo = Migrate Repository migrate.clone_address = Migrate / Clone From URL migrate.clone_address_desc = The HTTP(S) or Git 'clone' URL of an existing repository -migrate.github_token_desc = You can put one or more tokens with comma separated here to make migrating faster because of GitHub API rate limit. WARN: Abusing this feature may violate the service provider's policy and lead to account blocking. +migrate.github_token_desc = You can put one or more tokens here separated by commas to make migrating faster by circumventing the GitHub API rate limit. WARNING: Abusing this feature may violate the service provider's policy and may lead to getting your account(s) blocked. migrate.clone_local_path = or a local server path migrate.permission_denied = You are not allowed to import local repositories. migrate.permission_denied_blocked = You cannot import from disallowed hosts, please ask the admin to check ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS settings. @@ -1254,7 +1254,7 @@ create_new_repo_command = Creating a new repository on the command line push_exist_repo = Pushing an existing repository from the command line empty_message = This repository does not contain any content. broken_message = The Git data underlying this repository cannot be read. Contact the administrator of this instance or delete this repository. -no_branch = This repository doesn’t have any branches. +no_branch = This repository doesn't have any branches. code = Code code.desc = Access source code, files, commits and branches. @@ -1400,8 +1400,8 @@ editor.failed_to_commit = Failed to commit changes. editor.failed_to_commit_summary = Error Message: editor.fork_create = Fork Repository to Propose Changes -editor.fork_create_description = You can not edit this repository directly. Instead you can create a fork, make edits and create a pull request. -editor.fork_edit_description = You can not edit this repository directly. The changes will be written to your fork %s, so you can create a pull request. +editor.fork_create_description = You cannot edit this repository directly. Instead you can create a fork, make edits and create a pull request. +editor.fork_edit_description = You cannot edit this repository directly. The changes will be written to your fork %s, so you can create a pull request. editor.fork_not_editable = You have forked this repository but your fork is not editable. editor.fork_failed_to_push_branch = Failed to push branch %s to your repository. editor.fork_branch_exists = Branch "%s" already exists in your fork, please choose a new branch name. @@ -1700,7 +1700,7 @@ issues.lock_no_reason = "locked and limited conversation to collaborators %s" issues.unlock_comment = "unlocked this conversation %s" issues.lock_confirm = Lock issues.unlock_confirm = Unlock -issues.lock.notice_1 = - Other users can’t add new comments to this issue. +issues.lock.notice_1 = - Other users cannot add new comments to this issue. issues.lock.notice_2 = - You and other collaborators with access to this repository can still leave comments that others can see. issues.lock.notice_3 = - You can always unlock this issue again in the future. issues.unlock.notice_1 = - Everyone would be able to comment on this issue once more. @@ -1869,7 +1869,7 @@ pulls.select_commit_hold_shift_for_range = Select commit. Hold shift + click to pulls.review_only_possible_for_full_diff = Review is only possible when viewing the full diff pulls.filter_changes_by_commit = Filter by commit pulls.nothing_to_compare = These branches are equal. There is no need to create a pull request. -pulls.nothing_to_compare_have_tag = The selected branch/tag are equal. +pulls.nothing_to_compare_have_tag = The selected branches/tags are equal. pulls.nothing_to_compare_and_allow_empty_pr = These branches are equal. This PR will be empty. pulls.has_pull_request = `A pull request between these branches already exists: %[2]s#%[3]d` pulls.create = Create Pull Request @@ -2032,7 +2032,7 @@ milestones.filter_sort.most_issues = Most issues milestones.filter_sort.least_issues = Least issues signing.will_sign = This commit will be signed with key "%s". -signing.wont_sign.error = There was an error whilst checking if the commit could be signed. +signing.wont_sign.error = There was an error while checking if the commit could be signed. signing.wont_sign.nokey = There is no key available to sign this commit. signing.wont_sign.never = Commits are never signed. signing.wont_sign.always = Commits are always signed. @@ -2541,7 +2541,7 @@ settings.block_on_official_review_requests_desc = Merging will not be possible w settings.block_outdated_branch = Block merge if pull request is outdated settings.block_outdated_branch_desc = Merging will not be possible when head branch is behind base branch. settings.block_admin_merge_override = Administrators must follow branch protection rules -settings.block_admin_merge_override_desc = Administrators must follow branch protection rules and can not circumvent it. +settings.block_admin_merge_override_desc = Administrators must follow branch protection rules and cannot circumvent it. settings.default_branch_desc = Select a default repository branch for pull requests and code commits: settings.merge_style_desc = Merge Styles settings.default_merge_style_desc = Default Merge Style @@ -2568,10 +2568,10 @@ settings.matrix.homeserver_url = Homeserver URL settings.matrix.room_id = Room ID settings.matrix.message_type = Message Type settings.visibility.private.button = Make Private -settings.visibility.private.text = Changing the visibility to private will not only make the repo visible to only allowed members but may remove the relation between it and forks, watchers, and stars. +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. settings.visibility.private.bullet_title = Changing the visibility to private will: -settings.visibility.private.bullet_one = Make the repo visible to only allowed members. -settings.visibility.private.bullet_two = May remove the relation between it and forks, watchers, and stars. +settings.visibility.private.bullet_one = Make the repo visible only to allowed members. +settings.visibility.private.bullet_two = May remove the relationship between it and forks, watchers, and stars. settings.visibility.public.button = Make Public settings.visibility.public.text = Changing the visibility to public will make the repo visible to anyone. settings.visibility.public.bullet_title= Changing the visibility to public will: @@ -2826,7 +2826,7 @@ team_permission_desc = Permission team_unit_desc = Allow Access to Repository Sections team_unit_disabled = (Disabled) -form.name_been_taken = The organisation name "%s" has already been taken. +form.name_been_taken = The organization name "%s" has already been taken. form.name_reserved = The organization name "%s" is reserved. form.name_pattern_not_allowed = The pattern "%s" is not allowed in an organization name. form.create_org_not_allowed = You are not allowed to create an organization. @@ -2868,7 +2868,7 @@ settings.delete_notices_2 = This operation will permanently delete all the packages of %s. settings.delete_notices_4 = This operation will permanently delete all the projects of %s. settings.confirm_delete_account = Confirm Deletion -settings.delete_failed = Delete Organization failed because of internal error +settings.delete_failed = Delete Organization failed due to an internal error settings.delete_successful = Organization %s has been deleted successfully. settings.hooks_desc = Add webhooks which will be triggered for all repositories under this organization. @@ -3018,9 +3018,9 @@ dashboard.resync_all_sshprincipals = Update the '.ssh/authorized_principals' fil dashboard.resync_all_hooks = Resynchronize pre-receive, update and post-receive hooks of all repositories. dashboard.reinit_missing_repos = Reinitialize all missing Git repositories for which records exist dashboard.sync_external_users = Synchronize external user data -dashboard.cleanup_hook_task_table = Cleanup hook_task table -dashboard.cleanup_packages = Cleanup expired packages -dashboard.cleanup_actions = Cleanup expired actions resources +dashboard.cleanup_hook_task_table = Clean up hook_task table +dashboard.cleanup_packages = Clean up expired packages +dashboard.cleanup_actions = Clean up expired actions' resources dashboard.server_uptime = Server Uptime dashboard.current_goroutine = Current Goroutines dashboard.current_memory_usage = Current Memory Usage @@ -3457,7 +3457,7 @@ monitor.start = Start Time monitor.execute_time = Execution Time monitor.last_execution_result = Result monitor.process.cancel = Cancel process -monitor.process.cancel_desc = Cancelling a process may cause data loss +monitor.process.cancel_desc = Canceling a process may cause data loss monitor.process.children = Children monitor.queues = Queues @@ -3729,7 +3729,7 @@ owner.settings.cargo.initialize.success = The Cargo index was successfully creat owner.settings.cargo.rebuild = Rebuild Index owner.settings.cargo.rebuild.description = Rebuilding can be useful if the index is not synchronized with the stored Cargo packages. owner.settings.cargo.rebuild.error = Failed to rebuild Cargo index: %v -owner.settings.cargo.rebuild.success = The Cargo index was successfully rebuild. +owner.settings.cargo.rebuild.success = The Cargo index was successfully rebuilt. owner.settings.cleanuprules.title = Manage Cleanup Rules owner.settings.cleanuprules.add = Add Cleanup Rule owner.settings.cleanuprules.edit = Edit Cleanup Rule @@ -3817,7 +3817,7 @@ runners.delete_runner = Delete this runner runners.delete_runner_success = Runner deleted successfully runners.delete_runner_failed = Failed to delete runner runners.delete_runner_header = Confirm to delete this runner -runners.delete_runner_notice = If a task is running on this runner, it will be terminated and mark as failed. It may break building workflow. +runners.delete_runner_notice = If a task is running on this runner, it will be terminated and marked as failed. It may break building workflow. runners.none = No runners available runners.status.unspecified = Unknown runners.status.idle = Idle From d08459820dc1f3ac98b36bcd0adc1d3e05d62a14 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 14 Jul 2025 23:28:34 +0800 Subject: [PATCH 26/51] Improve submodule relative path handling (#35056) Fix #35054 --------- Co-authored-by: Giteabot --- modules/git/commit_info.go | 11 ++++ modules/git/commit_info_gogit.go | 18 ++----- modules/git/commit_info_nogogit.go | 18 ++----- modules/git/commit_info_test.go | 4 +- modules/git/commit_submodule_file.go | 64 ++++++++++++----------- modules/git/commit_submodule_file_test.go | 28 +++++----- routers/web/repo/commit.go | 2 +- routers/web/repo/compare.go | 2 +- routers/web/repo/pull.go | 2 +- routers/web/repo/treelist.go | 2 +- routers/web/repo/view.go | 2 +- routers/web/repo/wiki.go | 3 +- services/gitdiff/gitdiff.go | 4 +- services/gitdiff/submodule.go | 15 +++--- services/gitdiff/submodule_test.go | 2 +- services/repository/files/tree.go | 16 +++--- services/repository/files/tree_test.go | 7 +-- templates/repo/view_list.tmpl | 2 +- 18 files changed, 99 insertions(+), 103 deletions(-) diff --git a/modules/git/commit_info.go b/modules/git/commit_info.go index c046acbb508..a796f4a2040 100644 --- a/modules/git/commit_info.go +++ b/modules/git/commit_info.go @@ -3,9 +3,20 @@ package git +import "path" + // CommitInfo describes the first commit with the provided entry type CommitInfo struct { Entry *TreeEntry Commit *Commit SubmoduleFile *CommitSubmoduleFile } + +func getCommitInfoSubmoduleFile(repoLink string, entry *TreeEntry, commit *Commit, treePathDir string) (*CommitSubmoduleFile, error) { + fullPath := path.Join(treePathDir, entry.Name()) + submodule, err := commit.GetSubModule(fullPath) + if err != nil { + return nil, err + } + return NewCommitSubmoduleFile(repoLink, fullPath, submodule.URL, entry.ID.String()), nil +} diff --git a/modules/git/commit_info_gogit.go b/modules/git/commit_info_gogit.go index 314c2df7284..7e03e634a03 100644 --- a/modules/git/commit_info_gogit.go +++ b/modules/git/commit_info_gogit.go @@ -16,7 +16,7 @@ import ( ) // GetCommitsInfo gets information of all commits that are corresponding to these entries -func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) { +func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) { entryPaths := make([]string, len(tes)+1) // Get the commit for the treePath itself entryPaths[0] = "" @@ -71,22 +71,12 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath commitsInfo[i].Commit = entryCommit } - // If the entry is a submodule add a submodule file for this + // If the entry is a submodule, add a submodule file for this if entry.IsSubModule() { - subModuleURL := "" - var fullPath string - if len(treePath) > 0 { - fullPath = treePath + "/" + entry.Name() - } else { - fullPath = entry.Name() - } - if subModule, err := commit.GetSubModule(fullPath); err != nil { + commitsInfo[i].SubmoduleFile, err = getCommitInfoSubmoduleFile(repoLink, entry, commit, treePath) + if err != nil { return nil, nil, err - } else if subModule != nil { - subModuleURL = subModule.URL } - subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String()) - commitsInfo[i].SubmoduleFile = subModuleFile } } diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go index 1b45fc8a6c3..161edb7e966 100644 --- a/modules/git/commit_info_nogogit.go +++ b/modules/git/commit_info_nogogit.go @@ -15,7 +15,7 @@ import ( ) // GetCommitsInfo gets information of all commits that are corresponding to these entries -func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) { +func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) { entryPaths := make([]string, len(tes)+1) // Get the commit for the treePath itself entryPaths[0] = "" @@ -62,22 +62,12 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath log.Debug("missing commit for %s", entry.Name()) } - // If the entry is a submodule add a submodule file for this + // If the entry is a submodule, add a submodule file for this if entry.IsSubModule() { - subModuleURL := "" - var fullPath string - if len(treePath) > 0 { - fullPath = treePath + "/" + entry.Name() - } else { - fullPath = entry.Name() - } - if subModule, err := commit.GetSubModule(fullPath); err != nil { + commitsInfo[i].SubmoduleFile, err = getCommitInfoSubmoduleFile(repoLink, entry, commit, treePath) + if err != nil { return nil, nil, err - } else if subModule != nil { - subModuleURL = subModule.URL } - subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String()) - commitsInfo[i].SubmoduleFile = subModuleFile } } diff --git a/modules/git/commit_info_test.go b/modules/git/commit_info_test.go index ba518ab2455..caaa6a502b4 100644 --- a/modules/git/commit_info_test.go +++ b/modules/git/commit_info_test.go @@ -82,7 +82,7 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) { } // FIXME: Context.TODO() - if graceful has started we should use its Shutdown context otherwise use install signals in TestMain. - commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), commit, testCase.Path) + commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), "/any/repo-link", commit, testCase.Path) assert.NoError(t, err, "Unable to get commit information for entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err) if err != nil { t.FailNow() @@ -159,7 +159,7 @@ func BenchmarkEntries_GetCommitsInfo(b *testing.B) { b.ResetTimer() b.Run(benchmark.name, func(b *testing.B) { for b.Loop() { - _, _, err := entries.GetCommitsInfo(b.Context(), commit, "") + _, _, err := entries.GetCommitsInfo(b.Context(), "/any/repo-link", commit, "") if err != nil { b.Fatal(err) } diff --git a/modules/git/commit_submodule_file.go b/modules/git/commit_submodule_file.go index 5def80f3bdc..34f582c21f7 100644 --- a/modules/git/commit_submodule_file.go +++ b/modules/git/commit_submodule_file.go @@ -6,57 +6,61 @@ package git import ( "context" + "path" "strings" giturl "code.gitea.io/gitea/modules/git/url" + "code.gitea.io/gitea/modules/util" ) // CommitSubmoduleFile represents a file with submodule type. type CommitSubmoduleFile struct { - refURL string - refID string + repoLink string + fullPath string + refURL string + refID string - parsed bool - targetRepoLink string + parsed bool + parsedTargetLink string } // NewCommitSubmoduleFile create a new submodule file -func NewCommitSubmoduleFile(refURL, refID string) *CommitSubmoduleFile { - return &CommitSubmoduleFile{refURL: refURL, refID: refID} +func NewCommitSubmoduleFile(repoLink, fullPath, refURL, refID string) *CommitSubmoduleFile { + return &CommitSubmoduleFile{repoLink: repoLink, fullPath: fullPath, refURL: refURL, refID: refID} } func (sf *CommitSubmoduleFile) RefID() string { - return sf.refID // this function is only used in templates + return sf.refID } -// SubmoduleWebLink tries to make some web links for a submodule, it also works on "nil" receiver -func (sf *CommitSubmoduleFile) SubmoduleWebLink(ctx context.Context, optCommitID ...string) *SubmoduleWebLink { +func (sf *CommitSubmoduleFile) getWebLinkInTargetRepo(ctx context.Context, moreLinkPath string) *SubmoduleWebLink { if sf == nil { return nil } + if strings.HasPrefix(sf.refURL, "../") { + targetLink := path.Join(sf.repoLink, path.Dir(sf.fullPath), sf.refURL) + return &SubmoduleWebLink{RepoWebLink: targetLink, CommitWebLink: targetLink + moreLinkPath} + } if !sf.parsed { sf.parsed = true - if strings.HasPrefix(sf.refURL, "../") { - // FIXME: when handling relative path, this logic is not right. It needs to: - // 1. Remember the submodule's full path and its commit's repo home link - // 2. Resolve the relative path: targetRepoLink = path.Join(repoHomeLink, path.Dir(submoduleFullPath), refURL) - // Not an easy task and need to refactor related code a lot. - sf.targetRepoLink = sf.refURL - } else { - parsedURL, err := giturl.ParseRepositoryURL(ctx, sf.refURL) - if err != nil { - return nil - } - sf.targetRepoLink = giturl.MakeRepositoryWebLink(parsedURL) + parsedURL, err := giturl.ParseRepositoryURL(ctx, sf.refURL) + if err != nil { + return nil } + sf.parsedTargetLink = giturl.MakeRepositoryWebLink(parsedURL) } - var commitLink string - if len(optCommitID) == 2 { - commitLink = sf.targetRepoLink + "/compare/" + optCommitID[0] + "..." + optCommitID[1] - } else if len(optCommitID) == 1 { - commitLink = sf.targetRepoLink + "/tree/" + optCommitID[0] - } else { - commitLink = sf.targetRepoLink + "/tree/" + sf.refID - } - return &SubmoduleWebLink{RepoWebLink: sf.targetRepoLink, CommitWebLink: commitLink} + return &SubmoduleWebLink{RepoWebLink: sf.parsedTargetLink, CommitWebLink: sf.parsedTargetLink + moreLinkPath} +} + +// SubmoduleWebLinkTree tries to make the submodule's tree link in its own repo, it also works on "nil" receiver +func (sf *CommitSubmoduleFile) SubmoduleWebLinkTree(ctx context.Context, optCommitID ...string) *SubmoduleWebLink { + if sf == nil { + return nil + } + return sf.getWebLinkInTargetRepo(ctx, "/tree/"+util.OptionalArg(optCommitID, sf.refID)) +} + +// SubmoduleWebLinkCompare tries to make the submodule's compare link in its own repo, it also works on "nil" receiver +func (sf *CommitSubmoduleFile) SubmoduleWebLinkCompare(ctx context.Context, commitID1, commitID2 string) *SubmoduleWebLink { + return sf.getWebLinkInTargetRepo(ctx, "/compare/"+commitID1+"..."+commitID2) } diff --git a/modules/git/commit_submodule_file_test.go b/modules/git/commit_submodule_file_test.go index 103e55e920e..fc2ff39bda6 100644 --- a/modules/git/commit_submodule_file_test.go +++ b/modules/git/commit_submodule_file_test.go @@ -10,29 +10,29 @@ import ( ) func TestCommitSubmoduleLink(t *testing.T) { - wl := (*CommitSubmoduleFile)(nil).SubmoduleWebLink(t.Context()) - assert.Nil(t, wl) + assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkTree(t.Context())) + assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkCompare(t.Context(), "", "")) t.Run("GitHubRepo", func(t *testing.T) { - sf := NewCommitSubmoduleFile("git@github.com:user/repo.git", "aaaa") - - wl := sf.SubmoduleWebLink(t.Context()) + sf := NewCommitSubmoduleFile("/any/repo-link", "full-path", "git@github.com:user/repo.git", "aaaa") + wl := sf.SubmoduleWebLinkTree(t.Context()) assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) assert.Equal(t, "https://github.com/user/repo/tree/aaaa", wl.CommitWebLink) - wl = sf.SubmoduleWebLink(t.Context(), "1111") - assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) - assert.Equal(t, "https://github.com/user/repo/tree/1111", wl.CommitWebLink) - - wl = sf.SubmoduleWebLink(t.Context(), "1111", "2222") + wl = sf.SubmoduleWebLinkCompare(t.Context(), "1111", "2222") assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink) }) t.Run("RelativePath", func(t *testing.T) { - sf := NewCommitSubmoduleFile("../../user/repo", "aaaa") - wl := sf.SubmoduleWebLink(t.Context()) - assert.Equal(t, "../../user/repo", wl.RepoWebLink) - assert.Equal(t, "../../user/repo/tree/aaaa", wl.CommitWebLink) + sf := NewCommitSubmoduleFile("/subpath/any/repo-home-link", "full-path", "../../user/repo", "aaaa") + wl := sf.SubmoduleWebLinkTree(t.Context()) + assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink) + assert.Equal(t, "/subpath/user/repo/tree/aaaa", wl.CommitWebLink) + + sf = NewCommitSubmoduleFile("/subpath/any/repo-home-link", "dir/submodule", "../../../user/repo", "aaaa") + wl = sf.SubmoduleWebLinkCompare(t.Context(), "1111", "2222") + assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink) + assert.Equal(t, "/subpath/user/repo/compare/1111...2222", wl.CommitWebLink) }) } diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 8f5c6a42e65..9a06c9359b5 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -311,7 +311,7 @@ func Diff(ctx *context.Context) { maxLines, maxFiles = -1, -1 } - diff, err := gitdiff.GetDiffForRender(ctx, gitRepo, &gitdiff.DiffOptions{ + diff, err := gitdiff.GetDiffForRender(ctx, ctx.Repo.RepoLink, gitRepo, &gitdiff.DiffOptions{ AfterCommitID: commitID, SkipTo: ctx.FormString("skip-to"), MaxLines: maxLines, diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index de34a9375c5..c771b30e5ff 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -614,7 +614,7 @@ func PrepareCompareDiff( fileOnly := ctx.FormBool("file-only") - diff, err := gitdiff.GetDiffForRender(ctx, ci.HeadGitRepo, + diff, err := gitdiff.GetDiffForRender(ctx, ci.HeadRepo.Link(), ci.HeadGitRepo, &gitdiff.DiffOptions{ BeforeCommitID: beforeCommitID, AfterCommitID: headCommitID, diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index f662152e2e7..23402e3eb27 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -750,7 +750,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi diffOptions.BeforeCommitID = startCommitID } - diff, err := gitdiff.GetDiffForRender(ctx, gitRepo, diffOptions, files...) + diff, err := gitdiff.GetDiffForRender(ctx, ctx.Repo.RepoLink, gitRepo, diffOptions, files...) if err != nil { ctx.ServerError("GetDiff", err) return diff --git a/routers/web/repo/treelist.go b/routers/web/repo/treelist.go index 7d7f5a1473d..340b2bc0917 100644 --- a/routers/web/repo/treelist.go +++ b/routers/web/repo/treelist.go @@ -144,7 +144,7 @@ func transformDiffTreeForWeb(renderedIconPool *fileicon.RenderedIconPool, diffTr func TreeViewNodes(ctx *context.Context) { renderedIconPool := fileicon.NewRenderedIconPool() - results, err := files_service.GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path")) + results, err := files_service.GetTreeViewNodes(ctx, ctx.Repo.RepoLink, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path")) if err != nil { ctx.ServerError("GetTreeViewNodes", err) return diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 773919c054e..e47bc56d081 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -305,7 +305,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri defer cancel() } - files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath) + files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.RepoLink, ctx.Repo.Commit, ctx.Repo.TreePath) if err != nil { ctx.ServerError("GetCommitsInfo", err) return nil diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 69858c96925..a35b7b86e12 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -6,7 +6,6 @@ package repo import ( "bytes" - gocontext "context" "html/template" "io" "net/http" @@ -569,7 +568,7 @@ func WikiPages(ctx *context.Context) { } allEntries.CustomSort(base.NaturalSortLess) - entries, _, err := allEntries.GetCommitsInfo(gocontext.Context(ctx), commit, treePath) + entries, _, err := allEntries.GetCommitsInfo(ctx, ctx.Repo.RepoLink, commit, treePath) if err != nil { ctx.ServerError("GetCommitsInfo", err) return diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 0b6e6be82b7..e2cd01e25cd 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -1185,7 +1185,7 @@ func GetDiffForAPI(ctx context.Context, gitRepo *git.Repository, opts *DiffOptio return diff, err } -func GetDiffForRender(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) { +func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) { diff, beforeCommit, afterCommit, err := getDiffBasic(ctx, gitRepo, opts, files...) if err != nil { return nil, err @@ -1211,7 +1211,7 @@ func GetDiffForRender(ctx context.Context, gitRepo *git.Repository, opts *DiffOp // Populate Submodule URLs if diffFile.SubmoduleDiffInfo != nil { - diffFile.SubmoduleDiffInfo.PopulateURL(diffFile, beforeCommit, afterCommit) + diffFile.SubmoduleDiffInfo.PopulateURL(repoLink, diffFile, beforeCommit, afterCommit) } if !isVendored.Has() { diff --git a/services/gitdiff/submodule.go b/services/gitdiff/submodule.go index 02ca666544c..4347743e3d0 100644 --- a/services/gitdiff/submodule.go +++ b/services/gitdiff/submodule.go @@ -20,7 +20,7 @@ type SubmoduleDiffInfo struct { PreviousRefID string } -func (si *SubmoduleDiffInfo) PopulateURL(diffFile *DiffFile, leftCommit, rightCommit *git.Commit) { +func (si *SubmoduleDiffInfo) PopulateURL(repoLink string, diffFile *DiffFile, leftCommit, rightCommit *git.Commit) { si.SubmoduleName = diffFile.Name submoduleCommit := rightCommit // If the submodule is added or updated, check at the right commit if diffFile.IsDeleted { @@ -30,18 +30,19 @@ func (si *SubmoduleDiffInfo) PopulateURL(diffFile *DiffFile, leftCommit, rightCo return } - submodule, err := submoduleCommit.GetSubModule(diffFile.GetDiffFileName()) + submoduleFullPath := diffFile.GetDiffFileName() + submodule, err := submoduleCommit.GetSubModule(submoduleFullPath) if err != nil { - log.Error("Unable to PopulateURL for submodule %q: GetSubModule: %v", diffFile.GetDiffFileName(), err) + log.Error("Unable to PopulateURL for submodule %q: GetSubModule: %v", submoduleFullPath, err) return // ignore the error, do not cause 500 errors for end users } if submodule != nil { - si.SubmoduleFile = git.NewCommitSubmoduleFile(submodule.URL, submoduleCommit.ID.String()) + si.SubmoduleFile = git.NewCommitSubmoduleFile(repoLink, submoduleFullPath, submodule.URL, submoduleCommit.ID.String()) } } func (si *SubmoduleDiffInfo) CommitRefIDLinkHTML(ctx context.Context, commitID string) template.HTML { - webLink := si.SubmoduleFile.SubmoduleWebLink(ctx, commitID) + webLink := si.SubmoduleFile.SubmoduleWebLinkTree(ctx, commitID) if webLink == nil { return htmlutil.HTMLFormat("%s", base.ShortSha(commitID)) } @@ -49,7 +50,7 @@ func (si *SubmoduleDiffInfo) CommitRefIDLinkHTML(ctx context.Context, commitID s } func (si *SubmoduleDiffInfo) CompareRefIDLinkHTML(ctx context.Context) template.HTML { - webLink := si.SubmoduleFile.SubmoduleWebLink(ctx, si.PreviousRefID, si.NewRefID) + webLink := si.SubmoduleFile.SubmoduleWebLinkCompare(ctx, si.PreviousRefID, si.NewRefID) if webLink == nil { return htmlutil.HTMLFormat("%s...%s", base.ShortSha(si.PreviousRefID), base.ShortSha(si.NewRefID)) } @@ -57,7 +58,7 @@ func (si *SubmoduleDiffInfo) CompareRefIDLinkHTML(ctx context.Context) template. } func (si *SubmoduleDiffInfo) SubmoduleRepoLinkHTML(ctx context.Context) template.HTML { - webLink := si.SubmoduleFile.SubmoduleWebLink(ctx) + webLink := si.SubmoduleFile.SubmoduleWebLinkTree(ctx) if webLink == nil { return htmlutil.HTMLFormat("%s", si.SubmoduleName) } diff --git a/services/gitdiff/submodule_test.go b/services/gitdiff/submodule_test.go index 152c5b7066d..c793969f0e6 100644 --- a/services/gitdiff/submodule_test.go +++ b/services/gitdiff/submodule_test.go @@ -227,7 +227,7 @@ func TestSubmoduleInfo(t *testing.T) { assert.EqualValues(t, "aaaa...bbbb", sdi.CompareRefIDLinkHTML(ctx)) assert.EqualValues(t, "name", sdi.SubmoduleRepoLinkHTML(ctx)) - sdi.SubmoduleFile = git.NewCommitSubmoduleFile("https://github.com/owner/repo", "1234") + sdi.SubmoduleFile = git.NewCommitSubmoduleFile("/any/repo-link", "fullpath", "https://github.com/owner/repo", "1234") assert.EqualValues(t, `1111`, sdi.CommitRefIDLinkHTML(ctx, "1111")) assert.EqualValues(t, `aaaa...bbbb`, sdi.CompareRefIDLinkHTML(ctx)) assert.EqualValues(t, `name`, sdi.SubmoduleRepoLinkHTML(ctx)) diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go index f2cbacbf1c9..27b4cbf56d3 100644 --- a/services/repository/files/tree.go +++ b/services/repository/files/tree.go @@ -154,7 +154,7 @@ func (node *TreeViewNode) sortLevel() int { return util.Iif(node.EntryMode == "tree" || node.EntryMode == "commit", 0, 1) } -func newTreeViewNodeFromEntry(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode { +func newTreeViewNodeFromEntry(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode { node := &TreeViewNode{ EntryName: entry.Name(), EntryMode: entryModeString(entry.Mode()), @@ -172,8 +172,8 @@ func newTreeViewNodeFromEntry(ctx context.Context, renderedIconPool *fileicon.Re if subModule, err := commit.GetSubModule(node.FullPath); err != nil { log.Error("GetSubModule: %v", err) } else if subModule != nil { - submoduleFile := git.NewCommitSubmoduleFile(subModule.URL, entry.ID.String()) - webLink := submoduleFile.SubmoduleWebLink(ctx) + submoduleFile := git.NewCommitSubmoduleFile(repoLink, node.FullPath, subModule.URL, entry.ID.String()) + webLink := submoduleFile.SubmoduleWebLinkTree(ctx) node.SubmoduleURL = webLink.CommitWebLink } } @@ -192,7 +192,7 @@ func sortTreeViewNodes(nodes []*TreeViewNode) { }) } -func listTreeNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) { +func listTreeNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) { entries, err := tree.ListEntries() if err != nil { return nil, err @@ -201,14 +201,14 @@ func listTreeNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconP subPathDirName, subPathRemaining, _ := strings.Cut(subPath, "/") nodes := make([]*TreeViewNode, 0, len(entries)) for _, entry := range entries { - node := newTreeViewNodeFromEntry(ctx, renderedIconPool, commit, treePath, entry) + node := newTreeViewNodeFromEntry(ctx, repoLink, renderedIconPool, commit, treePath, entry) nodes = append(nodes, node) if entry.IsDir() && subPathDirName == entry.Name() { subTreePath := treePath + "/" + node.EntryName if subTreePath[0] == '/' { subTreePath = subTreePath[1:] } - subNodes, err := listTreeNodes(ctx, renderedIconPool, commit, entry.Tree(), subTreePath, subPathRemaining) + subNodes, err := listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), subTreePath, subPathRemaining) if err != nil { log.Error("listTreeNodes: %v", err) } else { @@ -220,10 +220,10 @@ func listTreeNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconP return nodes, nil } -func GetTreeViewNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) { +func GetTreeViewNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) { entry, err := commit.GetTreeEntryByPath(treePath) if err != nil { return nil, err } - return listTreeNodes(ctx, renderedIconPool, commit, entry.Tree(), treePath, subPath) + return listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), treePath, subPath) } diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go index a53f342d404..38ac9f25fc2 100644 --- a/services/repository/files/tree_test.go +++ b/services/repository/files/tree_test.go @@ -64,6 +64,7 @@ func TestGetTreeViewNodes(t *testing.T) { contexttest.LoadGitRepo(t, ctx) defer ctx.Repo.GitRepo.Close() + curRepoLink := "/any/repo-link" renderedIconPool := fileicon.NewRenderedIconPool() mockIconForFile := func(id string) template.HTML { return template.HTML(``) @@ -74,7 +75,7 @@ func TestGetTreeViewNodes(t *testing.T) { mockOpenIconForFolder := func(id string) template.HTML { return template.HTML(``) } - treeNodes, err := GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "") + treeNodes, err := GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "", "") assert.NoError(t, err) assert.Equal(t, []*TreeViewNode{ { @@ -86,7 +87,7 @@ func TestGetTreeViewNodes(t *testing.T) { }, }, treeNodes) - treeNodes, err = GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "docs/README.md") + treeNodes, err = GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "", "docs/README.md") assert.NoError(t, err) assert.Equal(t, []*TreeViewNode{ { @@ -106,7 +107,7 @@ func TestGetTreeViewNodes(t *testing.T) { }, }, treeNodes) - treeNodes, err = GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "docs", "README.md") + treeNodes, err = GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "docs", "README.md") assert.NoError(t, err) assert.Equal(t, []*TreeViewNode{ { diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index b655f735a37..145494aa1a9 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -18,7 +18,7 @@ From 39f145ae72c621536418dd5f40d9322011daa5f7 Mon Sep 17 00:00:00 2001 From: NorthRealm <155140859+NorthRealm@users.noreply.github.com> Date: Fri, 18 Jul 2025 02:12:02 +0800 Subject: [PATCH 44/51] Fix job status aggregation logic (#35000) For a run (assume 2 jobs) that has a failed job and a waiting job, the run status should be waiting, **as the run is not done yet.** Related #34823 --- models/actions/run_job.go | 4 ++-- models/actions/run_job_status_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/models/actions/run_job.go b/models/actions/run_job.go index bad895036d1..e7fa21270c1 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -187,10 +187,10 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status { return StatusCancelled case hasRunning: return StatusRunning - case hasFailure: - return StatusFailure case hasWaiting: return StatusWaiting + case hasFailure: + return StatusFailure case hasBlocked: return StatusBlocked default: diff --git a/models/actions/run_job_status_test.go b/models/actions/run_job_status_test.go index 2a5eb00a6f8..b9ae9f34bfd 100644 --- a/models/actions/run_job_status_test.go +++ b/models/actions/run_job_status_test.go @@ -64,7 +64,7 @@ func TestAggregateJobStatus(t *testing.T) { {[]Status{StatusFailure, StatusSuccess}, StatusFailure}, {[]Status{StatusFailure, StatusSkipped}, StatusFailure}, {[]Status{StatusFailure, StatusCancelled}, StatusCancelled}, - {[]Status{StatusFailure, StatusWaiting}, StatusFailure}, + {[]Status{StatusFailure, StatusWaiting}, StatusWaiting}, {[]Status{StatusFailure, StatusRunning}, StatusRunning}, {[]Status{StatusFailure, StatusBlocked}, StatusFailure}, From 2f138f7a03c9bc3dcbf3dda9423d3b733210082c Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 17 Jul 2025 22:53:03 +0200 Subject: [PATCH 45/51] Increase gap on latest commit (#35104) --- web_src/css/repo.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 0b50d1d1d7a..700650c4f22 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -141,7 +141,7 @@ td .commit-summary { align-items: center; overflow: hidden; text-overflow: ellipsis; - gap: 0.25em; + gap: 0.5em; } @media (max-width: 767.98px) { From 3e8aa52446bdca8c3cbdfdcef9343b2b28039d0b Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Fri, 18 Jul 2025 00:38:45 +0000 Subject: [PATCH 46/51] [skip ci] Updated translations via Crowdin --- options/locale/locale_ga-IE.ini | 32 ++++++++++++++++++++++++++++++++ options/locale/locale_pt-PT.ini | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index ea7e272ac89..23f7d045c8f 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -358,6 +358,7 @@ no_reply_address=Fearann Ríomhphoist Folaite no_reply_address_helper=Ainm fearainn d'úsáideoirí a bhfuil seoladh ríomhphoist i bhfolach acu. Mar shampla, logálfar an t-ainm úsáideora 'joe' i Git mar 'joe@noreply.example.org' má tá an fearainn ríomhphoist i bhfolach socraithe go 'noreply.example.org'. password_algorithm=Algartam Hais Pasfhocal invalid_password_algorithm=Algartam hais pasfhocail neamhbhailí +password_algorithm_helper=Socraigh an algartam haiseála pasfhocail. Bíonn riachtanais agus láidreachtaí difriúla ag halgartaim. Tá an algartam argon2 sách slán ach úsáideann sé go leor cuimhne agus d'fhéadfadh sé a bheith mí-oiriúnach do chórais bheaga. enable_update_checker=Cumasaigh Seiceoir Nuashonraithe enable_update_checker_helper=Seiceálacha ar eisiúintí leagan nua go tréimhsiúil trí nascadh le gitea.io. env_config_keys=Cumraíocht Comhshaoil @@ -451,6 +452,7 @@ use_scratch_code=Úsáid cód scratch twofa_scratch_used=D'úsáid tú do chód scratch. Tá tú atreoraithe chuig an leathanach socruithe dhá fhachtóir ionas gur féidir leat clárú do ghléas a bhaint nó cód scratch nua a ghiniúint. twofa_passcode_incorrect=Tá do phaschód mícheart. Má chuir tú do ghléas míchuir tú, bain úsáid as do chód scratch chun síniú isteach. twofa_scratch_token_incorrect=Tá do chód scratch mícheart. +twofa_required=Ní mór duit Fíordheimhniú Dhá Fhachtóir a shocrú chun rochtain a fháil ar stórtha, nó iarracht a dhéanamh logáil isteach arís. login_userpass=Sínigh isteach login_openid=OpenID oauth_signup_tab=Cláraigh Cuntas Nua @@ -462,6 +464,7 @@ oauth_signin_submit=Cuntas Nasc oauth.signin.error.general=Tharla earráid agus an t-iarratas údaraithe á phróiseáil: %s. Má leanann an earráid seo, téigh i dteagmháil le riarthóir an tsuímh. oauth.signin.error.access_denied=Diúltaíodh an t-iarratas ar údarú. oauth.signin.error.temporarily_unavailable=Theip ar údarú toisc nach bhfuil an fhreastalaí fíordheimhnithe ar fáil Bain triail as arís níos déanaí. +oauth_callback_unable_auto_reg=Tá Clárú Uathoibríoch cumasaithe, ach thug Soláthraí OAuth2 %[1]s réimsí ar iarraidh ar ais: %[2]s, ní féidir cuntas a chruthú go huathoibríoch. Cruthaigh cuntas nó déan nasc leis, nó déan teagmháil le riarthóir an tsuímh. openid_connect_submit=Ceangail openid_connect_title=Ceangail le cuntas atá ann cheana openid_connect_desc=Níl an URI OpenID roghnaithe ar eolas. Comhcheangail é le cuntas nua anseo. @@ -474,6 +477,7 @@ email_domain_blacklisted=Ní féidir leat clárú le do sheoladh ríomhphoist. authorize_application=Údaraigh an Feidhmchlár authorize_redirect_notice=Déanfar tú a atreorú chuig %s má údaraíonn tú an feidhmchlár seo. authorize_application_created_by=Chruthaigh %s an feidhmchlár seo. +authorize_application_description=Má dheonaíonn tú rochtain, beidh sé in ann rochtain a fháil ar fhaisnéis do chuntais go léir agus scríobh chuici, lena n-áirítear stórtha príobháideacha agus eagraíochtaí. authorize_application_with_scopes=Le scóip: %s authorize_title=Údaraigh "%s" chun rochtain a fháil ar do chuntas? authorization_failed=Theip ar údarú @@ -503,6 +507,7 @@ activate_email.text=Cliceáil ar an nasc seo a leanas le do sheoladh ríomhphois register_notify=Fáilte go dtí %s register_notify.title=%[1]s, fáilte go %[2]s register_notify.text_1=Seo do ríomhphost deimhnithe clárúcháin le haghaidh %s! +register_notify.text_2=Is féidir leat logáil isteach anois tríd an ainm úsáideora: %s. register_notify.text_3=Má cruthaíodh an cuntas seo duit, socraigh do phasfhocal ar dtús. reset_password=Aisghabháil do chuntas @@ -684,6 +689,7 @@ form.name_chars_not_allowed=Tá carachtair neamhbhailí in ainm úsáideora "%s" block.block=Bloc block.block.user=Bloc úsáideoir +block.block.org=Bac úsáideoir ón eagraíocht block.block.failure=Theip ar an úsáideoir a bhac: %s block.unblock=Díbhlocáil block.unblock.failure=Theip ar an úsáideoir a díbhlocáil: %s @@ -727,6 +733,7 @@ webauthn=Fíordheimhniú Dhá-Fachtóir (Eochracha Slándála) public_profile=Próifíl Phoiblí biography_placeholder=Inis dúinn beagán fút féin! (Is féidir leat Markdown a úsáid) location_placeholder=Comhroinn do shuíomh thart le daoine eile +profile_desc=Rialú conas a thaispeántar do phróifíl d'úsáideoirí eile. Úsáidfear do phríomhsheoladh ríomhphoist le haghaidh fógraí, aisghabháil pasfhocal agus oibríochtaí Git gréasánbhunaithe. password_username_disabled=Níl cead agat d'ainm úsáideora a athrú. Déan teagmháil le do riarthóir suímh le haghaidh tuilleadh sonraí. password_full_name_disabled=Níl cead agat d’ainm iomlán a athrú. Déan teagmháil le do riarthóir suímh le haghaidh tuilleadh sonraí. full_name=Ainm Iomlán @@ -805,6 +812,7 @@ activations_pending=Gníomhartha ar Feitheamh can_not_add_email_activations_pending=Tá gníomhachtú ar feitheamh, déan iarracht arís i gceann cúpla nóiméad más mian leat ríomhphost nua a chur leis. delete_email=Bain email_deletion=Bain Seoladh R-phoist +email_deletion_desc=Bainfear an seoladh ríomhphoist seo agus faisnéis ghaolmhar as do chuntas. Fanfaidh na gealltanais Git ón seoladh ríomhphoist seo gan athrú. Ar mhaith leat leanúint ar aghaidh? email_deletion_success=Tá an seoladh ríomhphoist bainte. theme_update_success=Nuashonraíodh do théama. theme_update_error=Níl an téama roghnaithe ann. @@ -1013,6 +1021,8 @@ email_notifications.onmention=Ríomhphost amháin ar luaigh email_notifications.disable=Díchumasaigh Fógraí Ríomhphoist email_notifications.submit=Socraigh rogha ríomhphoist email_notifications.andyourown=Agus Do Fógraí Féin +email_notifications.actions.desc=Fógraí le haghaidh rith sreabha oibre ar stórtha atá socraithe le Gníomhartha Gitea. +email_notifications.actions.failure_only=Fógra a thabhairt ach amháin i gcás ritheanna sreabha oibre nár éirigh leo visibility=Infheictheacht úsáideora visibility.public=Poiblí @@ -1094,6 +1104,7 @@ mirror_sync=sioncronaithe mirror_sync_on_commit=Sioncrónaigh nuair a bhrúitear geallúintí mirror_address=Clón Ó URL mirror_address_desc=Cuir aon dhintiúir riachtanacha sa chuid Údaraithe. +mirror_address_url_invalid=Tá an URL a cuireadh ar fáil neamhbhailí. Cinntigh go bhfuil gach comhpháirt den URL escaped i gceart. mirror_address_protocol_invalid=Tá an URL curtha ar fáil neamhbhailí. Ní féidir ach suíomhanna http (s)://nó git://a úsáid le haghaidh scátháin. mirror_lfs=Stóráil Comhad Móra (LFS) mirror_lfs_desc=Gníomhachtaigh scáthú sonraí LFS. @@ -1153,6 +1164,8 @@ template.issue_labels=Lipéid Eisiúna template.one_item=Ní mór mír teimpléad amháin ar a laghad a roghnú template.invalid=Ní mór stór teimpléad a roghnú +archive.title=Tá an stór seo cartlannaithe. Is féidir leat comhaid a fheiceáil agus é a chlónáil. Ní féidir leat saincheisteanna a oscailt, iarratais a tharraingt ná tiomnú a bhrú. +archive.title_date=Tá an stórlann seo cartlannaithe ar %s. Is féidir leat comhaid a fheiceáil agus é a chlónáil. Ní féidir leat saincheisteanna a oscailt, iarratais a tharraingt ná tiomnú a bhrú. archive.issue.nocomment=Tá an stóras seo i gcartlann. Ní féidir leat trácht a dhéanamh ar shaincheisteanna. archive.pull.nocomment=Tá an stóras seo i gcartlann. Ní féidir leat trácht a dhéanamh ar iarratais tarraingthe. @@ -1181,6 +1194,7 @@ migrate_items_releases=Eisiúintí migrate_repo=Stóras Imirc migrate.clone_address=Aimirce/ Clón Ó URL migrate.clone_address_desc=An URL 'clón' HTTP(S) nó Git de stóras atá ann cheana +migrate.github_token_desc=Is féidir leat comhartha amháin nó níos mó a chur anseo scartha le camóga chun an t-aistriú a dhéanamh níos tapúla trí theorainn ráta API GitHub a sheachaint. RABHADH: D’fhéadfadh mí-úsáid a bhaint as an ngné seo sárú a dhéanamh ar pholasaí an tsoláthraí seirbhíse agus d’fhéadfadh sé go gcuirfí bac ar do chuntas(í). migrate.clone_local_path=nó cosán freastalaí áitiúil migrate.permission_denied=Ní cheadaítear duit stórais áitiúla a iompórtáil. migrate.permission_denied_blocked=Ní féidir leat allmhairiú ó óstaigh neamh-cheadaithe, iarr ar an riarachán socruithe ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS a sheiceáil le do thoil. @@ -1242,6 +1256,7 @@ create_new_repo_command=Stóras nua a chruthú ar an líne ordaithe push_exist_repo=Stóras atá ann cheana a bhrú ón líne ordaithe empty_message=Níl aon ábhar sa stóras seo. broken_message=Ní féidir na sonraí Git atá mar bhunús leis an stóras seo a léamh. Déan teagmháil le riarthóir an chás seo nó scrios an stóras seo. +no_branch=Níl aon bhrainse ag an stóras seo. code=Cód code.desc=Rochtain ar chód foinse, comhaid, gealltanais agus brainsí. @@ -1387,6 +1402,8 @@ editor.failed_to_commit=Theip ar athruithe a chur i bhfeidhm. editor.failed_to_commit_summary=Teachtaireacht Earráide: editor.fork_create=Stóras Forc chun Athruithe a Mholadh +editor.fork_create_description=Ní féidir leat an stórlann seo a chur in eagar go díreach. Ina áit sin, is féidir leat forc a chruthú, eagarthóireachtaí a dhéanamh agus iarratas tarraingthe a chruthú. +editor.fork_edit_description=Ní féidir leat an stórlann seo a chur in eagar go díreach. Scríobhfar na hathruithe chuig do fhorc %s, ionas gur féidir leat iarratas tarraingthe a chruthú. editor.fork_not_editable=Tá forc déanta agat ar an stóras seo ach ní féidir do fhorc a chur in eagar. editor.fork_failed_to_push_branch=Theip ar bhrainse %s a bhrú chuig do stóras. editor.fork_branch_exists=Tá brainse "%s" ann cheana féin i do fhorc, roghnaigh ainm brainse nua le do thoil. @@ -1685,6 +1702,7 @@ issues.lock_no_reason=comhrá faoi ghlas agus teoranta do chomhoibrithe %s issues.unlock_comment=an comhrá seo a dhíghlasáil %s issues.lock_confirm=Glas issues.unlock_confirm=Díghlasáil +issues.lock.notice_1=- Ní féidir le húsáideoirí eile tuairimí nua a chur leis an gceist seo. issues.lock.notice_2=- Is féidir leatsa agus le comhoibrithe eile a bhfuil rochtain acu ar an stór seo fós tuairimí a fhágáil a fheiceann daoine eile. issues.lock.notice_3=- Is féidir leat an tsaincheist seo a dhíghlasáil arís sa todhchaí. issues.unlock.notice_1=- Bheadh gach duine in ann trácht a dhéanamh ar an gceist seo arís. @@ -1853,6 +1871,7 @@ pulls.select_commit_hold_shift_for_range=Roghnaigh tiomantas. Coinnigh shift + c pulls.review_only_possible_for_full_diff=Ní féidir athbhreithniú a dhéanamh ach amháin nuair a bhreathnaítear ar an difríocht iomlán pulls.filter_changes_by_commit=Scagaigh de réir tiomantas pulls.nothing_to_compare=Tá na brainsí seo cothrom. Ní gá iarratas tarraingthe a chruthú. +pulls.nothing_to_compare_have_tag=Tá na brainsí/clibeanna roghnaithe comhionann. pulls.nothing_to_compare_and_allow_empty_pr=Tá na brainsí seo cothrom. Beidh an PR seo folamh. pulls.has_pull_request=`Tá iarratas tarraingthe idir na brainsí seo ann cheana: %[2]s#%[3]d` pulls.create=Cruthaigh Iarratas Tarraing @@ -2015,6 +2034,7 @@ milestones.filter_sort.most_issues=Saincheisteanna is mó milestones.filter_sort.least_issues=Saincheisteanna is lú signing.will_sign=Síneofar an gealltanas seo le heochair "%s". +signing.wont_sign.error=Tharla earráid agus seiceáil á dhéanamh an bhféadfaí an tiomnú a shíniú. signing.wont_sign.nokey=Níl aon eochair ar fáil chun an tiomantas seo a shíniú. signing.wont_sign.never=Ní shínítear tiomáintí riamh. signing.wont_sign.always=Sínítear tiomáintí i gcónaí. @@ -2523,6 +2543,7 @@ settings.block_on_official_review_requests_desc=Ní bheidh sé indéanta cumasc settings.block_outdated_branch=Cuir bac ar chumasc má tá an t-iarratas tarraingthe as dáta settings.block_outdated_branch_desc=Ní bheidh cumasc indéanta nuair a bhíonn ceannbhrainse taobh thiar de bhronnbhrainse. settings.block_admin_merge_override=Ní mór do riarthóirí rialacha cosanta brainse a leanúint +settings.block_admin_merge_override_desc=Ní mór do riarthóirí rialacha cosanta brainse a leanúint agus ní féidir leo iad a sheachaint. settings.default_branch_desc=Roghnaigh brainse stóras réamhshocraithe le haghaidh iarratas tarraingte agus geallann an cód: settings.merge_style_desc=Stíleanna Cumaisc settings.default_merge_style_desc=Stíl Cumaisc Réamhshocraithe @@ -2549,7 +2570,10 @@ settings.matrix.homeserver_url=URL sheirbhíse baile settings.matrix.room_id=ID seomra settings.matrix.message_type=Cineál teachtaireachta settings.visibility.private.button=Déan Príobháideach +settings.visibility.private.text=Má athraítear an infheictheacht go príobháideach, ní bheidh an stór le feiceáil ach ag baill cheadaithe agus d’fhéadfadh sé go mbainfí an gaol idir é agus forcanna, faireoirí agus réaltaí atá ann cheana féin. settings.visibility.private.bullet_title=An infheictheacht a athrú go toil phríobháide +settings.visibility.private.bullet_one=Déan an stóras le feiceáil ag baill cheadaithe amháin. +settings.visibility.private.bullet_two=D’fhéadfadh sé an gaol idir é agus forcanna, faireoirí, agus réaltaí a bhaint. settings.visibility.public.button=Déan Poiblí settings.visibility.public.text=Má athraíonn an infheictheacht don phobal, beidh an stóras le feiceáil do dhuine ar bith. settings.visibility.public.bullet_title=Athróidh an infheictheacht go poiblí: @@ -2804,6 +2828,7 @@ team_permission_desc=Cead team_unit_desc=Ceadaigh Rochtain ar Rannóga Stóras team_unit_disabled=(Díchumasaithe) +form.name_been_taken=Tá ainm na heagraíochta "%s" tógtha cheana féin. form.name_reserved=Tá an t-ainm eagraíochta "%s" curtha in áirithe. form.name_pattern_not_allowed=Ní cheadaítear an patrún "%s" in ainm eagraíochta. form.create_org_not_allowed=Níl cead agat eagraíocht a chruthú. @@ -2845,6 +2870,7 @@ settings.delete_notices_2=Scriosfaidh an oibríocht seo go buan gach st settings.delete_notices_3=Scriosfaidh an oibríocht seo gach pacáiste de chuid %s go buan. settings.delete_notices_4=Scriosfaidh an oibríocht seo gach tionscadal de chuid %s go buan. settings.confirm_delete_account=Deimhnigh scriosadh +settings.delete_failed=Theip ar Scriosadh Eagraíochta mar gheall ar earráid inmheánach settings.delete_successful=Scriosadh an eagraíocht %s go rathúil. settings.hooks_desc=Cuir crúcaí gréasán in leis a spreagfar do gach stóras faoin eagraíocht seo. @@ -2994,6 +3020,9 @@ dashboard.resync_all_sshprincipals=Nuashonraigh an comhad '.ssh/authorized_princ dashboard.resync_all_hooks=Athshioncrónaigh crúcaí réamhfhála, nuashonraithe agus iar-fhála na stórtha go léir. dashboard.reinit_missing_repos=Aththosaigh gach stórais Git atá in easnamh a bhfuil taifid ann dóibh dashboard.sync_external_users=Sioncrónaigh sonraí úsáideoirí seachtracha +dashboard.cleanup_hook_task_table=Glan suas an tábla hook_task +dashboard.cleanup_packages=Glan suas pacáistí atá imithe in éag +dashboard.cleanup_actions=Glan suas acmhainní gníomhartha atá imithe in éag dashboard.server_uptime=Aga fónaimh Freastalaí dashboard.current_goroutine=Goroutines Reatha dashboard.current_memory_usage=Úsáid Cuimhne Reatha @@ -3430,6 +3459,7 @@ monitor.start=Am Tosaigh monitor.execute_time=Am Forghníomhaithe monitor.last_execution_result=Toradh monitor.process.cancel=Cealaigh próiseas +monitor.process.cancel_desc=D’fhéadfadh caillteanas sonraí a bheith mar thoradh ar phróiseas a chealú monitor.process.children=Leanaí monitor.queues=Scuaineanna @@ -3701,6 +3731,7 @@ owner.settings.cargo.initialize.success=Cruthaíodh an t-innéacs Cargo go rath owner.settings.cargo.rebuild=Innéacs Atógáil owner.settings.cargo.rebuild.description=Is féidir atógáil a bheith úsáideach mura bhfuil an t-innéacs sioncronaithe leis na pacáistí Cargo stóráilte. owner.settings.cargo.rebuild.error=Níorbh fhéidir an t-innéacs Cargo a atógáil: %v +owner.settings.cargo.rebuild.success=Rinneadh innéacs an Charga a atógáil go rathúil. owner.settings.cleanuprules.title=Bainistigh Rialacha Glanta owner.settings.cleanuprules.add=Cuir Riail Glantacháin leis owner.settings.cleanuprules.edit=Cuir Riail Glantacháin in eagar @@ -3788,6 +3819,7 @@ runners.delete_runner=Scrios an reathaí seo runners.delete_runner_success=Scriosadh an reathaí go rathúil runners.delete_runner_failed=Theip ar an reathaí a scriosadh runners.delete_runner_header=Deimhnigh an reathaí seo a scriosadh +runners.delete_runner_notice=Má tá tasc á rith ar an rithóir seo, cuirfear deireadh leis agus marcálfar é mar theip air. D’fhéadfadh sé seo cur isteach ar shreabhadh oibre na tógála. runners.none=Níl aon reathaí ar fáil runners.status.unspecified=Anaithnid runners.status.idle=Díomhaoin diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index d4c5753834d..c5dd56e40a1 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -358,6 +358,7 @@ no_reply_address=Domínio dos emails ocultos no_reply_address_helper=Nome de domínio para utilizadores com um endereço de email oculto. Por exemplo, o nome de utilizador 'silva' será registado no Git como 'silva@semresposta.exemplo.org' se o domínio de email oculto estiver definido como 'semresposta.exemplo.org'. password_algorithm=Algoritmo de Hash da Senha invalid_password_algorithm=Algoritmo de hash da senha inválido +password_algorithm_helper=Definir o algoritmo de hash da senha. Os algoritmos têm requisitos e resistências distintos. `argon2` é bastante seguro, mas usa muita memória e pode ser inapropriado para sistemas pequenos. enable_update_checker=Habilitar verificador de novidades enable_update_checker_helper=Verifica, periodicamente, se foi lançada alguma versão nova, fazendo uma ligação ao gitea.io. env_config_keys=Configuração do ambiente @@ -451,6 +452,7 @@ use_scratch_code=Usar um código de recuperação twofa_scratch_used=Você usou o seu código de recuperação. Foi reencaminhado para a página de configurações da autenticação em dois passos para poder remover o registo do seu dispositivo ou gerar um novo código de recuperação. twofa_passcode_incorrect=A senha está errada. Se perdeu o seu dispositivo, use o código de recuperação para iniciar a sessão. twofa_scratch_token_incorrect=O código de recuperação está errado. +twofa_required=Tem de configurar a autenticação em dois passos para obter acesso aos repositórios ou então tentar iniciar a sessão novamente. login_userpass=Iniciar sessão login_openid=OpenID oauth_signup_tab=Fazer inscrição @@ -462,6 +464,7 @@ oauth_signin_submit=Vincular conta oauth.signin.error.general=Ocorreu um erro durante o processamento do pedido de autorização: %s: Se este erro persistir, contacte o administrador. oauth.signin.error.access_denied=O pedido de autorização foi negado. oauth.signin.error.temporarily_unavailable=A autorização falhou porque o servidor de autenticação está temporariamente indisponível. Tente mais tarde. +oauth_callback_unable_auto_reg=O registo automático está habilitado, mas o fornecedor OAuth2 %[1]s sinalizou campos em falta: %[2]s, por isso não foi possível criar uma conta automaticamente. Crie ou vincule uma conta ou contacte o administrador do sítio. openid_connect_submit=Estabelecer ligação openid_connect_title=Estabelecer ligação a uma conta existente openid_connect_desc=O URI do OpenID escolhido é desconhecido. Associe-o a uma nova conta aqui. @@ -474,6 +477,7 @@ email_domain_blacklisted=Não pode fazer um registo com o seu endereço de email authorize_application=Autorizar aplicação authorize_redirect_notice=Irá ser reencaminhado para %s se autorizar esta aplicação. authorize_application_created_by=Esta aplicação foi criada por %s. +authorize_application_description=Se conceder o acesso, a aplicação terá privilégios para alterar toda a informação da conta, incluindo repositórios e organizações privados. authorize_application_with_scopes=Com âmbitos: %s authorize_title=Autorizar o acesso de "%s" à sua conta? authorization_failed=A autorização falhou @@ -503,6 +507,7 @@ activate_email.text=Por favor clique na seguinte ligação para validar o seu en register_notify=Bem-vindo(a) a %s register_notify.title=%[1]s, bem-vindo(a) a %[2]s register_notify.text_1=Este é o seu email de confirmação de registo para %s! +register_notify.text_2=Agora pode iniciar a sessão com o nome de utilizador: %s. register_notify.text_3=Se esta conta foi criada para si, defina a sua senha primeiro. reset_password=Recupere a sua conta @@ -684,6 +689,7 @@ form.name_chars_not_allowed=O nome de utilizador "%s" contém caracteres inváli block.block=Bloquear block.block.user=Bloquear utilizador +block.block.org=Bloquear utilizador da organização block.block.failure=Falhou o bloqueio do utilizador: %s block.unblock=Desbloquear block.unblock.failure=Falhou o desbloqueio do utilizador: %s @@ -727,6 +733,7 @@ webauthn=Autenticação em dois passos (Chaves de Segurança) public_profile=Perfil público biography_placeholder=Conte-nos um pouco sobre si! (Pode usar Markdown) location_placeholder=Partilhe a sua localização aproximada com outros +profile_desc=Controle como o seu perfil é apresentado aos outros utilizadores. O seu endereço de email principal será usado para notificações, recuperação de senha e operações Git baseadas na web. password_username_disabled=Não tem permissão para alterar o seu nome de utilizador. Entre em contacto com o administrador para saber mais detalhes. password_full_name_disabled=Não tem permissão para alterar o seu nome completo. Entre em contacto com o administrador para saber mais detalhes. full_name=Nome completo @@ -805,6 +812,7 @@ activations_pending=Habilitações pendentes can_not_add_email_activations_pending=Existe uma validação pendente. Tente de novo dentro de alguns minutos, se quiser adicionar um novo email. delete_email=Remover email_deletion=Remover endereço de email +email_deletion_desc=Este endereço de email e informações relacionadas serão removidos da sua conta. Os cometimentos feitos no Git com este endereço de email permanecerão inalterados. Quer continuar? email_deletion_success=O endereço de email foi removido. theme_update_success=O seu tema foi substituído. theme_update_error=O tema escolhido não existe. @@ -1013,6 +1021,8 @@ email_notifications.onmention=Enviar email somente quando mencionado(a) email_notifications.disable=Desabilitar notificações por email email_notifications.submit=Definir preferência do email email_notifications.andyourown=e as suas próprias notificações +email_notifications.actions.desc=Notificações para sequências de trabalho são executadas em repositórios configurados com Gitea Actions. +email_notifications.actions.failure_only=Notificar apenas as execuções de sequências de trabalho falhadas visibility=Visibilidade do utilizador visibility.public=Pública @@ -1094,6 +1104,7 @@ mirror_sync=sincronizado mirror_sync_on_commit=Sincronizar quando forem enviados cometimentos mirror_address=Clonar a partir do URL mirror_address_desc=Coloque, na secção de autorização, as credenciais que, eventualmente, sejam necessárias. +mirror_address_url_invalid=O URL fornecido é inválido. Certifique-se que codifica adequadamente todos os componentes do URL. mirror_address_protocol_invalid=O URL fornecido é inválido. Só se pode replicar a partir de endereços http(s):// ou git://. mirror_lfs=Armazenamento de Ficheiros Grandes (LFS) mirror_lfs_desc=Habilitar a réplica de dados LFS. @@ -1153,6 +1164,8 @@ template.issue_labels=Rótulos das questões template.one_item=Tem que escolher pelo menos um item do modelo template.invalid=Tem que escolher um repositório modelo +archive.title=Este repositório está arquivado. Pode ver os seus ficheiros e cloná-lo. Não pode lançar questões, fazer pedidos de integração nem fazer envios. +archive.title_date=Este repositório foi arquivado em %s. Pode ver os ficheiros e cloná-lo. Não pode abrir questões, fazer pedidos de integração nem enviar cometimentos. archive.issue.nocomment=Este repositório está arquivado. Não pode comentar nas questões. archive.pull.nocomment=Este repositório está arquivado. Não pode comentar nos pedidos de integração. @@ -1181,6 +1194,7 @@ migrate_items_releases=Lançamentos migrate_repo=Migrar o repositório migrate.clone_address=Migrar / clonar a partir do URL migrate.clone_address_desc=O URL de clonagem HTTP(S) ou Git de um repositório existente +migrate.github_token_desc=Pode colocar aqui um ou mais códigos separados por vírgulas para tornar mais rápida a migração, para compensar a limitação de velocidade da API do GitHub. AVISO: O abuso desta funcionalidade poderá violar a política do seu fornecedor de serviço e levar ao bloqueio da(s) sua(a) conta(s). migrate.clone_local_path=ou uma localização no servidor local migrate.permission_denied=Não está autorizado a importar repositórios locais. migrate.permission_denied_blocked=Não pode importar de servidores não permitidos, por favor peça ao administrador para verificar as configurações ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS. @@ -1242,6 +1256,7 @@ create_new_repo_command=Criando um novo repositório na linha de comandos push_exist_repo=Enviando, pela linha de comandos, um repositório existente empty_message=Este repositório não contém qualquer conteúdo. broken_message=Os dados Git subjacentes a este repositório não podem ser lidos. Contacte o administrador desta instância ou elimine este repositório. +no_branch=Este repositório não tem quaisquer ramos. code=Código code.desc=Aceder ao código fonte, ficheiros, cometimentos e ramos. @@ -1387,6 +1402,8 @@ editor.failed_to_commit=Falhou ao cometer as modificações. editor.failed_to_commit_summary=Mensagem de erro: editor.fork_create=Faça uma derivação do repositório para propor modificações +editor.fork_create_description=Não pode editar este repositório. Ao invés disso, crie uma derivação, faça as modificações nessa derivação e crie um pedido de integração. +editor.fork_edit_description=Não pode editar este repositório. As modificações irão ser escritas na sua derivação %s, para que possa criar um pedido de integração. editor.fork_not_editable=Fez uma derivação deste repositório, mas a sua derivação não é editável. editor.fork_failed_to_push_branch=Falhou ao enviar o ramo %s para o seu repositório. editor.fork_branch_exists=O ramo "%s" já existe na sua derivação, escolha outro nome para o ramo. @@ -1685,6 +1702,7 @@ issues.lock_no_reason=bloqueou o diálogo e restringiu-o aos colaboradores %s issues.unlock_comment=desbloqueou este diálogo %s issues.lock_confirm=Bloquear issues.unlock_confirm=Desbloquear +issues.lock.notice_1=- Os outros utilizadores não podem adicionar novos comentários a esta questão. issues.lock.notice_2=- Você e outros colaboradores com acesso a este repositório ainda poderão deixar comentários que outros possam ver. issues.lock.notice_3=- Poderá sempre voltar a desbloquear esta questão no futuro. issues.unlock.notice_1=- Toda gente poderá voltar a comentar nesta questão. @@ -1853,6 +1871,7 @@ pulls.select_commit_hold_shift_for_range=Escolha o comentimento. Mantenha premid pulls.review_only_possible_for_full_diff=A revisão só é possível ao visualizar o diff completo pulls.filter_changes_by_commit=Filtrar por cometimento pulls.nothing_to_compare=Estes ramos são iguais. Não há necessidade de criar um pedido de integração. +pulls.nothing_to_compare_have_tag=Os ramos/etiquetas escolhidos são iguais. pulls.nothing_to_compare_and_allow_empty_pr=Estes ramos são iguais. Este pedido de integração ficará vazio. pulls.has_pull_request=`Já existe um pedido de integração entre estes ramos: %[2]s#%[3]d` pulls.create=Criar um pedido de integração @@ -2015,6 +2034,7 @@ milestones.filter_sort.most_issues=Mais questões milestones.filter_sort.least_issues=Menos questões signing.will_sign=Este cometimento irá ser assinado com a chave "%s". +signing.wont_sign.error=Ocorreu um erro enquanto estava a ser verificado se o cometimento poderia ser assinado. signing.wont_sign.nokey=Não existe qualquer chave disponível para assinar este cometimento. signing.wont_sign.never=Os cometimentos nunca são assinados. signing.wont_sign.always=Os cometimentos são sempre assinados. @@ -2523,6 +2543,7 @@ settings.block_on_official_review_requests_desc=A integração não será possí settings.block_outdated_branch=Bloquear integração se o pedido de integração for obsoleto settings.block_outdated_branch_desc=A integração não será possível quando o ramo de topo estiver abaixo do ramo base. settings.block_admin_merge_override=Os administradores têm de cumprir as regras de salvaguarda dos ramos +settings.block_admin_merge_override_desc=Os administradores têm de cumprir as regras de salvaguarda do ramo e não as podem contornar. settings.default_branch_desc=Escolha um ramo do repositório como sendo o predefinido para pedidos de integração e cometimentos: settings.merge_style_desc=Estilos de integração settings.default_merge_style_desc=Tipo de integração predefinido @@ -2549,7 +2570,10 @@ settings.matrix.homeserver_url=URL do servidor caseiro settings.matrix.room_id=ID da sala settings.matrix.message_type=Tipo de mensagem settings.visibility.private.button=Tornar privado +settings.visibility.private.text=Mudar a visibilidade para privado irá tornar o repositório visível somente para membros autorizados e poderá remover a relação entre o repositório e derivações existentes, vigilâncias e favoritos. settings.visibility.private.bullet_title=Mudar a visibilidade para privado irá: +settings.visibility.private.bullet_one=Tornar o repositório visível somente para membros autorizados. +settings.visibility.private.bullet_two=Poderá remover a relação entre o repositório e derivações, vigilâncias e favoritos. settings.visibility.public.button=Tornar público settings.visibility.public.text=Mudar a visibilidade para público irá tornar o repositório visível para qualquer pessoa. settings.visibility.public.bullet_title=Mudar a visibilidade para público irá: @@ -2804,6 +2828,7 @@ team_permission_desc=Permissão team_unit_desc=Permitir acesso às secções do repositório team_unit_disabled=(desabilitada) +form.name_been_taken=O nome da organização "%s" já foi tomado. form.name_reserved=O nome de organização "%s" está reservado. form.name_pattern_not_allowed=O padrão "%s" não é permitido no nome de uma organização. form.create_org_not_allowed=Não tem permissão para criar uma organização. @@ -2845,6 +2870,7 @@ settings.delete_notices_2=Esta operação irá eliminar de forma permanente todo settings.delete_notices_3=Esta operação irá eliminar de forma permanente todos os pacotes de %s. settings.delete_notices_4=Esta operação irá eliminar de forma permanente todos os planeamentos de %s. settings.confirm_delete_account=Confirme a eliminação +settings.delete_failed=A eliminação da organização falhou por causa de um erro interno settings.delete_successful=A organização %s foi eliminada com sucesso. settings.hooks_desc=Adicionar automatismos web que serão despoletados para todos os repositórios desta organização. @@ -2994,6 +3020,9 @@ dashboard.resync_all_sshprincipals=Modificar o ficheiro '.ssh/authorized_princip dashboard.resync_all_hooks=Voltar a sincronizar automatismos de pré-acolhimento, modificação e pós-acolhimento de todos os repositórios. dashboard.reinit_missing_repos=Reinicializar todos os repositórios Git em falta para os quais existam registos dashboard.sync_external_users=Sincronizar dados externos do utilizador +dashboard.cleanup_hook_task_table=Limpar a tabela hook_task +dashboard.cleanup_packages=Limpar pacotes expirados +dashboard.cleanup_actions=Limpar recursos das operações expirados dashboard.server_uptime=Tempo em funcionamento contínuo do servidor dashboard.current_goroutine=Goroutines em execução dashboard.current_memory_usage=Utilização de memória corrente @@ -3430,6 +3459,7 @@ monitor.start=Início monitor.execute_time=Tempo de execução monitor.last_execution_result=Resultado monitor.process.cancel=Cancelar processo +monitor.process.cancel_desc=Cancelar um processo pode resultar na perda de dados monitor.process.children=Descendentes monitor.queues=Filas @@ -3701,6 +3731,7 @@ owner.settings.cargo.initialize.success=O índice do Cargo foi criado com sucess owner.settings.cargo.rebuild=Reconstruir índice owner.settings.cargo.rebuild.description=Reconstruir pode ser útil se o índice não estiver sincronizado com os pacotes de Cargo armazenados. owner.settings.cargo.rebuild.error=Falhou ao reconstruir o índice do Cargo: %v +owner.settings.cargo.rebuild.success=O índice do Cargo foi reconstruído com sucesso. owner.settings.cleanuprules.title=Gerir regras de limpeza owner.settings.cleanuprules.add=Adicionar regra de limpeza owner.settings.cleanuprules.edit=Editar regra de limpeza @@ -3788,6 +3819,7 @@ runners.delete_runner=Eliminar este executor runners.delete_runner_success=O executor foi eliminado com sucesso runners.delete_runner_failed=Falhou ao eliminar o executor runners.delete_runner_header=Confirme que quer eliminar este executor +runners.delete_runner_notice=Se uma tarefa estiver a correr neste executor, será terminada e marcada como tendo falhado. Isso poderá quebrar a sequência de trabalho de construção. runners.none=Não há executores disponíveis runners.status.unspecified=Desconhecido runners.status.idle=Parado From 13b9659952f72cbbca05a35fd90375cb80c0841f Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 18 Jul 2025 11:13:32 +0200 Subject: [PATCH 47/51] Align `issue-title-buttons` with `list-header` (#35018) This change concerns the "Edit" and "New Issue" button on top right. With this change, switching from the issue list into an issue, the "New Issue" button will no longer "shift" from the postion on the previous page. Screenshot 2025-07-09 at 17 37 31 Screenshot 2025-07-09 at 17 37 19 --------- Signed-off-by: silverwind Co-authored-by: wxiaoguang --- web_src/css/repo.css | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 700650c4f22..5238e3a2e52 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -334,7 +334,7 @@ td .commit-summary { display: flex; gap: 0.5em; margin-bottom: 8px; - min-height: 40px; /* avoid layout shift on edit */ + min-height: 36px; /* avoid layout shift on edit */ } .repository.view.issue .issue-title h1 { @@ -342,20 +342,12 @@ td .commit-summary { width: 100%; font-weight: var(--font-weight-normal); font-size: 32px; - line-height: 40px; + line-height: 36px; /* vertically center single-line text with .issue-title-buttons */ margin: 0; padding-right: 0.25rem; overflow-wrap: anywhere; } -.repository.view.issue .issue-title#issue-title-display .issue-title-buttons { - margin-top: 4px; /* the title's height is 40px, fine tune to align the buttons */ -} - -.repository.view.issue .issue-title#issue-title-editor { - padding-top: 4px; -} - @media (max-width: 767.98px) { .repository.view.issue .issue-title { flex-direction: column; From 8f91bfe9d82e6da43c12a60b5a54f910e076a560 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 18 Jul 2025 17:42:44 +0800 Subject: [PATCH 48/51] Fix submodule parsing when the gitmodules is missing (#35109) Follow up #35096, fix #35095, fix #35115 and add more tests The old code used some fragile behaviors which depend on the "nil" receiver. This PR should be a complete fix for more edge cases. --- modules/git/commit_info.go | 6 +++++- modules/git/commit_info_test.go | 9 ++++++++- modules/git/commit_submodule_file.go | 13 ++++++++----- modules/git/commit_submodule_file_test.go | 2 ++ routers/web/repo/view_home.go | 3 ++- services/repository/files/tree.go | 4 +++- 6 files changed, 28 insertions(+), 9 deletions(-) diff --git a/modules/git/commit_info.go b/modules/git/commit_info.go index 9079e366777..b44e9fa51da 100644 --- a/modules/git/commit_info.go +++ b/modules/git/commit_info.go @@ -15,8 +15,12 @@ type CommitInfo struct { func getCommitInfoSubmoduleFile(repoLink string, entry *TreeEntry, commit *Commit, treePathDir string) (*CommitSubmoduleFile, error) { fullPath := path.Join(treePathDir, entry.Name()) submodule, err := commit.GetSubModule(fullPath) - if submodule == nil || err != nil { + if err != nil { return nil, err } + if submodule == nil { + // unable to find submodule from ".gitmodules" file + return NewCommitSubmoduleFile(repoLink, fullPath, "", entry.ID.String()), nil + } return NewCommitSubmoduleFile(repoLink, fullPath, submodule.URL, entry.ID.String()), nil } diff --git a/modules/git/commit_info_test.go b/modules/git/commit_info_test.go index f8060bc33d4..5f2eb5e1292 100644 --- a/modules/git/commit_info_test.go +++ b/modules/git/commit_info_test.go @@ -129,7 +129,14 @@ func TestEntries_GetCommitsInfo(t *testing.T) { require.NoError(t, err) cisf, err := getCommitInfoSubmoduleFile("/any/repo-link", tree, commit, "") require.NoError(t, err) - assert.Nil(t, cisf) + assert.Equal(t, &CommitSubmoduleFile{ + repoLink: "/any/repo-link", + fullPath: "file1.txt", + refURL: "", + refID: "e2129701f1a4d54dc44f03c93bca0a2aec7c5449", + }, cisf) + // since there is no refURL, it means that the submodule info doesn't exist, so it won't have a web link + assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context())) }) } diff --git a/modules/git/commit_submodule_file.go b/modules/git/commit_submodule_file.go index 34f582c21f7..a3f63710de2 100644 --- a/modules/git/commit_submodule_file.go +++ b/modules/git/commit_submodule_file.go @@ -29,12 +29,16 @@ func NewCommitSubmoduleFile(repoLink, fullPath, refURL, refID string) *CommitSub return &CommitSubmoduleFile{repoLink: repoLink, fullPath: fullPath, refURL: refURL, refID: refID} } +// RefID returns the commit ID of the submodule, it returns empty string for nil receiver func (sf *CommitSubmoduleFile) RefID() string { + if sf == nil { + return "" + } return sf.refID } func (sf *CommitSubmoduleFile) getWebLinkInTargetRepo(ctx context.Context, moreLinkPath string) *SubmoduleWebLink { - if sf == nil { + if sf == nil || sf.refURL == "" { return nil } if strings.HasPrefix(sf.refURL, "../") { @@ -53,14 +57,13 @@ func (sf *CommitSubmoduleFile) getWebLinkInTargetRepo(ctx context.Context, moreL } // SubmoduleWebLinkTree tries to make the submodule's tree link in its own repo, it also works on "nil" receiver +// It returns nil if the submodule does not have a valid URL or is nil func (sf *CommitSubmoduleFile) SubmoduleWebLinkTree(ctx context.Context, optCommitID ...string) *SubmoduleWebLink { - if sf == nil { - return nil - } - return sf.getWebLinkInTargetRepo(ctx, "/tree/"+util.OptionalArg(optCommitID, sf.refID)) + return sf.getWebLinkInTargetRepo(ctx, "/tree/"+util.OptionalArg(optCommitID, sf.RefID())) } // SubmoduleWebLinkCompare tries to make the submodule's compare link in its own repo, it also works on "nil" receiver +// It returns nil if the submodule does not have a valid URL or is nil func (sf *CommitSubmoduleFile) SubmoduleWebLinkCompare(ctx context.Context, commitID1, commitID2 string) *SubmoduleWebLink { return sf.getWebLinkInTargetRepo(ctx, "/compare/"+commitID1+"..."+commitID2) } diff --git a/modules/git/commit_submodule_file_test.go b/modules/git/commit_submodule_file_test.go index fc2ff39bda6..203939fb1b5 100644 --- a/modules/git/commit_submodule_file_test.go +++ b/modules/git/commit_submodule_file_test.go @@ -12,6 +12,8 @@ import ( func TestCommitSubmoduleLink(t *testing.T) { assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkTree(t.Context())) assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkCompare(t.Context(), "", "")) + assert.Nil(t, (&CommitSubmoduleFile{}).SubmoduleWebLinkTree(t.Context())) + assert.Nil(t, (&CommitSubmoduleFile{}).SubmoduleWebLinkCompare(t.Context(), "", "")) t.Run("GitHubRepo", func(t *testing.T) { sf := NewCommitSubmoduleFile("/any/repo-link", "full-path", "git@github.com:user/repo.git", "aaaa") diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index 5482780c980..fd6e746381b 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -259,9 +259,10 @@ func handleRepoEmptyOrBroken(ctx *context.Context) { } func handleRepoViewSubmodule(ctx *context.Context, submodule *git.SubModule) { + // TODO: it needs to use git.NewCommitSubmoduleFile and SubmoduleWebLinkTree to correctly handle relative paths submoduleRepoURL, err := giturl.ParseRepositoryURL(ctx, submodule.URL) if err != nil { - HandleGitError(ctx, "prepareToRenderDirOrFile: ParseRepositoryURL", err) + HandleGitError(ctx, "handleRepoViewSubmodule: ParseRepositoryURL", err) return } submoduleURL := giturl.MakeRepositoryWebLink(submoduleRepoURL) diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go index 27b4cbf56d3..419dbedd74d 100644 --- a/services/repository/files/tree.go +++ b/services/repository/files/tree.go @@ -174,7 +174,9 @@ func newTreeViewNodeFromEntry(ctx context.Context, repoLink string, renderedIcon } else if subModule != nil { submoduleFile := git.NewCommitSubmoduleFile(repoLink, node.FullPath, subModule.URL, entry.ID.String()) webLink := submoduleFile.SubmoduleWebLinkTree(ctx) - node.SubmoduleURL = webLink.CommitWebLink + if webLink != nil { + node.SubmoduleURL = webLink.CommitWebLink + } } } From c4f5b2b53192ddf7f7585153a3f95ea8fd0f4861 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 18 Jul 2025 19:16:27 +0800 Subject: [PATCH 49/51] Don't use full-file highlight when there is a git diff textconv (#35114) Fix #35106 --- modules/git/attribute/attribute.go | 1 + services/gitdiff/gitdiff.go | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/git/attribute/attribute.go b/modules/git/attribute/attribute.go index adf323ef41c..9c01cb339e0 100644 --- a/modules/git/attribute/attribute.go +++ b/modules/git/attribute/attribute.go @@ -20,6 +20,7 @@ const ( GitlabLanguage = "gitlab-language" Lockable = "lockable" Filter = "filter" + Diff = "diff" ) var LinguistAttributes = []string{ diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index e2cd01e25cd..7c99e049d54 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -1191,7 +1191,7 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit return nil, err } - checker, err := attribute.NewBatchChecker(gitRepo, opts.AfterCommitID, []string{attribute.LinguistVendored, attribute.LinguistGenerated, attribute.LinguistLanguage, attribute.GitlabLanguage}) + checker, err := attribute.NewBatchChecker(gitRepo, opts.AfterCommitID, []string{attribute.LinguistVendored, attribute.LinguistGenerated, attribute.LinguistLanguage, attribute.GitlabLanguage, attribute.Diff}) if err != nil { return nil, err } @@ -1200,6 +1200,7 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit for _, diffFile := range diff.Files { isVendored := optional.None[bool]() isGenerated := optional.None[bool]() + attrDiff := optional.None[string]() attrs, err := checker.CheckPath(diffFile.Name) if err == nil { isVendored, isGenerated = attrs.GetVendored(), attrs.GetGenerated() @@ -1207,6 +1208,7 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit if language.Has() { diffFile.Language = language.Value() } + attrDiff = attrs.Get(attribute.Diff).ToString() } // Populate Submodule URLs @@ -1228,7 +1230,8 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit diffFile.Sections = append(diffFile.Sections, tailSection) } - if !setting.Git.DisableDiffHighlight { + shouldFullFileHighlight := !setting.Git.DisableDiffHighlight && attrDiff.Value() == "" + if shouldFullFileHighlight { if limitedContent.LeftContent != nil && limitedContent.LeftContent.buf.Len() < MaxDiffHighlightEntireFileSize { diffFile.highlightedLeftLines = highlightCodeLines(diffFile, true /* left */, limitedContent.LeftContent.buf.String()) } From 3531e9dbfd3af9f0ff69794a7dd609c8a8c23b33 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 18 Jul 2025 16:02:57 +0200 Subject: [PATCH 50/51] Replace `setup-python` with `setup-uv` (#35116) --- .github/workflows/pull-compliance.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 94d2d154062..56685ffb460 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -32,15 +32,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.12" + - uses: astral-sh/setup-uv@v6 + - run: uv python install 3.12 - uses: actions/setup-node@v4 with: node-version: 24 cache: npm cache-dependency-path: package-lock.json - - run: pip install uv - run: make deps-py - run: make deps-frontend - run: make lint-templates @@ -51,10 +49,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - run: pip install uv + - uses: astral-sh/setup-uv@v6 + - run: uv python install 3.12 - run: make deps-py - run: make lint-yaml From 86aafea3fbaa69df05a104df697b0bbfc4ce6d1b Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 20 Jul 2025 09:49:36 +0800 Subject: [PATCH 51/51] Fix session gob (#35128) Fix #35126 --- modules/session/mem.go | 68 +++++++++++++++++++++++++++ modules/session/mock.go | 26 ---------- modules/session/store.go | 22 ++++----- modules/session/virtual.go | 6 +-- routers/web/auth/auth.go | 4 +- routers/web/auth/auth_test.go | 5 +- routers/web/auth/linkaccount.go | 15 ++++-- routers/web/auth/oauth.go | 29 +++++++----- routers/web/auth/oauth_signin_sync.go | 9 +++- services/auth/source/oauth2/store.go | 15 +++--- services/contexttest/context_tests.go | 2 +- tests/integration/signin_test.go | 2 +- 12 files changed, 131 insertions(+), 72 deletions(-) create mode 100644 modules/session/mem.go delete mode 100644 modules/session/mock.go diff --git a/modules/session/mem.go b/modules/session/mem.go new file mode 100644 index 00000000000..bb807bc91a1 --- /dev/null +++ b/modules/session/mem.go @@ -0,0 +1,68 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package session + +import ( + "bytes" + "encoding/gob" + "net/http" + + "gitea.com/go-chi/session" +) + +type mockMemRawStore struct { + s *session.MemStore +} + +var _ session.RawStore = (*mockMemRawStore)(nil) + +func (m *mockMemRawStore) Set(k, v any) error { + // We need to use gob to encode the value, to make it have the same behavior as other stores and catch abuses. + // Because gob needs to "Register" the type before it can encode it, and it's unable to decode a struct to "any" so use a map to help to decode the value. + var buf bytes.Buffer + if err := gob.NewEncoder(&buf).Encode(map[string]any{"v": v}); err != nil { + return err + } + return m.s.Set(k, buf.Bytes()) +} + +func (m *mockMemRawStore) Get(k any) (ret any) { + v, ok := m.s.Get(k).([]byte) + if !ok { + return nil + } + var w map[string]any + _ = gob.NewDecoder(bytes.NewBuffer(v)).Decode(&w) + return w["v"] +} + +func (m *mockMemRawStore) Delete(k any) error { + return m.s.Delete(k) +} + +func (m *mockMemRawStore) ID() string { + return m.s.ID() +} + +func (m *mockMemRawStore) Release() error { + return m.s.Release() +} + +func (m *mockMemRawStore) Flush() error { + return m.s.Flush() +} + +type mockMemStore struct { + *mockMemRawStore +} + +var _ Store = (*mockMemStore)(nil) + +func (m mockMemStore) Destroy(writer http.ResponseWriter, request *http.Request) error { + return nil +} + +func NewMockMemStore(sid string) Store { + return &mockMemStore{&mockMemRawStore{session.NewMemStore(sid)}} +} diff --git a/modules/session/mock.go b/modules/session/mock.go deleted file mode 100644 index 95231a3655f..00000000000 --- a/modules/session/mock.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package session - -import ( - "net/http" - - "gitea.com/go-chi/session" -) - -type MockStore struct { - *session.MemStore -} - -func (m *MockStore) Destroy(writer http.ResponseWriter, request *http.Request) error { - return nil -} - -type mockStoreContextKeyStruct struct{} - -var MockStoreContextKey = mockStoreContextKeyStruct{} - -func NewMockStore(sid string) *MockStore { - return &MockStore{session.NewMemStore(sid)} -} diff --git a/modules/session/store.go b/modules/session/store.go index 09d1ef44dd7..0217ed97aca 100644 --- a/modules/session/store.go +++ b/modules/session/store.go @@ -11,25 +11,25 @@ import ( "gitea.com/go-chi/session" ) -// Store represents a session store +type RawStore = session.RawStore + type Store interface { - Get(any) any - Set(any, any) error - Delete(any) error - ID() string - Release() error - Flush() error + RawStore Destroy(http.ResponseWriter, *http.Request) error } +type mockStoreContextKeyStruct struct{} + +var MockStoreContextKey = mockStoreContextKeyStruct{} + // RegenerateSession regenerates the underlying session and returns the new store func RegenerateSession(resp http.ResponseWriter, req *http.Request) (Store, error) { for _, f := range BeforeRegenerateSession { f(resp, req) } if setting.IsInTesting { - if store, ok := req.Context().Value(MockStoreContextKey).(*MockStore); ok { - return store, nil + if store := req.Context().Value(MockStoreContextKey); store != nil { + return store.(Store), nil } } return session.RegenerateSession(resp, req) @@ -37,8 +37,8 @@ func RegenerateSession(resp http.ResponseWriter, req *http.Request) (Store, erro func GetContextSession(req *http.Request) Store { if setting.IsInTesting { - if store, ok := req.Context().Value(MockStoreContextKey).(*MockStore); ok { - return store + if store := req.Context().Value(MockStoreContextKey); store != nil { + return store.(Store) } } return session.GetSession(req) diff --git a/modules/session/virtual.go b/modules/session/virtual.go index 80352b6e721..2e29b5fc6f6 100644 --- a/modules/session/virtual.go +++ b/modules/session/virtual.go @@ -22,8 +22,8 @@ type VirtualSessionProvider struct { provider session.Provider } -// Init initializes the cookie session provider with given root path. -func (o *VirtualSessionProvider) Init(gclifetime int64, config string) error { +// Init initializes the cookie session provider with the given config. +func (o *VirtualSessionProvider) Init(gcLifetime int64, config string) error { var opts session.Options if err := json.Unmarshal([]byte(config), &opts); err != nil { return err @@ -52,7 +52,7 @@ func (o *VirtualSessionProvider) Init(gclifetime int64, config string) error { default: return fmt.Errorf("VirtualSessionProvider: Unknown Provider: %s", opts.Provider) } - return o.provider.Init(gclifetime, opts.ProviderConfig) + return o.provider.Init(gcLifetime, opts.ProviderConfig) } // Read returns raw session store by session ID. diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 13cd0837711..2ccd1c71b5c 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -565,7 +565,7 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, oauth2LinkAccount(ctx, user, possibleLinkAccountData, true) return false // user is already created here, all redirects are handled case setting.OAuth2AccountLinkingLogin: - showLinkingLogin(ctx, &possibleLinkAccountData.AuthSource, possibleLinkAccountData.GothUser) + showLinkingLogin(ctx, possibleLinkAccountData.AuthSourceID, possibleLinkAccountData.GothUser) return false // user will be created only after linking login } } @@ -633,7 +633,7 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, possibleLinkAcc // update external user information if possibleLinkAccountData != nil { - if err := externalaccount.EnsureLinkExternalToUser(ctx, possibleLinkAccountData.AuthSource.ID, u, possibleLinkAccountData.GothUser); err != nil { + if err := externalaccount.EnsureLinkExternalToUser(ctx, possibleLinkAccountData.AuthSourceID, u, possibleLinkAccountData.GothUser); err != nil { log.Error("EnsureLinkExternalToUser failed: %v", err) } } diff --git a/routers/web/auth/auth_test.go b/routers/web/auth/auth_test.go index e238125407b..a0fd5c0e506 100644 --- a/routers/web/auth/auth_test.go +++ b/routers/web/auth/auth_test.go @@ -64,13 +64,14 @@ func TestUserLogin(t *testing.T) { func TestSignUpOAuth2Login(t *testing.T) { defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() + _ = oauth2.Init(t.Context()) addOAuth2Source(t, "dummy-auth-source", oauth2.Source{}) t.Run("OAuth2MissingField", func(t *testing.T) { defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) { return goth.User{Provider: "dummy-auth-source", UserID: "dummy-user"}, nil })() - mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockStore("dummy-sid")} + mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")} ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback?code=dummy-code", mockOpt) ctx.SetPathParam("provider", "dummy-auth-source") SignInOAuthCallback(ctx) @@ -84,7 +85,7 @@ func TestSignUpOAuth2Login(t *testing.T) { }) t.Run("OAuth2CallbackError", func(t *testing.T) { - mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockStore("dummy-sid")} + mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")} ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback", mockOpt) ctx.SetPathParam("provider", "dummy-auth-source") SignInOAuthCallback(ctx) diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index cf1aa302c4c..c624d896ca7 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -170,7 +170,7 @@ func LinkAccountPostSignIn(ctx *context.Context) { } func oauth2LinkAccount(ctx *context.Context, u *user_model.User, linkAccountData *LinkAccountData, remember bool) { - oauth2SignInSync(ctx, &linkAccountData.AuthSource, u, linkAccountData.GothUser) + oauth2SignInSync(ctx, linkAccountData.AuthSourceID, u, linkAccountData.GothUser) if ctx.Written() { return } @@ -185,7 +185,7 @@ func oauth2LinkAccount(ctx *context.Context, u *user_model.User, linkAccountData return } - err = externalaccount.LinkAccountToUser(ctx, linkAccountData.AuthSource.ID, u, linkAccountData.GothUser) + err = externalaccount.LinkAccountToUser(ctx, linkAccountData.AuthSourceID, u, linkAccountData.GothUser) if err != nil { ctx.ServerError("UserLinkAccount", err) return @@ -295,7 +295,7 @@ func LinkAccountPostRegister(ctx *context.Context) { Email: form.Email, Passwd: form.Password, LoginType: auth.OAuth2, - LoginSource: linkAccountData.AuthSource.ID, + LoginSource: linkAccountData.AuthSourceID, LoginName: linkAccountData.GothUser.UserID, } @@ -304,7 +304,12 @@ func LinkAccountPostRegister(ctx *context.Context) { return } - source := linkAccountData.AuthSource.Cfg.(*oauth2.Source) + authSource, err := auth.GetSourceByID(ctx, linkAccountData.AuthSourceID) + if err != nil { + ctx.ServerError("GetSourceByID", err) + return + } + source := authSource.Cfg.(*oauth2.Source) if err := syncGroupsToTeams(ctx, source, &linkAccountData.GothUser, u); err != nil { ctx.ServerError("SyncGroupsToTeams", err) return @@ -318,5 +323,5 @@ func linkAccountFromContext(ctx *context.Context, user *user_model.User) error { if linkAccountData == nil { return errors.New("not in LinkAccount session") } - return externalaccount.LinkAccountToUser(ctx, linkAccountData.AuthSource.ID, user, linkAccountData.GothUser) + return externalaccount.LinkAccountToUser(ctx, linkAccountData.AuthSourceID, user, linkAccountData.GothUser) } diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 3df2734bb62..f1c155e78f5 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -4,6 +4,7 @@ package auth import ( + "encoding/gob" "errors" "fmt" "html" @@ -171,7 +172,7 @@ func SignInOAuthCallback(ctx *context.Context) { gothUser.RawData = make(map[string]any) } gothUser.RawData["__giteaAutoRegMissingFields"] = missingFields - showLinkingLogin(ctx, authSource, gothUser) + showLinkingLogin(ctx, authSource.ID, gothUser) return } u = &user_model.User{ @@ -192,7 +193,7 @@ func SignInOAuthCallback(ctx *context.Context) { u.IsAdmin = isAdmin.ValueOrDefault(user_service.UpdateOptionField[bool]{FieldValue: false}).FieldValue u.IsRestricted = isRestricted.ValueOrDefault(setting.Service.DefaultUserIsRestricted) - linkAccountData := &LinkAccountData{*authSource, gothUser} + linkAccountData := &LinkAccountData{authSource.ID, gothUser} if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingDisabled { linkAccountData = nil } @@ -207,7 +208,7 @@ func SignInOAuthCallback(ctx *context.Context) { } } else { // no existing user is found, request attach or new account - showLinkingLogin(ctx, authSource, gothUser) + showLinkingLogin(ctx, authSource.ID, gothUser) return } } @@ -272,11 +273,12 @@ func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *g } type LinkAccountData struct { - AuthSource auth.Source - GothUser goth.User + AuthSourceID int64 + GothUser goth.User } func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData { + gob.Register(LinkAccountData{}) v, ok := ctx.Session.Get("linkAccountData").(LinkAccountData) if !ok { return nil @@ -284,11 +286,16 @@ func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData { return &v } -func showLinkingLogin(ctx *context.Context, authSource *auth.Source, gothUser goth.User) { - if err := updateSession(ctx, nil, map[string]any{ - "linkAccountData": LinkAccountData{*authSource, gothUser}, - }); err != nil { - ctx.ServerError("updateSession", err) +func Oauth2SetLinkAccountData(ctx *context.Context, linkAccountData LinkAccountData) error { + gob.Register(LinkAccountData{}) + return updateSession(ctx, nil, map[string]any{ + "linkAccountData": linkAccountData, + }) +} + +func showLinkingLogin(ctx *context.Context, authSourceID int64, gothUser goth.User) { + if err := Oauth2SetLinkAccountData(ctx, LinkAccountData{authSourceID, gothUser}); err != nil { + ctx.ServerError("Oauth2SetLinkAccountData", err) return } ctx.Redirect(setting.AppSubURL + "/user/link_account") @@ -313,7 +320,7 @@ func oauth2UpdateAvatarIfNeed(ctx *context.Context, url string, u *user_model.Us } func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_model.User, gothUser goth.User) { - oauth2SignInSync(ctx, authSource, u, gothUser) + oauth2SignInSync(ctx, authSource.ID, u, gothUser) if ctx.Written() { return } diff --git a/routers/web/auth/oauth_signin_sync.go b/routers/web/auth/oauth_signin_sync.go index 787ea9223cf..86d19660245 100644 --- a/routers/web/auth/oauth_signin_sync.go +++ b/routers/web/auth/oauth_signin_sync.go @@ -18,9 +18,14 @@ import ( "github.com/markbates/goth" ) -func oauth2SignInSync(ctx *context.Context, authSource *auth.Source, u *user_model.User, gothUser goth.User) { +func oauth2SignInSync(ctx *context.Context, authSourceID int64, u *user_model.User, gothUser goth.User) { oauth2UpdateAvatarIfNeed(ctx, gothUser.AvatarURL, u) + authSource, err := auth.GetSourceByID(ctx, authSourceID) + if err != nil { + ctx.ServerError("GetSourceByID", err) + return + } oauth2Source, _ := authSource.Cfg.(*oauth2.Source) if !authSource.IsOAuth2() || oauth2Source == nil { ctx.ServerError("oauth2SignInSync", fmt.Errorf("source %s is not an OAuth2 source", gothUser.Provider)) @@ -45,7 +50,7 @@ func oauth2SignInSync(ctx *context.Context, authSource *auth.Source, u *user_mod } } - err := oauth2UpdateSSHPubIfNeed(ctx, authSource, &gothUser, u) + err = oauth2UpdateSSHPubIfNeed(ctx, authSource, &gothUser, u) if err != nil { log.Error("Unable to sync OAuth2 SSH public key %s: %v", gothUser.Provider, err) } diff --git a/services/auth/source/oauth2/store.go b/services/auth/source/oauth2/store.go index 90fa965602a..7b6b26edc83 100644 --- a/services/auth/source/oauth2/store.go +++ b/services/auth/source/oauth2/store.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/modules/log" session_module "code.gitea.io/gitea/modules/session" - chiSession "gitea.com/go-chi/session" "github.com/gorilla/sessions" ) @@ -35,11 +34,11 @@ func (st *SessionsStore) New(r *http.Request, name string) (*sessions.Session, e // getOrNew gets the session from the chi-session if it exists. Override permits the overriding of an unexpected object. func (st *SessionsStore) getOrNew(r *http.Request, name string, override bool) (*sessions.Session, error) { - chiStore := chiSession.GetSession(r) + store := session_module.GetContextSession(r) session := sessions.NewSession(st, name) - rawData := chiStore.Get(name) + rawData := store.Get(name) if rawData != nil { oldSession, ok := rawData.(*sessions.Session) if ok { @@ -56,21 +55,21 @@ func (st *SessionsStore) getOrNew(r *http.Request, name string, override bool) ( } session.IsNew = override - session.ID = chiStore.ID() // Simply copy the session id from the chi store + session.ID = store.ID() // Simply copy the session id from the chi store - return session, chiStore.Set(name, session) + return session, store.Set(name, session) } // Save should persist session to the underlying store implementation. func (st *SessionsStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error { - chiStore := chiSession.GetSession(r) + store := session_module.GetContextSession(r) if session.IsNew { _, _ = session_module.RegenerateSession(w, r) session.IsNew = false } - if err := chiStore.Set(session.Name(), session); err != nil { + if err := store.Set(session.Name(), session); err != nil { return err } @@ -83,7 +82,7 @@ func (st *SessionsStore) Save(r *http.Request, w http.ResponseWriter, session *s } } - return chiStore.Release() + return store.Release() } type sizeWriter struct { diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go index b54023897b0..44d9f4a70f0 100644 --- a/services/contexttest/context_tests.go +++ b/services/contexttest/context_tests.go @@ -49,7 +49,7 @@ func mockRequest(t *testing.T, reqPath string) *http.Request { type MockContextOption struct { Render context.Render - SessionStore *session.MockStore + SessionStore session.Store } // MockContext mock context for unit tests diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index aa1571c163f..fa37145d98c 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -107,7 +107,7 @@ func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) { mockLinkAccount := func(ctx *context.Context) { authSource := auth_model.Source{ID: 1} gothUser := goth.User{Email: "invalid-email", Name: "."} - _ = ctx.Session.Set("linkAccountData", auth.LinkAccountData{AuthSource: authSource, GothUser: gothUser}) + _ = auth.Oauth2SetLinkAccountData(ctx, auth.LinkAccountData{AuthSourceID: authSource.ID, GothUser: gothUser}) } t.Run("EnablePasswordSignInForm=false", func(t *testing.T) {
ID{{SortArrow "oldest" "newest" .SortType false}}