diff --git a/.golangci.yml b/.golangci.yml index 6e80af8c0eb29..982ab06f0b44a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -171,3 +171,7 @@ issues: - path: models/user/openid.go linters: - golint + - path: models/user/badge.go + linters: + - revive + text: "exported: type name will be used as user.UserBadge by other packages, and that stutters; consider calling this Badge" diff --git a/docs/content/doc/installation/with-docker.zh-cn.md b/docs/content/doc/installation/with-docker.zh-cn.md index 91892fdaf95c0..2c63c9d4e1e4e 100644 --- a/docs/content/doc/installation/with-docker.zh-cn.md +++ b/docs/content/doc/installation/with-docker.zh-cn.md @@ -23,7 +23,7 @@ Gitea 在其 Docker Hub 组织内提供自动更新的 Docker 镜像。可以始 ## 基本 -最简单的设置只是创建一个卷和一个网络,然后将 `gitea/gitea:latest` 镜像作为服务启动。由于没有可用的数据库,因此可以使用 SQLite3 初始化数据库。创建一个类似 `gitea` 的目录,并将以下内容粘贴到名为 `docker-compose.yml` 的文件中。请注意,该卷应由配置文件中指定的 UID/GID 的用户/组拥有。如果您不授予卷正确的权限,则容器可能无法启动。另请注意,标签 `:latest` 将安装当前的开发版本。对于稳定的发行版,您可以使用 `:1` 或指定某个发行版,例如 `:1.13.0`。 +最简单的设置只是创建一个卷和一个网络,然后将 `gitea/gitea:latest` 镜像作为服务启动。由于没有可用的数据库,因此可以使用 SQLite3 初始化数据库。创建一个类似 `gitea` 的目录,并将以下内容粘贴到名为 `docker-compose.yml` 的文件中。请注意,该卷应由配置文件中指定的 UID/GID 的用户/组拥有。如果您不授予卷正确的权限,则容器可能无法启动。另请注意,标签 `:latest` 将安装当前的开发版本。对于稳定的发行版,您可以使用 `:1` 或指定某个发行版,例如 `{{< version >}}`。 ```yaml version: "3" @@ -103,11 +103,11 @@ services: environment: - USER_UID=1000 - USER_GID=1000 -+ - DB_TYPE=mysql -+ - DB_HOST=db:3306 -+ - DB_NAME=gitea -+ - DB_USER=gitea -+ - DB_PASSWD=gitea ++ - GITEA__database__DB_TYPE=mysql ++ - GITEA__database__HOST=db:3306 ++ - GITEA__database__NAME=gitea ++ - GITEA__database__USER=gitea ++ - GITEA__database__PASSWD=gitea restart: always networks: - gitea @@ -153,11 +153,11 @@ services: environment: - USER_UID=1000 - USER_GID=1000 -+ - DB_TYPE=postgres -+ - DB_HOST=db:5432 -+ - DB_NAME=gitea -+ - DB_USER=gitea -+ - DB_PASSWD=gitea ++ - GITEA__database__DB_TYPE=postgres ++ - GITEA__database__HOST=db:5432 ++ - GITEA__database__NAME=gitea ++ - GITEA__database__USER=gitea ++ - GITEA__database__PASSWD=gitea restart: always networks: - gitea @@ -276,6 +276,42 @@ docker-compose pull docker-compose up -d ``` +## 使用环境变量管理部署 + +除了上面的环境变量之外,`app.ini` 中的任何设置都可以使用以下形式的环境变量进行设置或覆盖:`GITEA__SECTION_NAME__KEY_NAME`。 每次 docker 容器启动时都会应用这些设置。 完整信息在[这里](https://github.com/go-gitea/gitea/tree/master/contrib/environment-to-ini)。 + +```bash +... +services: + server: + environment: + - GITEA__mailer__ENABLED=true + - GITEA__mailer__FROM=${GITEA__mailer__FROM:?GITEA__mailer__FROM not set} + - GITEA__mailer__MAILER_TYPE=smtp + - GITEA__mailer__HOST=${GITEA__mailer__HOST:?GITEA__mailer__HOST not set} + - GITEA__mailer__IS_TLS_ENABLED=true + - GITEA__mailer__USER=${GITEA__mailer__USER:-apikey} + - GITEA__mailer__PASSWD="""${GITEA__mailer__PASSWD:?GITEA__mailer__PASSWD not set}""" +``` + +Gitea 将为每次新安装自动生成新的 `SECRET_KEY` 并将它们写入 `app.ini`。 如果您想手动设置 `SECRET_KEY`,您可以使用以下 docker 命令来使用 Gitea 内置的[方法](https://docs.gitea.io/en-us/command-line/#generate)生成 `SECRET_KEY`。 安装后请妥善保管您的 `SECRET_KEY`,如若丢失则无法解密已加密的数据。 + +以下命令将向 `stdout` 输出一个新的 `SECRET_KEY` 和 `INTERNAL_TOKEN`,然后您可以将其放入环境变量中。 + +```bash +docker run -it --rm gitea/gitea:1 gitea generate secret SECRET_KEY +docker run -it --rm gitea/gitea:1 gitea generate secret INTERNAL_TOKEN +``` + +```yaml +... +services: + server: + environment: + - GITEA__security__SECRET_KEY=[value returned by generate secret SECRET_KEY] + - GITEA__security__INTERNAL_TOKEN=[value returned by generate secret INTERNAL_TOKEN] +``` + ## SSH 容器直通 由于 SSH 在容器内运行,因此,如果需要 SSH 支持,则需要将 SSH 从主机传递到容器。一种选择是在非标准端口上运行容器 SSH(或将主机端口移至非标准端口)。另一个可能更直接的选择是将 SSH 连接从主机转发到容器。下面将说明此设置。 diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go index 5a58ec62b7d1d..ad1d80e25a85b 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -512,10 +512,14 @@ func GetActiveOAuth2ProviderSources() ([]*Source, error) { func GetActiveOAuth2SourceByName(name string) (*Source, error) { authSource := new(Source) has, err := db.GetEngine(db.DefaultContext).Where("name = ? and type = ? and is_active = ?", name, OAuth2, true).Get(authSource) - if !has || err != nil { + if err != nil { return nil, err } + if !has { + return nil, fmt.Errorf("oauth2 source not found, name: %q", name) + } + return authSource, nil } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 2719f45efbbbc..30c4ad250c084 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -406,6 +406,8 @@ var migrations = []Migration{ NewMigration("Drop old CredentialID column", dropOldCredentialIDColumn), // v223 -> v224 NewMigration("Rename CredentialIDBytes column to CredentialID", renameCredentialIDBytes), + // v224 -> v225 + NewMigration("Add badges to users", creatUserBadgesTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v224.go b/models/migrations/v224.go new file mode 100644 index 0000000000000..d684d538df222 --- /dev/null +++ b/models/migrations/v224.go @@ -0,0 +1,28 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "xorm.io/xorm" +) + +func creatUserBadgesTable(x *xorm.Engine) error { + type Badge struct { + ID int64 `xorm:"pk autoincr"` + Description string + ImageURL string + } + + type userBadge struct { + ID int64 `xorm:"pk autoincr"` + BadgeID int64 + UserID int64 `xorm:"INDEX"` + } + + if err := x.Sync2(new(Badge)); err != nil { + return err + } + return x.Sync2(new(userBadge)) +} diff --git a/models/repo/mirror.go b/models/repo/mirror.go index 8f96e8cee1851..e94bbda2c181d 100644 --- a/models/repo/mirror.go +++ b/models/repo/mirror.go @@ -153,7 +153,9 @@ func (repos MirrorRepositoryList) loadAttributes(ctx context.Context) error { } for i := range repos { repos[i].Mirror = set[repos[i].ID] - repos[i].Mirror.Repo = repos[i] + if repos[i].Mirror != nil { + repos[i].Mirror.Repo = repos[i] + } } return nil } diff --git a/models/user.go b/models/user.go index 86a714e746bb2..4afbb9bea5c7d 100644 --- a/models/user.go +++ b/models/user.go @@ -85,6 +85,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) (err error) &organization.TeamUser{UID: u.ID}, &issues_model.Stopwatch{UserID: u.ID}, &user_model.Setting{UserID: u.ID}, + &user_model.UserBadge{UserID: u.ID}, &pull_model.AutoMerge{DoerID: u.ID}, &pull_model.ReviewState{UserID: u.ID}, ); err != nil { diff --git a/models/user/badge.go b/models/user/badge.go new file mode 100644 index 0000000000000..5ff840cb8c35e --- /dev/null +++ b/models/user/badge.go @@ -0,0 +1,42 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package user + +import ( + "context" + + "code.gitea.io/gitea/models/db" +) + +// Badge represents a user badge +type Badge struct { + ID int64 `xorm:"pk autoincr"` + Description string + ImageURL string +} + +// UserBadge represents a user badge +type UserBadge struct { + ID int64 `xorm:"pk autoincr"` + BadgeID int64 + UserID int64 `xorm:"INDEX"` +} + +func init() { + db.RegisterModel(new(Badge)) + db.RegisterModel(new(UserBadge)) +} + +// GetUserBadges returns the user's badges. +func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) { + sess := db.GetEngine(ctx). + Select("`badge`.*"). + Join("INNER", "user_badge", "`user_badge`.badge_id=badge.id"). + Where("user_badge.user_id=?", u.ID) + + badges := make([]*Badge, 0, 8) + count, err := sess.FindAndCount(&badges) + return badges, count, err +} diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index a5f3418706758..84630dbc34856 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -1031,6 +1031,7 @@ file_view_rendered=Ansicht rendern file_view_raw=Originalformat anzeigen file_permalink=Permalink file_too_large=Die Datei ist zu groß zum Anzeigen. +ambiguous_character=`%[1]c [U+%04[1]X] kann mit %[2]c [U+%04[2]X] verwechselt werden` escape_control_characters=Escapen unescape_control_characters=Unescapen @@ -1051,6 +1052,7 @@ normal_view=Normale Ansicht line=zeile lines=Zeilen +editor.add_file=Datei hinzufügen editor.new_file=Neue Datei editor.upload_file=Datei hochladen editor.edit_file=Datei bearbeiten @@ -1256,6 +1258,8 @@ issues.filter_milestone=Meilenstein issues.filter_milestone_no_select=Alle Meilensteine issues.filter_assignee=Zuständig issues.filter_assginee_no_select=Alle Zuständigen +issues.filter_poster=Autor +issues.filter_poster_no_select=Alle Autoren issues.filter_type=Typ issues.filter_type.all_issues=Alle Issues issues.filter_type.assigned_to_you=Dir zugewiesen @@ -2782,7 +2786,9 @@ config.deliver_timeout=Zeitlimit für Zustellung config.skip_tls_verify=TLS-Verifikation überspringen config.mailer_enabled=Aktiviert +config.mailer_enable_helo=HELO aktivieren config.mailer_name=Name +config.mailer_protocol=Protokoll config.mailer_user=Benutzer config.mailer_use_sendmail=Sendmail benutzen config.mailer_sendmail_path=Sendmail-Pfad @@ -3089,6 +3095,7 @@ npm.dependencies.development=Entwicklungsabhängigkeiten npm.dependencies.peer=Peer Abhängigkeiten npm.dependencies.optional=Optionale Abhängigkeiten npm.details.tag=Tag +pub.install=Um das Paket mit Dart zu installieren, führe den folgenden Befehl aus: pypi.requires=Erfordert Python pypi.install=Nutze folgenden Befehl, um das Paket mit pip zu installieren: pypi.documentation=Weitere Informationen zur PyPI-Paketverwaltung findest du in der Dokumentation. diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 3fe2f905196c5..18eb44b16a3d5 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -372,6 +372,7 @@ reset_password.text=Veuillez cliquer sur le lien suivant pour récupérer votre register_success=Inscription réussie +issue_assigned.pull=@%[1]s vous a assigné à la demande d’ajout %[2]s dans le dépôt %[3]s. issue_assigned.issue=@%[1]s vous a assigné le ticket %[2]s dans le dépôt %[3]s. issue.x_mentioned_you=@%s vous a mentionné: @@ -1418,6 +1419,7 @@ compare.compare_head=comparer pulls.desc=Activer les demandes de fusion et la revue de code. pulls.new=Nouvelle demande d'ajout +pulls.view=Voir la demande d'ajout pulls.compare_changes=Nouvelle demande de fusion pulls.compare_changes_desc=Sélectionnez la branche dans laquelle fusionner et la branche depuis laquelle tirer les modifications. pulls.compare_base=fusionner dans @@ -1427,6 +1429,7 @@ pulls.filter_branch=Filtre de branche pulls.no_results=Aucun résultat trouvé. pulls.nothing_to_compare=Ces branches sont identiques. Il n'y a pas besoin de créer une demande de fusion. pulls.nothing_to_compare_and_allow_empty_pr=Ces branches sont égales. Cette demande d'ajout sera vide. +pulls.has_pull_request='Il existe déjà une demande d'ajout entre ces deux branches : %[2]s#%[3]d' pulls.create=Créer une demande d'ajout pulls.title_desc=veut fusionner %[1]d révision(s) depuis %[2]s vers %[3]s pulls.merged_title_desc=a fusionné %[1]d révision(s) à partir de %[2]s vers %[3]s %[4]s @@ -1455,7 +1458,6 @@ pulls.required_status_check_missing=Certains contrôles requis sont manquants. pulls.required_status_check_administrator=En tant qu'administrateur, vous pouvez toujours fusionner cette requête de pull. pulls.blocked_by_approvals=Cette demande d'ajout n'a pas assez d'approbation. %d sur %d approbations accordées. pulls.blocked_by_rejection=Cette demande de fusion a des modifications demandées par un réviseur officiel. -pulls.blocked_by_official_review_requests=Cette demande d'ajout a des demandes de revue officielles. pulls.blocked_by_outdated_branch=Cette demande d'ajout est bloquée car elle est obsolète. pulls.blocked_by_changed_protected_files_1=Cette demande d'ajout est bloquée car elle modifie un fichier protégé : pulls.blocked_by_changed_protected_files_n=Cette Pull Request est bloquée car elle modifie les fichiers protégés : @@ -1477,6 +1479,10 @@ pulls.no_merge_helper=Activez des options de fusion dans les paramètres du dép pulls.no_merge_wip=Cette demande d'ajout ne peut pas être fusionnée car elle est marquée comme en cours de chantier. pulls.no_merge_not_ready=Cette demande d'ajout n'est pas prête à être fusionnée, vérifiez l'état de la revue et les vérifications. pulls.no_merge_access=Vous n'êtes pas autorisé⋅e à fusionner cette demande d'ajout. +pulls.merge_pull_request=Créer une révision de fusion +pulls.rebase_merge_pull_request=Rebaser puis avancer rapidement +pulls.rebase_merge_commit_pull_request=Rebaser puis créer une révision de fusion +pulls.squash_merge_pull_request=Créer une révision de concaténation pulls.merge_manually=Fusionné manuellement pulls.merge_commit_id=L'ID de la révision de fusion pulls.require_signed_wont_sign=La branche nécessite des révisions signées mais cette fusion ne sera pas signée @@ -1507,9 +1513,17 @@ pulls.merge_instruction_hint=`Vous pouvez également voir %s rename_repo=a rebaptisé le dépôt de %[1]s vers %[3]s +create_pull_request=`a créé la demande d'ajout %[3]s#%[2]s` +close_pull_request=`a fermé la demande d'ajout %[3]s#%[2]s` +reopen_pull_request=`a réouvert la demande d'ajout %[3]s#%[2]s` +comment_pull=`a commenté la demande d'ajout %[3]s#%[2]s` +merge_pull_request=`a fusionné la demande d'ajout %[3]s#%[2]s` transfer_repo=a transféré le dépôt %s à %s delete_tag=étiquette supprimée %[2]s de %[3]s delete_branch=branche %[2]s supprimée de %[3]s @@ -2678,6 +2698,8 @@ compare_branch=Comparer compare_commits=Comparer %d révisions compare_commits_general=Comparer les révisions mirror_sync_delete=a synchronisé puis supprimé la nouvelle référence %[2]s vers %[3]s depuis le miroir +approve_pull_request=`a approuvé %[3]s#%[2]s` +reject_pull_request=`a suggérés des changements pour %[3]s#%[2]s` review_dismissed_reason=Raison : [tool] diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 33d33858ba8ff..d92c46eae4200 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -1038,6 +1038,7 @@ file_too_large=Bu dosya görüntülemek için çok büyük. invisible_runes_header=`Bu dosya görünmez Evrensel Kodlu karakter içeriyor!` invisible_runes_description=`Bu dosya, aşağıda görünenden farklı bir şekilde işlenebilecek görünmez Evrensel Kodlu karakter içeriyor. Eğer bunu kasıtlı ve meşru olarak yaptıysanız bu uyarıyı yok sayabilirsiniz. Gizli karakterleri göstermek için Kaçış düğmesine tıklayın.` ambiguous_runes_header=`Bu dosya muğlak Evrensel Kodlu karakter içeriyor!` +ambiguous_runes_description=`Bu dosya, aşağıda görünenden farklı bir şekilde işlenebilecek muğlak Evrensel Kodlu karakter içeriyor. Eğer bunu kasıtlı ve meşru olarak yaptıysanız bu uyarıyı yok sayabilirsiniz. Bu karakterleri göstermek için Kaçış düğmesine tıklayın.` invisible_runes_line=`Bu satırda görünmez evrensel kodlu karakter var` ambiguous_runes_line=`Bu satırda muğlak evrensel kodlu karakter var` ambiguous_character=`%[1]c [U+%04[1]X], %[2]c [U+%04[2]X] ile karıştırılabilir` @@ -2894,6 +2895,7 @@ monitor.queue.nopool.title=Çalışan Havuzu Yok monitor.queue.nopool.desc=Bu kuyruk diğer kuyrukları sarar ve kendisinin bir işçi havuzu yoktur. monitor.queue.wrapped.desc=Sarılmış bir kuyruk, yavaş bir başlangıç kuyruğunu sararak kanaldaki kuyruk isteklerini arabelleğe alır. Bir işçi havuzu yoktur. monitor.queue.persistable-channel.desc=Kesintisiz bir kanal, kendi alt havuzuna sahip bir kanal kuyruğu ve önceki kapanmalardan gelen kalıcı istekler için bir seviye kuyruğu olan iki kuyruğu sarar. Bir işçi havuzu yoktur. +monitor.queue.flush=Çalışanı boşalt monitor.queue.pool.timeout=Zaman aşımı monitor.queue.pool.addworkers.title=Çalışan Ekle monitor.queue.pool.addworkers.submit=Çalışan Ekle diff --git a/routers/web/user/home.go b/routers/web/user/home.go index f338c525b4d3e..5e17239e348ce 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -607,10 +607,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { var shownIssues int if !isShowClosed { shownIssues = int(issueStats.OpenCount) - ctx.Data["TotalIssueCount"] = shownIssues } else { shownIssues = int(issueStats.ClosedCount) - ctx.Data["TotalIssueCount"] = shownIssues } if len(repoIDs) != 0 { shownIssues = 0 @@ -618,6 +616,13 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { shownIssues += int(issueCountByRepo[repoID]) } } + + var allIssueCount int64 + for _, issueCount := range issueCountByRepo { + allIssueCount += issueCount + } + ctx.Data["TotalIssueCount"] = allIssueCount + if len(repoIDs) == 1 { repo := showReposMap[repoIDs[0]] if repo != nil { diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 6f23d239e26a5..c804be3c5f74a 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -105,6 +105,13 @@ func Profile(ctx *context.Context) { ctx.Data["Orgs"] = orgs ctx.Data["HasOrgsVisible"] = organization.HasOrgsVisible(orgs, ctx.Doer) + badges, _, err := user_model.GetUserBadges(ctx, ctx.ContextUser) + if err != nil { + ctx.ServerError("GetUserBadges", err) + return + } + ctx.Data["Badges"] = badges + tab := ctx.FormString("tab") ctx.Data["TabName"] = tab diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index 947c8fcebe281..2a973c2d5bc04 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -69,6 +69,17 @@ {{end}} + {{if .Badges}} +
  • + +
  • + {{end}} {{if and .IsSigned (ne .SignedUserName .Owner.Name)}}
  • {{if $.IsFollowing}} diff --git a/web_src/js/features/tribute.js b/web_src/js/features/tribute.js index 053804c43d486..5678acdf47e1e 100644 --- a/web_src/js/features/tribute.js +++ b/web_src/js/features/tribute.js @@ -1,5 +1,6 @@ import {emojiKeys, emojiHTML, emojiString} from './emoji.js'; import {uniq} from '../utils.js'; +import {htmlEscape} from 'escape-goat'; function makeCollections({mentions, emoji}) { const collections = []; @@ -24,7 +25,7 @@ function makeCollections({mentions, emoji}) { return emojiString(item.original); }, menuItemTemplate: (item) => { - return `
    ${emojiHTML(item.original)}${item.original}
    `; + return `
    ${emojiHTML(item.original)}${htmlEscape(item.original)}
    `; } }); } @@ -36,9 +37,9 @@ function makeCollections({mentions, emoji}) { menuItemTemplate: (item) => { return `
    - - ${item.original.name} - ${item.original.fullname && item.original.fullname !== '' ? `${item.original.fullname}` : ''} + + ${htmlEscape(item.original.name)} + ${item.original.fullname && item.original.fullname !== '' ? `${htmlEscape(item.original.fullname)}` : ''}
    `; } diff --git a/web_src/less/_user.less b/web_src/less/_user.less index a9b6c02fb7af0..eb9e791d86e87 100644 --- a/web_src/less/_user.less +++ b/web_src/less/_user.less @@ -169,6 +169,15 @@ } } +.user-badges { + display: grid; + grid-template-columns: repeat(auto-fill, 64px); + gap: 2px; +} + +.user-badges img { + object-fit: contain; +} #notification_div .tab.segment { overflow-x: auto; }