diff --git a/.deadcode-out b/.deadcode-out index cf5f7c479b..fd7b3ad858 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -189,6 +189,7 @@ package "code.gitea.io/gitea/modules/gitgraph" package "code.gitea.io/gitea/modules/gitrepo" func GetBranchCommitID + func GetWikiDefaultBranch package "code.gitea.io/gitea/modules/graceful" func (*Manager).TerminateContext diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9e290fb6a5..8563aafd04 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,9 @@ }, "ghcr.io/devcontainers/features/git-lfs:1.1.0": {}, "ghcr.io/devcontainers-contrib/features/poetry:2": {}, - "ghcr.io/devcontainers/features/python:1": {} + "ghcr.io/devcontainers/features/python:1": { + "version": "3.12" + } }, "customizations": { "vscode": { diff --git a/Makefile b/Makefile index bc3837f065..1a835e31fd 100644 --- a/Makefile +++ b/Makefile @@ -961,6 +961,7 @@ fomantic: cd $(FOMANTIC_WORK_DIR) && npm install --no-save cp -f $(FOMANTIC_WORK_DIR)/theme.config.less $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/theme.config cp -rf $(FOMANTIC_WORK_DIR)/_site $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/ + $(SED_INPLACE) -e 's/ overrideBrowserslist\r/ overrideBrowserslist: ["defaults"]\r/g' $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/tasks/config/tasks.js cd $(FOMANTIC_WORK_DIR) && npx gulp -f node_modules/fomantic-ui/gulpfile.js build # fomantic uses "touchstart" as click event for some browsers, it's not ideal, so we force fomantic to always use "click" as click event $(SED_INPLACE) -e 's/clickEvent[ \t]*=/clickEvent = "click", unstableClickEvent =/g' $(FOMANTIC_WORK_DIR)/build/semantic.js diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 4d077643f5..b3896bc31c 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1496,10 +1496,11 @@ LEVEL = Info ;; ;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled ;DEFAULT_EMAIL_NOTIFICATIONS = enabled -;; Disabled features for users, could be "deletion","manage_gpg_keys" more features can be disabled in future +;; Send an email to all admins when a new user signs up to inform the admins about this act. Options: true, false ;SEND_NOTIFICATION_EMAIL_ON_NEW_USER = false -;; Disabled features for users, could be "deletion", more features can be disabled in future +;; Disabled features for users, could be "deletion", "manage_ssh_keys","manage_gpg_keys" more features can be disabled in future ;; - deletion: a user cannot delete their own account +;; - manage_ssh_keys: a user cannot configure ssh keys ;; - manage_gpg_keys: a user cannot configure gpg keys ;USER_DISABLED_FEATURES = diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index ea6e1eb1a4..43ec470ad0 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -518,9 +518,10 @@ And the following unique queues: - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled - `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations. -- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_gpg_keys` and more features can be added in future. +- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys` and more features can be added in future. - `deletion`: User cannot delete their own account. - - `manage_gpg_keys`: User cannot configure gpg keys + - `manage_ssh_keys`: User cannot configure ssh keys. + - `manage_gpg_keys`: User cannot configure gpg keys. ## Security (`security`) @@ -831,7 +832,7 @@ Default templates for project boards: ## Issue and pull request attachments (`attachment`) - `ENABLED`: **true**: Whether issue and pull request attachments are enabled. -- `ALLOWED_TYPES`: **.csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip**: Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types. +- `ALLOWED_TYPES`: **.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip**: Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types. - `MAX_SIZE`: **2048**: Maximum size (MB). - `MAX_FILES`: **5**: Maximum number of attachments that can be uploaded at once. - `STORAGE_TYPE`: **local**: Storage type for attachments, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]` diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md index 5cc5734359..1f98db78aa 100644 --- a/docs/content/administration/config-cheat-sheet.zh-cn.md +++ b/docs/content/administration/config-cheat-sheet.zh-cn.md @@ -497,9 +497,10 @@ Gitea 创建以下非唯一队列: - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**:用户电子邮件通知的默认配置(用户可配置)。选项:enabled、onmention、disabled - `DISABLE_REGULAR_ORG_CREATION`: **false**:禁止普通(非管理员)用户创建组织。 -- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`,`manage_gpg_keys` 未来可以增加更多设置。 +- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`,`manage_ssh_keys`, `manage_gpg_keys` 未来可以增加更多设置。 - `deletion`: 用户不能通过界面或者API删除他自己。 - - `manage_gpg_keys`: 用户不能配置 GPG 密钥 + - `manage_ssh_keys`: 用户不能通过界面或者API配置SSH Keys。 + - `manage_gpg_keys`: 用户不能配置 GPG 密钥。 ## 安全性 (`security`) @@ -781,7 +782,7 @@ Gitea 创建以下非唯一队列: ## 工单和合并请求的附件 (`attachment`) - `ENABLED`: **true**: 是否允许用户上传附件。 -- `ALLOWED_TYPES`: **.csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip**: 允许的文件扩展名(`.zip`)、mime 类型(`text/plain`)或通配符类型(`image/*`、`audio/*`、`video/*`)的逗号分隔列表。空值或 `*/*` 允许所有类型。 +- `ALLOWED_TYPES`: **.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip**: 允许的文件扩展名(`.zip`)、mime 类型(`text/plain`)或通配符类型(`image/*`、`audio/*`、`video/*`)的逗号分隔列表。空值或 `*/*` 允许所有类型。 - `MAX_SIZE`: **2048**: 附件的最大限制(MB)。 - `MAX_FILES`: **5**: 一次最多上传的附件数量。 - `STORAGE_TYPE`: **local**: 附件的存储类型,`local` 表示本地磁盘,`minio` 表示兼容 S3 的对象存储服务,如果未设置将使用默认值 `local` 或其他在 `[storage.xxx]` 中定义的名称。 diff --git a/docs/content/administration/mail-templates.en-us.md b/docs/content/administration/mail-templates.en-us.md index 0154fe55d0..4026b89975 100644 --- a/docs/content/administration/mail-templates.en-us.md +++ b/docs/content/administration/mail-templates.en-us.md @@ -224,7 +224,7 @@ Please check [Gitea's logs](administration/logging-config.md) for error messages {{if not (eq .Body "")}}

Message content


- {{.Body | SanitizeHTML}} + {{.Body}} {{end}}


diff --git a/docs/content/administration/mail-templates.zh-cn.md b/docs/content/administration/mail-templates.zh-cn.md index e8c2817336..3c7c2a9397 100644 --- a/docs/content/administration/mail-templates.zh-cn.md +++ b/docs/content/administration/mail-templates.zh-cn.md @@ -207,7 +207,7 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/ {{if not (eq .Body "")}}

消息内容:


- {{.Body | SanitizeHTML}} + {{.Body}} {{end}}


diff --git a/docs/content/contributing/guidelines-frontend.en-us.md b/docs/content/contributing/guidelines-frontend.en-us.md index edd89e1231..2637780718 100644 --- a/docs/content/contributing/guidelines-frontend.en-us.md +++ b/docs/content/contributing/guidelines-frontend.en-us.md @@ -47,7 +47,7 @@ We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/h 9. Avoid unnecessary `!important` in CSS, add comments to explain why it's necessary if it can't be avoided. 10. Avoid mixing different events in one event listener, prefer to use individual event listeners for every event. 11. Custom event names are recommended to use `ce-` prefix. -12. Gitea's tailwind-style CSS classes use `gt-` prefix (`gt-relative`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`). +12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-df`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`). 13. Avoid inline scripts & styles as much as possible, it's recommended to put JS code into JS files and use CSS classes. If inline scripts & styles are unavoidable, explain the reason why it can't be avoided. ### Accessibility / ARIA diff --git a/docs/content/contributing/guidelines-frontend.zh-cn.md b/docs/content/contributing/guidelines-frontend.zh-cn.md index 66a4d4b4d6..ace0d97f49 100644 --- a/docs/content/contributing/guidelines-frontend.zh-cn.md +++ b/docs/content/contributing/guidelines-frontend.zh-cn.md @@ -34,7 +34,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。 我们推荐使用[Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html)和[Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html)。 -## Gitea 特定准则: +## Gitea 特定准则 1. 每个功能(Fomantic-UI/jQuery 模块)应放在单独的文件/目录中。 2. HTML 的 id 和 class 应使用 kebab-case,最好包含2-3个与功能相关的关键词。 @@ -47,7 +47,8 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。 9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。 10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。 11. 推荐使用自定义事件名称前缀`ce-`。 -12. Gitea 的 tailwind-style CSS 类使用`gt-`前缀(`gt-relative`),而 Gitea 自身的私有框架级 CSS 类使用`g-`前缀(`g-modal-confirm`)。 +12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-df`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。 +13. 尽量避免内联脚本和样式,建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免,请解释无法避免的原因。 ### 可访问性 / ARIA @@ -64,18 +65,21 @@ Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`ari * Vue + Vanilla JS * Fomantic-UI(jQuery) +* htmx (部分页面重新加载其他静态组件) * Vanilla JS 不推荐的实现方式: * Vue + Fomantic-UI(jQuery) * jQuery + Vanilla JS +* htmx + 任何其他需要大量 JavaScript 代码或不必要的功能,如 htmx 脚本 (`hx-on`) 为了保持界面一致,Vue 组件可以使用 Fomantic-UI 的 CSS 类。 尽管不建议混合使用不同的框架, +我们使用 htmx 进行简单的交互。您可以在此 [PR](https://github.com/go-gitea/gitea/pull/28908) 中查看一个简单交互的示例,其中应使用 htmx。如果您需要更高级的反应性,请不要使用 htmx,请使用其他框架(Vue/Vanilla JS)。 但如果混合使用是必要的,并且代码设计良好且易于维护,也可以工作。 -### async 函数 +### `async` 函数 只有当函数内部存在`await`调用或返回`Promise`时,才将函数标记为`async`。 @@ -91,6 +95,12 @@ Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`ari 这是有意为之的,我们想调用异步函数并忽略Promise。 一些 lint 规则和 IDE 也会在未处理返回的 Promise 时发出警告。 +### 获取数据 + +要获取数据,请使用`modules/fetch.js`中的包装函数`GET`、`POST`等。他们 +接受内容的`data`选项,将自动设置 CSRF 令牌并返回 +[Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)。 + ### HTML 属性和 dataset 禁止使用`dataset`,它的驼峰命名行为使得搜索属性变得困难。 @@ -132,3 +142,7 @@ Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`ari ### Vue3 和 JSX Gitea 现在正在使用 Vue3。我们决定不引入 JSX,以保持 HTML 代码和 JavaScript 代码分离。 + +### UI示例 + +Gitea 使用一些自制的 UI 元素并自定义其他元素,以将它们更好地集成到通用 UI 方法中。当在开发模式(`RUN_MODE=dev`)下运行 Gitea 时,在 `http(s)://your-gitea-url:port/devtest` 下会提供一个包含一些标准化 UI 示例的页面。 diff --git a/go.mod b/go.mod index a2bbb1dfd9..837b233171 100644 --- a/go.mod +++ b/go.mod @@ -47,7 +47,7 @@ require ( github.com/go-ldap/ldap/v3 v3.4.6 github.com/go-sql-driver/mysql v1.8.0 github.com/go-swagger/go-swagger v0.30.5 - github.com/go-testfixtures/testfixtures/v3 v3.9.0 + github.com/go-testfixtures/testfixtures/v3 v3.10.0 github.com/go-webauthn/webauthn v0.10.0 github.com/gobwas/glob v0.2.3 github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f @@ -55,7 +55,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.0 github.com/google/go-github/v57 v57.0.0 github.com/google/pprof v0.0.0-20240117000934-35fc243c5815 - github.com/google/uuid v1.5.0 + github.com/google/uuid v1.6.0 github.com/gorilla/feeds v1.1.2 github.com/gorilla/sessions v1.2.2 github.com/hashicorp/go-version v1.6.0 @@ -71,7 +71,7 @@ require ( github.com/lib/pq v1.10.9 github.com/markbates/goth v1.78.0 github.com/mattn/go-isatty v0.0.20 - github.com/mattn/go-sqlite3 v1.14.19 + github.com/mattn/go-sqlite3 v1.14.22 github.com/meilisearch/meilisearch-go v0.26.1 github.com/mholt/archiver/v3 v3.5.1 github.com/microcosm-cc/bluemonday v1.0.26 @@ -126,7 +126,7 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect github.com/ClickHouse/ch-go v0.61.1 // indirect - github.com/ClickHouse/clickhouse-go/v2 v2.17.1 // indirect + github.com/ClickHouse/clickhouse-go/v2 v2.18.0 // indirect github.com/DataDog/zstd v1.5.5 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect @@ -239,7 +239,7 @@ require ( github.com/oklog/ulid v1.3.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/onsi/ginkgo v1.16.5 // indirect - github.com/paulmach/orb v0.11.0 // indirect + github.com/paulmach/orb v0.11.1 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect @@ -299,7 +299,7 @@ replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1 replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 -replace github.com/nektos/act => gitea.com/gitea/act v0.2.51 +replace github.com/nektos/act => gitea.com/gitea/act v0.259.1 replace github.com/gorilla/feeds => github.com/yardenshoham/feeds v0.0.0-20240110072658-f3d0c21c0bd5 diff --git a/go.sum b/go.sum index ceb999d230..5f7c04bb0e 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= -gitea.com/gitea/act v0.2.51 h1:gXc/B4OlTciTTzAx9cmNyw04n2SDO7exPjAsR5Idu+c= -gitea.com/gitea/act v0.2.51/go.mod h1:CoaX2053jqBlD6JMgu4d4UgFL/rp2I14Kt5mMqcs0Z0= +gitea.com/gitea/act v0.259.1 h1:8GG1o/xtUHl3qjn5f0h/2FXrT5ubBn05TJOM5ry+FBw= +gitea.com/gitea/act v0.259.1/go.mod h1:UxZWRYqQG2Yj4+4OqfGWW5a3HELwejyWFQyU7F1jUD8= gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669 h1:RUBX+MK/TsDxpHmymaOaydfigEbbzqUnG1OTZU/HAeo= gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc= gitea.com/go-chi/cache v0.0.0-20210110083709-82c4c9ce2d5e/go.mod h1:k2V/gPDEtXGjjMGuBJiapffAXTv76H4snSmlJRLUhH0= @@ -82,8 +82,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ClickHouse/ch-go v0.61.1 h1:j5rx3qnvcnYjhnP1IdXE/vdIRQiqgwAzyqOaasA6QCw= github.com/ClickHouse/ch-go v0.61.1/go.mod h1:myxt/JZgy2BYHFGQqzmaIpbfr5CMbs3YHVULaWQj5YU= -github.com/ClickHouse/clickhouse-go/v2 v2.17.1 h1:ZCmAYWpu75IyEi7+Yrs/uaAjiCGY5wfW5kXo64exkX4= -github.com/ClickHouse/clickhouse-go/v2 v2.17.1/go.mod h1:rkGTvFDTLqLIm0ma+13xmcCfr/08Gvs7KmFt1tgiWHQ= +github.com/ClickHouse/clickhouse-go/v2 v2.18.0 h1:O1LicIeg2JS2V29fKRH4+yT3f6jvvcJBm506dpVQ4mQ= +github.com/ClickHouse/clickhouse-go/v2 v2.18.0/go.mod h1:ztQvX6wm7kAbhJslS87EXEhOVNY/TObXwyURnGju5FQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= @@ -353,8 +353,8 @@ github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.m github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-testfixtures/testfixtures/v3 v3.9.0 h1:938g5V+GWLVejm3Hc+nWCuEXRlcglZDDlN/t1gWzcSY= -github.com/go-testfixtures/testfixtures/v3 v3.9.0/go.mod h1:cdsKD2ApFBjdog9jRsz6EJqF+LClq/hrwE9K/1Dzo4s= +github.com/go-testfixtures/testfixtures/v3 v3.10.0 h1:BrBwN7AuC+74g5qtk9D59TLGOaEa8Bw1WmIsf+SyzWc= +github.com/go-testfixtures/testfixtures/v3 v3.10.0/go.mod h1:z8RoleoNtibi6Ar8ziCW7e6PQ+jWiqbUWvuv8AMe4lo= github.com/go-webauthn/webauthn v0.10.0 h1:yuW2e1tXnRAwAvKrR4q4LQmc6XtCMH639/ypZGhZCwk= github.com/go-webauthn/webauthn v0.10.0/go.mod h1:l0NiauXhL6usIKqNLCUM3Qir43GK7ORg8ggold0Uv/Y= github.com/go-webauthn/x v0.1.6 h1:QNAX+AWeqRt9loE8mULeWJCqhVG5D/jvdmJ47fIWCkQ= @@ -457,8 +457,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -609,8 +609,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= -github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/meilisearch/meilisearch-go v0.26.1 h1:3bmo2uLijX7kvBmiZ9LupVfC95TFcRJDgrRTzbOoE4A= github.com/meilisearch/meilisearch-go v0.26.1/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0= github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30= @@ -681,8 +681,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU= github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/paulmach/orb v0.11.0 h1:JfVXJUBeH9ifc/OrhBY0lL16QsmPgpCHMlqSSYhcgAA= -github.com/paulmach/orb v0.11.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= +github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= +github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= diff --git a/models/actions/variable.go b/models/actions/variable.go index 12717e0ae4..14ded60fac 100644 --- a/models/actions/variable.go +++ b/models/actions/variable.go @@ -10,6 +10,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -82,3 +83,35 @@ func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) }) return count != 0, err } + +func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string, error) { + variables := map[string]string{} + + // Global + globalVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{}) + if err != nil { + log.Error("find global variables: %v", err) + return nil, err + } + + // Org / User level + ownerVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{OwnerID: run.Repo.OwnerID}) + if err != nil { + log.Error("find variables of org: %d, error: %v", run.Repo.OwnerID, err) + return nil, err + } + + // Repo level + repoVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{RepoID: run.RepoID}) + if err != nil { + log.Error("find variables of repo: %d, error: %v", run.RepoID, err) + return nil, err + } + + // Level precedence: Repo > Org / User > Global + for _, v := range append(globalVariables, append(ownerVariables, repoVariables...)...) { + variables[v.Name] = v.Data + } + + return variables, nil +} diff --git a/models/activities/action.go b/models/activities/action.go index 6df113138b..6593767679 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -395,10 +395,14 @@ func (a *Action) GetCreate() time.Time { return a.CreatedUnix.AsTime() } -// GetIssueInfos returns a list of issues associated with -// the action. +// GetIssueInfos returns a list of associated information with the action. func (a *Action) GetIssueInfos() []string { - return strings.SplitN(a.Content, "|", 3) + // make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length + ret := strings.SplitN(a.Content, "|", 3) + for len(ret) < 3 { + ret = append(ret, "") + } + return ret } // GetIssueTitle returns the title of first issue associated with the action. diff --git a/models/db/collation.go b/models/db/collation.go index 2f5ff2bf05..c128cf5029 100644 --- a/models/db/collation.go +++ b/models/db/collation.go @@ -166,8 +166,7 @@ func preprocessDatabaseCollation(x *xorm.Engine) { // try to alter database collation to expected if the database is empty, it might fail in some cases (and it isn't necessary to succeed) // at the moment, there is no "altering" solution for MSSQL, site admin should manually change the database collation - // and there is a bug https://github.com/go-testfixtures/testfixtures/pull/182 mssql: Invalid object name 'information_schema.tables'. - if !r.CollationEquals(r.DatabaseCollation, r.ExpectedCollation) && r.ExistingTableNumber == 0 && x.Dialect().URI().DBType == schemas.MYSQL { + if !r.CollationEquals(r.DatabaseCollation, r.ExpectedCollation) && r.ExistingTableNumber == 0 { if err = alterDatabaseCollation(x, r.ExpectedCollation); err != nil { log.Error("Failed to change database collation to %q: %v", r.ExpectedCollation, err) } else { diff --git a/models/fixtures/hook_task.yml b/models/fixtures/hook_task.yml index 6dbb10151a..d573406b36 100644 --- a/models/fixtures/hook_task.yml +++ b/models/fixtures/hook_task.yml @@ -3,3 +3,35 @@ hook_id: 1 uuid: uuid1 is_delivered: true + is_succeed: false + request_content: > + { + "url": "/matrix-delivered", + "http_method":"PUT", + "headers": { + "X-Head": "42" + }, + "body": "{}" + } + +- + id: 2 + hook_id: 1 + uuid: uuid2 + is_delivered: false + +- + id: 3 + hook_id: 1 + uuid: uuid3 + is_delivered: true + is_succeed: true + payload_content: '{"key":"value"}' # legacy task, payload saved in payload_content (and not in request_content) + request_content: > + { + "url": "/matrix-success", + "http_method":"PUT", + "headers": { + "X-Head": "42" + } + } diff --git a/models/issues/pull.go b/models/issues/pull.go index 84a62ddc90..a2c116c4c0 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -909,12 +909,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, pull *Issue, pr *PullReque } defer repo.Close() - branch, err := repo.GetDefaultBranch() - if err != nil { - return err - } - - commit, err := repo.GetBranchCommit(branch) + commit, err := repo.GetBranchCommit(pr.BaseRepo.DefaultBranch) if err != nil { return err } diff --git a/models/migrations/base/db_test.go b/models/migrations/base/db_test.go index 4d61b758cf..80bf00b22a 100644 --- a/models/migrations/base/db_test.go +++ b/models/migrations/base/db_test.go @@ -36,12 +36,14 @@ func Test_DropTableColumns(t *testing.T) { "updated_unix", } + x.SetMapper(names.GonicMapper{}) + for i := range columns { - x.SetMapper(names.GonicMapper{}) if err := x.Sync(new(DropTest)); err != nil { t.Errorf("unable to create DropTest table: %v", err) return } + sess := x.NewSession() if err := sess.Begin(); err != nil { sess.Close() @@ -64,7 +66,6 @@ func Test_DropTableColumns(t *testing.T) { return } for j := range columns[i+1:] { - x.SetMapper(names.GonicMapper{}) if err := x.Sync(new(DropTest)); err != nil { t.Errorf("unable to create DropTest table: %v", err) return diff --git a/models/migrations/fixtures/Test_AddIssueResourceIndexTable/issue.yml b/models/migrations/fixtures/Test_AddIssueResourceIndexTable/issue.yml new file mode 100644 index 0000000000..f95d47916b --- /dev/null +++ b/models/migrations/fixtures/Test_AddIssueResourceIndexTable/issue.yml @@ -0,0 +1,4 @@ +- + id: 1 + repo_id: 1 + index: 1 diff --git a/models/migrations/fixtures/Test_AddPayloadVersionToHookTaskTable/hook_task.yml b/models/migrations/fixtures/Test_AddPayloadVersionToHookTaskTable/hook_task.yml new file mode 100644 index 0000000000..716a2a017d --- /dev/null +++ b/models/migrations/fixtures/Test_AddPayloadVersionToHookTaskTable/hook_task.yml @@ -0,0 +1,16 @@ +- id: 11 + uuid: uuid11 + hook_id: 1 + payload_content: > + {"data":"payload"} + event_type: create + delivered: 1706106005 + +- id: 101 + uuid: uuid101 + hook_id: 1 + payload_content: > + {"data":"payload"} + event_type: create + delivered: 1706106006 + is_delivered: true diff --git a/models/migrations/fixtures/Test_AddPayloadVersionToHookTaskTable/hook_task_migrated.yml b/models/migrations/fixtures/Test_AddPayloadVersionToHookTaskTable/hook_task_migrated.yml new file mode 100644 index 0000000000..913d927d91 --- /dev/null +++ b/models/migrations/fixtures/Test_AddPayloadVersionToHookTaskTable/hook_task_migrated.yml @@ -0,0 +1,18 @@ +- id: 11 + uuid: uuid11 + hook_id: 1 + payload_content: > + {"data":"payload"} + event_type: create + delivered: 1706106005 + payload_version: 1 + +- id: 101 + uuid: uuid101 + hook_id: 1 + payload_content: > + {"data":"payload"} + event_type: create + delivered: 1706106006 + is_delivered: true + payload_version: 1 diff --git a/models/migrations/fixtures/Test_AddRepoIDForAttachment/attachment.yml b/models/migrations/fixtures/Test_AddRepoIDForAttachment/attachment.yml new file mode 100644 index 0000000000..056236ba9e --- /dev/null +++ b/models/migrations/fixtures/Test_AddRepoIDForAttachment/attachment.yml @@ -0,0 +1,11 @@ +- + id: 1 + uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 + issue_id: 1 + release_id: 0 + +- + id: 2 + uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12 + issue_id: 0 + release_id: 1 diff --git a/models/migrations/fixtures/Test_AddRepoIDForAttachment/issue.yml b/models/migrations/fixtures/Test_AddRepoIDForAttachment/issue.yml new file mode 100644 index 0000000000..7f3255096d --- /dev/null +++ b/models/migrations/fixtures/Test_AddRepoIDForAttachment/issue.yml @@ -0,0 +1,3 @@ +- + id: 1 + repo_id: 1 diff --git a/models/migrations/fixtures/Test_AddRepoIDForAttachment/release.yml b/models/migrations/fixtures/Test_AddRepoIDForAttachment/release.yml new file mode 100644 index 0000000000..7f3255096d --- /dev/null +++ b/models/migrations/fixtures/Test_AddRepoIDForAttachment/release.yml @@ -0,0 +1,3 @@ +- + id: 1 + repo_id: 1 diff --git a/models/migrations/fixtures/Test_RepositoryFormat/comment.yml b/models/migrations/fixtures/Test_RepositoryFormat/comment.yml new file mode 100644 index 0000000000..1197b086e3 --- /dev/null +++ b/models/migrations/fixtures/Test_RepositoryFormat/comment.yml @@ -0,0 +1,3 @@ +- + id: 1 + commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d diff --git a/models/migrations/fixtures/Test_RepositoryFormat/commit_status.yml b/models/migrations/fixtures/Test_RepositoryFormat/commit_status.yml new file mode 100644 index 0000000000..ca0aaec4cc --- /dev/null +++ b/models/migrations/fixtures/Test_RepositoryFormat/commit_status.yml @@ -0,0 +1,3 @@ +- + id: 1 + context_hash: 19fe5caf872476db265596eaac1dc35ad1c6422d diff --git a/models/migrations/fixtures/Test_RepositoryFormat/pull_request.yml b/models/migrations/fixtures/Test_RepositoryFormat/pull_request.yml new file mode 100644 index 0000000000..380cc079ee --- /dev/null +++ b/models/migrations/fixtures/Test_RepositoryFormat/pull_request.yml @@ -0,0 +1,5 @@ +- + id: 1 + commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d + merge_base: 19fe5caf872476db265596eaac1dc35ad1c6422d + merged_commit_id: 19fe5caf872476db265596eaac1dc35ad1c6422d diff --git a/models/migrations/fixtures/Test_RepositoryFormat/release.yml b/models/migrations/fixtures/Test_RepositoryFormat/release.yml new file mode 100644 index 0000000000..ffabe4ab9e --- /dev/null +++ b/models/migrations/fixtures/Test_RepositoryFormat/release.yml @@ -0,0 +1,3 @@ +- + id: 1 + sha1: 19fe5caf872476db265596eaac1dc35ad1c6422d diff --git a/models/migrations/fixtures/Test_RepositoryFormat/repo_archiver.yml b/models/migrations/fixtures/Test_RepositoryFormat/repo_archiver.yml new file mode 100644 index 0000000000..f04cb3b340 --- /dev/null +++ b/models/migrations/fixtures/Test_RepositoryFormat/repo_archiver.yml @@ -0,0 +1,3 @@ +- + id: 1 + commit_id: 19fe5caf872476db265596eaac1dc35ad1c6422d diff --git a/models/migrations/fixtures/Test_RepositoryFormat/repo_indexer_status.yml b/models/migrations/fixtures/Test_RepositoryFormat/repo_indexer_status.yml new file mode 100644 index 0000000000..1197b086e3 --- /dev/null +++ b/models/migrations/fixtures/Test_RepositoryFormat/repo_indexer_status.yml @@ -0,0 +1,3 @@ +- + id: 1 + commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d diff --git a/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml b/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml new file mode 100644 index 0000000000..1197b086e3 --- /dev/null +++ b/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml @@ -0,0 +1,3 @@ +- + id: 1 + commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d diff --git a/models/migrations/fixtures/Test_UpdateBadgeColName/badge.yml b/models/migrations/fixtures/Test_UpdateBadgeColName/badge.yml new file mode 100644 index 0000000000..7025144106 --- /dev/null +++ b/models/migrations/fixtures/Test_UpdateBadgeColName/badge.yml @@ -0,0 +1,4 @@ +- + id: 1 + description: the badge + image_url: https://gitea.com/myimage.png diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 1c8563cebe..173d37234a 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -560,6 +560,14 @@ var migrations = []Migration{ NewMigration("Add PreviousDuration to ActionRun", v1_22.AddPreviousDurationToActionRun), // v286 -> v287 NewMigration("Add support for SHA256 git repositories", v1_22.AdjustDBForSha256), + // v287 -> v288 + NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges), + // v288 -> v289 + NewMigration("Add user_blocking table", v1_22.AddUserBlockingTable), + // v289 -> v290 + NewMigration("Add default_wiki_branch to repository table", v1_22.AddDefaultWikiBranch), + // v290 -> v291 + NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_16/v193_test.go b/models/migrations/v1_16/v193_test.go index 17669a012e..d99bbc2962 100644 --- a/models/migrations/v1_16/v193_test.go +++ b/models/migrations/v1_16/v193_test.go @@ -15,7 +15,6 @@ func Test_AddRepoIDForAttachment(t *testing.T) { type Attachment struct { ID int64 `xorm:"pk autoincr"` UUID string `xorm:"uuid UNIQUE"` - RepoID int64 `xorm:"INDEX"` // this should not be zero IssueID int64 `xorm:"INDEX"` // maybe zero when creating ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating UploaderID int64 `xorm:"INDEX DEFAULT 0"` @@ -44,12 +43,21 @@ func Test_AddRepoIDForAttachment(t *testing.T) { return } - var issueAttachments []*Attachment - err := x.Where("issue_id > 0").Find(&issueAttachments) + type NewAttachment struct { + ID int64 `xorm:"pk autoincr"` + UUID string `xorm:"uuid UNIQUE"` + RepoID int64 `xorm:"INDEX"` // this should not be zero + IssueID int64 `xorm:"INDEX"` // maybe zero when creating + ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating + UploaderID int64 `xorm:"INDEX DEFAULT 0"` + } + + var issueAttachments []*NewAttachment + err := x.Table("attachment").Where("issue_id > 0").Find(&issueAttachments) assert.NoError(t, err) for _, attach := range issueAttachments { - assert.Greater(t, attach.RepoID, 0) - assert.Greater(t, attach.IssueID, 0) + assert.Greater(t, attach.RepoID, int64(0)) + assert.Greater(t, attach.IssueID, int64(0)) var issue Issue has, err := x.ID(attach.IssueID).Get(&issue) assert.NoError(t, err) @@ -57,12 +65,12 @@ func Test_AddRepoIDForAttachment(t *testing.T) { assert.EqualValues(t, attach.RepoID, issue.RepoID) } - var releaseAttachments []*Attachment - err = x.Where("release_id > 0").Find(&releaseAttachments) + var releaseAttachments []*NewAttachment + err = x.Table("attachment").Where("release_id > 0").Find(&releaseAttachments) assert.NoError(t, err) for _, attach := range releaseAttachments { - assert.Greater(t, attach.RepoID, 0) - assert.Greater(t, attach.IssueID, 0) + assert.Greater(t, attach.RepoID, int64(0)) + assert.Greater(t, attach.ReleaseID, int64(0)) var release Release has, err := x.ID(attach.ReleaseID).Get(&release) assert.NoError(t, err) diff --git a/models/migrations/v1_22/v283.go b/models/migrations/v1_22/v283.go index 97b22f72a0..0a45c51245 100644 --- a/models/migrations/v1_22/v283.go +++ b/models/migrations/v1_22/v283.go @@ -4,10 +4,40 @@ package v1_22 //nolint import ( + "fmt" + "xorm.io/xorm" + "xorm.io/xorm/schemas" ) func AddCombinedIndexToIssueUser(x *xorm.Engine) error { + type OldIssueUser struct { + IssueID int64 + UID int64 + Cnt int64 + } + + var duplicatedIssueUsers []OldIssueUser + if err := x.SQL("select * from (select issue_id, uid, count(1) as cnt from issue_user group by issue_id, uid) a where a.cnt > 1"). + Find(&duplicatedIssueUsers); err != nil { + return err + } + for _, issueUser := range duplicatedIssueUsers { + if x.Dialect().URI().DBType == schemas.MSSQL { + if _, err := x.Exec(fmt.Sprintf("delete from issue_user where id in (SELECT top %d id FROM issue_user WHERE issue_id = ? and uid = ?)", issueUser.Cnt-1), issueUser.IssueID, issueUser.UID); err != nil { + return err + } + } else { + var ids []int64 + if err := x.SQL("SELECT id FROM issue_user WHERE issue_id = ? and uid = ? limit ?", issueUser.IssueID, issueUser.UID, issueUser.Cnt-1).Find(&ids); err != nil { + return err + } + if _, err := x.Table("issue_user").In("id", ids).Delete(); err != nil { + return err + } + } + } + type IssueUser struct { UID int64 `xorm:"INDEX unique(uid_to_issue)"` // User ID. IssueID int64 `xorm:"INDEX unique(uid_to_issue)"` diff --git a/models/migrations/v1_22/v286.go b/models/migrations/v1_22/v286.go index ef19f64221..fbbd87344f 100644 --- a/models/migrations/v1_22/v286.go +++ b/models/migrations/v1_22/v286.go @@ -36,9 +36,9 @@ func expandHashReferencesToSha256(x *xorm.Engine) error { if setting.Database.Type.IsMSSQL() { // drop indexes that need to be re-created afterwards droppedIndexes := []string{ - "DROP INDEX commit_status.IDX_commit_status_context_hash", - "DROP INDEX review_state.UQE_review_state_pull_commit_user", - "DROP INDEX repo_archiver.UQE_repo_archiver_s", + "DROP INDEX IF EXISTS [IDX_commit_status_context_hash] ON [commit_status]", + "DROP INDEX IF EXISTS [UQE_review_state_pull_commit_user] ON [review_state]", + "DROP INDEX IF EXISTS [UQE_repo_archiver_s] ON [repo_archiver]", } for _, s := range droppedIndexes { _, err := db.Exec(s) @@ -53,7 +53,7 @@ func expandHashReferencesToSha256(x *xorm.Engine) error { if setting.Database.Type.IsMySQL() { _, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` MODIFY COLUMN `%s` VARCHAR(64)", alts[0], alts[1])) } else if setting.Database.Type.IsMSSQL() { - _, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` VARCHAR(64)", alts[0], alts[1])) + _, err = db.Exec(fmt.Sprintf("ALTER TABLE [%s] ALTER COLUMN [%s] VARCHAR(64)", alts[0], alts[1])) } else { _, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` TYPE VARCHAR(64)", alts[0], alts[1])) } diff --git a/models/migrations/v1_22/v286_test.go b/models/migrations/v1_22/v286_test.go index 6493bfba2f..7c353747e3 100644 --- a/models/migrations/v1_22/v286_test.go +++ b/models/migrations/v1_22/v286_test.go @@ -14,59 +14,75 @@ import ( func PrepareOldRepository(t *testing.T) (*xorm.Engine, func()) { type Repository struct { // old struct - ID int64 `xorm:"pk autoincr"` - ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"` + ID int64 `xorm:"pk autoincr"` } - type CommitStatus struct { // old struct - ID int64 `xorm:"pk autoincr"` - ContextHash string `xorm:"char(40)"` + type CommitStatus struct { + ID int64 + ContextHash string } - type Comment struct { // old struct - ID int64 `xorm:"pk autoincr"` - CommitSHA string `xorm:"VARCHAR(40)"` + type RepoArchiver struct { + ID int64 + RepoID int64 + Type int + CommitID string } - type PullRequest struct { // old struct - ID int64 `xorm:"pk autoincr"` - MergeBase string `xorm:"VARCHAR(40)"` - MergedCommitID string `xorm:"VARCHAR(40)"` + type ReviewState struct { + ID int64 + CommitSHA string + UserID int64 + PullID int64 } - type Review struct { // old struct - ID int64 `xorm:"pk autoincr"` - CommitID string `xorm:"VARCHAR(40)"` + type Comment struct { + ID int64 + CommitSHA string } - type ReviewState struct { // old struct - ID int64 `xorm:"pk autoincr"` - CommitSHA string `xorm:"VARCHAR(40)"` + type PullRequest struct { + ID int64 + CommitSHA string + MergeBase string + MergedCommitID string } - type RepoArchiver struct { // old struct - ID int64 `xorm:"pk autoincr"` - CommitID string `xorm:"VARCHAR(40)"` + type Release struct { + ID int64 + Sha1 string } - type Release struct { // old struct - ID int64 `xorm:"pk autoincr"` - Sha1 string `xorm:"VARCHAR(40)"` + type RepoIndexerStatus struct { + ID int64 + CommitSHA string } - type RepoIndexerStatus struct { // old struct - ID int64 `xorm:"pk autoincr"` - CommitSha string `xorm:"VARCHAR(40)"` + type Review struct { + ID int64 + CommitID string } // Prepare and load the testing database - return base.PrepareTestEnv(t, 0, new(Repository), new(CommitStatus), new(Comment), new(PullRequest), new(Review), new(ReviewState), new(RepoArchiver), new(Release), new(RepoIndexerStatus)) + return base.PrepareTestEnv(t, 0, + new(Repository), + new(CommitStatus), + new(RepoArchiver), + new(ReviewState), + new(Review), + new(Comment), + new(PullRequest), + new(Release), + new(RepoIndexerStatus), + ) } func Test_RepositoryFormat(t *testing.T) { x, deferable := PrepareOldRepository(t) defer deferable() + assert.NoError(t, AdjustDBForSha256(x)) + type Repository struct { ID int64 `xorm:"pk autoincr"` ObjectFormatName string `xorg:"not null default('sha1')"` @@ -79,12 +95,10 @@ func Test_RepositoryFormat(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, 4, count) - assert.NoError(t, AdjustDBForSha256(x)) - - repo.ID = 20 repo.ObjectFormatName = "sha256" _, err = x.Insert(repo) assert.NoError(t, err) + id := repo.ID count, err = x.Count(new(Repository)) assert.NoError(t, err) @@ -97,7 +111,7 @@ func Test_RepositoryFormat(t *testing.T) { assert.EqualValues(t, "sha1", repo.ObjectFormatName) repo = new(Repository) - ok, err = x.ID(20).Get(repo) + ok, err = x.ID(id).Get(repo) assert.NoError(t, err) assert.EqualValues(t, true, ok) assert.EqualValues(t, "sha256", repo.ObjectFormatName) diff --git a/models/migrations/v1_22/v287.go b/models/migrations/v1_22/v287.go new file mode 100644 index 0000000000..c8b1593286 --- /dev/null +++ b/models/migrations/v1_22/v287.go @@ -0,0 +1,46 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "xorm.io/xorm" +) + +type BadgeUnique struct { + ID int64 `xorm:"pk autoincr"` + Slug string `xorm:"UNIQUE"` +} + +func (BadgeUnique) TableName() string { + return "badge" +} + +func UseSlugInsteadOfIDForBadges(x *xorm.Engine) error { + type Badge struct { + Slug string + } + + err := x.Sync(new(Badge)) + if err != nil { + return err + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + _, err = sess.Exec("UPDATE `badge` SET `slug` = `id` Where `slug` IS NULL") + if err != nil { + return err + } + + err = sess.Sync(new(BadgeUnique)) + if err != nil { + return err + } + + return sess.Commit() +} diff --git a/models/migrations/v1_22/v288.go b/models/migrations/v1_22/v288.go new file mode 100644 index 0000000000..7c93bfcc66 --- /dev/null +++ b/models/migrations/v1_22/v288.go @@ -0,0 +1,26 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +type Blocking struct { + ID int64 `xorm:"pk autoincr"` + BlockerID int64 `xorm:"UNIQUE(block)"` + BlockeeID int64 `xorm:"UNIQUE(block)"` + Note string + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` +} + +func (*Blocking) TableName() string { + return "user_blocking" +} + +func AddUserBlockingTable(x *xorm.Engine) error { + return x.Sync(&Blocking{}) +} diff --git a/models/migrations/v1_22/v289.go b/models/migrations/v1_22/v289.go new file mode 100644 index 0000000000..e2dfc48715 --- /dev/null +++ b/models/migrations/v1_22/v289.go @@ -0,0 +1,18 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import "xorm.io/xorm" + +func AddDefaultWikiBranch(x *xorm.Engine) error { + type Repository struct { + ID int64 + DefaultWikiBranch string + } + if err := x.Sync(&Repository{}); err != nil { + return err + } + _, err := x.Exec("UPDATE `repository` SET default_wiki_branch = 'master' WHERE (default_wiki_branch IS NULL) OR (default_wiki_branch = '')") + return err +} diff --git a/models/migrations/v1_22/v290.go b/models/migrations/v1_22/v290.go new file mode 100644 index 0000000000..e9c471b3dd --- /dev/null +++ b/models/migrations/v1_22/v290.go @@ -0,0 +1,39 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "code.gitea.io/gitea/modules/timeutil" + webhook_module "code.gitea.io/gitea/modules/webhook" + + "xorm.io/xorm" +) + +// HookTask represents a hook task. +// exact copy of models/webhook/hooktask.go when this migration was created +// - xorm:"-" fields deleted +type HookTask struct { + ID int64 `xorm:"pk autoincr"` + HookID int64 `xorm:"index"` + UUID string `xorm:"unique"` + PayloadContent string `xorm:"LONGTEXT"` + EventType webhook_module.HookEventType + IsDelivered bool + Delivered timeutil.TimeStampNano + + // History info. + IsSucceed bool + RequestContent string `xorm:"LONGTEXT"` + ResponseContent string `xorm:"LONGTEXT"` + + // Version number to allow for smooth version upgrades: + // - Version 1: PayloadContent contains the JSON as send to the URL + // - Version 2: PayloadContent contains the original event + PayloadVersion int `xorm:"DEFAULT 1"` +} + +func AddPayloadVersionToHookTaskTable(x *xorm.Engine) error { + // create missing column + return x.Sync(new(HookTask)) +} diff --git a/models/migrations/v1_22/v290_test.go b/models/migrations/v1_22/v290_test.go new file mode 100644 index 0000000000..24a1c0b0a5 --- /dev/null +++ b/models/migrations/v1_22/v290_test.go @@ -0,0 +1,58 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "strconv" + "testing" + + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/modules/timeutil" + webhook_module "code.gitea.io/gitea/modules/webhook" + + "github.com/stretchr/testify/assert" +) + +func Test_AddPayloadVersionToHookTaskTable(t *testing.T) { + type HookTaskMigrated HookTask + + // HookTask represents a hook task, as of before the migration + type HookTask struct { + ID int64 `xorm:"pk autoincr"` + HookID int64 `xorm:"index"` + UUID string `xorm:"unique"` + PayloadContent string `xorm:"LONGTEXT"` + EventType webhook_module.HookEventType + IsDelivered bool + Delivered timeutil.TimeStampNano + + // History info. + IsSucceed bool + RequestContent string `xorm:"LONGTEXT"` + ResponseContent string `xorm:"LONGTEXT"` + } + + // Prepare and load the testing database + x, deferable := base.PrepareTestEnv(t, 0, new(HookTask), new(HookTaskMigrated)) + defer deferable() + if x == nil || t.Failed() { + return + } + + assert.NoError(t, AddPayloadVersionToHookTaskTable(x)) + + expected := []HookTaskMigrated{} + assert.NoError(t, x.Table("hook_task_migrated").Asc("id").Find(&expected)) + assert.Len(t, expected, 2) + + got := []HookTaskMigrated{} + assert.NoError(t, x.Table("hook_task").Asc("id").Find(&got)) + + for i, expected := range expected { + expected, got := expected, got[i] + t.Run(strconv.FormatInt(expected.ID, 10), func(t *testing.T) { + assert.Equal(t, expected.PayloadVersion, got.PayloadVersion) + }) + } +} diff --git a/models/project/board.go b/models/project/board.go index 3e2d8e0472..c0e6529880 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -232,7 +232,7 @@ func UpdateBoard(ctx context.Context, board *Board) error { func (p *Project) GetBoards(ctx context.Context) (BoardList, error) { boards := make([]*Board, 0, 5) - if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("Sorting").Find(&boards); err != nil { + if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("sorting").Find(&boards); err != nil { return nil, err } diff --git a/models/secret/secret.go b/models/secret/secret.go index 41e860d7f6..35bed500b9 100644 --- a/models/secret/secret.go +++ b/models/secret/secret.go @@ -9,7 +9,10 @@ import ( "fmt" "strings" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + actions_module "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/log" secret_module "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -112,3 +115,39 @@ func UpdateSecret(ctx context.Context, secretID int64, data string) error { } return err } + +func GetSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[string]string, error) { + secrets := map[string]string{} + + secrets["GITHUB_TOKEN"] = task.Token + secrets["GITEA_TOKEN"] = task.Token + + if task.Job.Run.IsForkPullRequest && task.Job.Run.TriggerEvent != actions_module.GithubEventPullRequestTarget { + // ignore secrets for fork pull request, except GITHUB_TOKEN and GITEA_TOKEN which are automatically generated. + // for the tasks triggered by pull_request_target event, they could access the secrets because they will run in the context of the base branch + // see the documentation: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target + return secrets, nil + } + + ownerSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID}) + if err != nil { + log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err) + return nil, err + } + repoSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{RepoID: task.Job.Run.RepoID}) + if err != nil { + log.Error("find secrets of repo %v: %v", task.Job.Run.RepoID, err) + return nil, err + } + + for _, secret := range append(ownerSecrets, repoSecrets...) { + v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data) + if err != nil { + log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err) + return nil, err + } + secrets[secret.Name] = v + } + + return secrets, nil +} diff --git a/models/user/email_address.go b/models/user/email_address.go index 6f3d5b1dde..1d90b127bf 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -157,37 +157,18 @@ func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error { var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") -// ValidateEmail check if email is a allowed address +// ValidateEmail check if email is a valid & allowed address func ValidateEmail(email string) error { - if len(email) == 0 { - return ErrEmailInvalid{email} + if err := validateEmailBasic(email); err != nil { + return err } + return validateEmailDomain(email) +} - if !emailRegexp.MatchString(email) { - return ErrEmailCharIsNotSupported{email} - } - - if email[0] == '-' { - return ErrEmailInvalid{email} - } - - if _, err := mail.ParseAddress(email); err != nil { - return ErrEmailInvalid{email} - } - - // if there is no allow list, then check email against block list - if len(setting.Service.EmailDomainAllowList) == 0 && - validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) { - return ErrEmailInvalid{email} - } - - // if there is an allow list, then check email against allow list - if len(setting.Service.EmailDomainAllowList) > 0 && - !validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) { - return ErrEmailInvalid{email} - } - - return nil +// ValidateEmailForAdmin check if email is a valid address when admins manually add or edit users +func ValidateEmailForAdmin(email string) error { + return validateEmailBasic(email) + // In this case we do not need to check the email domain } func GetEmailAddressByEmail(ctx context.Context, email string) (*EmailAddress, error) { @@ -543,3 +524,41 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate return committer.Commit() } + +// validateEmailBasic checks whether the email complies with the rules +func validateEmailBasic(email string) error { + if len(email) == 0 { + return ErrEmailInvalid{email} + } + + if !emailRegexp.MatchString(email) { + return ErrEmailCharIsNotSupported{email} + } + + if email[0] == '-' { + return ErrEmailInvalid{email} + } + + if _, err := mail.ParseAddress(email); err != nil { + return ErrEmailInvalid{email} + } + + return nil +} + +// validateEmailDomain checks whether the email domain is allowed or blocked +func validateEmailDomain(email string) error { + // if there is no allow list, then check email against block list + if len(setting.Service.EmailDomainAllowList) == 0 && + validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) { + return ErrEmailInvalid{email} + } + + // if there is an allow list, then check email against allow list + if len(setting.Service.EmailDomainAllowList) > 0 && + !validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) { + return ErrEmailInvalid{email} + } + + return nil +} diff --git a/models/user/user.go b/models/user/user.go index 710fab9f4e..6d8eea6585 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -598,6 +598,16 @@ type CreateUserOverwriteOptions struct { // CreateUser creates record of a new user. func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { + return createUser(ctx, u, false, overwriteDefault...) +} + +// AdminCreateUser is used by admins to manually create users +func AdminCreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { + return createUser(ctx, u, true, overwriteDefault...) +} + +// createUser creates record of a new user. +func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { if err = IsUsableUsername(u.Name); err != nil { return err } @@ -651,8 +661,14 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve return err } - if err := ValidateEmail(u.Email); err != nil { - return err + if createdByAdmin { + if err := ValidateEmailForAdmin(u.Email); err != nil { + return err + } + } else { + if err := ValidateEmail(u.Email); err != nil { + return err + } } ctx, committer, err := db.TxContext(ctx) diff --git a/models/webhook/hooktask.go b/models/webhook/hooktask.go index 2fb655ebca..ff3fdbadb2 100644 --- a/models/webhook/hooktask.go +++ b/models/webhook/hooktask.go @@ -5,13 +5,13 @@ package webhook import ( "context" + "errors" "time" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -31,6 +31,7 @@ type HookRequest struct { URL string `json:"url"` HTTPMethod string `json:"http_method"` Headers map[string]string `json:"headers"` + Body string `json:"body"` } // HookResponse represents hook task response information. @@ -45,11 +46,15 @@ type HookTask struct { ID int64 `xorm:"pk autoincr"` HookID int64 `xorm:"index"` UUID string `xorm:"unique"` - api.Payloader `xorm:"-"` PayloadContent string `xorm:"LONGTEXT"` - EventType webhook_module.HookEventType - IsDelivered bool - Delivered timeutil.TimeStampNano + // PayloadVersion number to allow for smooth version upgrades: + // - PayloadVersion 1: PayloadContent contains the JSON as sent to the URL + // - PayloadVersion 2: PayloadContent contains the original event + PayloadVersion int `xorm:"DEFAULT 1"` + + EventType webhook_module.HookEventType + IsDelivered bool + Delivered timeutil.TimeStampNano // History info. IsSucceed bool @@ -115,16 +120,12 @@ func HookTasks(ctx context.Context, hookID int64, page int) ([]*HookTask, error) // it handles conversion from Payload to PayloadContent. func CreateHookTask(ctx context.Context, t *HookTask) (*HookTask, error) { t.UUID = gouuid.New().String() - if t.Payloader != nil { - data, err := t.Payloader.JSONPayload() - if err != nil { - return nil, err - } - t.PayloadContent = string(data) - } if t.Delivered == 0 { t.Delivered = timeutil.TimeStampNanoNow() } + if t.PayloadVersion == 0 { + return nil, errors.New("missing HookTask.PayloadVersion") + } return t, db.Insert(ctx, t) } @@ -165,6 +166,7 @@ func ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask, HookID: task.HookID, PayloadContent: task.PayloadContent, EventType: task.EventType, + PayloadVersion: task.PayloadVersion, }) } diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index c70c8e99fc..f4403776ce 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -12,7 +12,6 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/optional" - api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -35,8 +34,10 @@ func TestWebhook_History(t *testing.T) { webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 1}) tasks, err := webhook.History(db.DefaultContext, 0) assert.NoError(t, err) - if assert.Len(t, tasks, 1) { - assert.Equal(t, int64(1), tasks[0].ID) + if assert.Len(t, tasks, 3) { + assert.Equal(t, int64(3), tasks[0].ID) + assert.Equal(t, int64(2), tasks[1].ID) + assert.Equal(t, int64(1), tasks[2].ID) } webhook = unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2}) @@ -197,8 +198,10 @@ func TestHookTasks(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTasks, err := HookTasks(db.DefaultContext, 1, 1) assert.NoError(t, err) - if assert.Len(t, hookTasks, 1) { - assert.Equal(t, int64(1), hookTasks[0].ID) + if assert.Len(t, hookTasks, 3) { + assert.Equal(t, int64(3), hookTasks[0].ID) + assert.Equal(t, int64(2), hookTasks[1].ID) + assert.Equal(t, int64(1), hookTasks[2].ID) } hookTasks, err = HookTasks(db.DefaultContext, unittest.NonexistentID, 1) @@ -209,8 +212,8 @@ func TestHookTasks(t *testing.T) { func TestCreateHookTask(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 3, - Payloader: &api.PushPayload{}, + HookID: 3, + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -232,10 +235,10 @@ func TestUpdateHookTask(t *testing.T) { func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 3, - Payloader: &api.PushPayload{}, - IsDelivered: true, - Delivered: timeutil.TimeStampNanoNow(), + HookID: 3, + IsDelivered: true, + Delivered: timeutil.TimeStampNanoNow(), + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -249,9 +252,9 @@ func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) { func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 4, - Payloader: &api.PushPayload{}, - IsDelivered: false, + HookID: 4, + IsDelivered: false, + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -265,10 +268,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) { func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 4, - Payloader: &api.PushPayload{}, - IsDelivered: true, - Delivered: timeutil.TimeStampNanoNow(), + HookID: 4, + IsDelivered: true, + Delivered: timeutil.TimeStampNanoNow(), + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -282,10 +285,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) { func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 3, - Payloader: &api.PushPayload{}, - IsDelivered: true, - Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()), + HookID: 3, + IsDelivered: true, + Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()), + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -299,9 +302,9 @@ func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) { func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 4, - Payloader: &api.PushPayload{}, - IsDelivered: false, + HookID: 4, + IsDelivered: false, + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -315,10 +318,10 @@ func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) { func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 4, - Payloader: &api.PushPayload{}, - IsDelivered: true, - Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()), + HookID: 4, + IsDelivered: true, + Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()), + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index 979c5dec91..552ae2bb8c 100644 --- a/modules/git/repo_branch.go +++ b/modules/git/repo_branch.go @@ -55,15 +55,8 @@ func (repo *Repository) GetHEADBranch() (*Branch, error) { }, nil } -// SetDefaultBranch sets default branch of repository. -func (repo *Repository) SetDefaultBranch(name string) error { - _, _, err := NewCommand(repo.Ctx, "symbolic-ref", "HEAD").AddDynamicArguments(BranchPrefix + name).RunStdString(&RunOpts{Dir: repo.Path}) - return err -} - -// GetDefaultBranch gets default branch of repository. -func (repo *Repository) GetDefaultBranch() (string, error) { - stdout, _, err := NewCommand(repo.Ctx, "symbolic-ref", "HEAD").RunStdString(&RunOpts{Dir: repo.Path}) +func GetDefaultBranch(ctx context.Context, repoPath string) (string, error) { + stdout, _, err := NewCommand(ctx, "symbolic-ref", "HEAD").RunStdString(&RunOpts{Dir: repoPath}) if err != nil { return "", err } diff --git a/modules/gitrepo/branch.go b/modules/gitrepo/branch.go index dcaf92668d..e13a4c82e1 100644 --- a/modules/gitrepo/branch.go +++ b/modules/gitrepo/branch.go @@ -30,3 +30,20 @@ func GetBranchCommitID(ctx context.Context, repo Repository, branch string) (str return gitRepo.GetBranchCommitID(branch) } + +// SetDefaultBranch sets default branch of repository. +func SetDefaultBranch(ctx context.Context, repo Repository, name string) error { + _, _, err := git.NewCommand(ctx, "symbolic-ref", "HEAD"). + AddDynamicArguments(git.BranchPrefix + name). + RunStdString(&git.RunOpts{Dir: repoPath(repo)}) + return err +} + +// GetDefaultBranch gets default branch of repository. +func GetDefaultBranch(ctx context.Context, repo Repository) (string, error) { + return git.GetDefaultBranch(ctx, repoPath(repo)) +} + +func GetWikiDefaultBranch(ctx context.Context, repo Repository) (string, error) { + return git.GetDefaultBranch(ctx, wikiPath(repo)) +} diff --git a/modules/graceful/manager_unix.go b/modules/graceful/manager_unix.go index f4af4993d9..edf5fc248f 100644 --- a/modules/graceful/manager_unix.go +++ b/modules/graceful/manager_unix.go @@ -59,7 +59,15 @@ func (g *Manager) start() { go func() { defer close(startupDone) // Wait till we're done getting all the listeners and then close the unused ones - g.createServerWaitGroup.Wait() + func() { + // FIXME: there is a fundamental design problem of the "manager" and the "wait group". + // If nothing has started, the "Wait" just panics: sync: WaitGroup is reused before previous Wait has returned + // There is no clear solution besides a complete rewriting of the "manager" + defer func() { + _ = recover() + }() + g.createServerWaitGroup.Wait() + }() // Ignore the error here there's not much we can do with it, they're logged in the CloseProvidedListeners function _ = CloseProvidedListeners() g.notify(readyMsg) diff --git a/modules/graceful/manager_windows.go b/modules/graceful/manager_windows.go index 0248dcb24d..ecf30af3f3 100644 --- a/modules/graceful/manager_windows.go +++ b/modules/graceful/manager_windows.go @@ -150,7 +150,15 @@ func (g *Manager) awaitServer(limit time.Duration) bool { c := make(chan struct{}) go func() { defer close(c) - g.createServerWaitGroup.Wait() + func() { + // FIXME: there is a fundamental design problem of the "manager" and the "wait group". + // If nothing has started, the "Wait" just panics: sync: WaitGroup is reused before previous Wait has returned + // There is no clear solution besides a complete rewriting of the "manager" + defer func() { + _ = recover() + }() + g.createServerWaitGroup.Wait() + }() }() if limit > 0 { select { diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go index 8ba50ed77c..107dd23598 100644 --- a/modules/indexer/code/bleve/bleve.go +++ b/modules/indexer/code/bleve/bleve.go @@ -233,21 +233,21 @@ func (b *Indexer) Delete(_ context.Context, repoID int64) error { // Search searches for files in the specified repo. // Returns the matching file-paths -func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { +func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { var ( indexerQuery query.Query keywordQuery query.Query ) - if isMatch { - prefixQuery := bleve.NewPrefixQuery(keyword) - prefixQuery.FieldVal = "Content" - keywordQuery = prefixQuery - } else { + if isFuzzy { phraseQuery := bleve.NewMatchPhraseQuery(keyword) phraseQuery.FieldVal = "Content" phraseQuery.Analyzer = repoIndexerAnalyzer keywordQuery = phraseQuery + } else { + prefixQuery := bleve.NewPrefixQuery(keyword) + prefixQuery.FieldVal = "Content" + keywordQuery = prefixQuery } if len(repoIDs) > 0 { diff --git a/modules/indexer/code/elasticsearch/elasticsearch.go b/modules/indexer/code/elasticsearch/elasticsearch.go index 0f70f13485..065b0b2061 100644 --- a/modules/indexer/code/elasticsearch/elasticsearch.go +++ b/modules/indexer/code/elasticsearch/elasticsearch.go @@ -281,10 +281,10 @@ func extractAggs(searchResult *elastic.SearchResult) []*internal.SearchResultLan } // Search searches for codes and language stats by given conditions. -func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { - searchType := esMultiMatchTypeBestFields - if isMatch { - searchType = esMultiMatchTypePhrasePrefix +func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { + searchType := esMultiMatchTypePhrasePrefix + if isFuzzy { + searchType = esMultiMatchTypeBestFields } kwQuery := elastic.NewMultiMatchQuery(keyword, "content").Type(searchType) diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index 5eb8e61e3d..23dbd63410 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -70,7 +70,7 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) { for _, kw := range keywords { t.Run(kw.Keyword, func(t *testing.T) { - total, res, langs, err := indexer.Search(context.TODO(), kw.RepoIDs, "", kw.Keyword, 1, 10, false) + total, res, langs, err := indexer.Search(context.TODO(), kw.RepoIDs, "", kw.Keyword, 1, 10, true) assert.NoError(t, err) assert.Len(t, kw.IDs, int(total)) assert.Len(t, langs, kw.Langs) diff --git a/modules/indexer/code/internal/indexer.go b/modules/indexer/code/internal/indexer.go index da3ac3623c..c92419deb2 100644 --- a/modules/indexer/code/internal/indexer.go +++ b/modules/indexer/code/internal/indexer.go @@ -16,7 +16,7 @@ type Indexer interface { internal.Indexer Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *RepoChanges) error Delete(ctx context.Context, repoID int64) error - Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) + Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*SearchResult, []*SearchResultLanguages, error) } // NewDummyIndexer returns a dummy indexer @@ -38,6 +38,6 @@ func (d *dummyIndexer) Delete(ctx context.Context, repoID int64) error { return fmt.Errorf("indexer is not ready") } -func (d *dummyIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) { +func (d *dummyIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*SearchResult, []*SearchResultLanguages, error) { return 0, nil, nil, fmt.Errorf("indexer is not ready") } diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go index 2ddc2397fa..89a62a8d3e 100644 --- a/modules/indexer/code/search.go +++ b/modules/indexer/code/search.go @@ -124,12 +124,13 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res } // PerformSearch perform a search on a repository -func PerformSearch(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int, []*Result, []*internal.SearchResultLanguages, error) { +// if isFuzzy is true set the Damerau-Levenshtein distance from 0 to 2 +func PerformSearch(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int, []*Result, []*internal.SearchResultLanguages, error) { if len(keyword) == 0 { return 0, nil, nil, nil } - total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, repoIDs, language, keyword, page, pageSize, isMatch) + total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, repoIDs, language, keyword, page, pageSize, isFuzzy) if err != nil { return 0, nil, nil, err } diff --git a/modules/indexer/internal/bleve/query.go b/modules/indexer/internal/bleve/query.go index c7d66538c1..2a427c4020 100644 --- a/modules/indexer/internal/bleve/query.go +++ b/modules/indexer/internal/bleve/query.go @@ -25,6 +25,13 @@ func MatchPhraseQuery(matchPhrase, field, analyzer string) *query.MatchPhraseQue return q } +// PrefixQuery generates a match prefix query for the given prefix and field +func PrefixQuery(matchPrefix, field string) *query.PrefixQuery { + q := bleve.NewPrefixQuery(matchPrefix) + q.FieldVal = field + return q +} + // BoolFieldQuery generates a bool field query for the given value and field func BoolFieldQuery(value bool, field string) *query.BoolFieldQuery { q := bleve.NewBoolFieldQuery(value) diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go index 6a5d65cb66..aaea854efa 100644 --- a/modules/indexer/issues/bleve/bleve.go +++ b/modules/indexer/issues/bleve/bleve.go @@ -156,12 +156,19 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( var queries []query.Query if options.Keyword != "" { - keywordQueries := []query.Query{ - inner_bleve.MatchPhraseQuery(options.Keyword, "title", issueIndexerAnalyzer), - inner_bleve.MatchPhraseQuery(options.Keyword, "content", issueIndexerAnalyzer), - inner_bleve.MatchPhraseQuery(options.Keyword, "comments", issueIndexerAnalyzer), + if options.IsFuzzyKeyword { + queries = append(queries, bleve.NewDisjunctionQuery([]query.Query{ + inner_bleve.MatchPhraseQuery(options.Keyword, "title", issueIndexerAnalyzer), + inner_bleve.MatchPhraseQuery(options.Keyword, "content", issueIndexerAnalyzer), + inner_bleve.MatchPhraseQuery(options.Keyword, "comments", issueIndexerAnalyzer), + }...)) + } else { + queries = append(queries, bleve.NewDisjunctionQuery([]query.Query{ + inner_bleve.PrefixQuery(options.Keyword, "title"), + inner_bleve.PrefixQuery(options.Keyword, "content"), + inner_bleve.PrefixQuery(options.Keyword, "comments"), + }...)) } - queries = append(queries, bleve.NewDisjunctionQuery(keywordQueries...)) } if len(options.RepoIDs) > 0 || options.AllPublic { diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go index 3acd3ade71..0077da263a 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch.go @@ -19,6 +19,10 @@ import ( const ( issueIndexerLatestVersion = 1 + // multi-match-types, currently only 2 types are used + // Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types + esMultiMatchTypeBestFields = "best_fields" + esMultiMatchTypePhrasePrefix = "phrase_prefix" ) var _ internal.Indexer = &Indexer{} @@ -141,7 +145,13 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query := elastic.NewBoolQuery() if options.Keyword != "" { - query.Must(elastic.NewMultiMatchQuery(options.Keyword, "title", "content", "comments")) + + searchType := esMultiMatchTypePhrasePrefix + if options.IsFuzzyKeyword { + searchType = esMultiMatchTypeBestFields + } + + query.Must(elastic.NewMultiMatchQuery(options.Keyword, "title", "content", "comments").Type(searchType)) } if len(options.RepoIDs) > 0 { diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index 947335d8ce..d41fec4aba 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -74,6 +74,8 @@ type SearchResult struct { type SearchOptions struct { Keyword string // keyword to search + IsFuzzyKeyword bool // if false the levenshtein distance is 0 + RepoIDs []int64 // repository IDs which the issues belong to AllPublic bool // if include all public repositories diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go index 325883196b..c429920065 100644 --- a/modules/indexer/issues/meilisearch/meilisearch.go +++ b/modules/indexer/issues/meilisearch/meilisearch.go @@ -5,6 +5,7 @@ package meilisearch import ( "context" + "errors" "strconv" "strings" @@ -16,12 +17,15 @@ import ( ) const ( - issueIndexerLatestVersion = 2 + issueIndexerLatestVersion = 3 // TODO: make this configurable if necessary maxTotalHits = 10000 ) +// ErrMalformedResponse is never expected as we initialize the indexer ourself and so define the types. +var ErrMalformedResponse = errors.New("meilisearch returned unexpected malformed content") + var _ internal.Indexer = &Indexer{} // Indexer implements Indexer interface @@ -47,6 +51,9 @@ func NewIndexer(url, apiKey, indexerName string) *Indexer { }, DisplayedAttributes: []string{ "id", + "title", + "content", + "comments", }, FilterableAttributes: []string{ "repo_id", @@ -221,11 +228,9 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( return nil, err } - hits := make([]internal.Match, 0, len(searchRes.Hits)) - for _, hit := range searchRes.Hits { - hits = append(hits, internal.Match{ - ID: int64(hit.(map[string]any)["id"].(float64)), - }) + hits, err := nonFuzzyWorkaround(searchRes, options.Keyword, options.IsFuzzyKeyword) + if err != nil { + return nil, err } return &internal.SearchResult{ @@ -241,3 +246,77 @@ func parseSortBy(sortBy internal.SortBy) string { } return field + ":asc" } + +// nonFuzzyWorkaround is needed as meilisearch does not have an exact search +// and you can only change "typo tolerance" per index. So we have to post-filter the results +// https://www.meilisearch.com/docs/learn/configuration/typo_tolerance#configuring-typo-tolerance +// TODO: remove once https://github.com/orgs/meilisearch/discussions/377 is addressed +func nonFuzzyWorkaround(searchRes *meilisearch.SearchResponse, keyword string, isFuzzy bool) ([]internal.Match, error) { + hits := make([]internal.Match, 0, len(searchRes.Hits)) + for _, hit := range searchRes.Hits { + hit, ok := hit.(map[string]any) + if !ok { + return nil, ErrMalformedResponse + } + + if !isFuzzy { + keyword = strings.ToLower(keyword) + + // declare a anon func to check if the title, content or at least one comment contains the keyword + found, err := func() (bool, error) { + // check if title match first + title, ok := hit["title"].(string) + if !ok { + return false, ErrMalformedResponse + } else if strings.Contains(strings.ToLower(title), keyword) { + return true, nil + } + + // check if content has a match + content, ok := hit["content"].(string) + if !ok { + return false, ErrMalformedResponse + } else if strings.Contains(strings.ToLower(content), keyword) { + return true, nil + } + + // now check for each comment if one has a match + // so we first try to cast and skip if there are no comments + comments, ok := hit["comments"].([]any) + if !ok { + return false, ErrMalformedResponse + } else if len(comments) == 0 { + return false, nil + } + + // now we iterate over all and report as soon as we detect one match + for i := range comments { + comment, ok := comments[i].(string) + if !ok { + return false, ErrMalformedResponse + } + if strings.Contains(strings.ToLower(comment), keyword) { + return true, nil + } + } + + // we got no match + return false, nil + }() + + if err != nil { + return nil, err + } else if !found { + continue + } + } + issueID, ok := hit["id"].(float64) + if !ok { + return nil, ErrMalformedResponse + } + hits = append(hits, internal.Match{ + ID: int64(issueID), + }) + } + return hits, nil +} diff --git a/modules/indexer/issues/meilisearch/meilisearch_test.go b/modules/indexer/issues/meilisearch/meilisearch_test.go index 8a6b0a61d3..1a9bbeef10 100644 --- a/modules/indexer/issues/meilisearch/meilisearch_test.go +++ b/modules/indexer/issues/meilisearch/meilisearch_test.go @@ -10,7 +10,11 @@ import ( "testing" "time" + "code.gitea.io/gitea/modules/indexer/issues/internal" "code.gitea.io/gitea/modules/indexer/issues/internal/tests" + + "github.com/meilisearch/meilisearch-go" + "github.com/stretchr/testify/assert" ) func TestMeilisearchIndexer(t *testing.T) { @@ -49,3 +53,44 @@ func TestMeilisearchIndexer(t *testing.T) { tests.TestIndexer(t, indexer) } + +func TestNonFuzzyWorkaround(t *testing.T) { + // get unexpected return + _, err := nonFuzzyWorkaround(&meilisearch.SearchResponse{ + Hits: []any{"aa", "bb", "cc", "dd"}, + }, "bowling", false) + assert.ErrorIs(t, err, ErrMalformedResponse) + + validResponse := &meilisearch.SearchResponse{ + Hits: []any{ + map[string]any{ + "id": float64(11), + "title": "a title", + "content": "issue body with no match", + "comments": []any{"hey whats up?", "I'm currently bowling", "nice"}, + }, + map[string]any{ + "id": float64(22), + "title": "Bowling as title", + "content": "", + "comments": []any{}, + }, + map[string]any{ + "id": float64(33), + "title": "Bowl-ing as fuzzy match", + "content": "", + "comments": []any{}, + }, + }, + } + + // nonFuzzy + hits, err := nonFuzzyWorkaround(validResponse, "bowling", false) + assert.NoError(t, err) + assert.EqualValues(t, []internal.Match{{ID: 11}, {ID: 22}}, hits) + + // fuzzy + hits, err = nonFuzzyWorkaround(validResponse, "bowling", true) + assert.NoError(t, err) + assert.EqualValues(t, []internal.Match{{ID: 11}, {ID: 22}, {ID: 33}}, hits) +} diff --git a/modules/markup/csv/csv.go b/modules/markup/csv/csv.go index 7af34a6cbc..12458e954a 100644 --- a/modules/markup/csv/csv.go +++ b/modules/markup/csv/csv.go @@ -93,8 +93,10 @@ func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Wri if _, err := tmpBlock.WriteString(html.EscapeString(string(rawBytes))); err != nil { return err } - _, err = tmpBlock.WriteString("") - return err + if _, err := tmpBlock.WriteString(""); err != nil { + return err + } + return tmpBlock.Flush() } rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, bytes.NewReader(rawBytes)) diff --git a/modules/setting/admin.go b/modules/setting/admin.go index c292db9c8f..35ffa9efbf 100644 --- a/modules/setting/admin.go +++ b/modules/setting/admin.go @@ -22,5 +22,6 @@ func loadAdminFrom(rootCfg ConfigProvider) { const ( UserFeatureDeletion = "deletion" + UserFeatureManageSSHKeys = "manage_ssh_keys" UserFeatureManageGPGKeys = "manage_gpg_keys" ) diff --git a/modules/setting/attachment.go b/modules/setting/attachment.go index 934d4d7f46..0fdabb5032 100644 --- a/modules/setting/attachment.go +++ b/modules/setting/attachment.go @@ -12,7 +12,7 @@ var Attachment = struct { Enabled bool }{ Storage: &Storage{}, - AllowedTypes: ".csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip", + AllowedTypes: ".cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip", MaxSize: 2048, MaxFiles: 5, Enabled: true, @@ -25,7 +25,7 @@ func loadAttachmentFrom(rootCfg ConfigProvider) (err error) { return err } - Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip") + Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip") Attachment.MaxSize = sec.Key("MAX_SIZE").MustInt64(2048) Attachment.MaxFiles = sec.Key("MAX_FILES").MustInt(5) Attachment.Enabled = sec.Key("ENABLED").MustBool(true) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 9afcb96fc9..7ef051cc0b 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -211,14 +211,8 @@ func SafeHTML(s any) template.HTML { } // SanitizeHTML sanitizes the input by pre-defined markdown rules -func SanitizeHTML(s any) template.HTML { - switch v := s.(type) { - case string: - return template.HTML(markup.Sanitize(v)) - case template.HTML: - return template.HTML(markup.Sanitize(string(v))) - } - panic(fmt.Sprintf("unexpected type %T", s)) +func SanitizeHTML(s string) template.HTML { + return template.HTML(markup.Sanitize(s)) } func HTMLEscape(s any) template.HTML { diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go index 3365278ac2..64f29d033e 100644 --- a/modules/templates/helper_test.go +++ b/modules/templates/helper_test.go @@ -64,5 +64,4 @@ func TestHTMLFormat(t *testing.T) { func TestSanitizeHTML(t *testing.T) { assert.Equal(t, template.HTML(`link xss
inline
`), SanitizeHTML(`link xss
inline
`)) - assert.Equal(t, template.HTML(`link xss
inline
`), SanitizeHTML(template.HTML(`link xss
inline
`))) } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 32aab26ffa..a7f4de48a8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -595,6 +595,8 @@ enterred_invalid_repo_name = The repository name you entered is incorrect. enterred_invalid_org_name = The organization name you entered is incorrect. enterred_invalid_owner_name = The new owner name is not valid. enterred_invalid_password = The password you entered is incorrect. +unset_password = The login user has not set the password. +unsupported_login_type = The login type is not supported to delete account. user_not_exist = The user does not exist. team_not_exist = The team does not exist. last_org_owner = You cannot remove the last user from the "owners" team. There must be at least one owner for an organization. @@ -2671,6 +2673,7 @@ find_file.no_matching = No matching file found error.csv.too_large = Can't render this file because it is too large. error.csv.unexpected = Can't render this file because it contains an unexpected character in line %d and column %d. error.csv.invalid_field_count = Can't render this file because it has a wrong number of fields in line %d. +error.broken_git_hook = Git hooks of this repository seem to be broken. Please follow the documentation to fix them, then push some commits to refresh the status. [graphs] component_loading = Loading %s... diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 459dec2b62..8922eaaecc 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -162,8 +162,8 @@ footer.software=ソフトウェアについて footer.links=リンク [heatmap] -number_of_contributions_in_the_last_12_months=過去 12 か月間で %s 個の貢献 -no_contributions=貢献なし +number_of_contributions_in_the_last_12_months=過去 12 か月間で %s 件の実績 +no_contributions=実績なし less=少 more=多 @@ -1547,7 +1547,7 @@ issues.role.member_helper=このユーザーはこのリポジトリを所有し issues.role.collaborator=共同作業者 issues.role.collaborator_helper=このユーザーはリポジトリ上で共同作業するように招待されています。 issues.role.first_time_contributor=初めての貢献者 -issues.role.first_time_contributor_helper=これは、このユーザーのリポジトリへの最初の貢献です。 +issues.role.first_time_contributor_helper=これは、このユーザーによるリポジトリへの最初の貢献です。 issues.role.contributor=貢献者 issues.role.contributor_helper=このユーザーは以前にリポジトリにコミットしています。 issues.re_request_review=レビューを再依頼 @@ -2048,7 +2048,8 @@ settings.mirror_settings.docs.more_information_if_disabled=プッシュミラー settings.mirror_settings.docs.doc_link_title=リポジトリをミラーリングするには? settings.mirror_settings.docs.doc_link_pull_section=ドキュメントの「リモートリポジトリからのプル」セクション。 settings.mirror_settings.docs.pulling_remote_title=リモートリポジトリからのプル -settings.mirror_settings.mirrored_repository=同期するリポジトリ +settings.mirror_settings.mirrored_repository=ミラー元のリポジトリ +settings.mirror_settings.pushed_repository=プッシュ先のリポジトリ settings.mirror_settings.direction=方向 settings.mirror_settings.direction.pull=プル settings.mirror_settings.direction.push=プッシュ @@ -3591,6 +3592,8 @@ runs.actors_no_select=すべてのアクター runs.status_no_select=すべてのステータス runs.no_results=一致する結果はありません。 runs.no_workflows=ワークフローはまだありません。 +runs.no_workflows.quick_start=Gitea Actions の始め方がわからない? ではクイックスタートガイドをご覧ください。 +runs.no_workflows.documentation=Gitea Actions の詳細については、ドキュメントを参照してください。 runs.no_runs=ワークフローはまだ実行されていません。 runs.empty_commit_message=(空のコミットメッセージ) diff --git a/package-lock.json b/package-lock.json index 6d1d2850c1..1df2a8b941 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,6 @@ "chartjs-plugin-zoom": "2.0.1", "clippie": "4.0.7", "css-loader": "6.10.0", - "css-variables-parser": "1.0.1", "dayjs": "1.11.10", "dropzone": "6.0.0-beta.2", "easymde": "2.18.0", @@ -4032,35 +4031,6 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "node_modules/css-variables-parser": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/css-variables-parser/-/css-variables-parser-1.0.1.tgz", - "integrity": "sha512-GWaqrwGtAWVr/yjjE17iyvbcy+W3voe0vko1/xLCwFeYd3kTLstzUdVH+g5TTXejrtlsb1FS4L9rP6PmeTa8wQ==", - "dependencies": { - "postcss": "^7.0.36" - } - }, - "node_modules/css-variables-parser/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/css-variables-parser/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", diff --git a/package.json b/package.json index 1152bfef72..d5e8170228 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "chartjs-plugin-zoom": "2.0.1", "clippie": "4.0.7", "css-loader": "6.10.0", - "css-variables-parser": "1.0.1", "dayjs": "1.11.10", "dropzone": "6.0.0-beta.2", "easymde": "2.18.0", diff --git a/public/assets/img/svg/gitea-bitbucket.svg b/public/assets/img/svg/gitea-bitbucket.svg index b900335ea1..83e4c5c6e7 100644 --- a/public/assets/img/svg/gitea-bitbucket.svg +++ b/public/assets/img/svg/gitea-bitbucket.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/assets/img/svg/gitea-facebook.svg b/public/assets/img/svg/gitea-facebook.svg index cbeb76b127..6101becad2 100644 --- a/public/assets/img/svg/gitea-facebook.svg +++ b/public/assets/img/svg/gitea-facebook.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/assets/img/svg/gitea-jetbrains.svg b/public/assets/img/svg/gitea-jetbrains.svg new file mode 100644 index 0000000000..5821736225 --- /dev/null +++ b/public/assets/img/svg/gitea-jetbrains.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/img/svg/gitea-microsoftonline.svg b/public/assets/img/svg/gitea-microsoftonline.svg index ce4f1a5c8f..f2ce13ac22 100644 --- a/public/assets/img/svg/gitea-microsoftonline.svg +++ b/public/assets/img/svg/gitea-microsoftonline.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/assets/img/svg/gitea-twitter.svg b/public/assets/img/svg/gitea-twitter.svg index 5d11c6eaec..5ed1e264ca 100644 --- a/public/assets/img/svg/gitea-twitter.svg +++ b/public/assets/img/svg/gitea-twitter.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/assets/img/svg/gitea-vscodium.svg b/public/assets/img/svg/gitea-vscodium.svg new file mode 100644 index 0000000000..6aad3d3a64 --- /dev/null +++ b/public/assets/img/svg/gitea-vscodium.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go index a7cb31288c..ff6ec5bd54 100644 --- a/routers/api/actions/runner/utils.go +++ b/routers/api/actions/runner/utils.go @@ -15,7 +15,6 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" - secret_module "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/actions" @@ -32,14 +31,24 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv return nil, false, nil } + secrets, err := secret_model.GetSecretsOfTask(ctx, t) + if err != nil { + return nil, false, fmt.Errorf("GetSecretsOfTask: %w", err) + } + + vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run) + if err != nil { + return nil, false, fmt.Errorf("GetVariablesOfRun: %w", err) + } + actions.CreateCommitStatus(ctx, t.Job) task := &runnerv1.Task{ Id: t.ID, WorkflowPayload: t.Job.WorkflowPayload, Context: generateTaskContext(t), - Secrets: getSecretsOfTask(ctx, t), - Vars: getVariablesOfTask(ctx, t), + Secrets: secrets, + Vars: vars, } if needs, err := findTaskNeeds(ctx, t); err != nil { @@ -55,71 +64,6 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv return task, true, nil } -func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string { - secrets := map[string]string{} - - secrets["GITHUB_TOKEN"] = task.Token - secrets["GITEA_TOKEN"] = task.Token - - if task.Job.Run.IsForkPullRequest && task.Job.Run.TriggerEvent != actions_module.GithubEventPullRequestTarget { - // ignore secrets for fork pull request, except GITHUB_TOKEN and GITEA_TOKEN which are automatically generated. - // for the tasks triggered by pull_request_target event, they could access the secrets because they will run in the context of the base branch - // see the documentation: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target - return secrets - } - - ownerSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID}) - if err != nil { - log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err) - // go on - } - repoSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{RepoID: task.Job.Run.RepoID}) - if err != nil { - log.Error("find secrets of repo %v: %v", task.Job.Run.RepoID, err) - // go on - } - - for _, secret := range append(ownerSecrets, repoSecrets...) { - if v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data); err != nil { - log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err) - // go on - } else { - secrets[secret.Name] = v - } - } - - return secrets -} - -func getVariablesOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string { - variables := map[string]string{} - - // Global - globalVariables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{}) - if err != nil { - log.Error("find global variables: %v", err) - } - - // Org / User level - ownerVariables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{OwnerID: task.Job.Run.Repo.OwnerID}) - if err != nil { - log.Error("find variables of org: %d, error: %v", task.Job.Run.Repo.OwnerID, err) - } - - // Repo level - repoVariables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{RepoID: task.Job.Run.RepoID}) - if err != nil { - log.Error("find variables of repo: %d, error: %v", task.Job.Run.RepoID, err) - } - - // Level precedence: Repo > Org / User > Global - for _, v := range append(globalVariables, append(ownerVariables, repoVariables...)...) { - variables[v.Name] = v.Data - } - - return variables -} - func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { event := map[string]any{} _ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event) diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 64315108b0..986305d423 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -133,7 +133,7 @@ func CreateUser(ctx *context.APIContext) { u.UpdatedUnix = u.CreatedUnix } - if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil { + if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil { if user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err) || db.IsErrNameReserved(err) || @@ -209,7 +209,7 @@ func EditUser(ctx *context.APIContext) { } if form.Email != nil { - if err := user_service.AddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil { + if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil { switch { case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err): ctx.Error(http.StatusBadRequest, "EmailInvalid", err) diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 40960144c7..316a1161da 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -720,7 +720,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err if ctx.Repo.GitRepo == nil && !repo.IsEmpty { var err error - ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, repo) if err != nil { ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err) return err @@ -731,7 +731,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err // Default branch only updated if changed and exist or the repository is empty if opts.DefaultBranch != nil && repo.DefaultBranch != *opts.DefaultBranch && (repo.IsEmpty || ctx.Repo.GitRepo.IsBranchExist(*opts.DefaultBranch)) { if !repo.IsEmpty { - if err := ctx.Repo.GitRepo.SetDefaultBranch(*opts.DefaultBranch); err != nil { + if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, *opts.DefaultBranch); err != nil { if !git.IsErrUnsupportedVersion(err) { ctx.Error(http.StatusInternalServerError, "SetDefaultBranch", err) return err diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go index 53711bedeb..9e36ea0aed 100644 --- a/routers/api/v1/repo/status.go +++ b/routers/api/v1/repo/status.go @@ -14,7 +14,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" - files_service "code.gitea.io/gitea/services/repository/files" + commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus" ) // NewCommitStatus creates a new CommitStatus @@ -64,7 +64,7 @@ func NewCommitStatus(ctx *context.APIContext) { Description: form.Description, Context: form.Context, } - if err := files_service.CreateCommitStatus(ctx, ctx.Repo.Repository, ctx.Doer, sha, status); err != nil { + if err := commitstatus_service.CreateCommitStatus(ctx, ctx.Repo.Repository, ctx.Doer, sha, status); err != nil { ctx.Error(http.StatusInternalServerError, "CreateCommitStatus", err) return } diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go index ada6759f8e..bcbfd93bd3 100644 --- a/routers/api/v1/user/key.go +++ b/routers/api/v1/user/key.go @@ -5,6 +5,7 @@ package user import ( std_ctx "context" + "fmt" "net/http" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -198,6 +199,11 @@ func GetPublicKey(ctx *context.APIContext) { // CreateUserPublicKey creates new public key to given user by ID. func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid int64) { + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) { + ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + return + } + content, err := asymkey_model.CheckPublicKeyString(form.Key) if err != nil { repo.HandleCheckKeyStringError(ctx, err) @@ -263,6 +269,11 @@ func DeletePublicKey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) { + ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + return + } + id := ctx.ParamsInt64(":id") externallyManaged, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, id) if err != nil { diff --git a/routers/private/default_branch.go b/routers/private/default_branch.go index 2e323129ef..33890be6a9 100644 --- a/routers/private/default_branch.go +++ b/routers/private/default_branch.go @@ -9,6 +9,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/private" gitea_context "code.gitea.io/gitea/services/context" ) @@ -20,7 +21,7 @@ func SetDefaultBranch(ctx *gitea_context.PrivateContext) { branch := ctx.Params(":branch") ctx.Repo.Repository.DefaultBranch = branch - if err := ctx.Repo.GitRepo.SetDefaultBranch(ctx.Repo.Repository.DefaultBranch); err != nil { + if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil { if !git.IsErrUnsupportedVersion(err) { ctx.JSON(http.StatusInternalServerError, private.Response{ Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err), diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index a34e0d0f0d..671a0d8885 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -177,7 +177,7 @@ func NewUserPost(ctx *context.Context) { u.MustChangePassword = form.MustChangePassword } - if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil { + if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil { switch { case user_model.IsErrUserAlreadyExist(err): ctx.Data["Err_UserName"] = true @@ -412,7 +412,7 @@ func EditUserPost(ctx *context.Context) { } if form.Email != "" { - if err := user_service.AddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil { + if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil { switch { case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err): ctx.Data["Err_Email"] = true diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 1adb891840..1c55256db4 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -135,9 +135,21 @@ func resetLocale(ctx *context.Context, u *user_model.User) error { return nil } +func RedirectAfterLogin(ctx *context.Context) { + redirectTo := ctx.FormString("redirect_to") + if redirectTo == "" { + redirectTo = ctx.GetSiteCookie("redirect_to") + } + middleware.DeleteRedirectToCookie(ctx.Resp) + nextRedirectTo := setting.AppSubURL + string(setting.LandingPageURL) + if setting.LandingPageURL == setting.LandingPageLogin { + nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page + } + ctx.RedirectToFirst(redirectTo, nextRedirectTo) +} + func CheckAutoLogin(ctx *context.Context) bool { - // Check auto-login - isSucceed, err := autoSignIn(ctx) + isSucceed, err := autoSignIn(ctx) // try to auto-login if err != nil { ctx.ServerError("autoSignIn", err) return true @@ -146,17 +158,10 @@ func CheckAutoLogin(ctx *context.Context) bool { redirectTo := ctx.FormString("redirect_to") if len(redirectTo) > 0 { middleware.SetRedirectToCookie(ctx.Resp, redirectTo) - } else { - redirectTo = ctx.GetSiteCookie("redirect_to") } if isSucceed { - middleware.DeleteRedirectToCookie(ctx.Resp) - nextRedirectTo := setting.AppSubURL + string(setting.LandingPageURL) - if setting.LandingPageURL == setting.LandingPageLogin { - nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page - } - ctx.RedirectToFirst(redirectTo, nextRedirectTo) + RedirectAfterLogin(ctx) return true } @@ -171,6 +176,11 @@ func SignIn(ctx *context.Context) { return } + if ctx.IsSigned { + RedirectAfterLogin(ctx) + return + } + oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) if err != nil { ctx.ServerError("UserSignIn", err) diff --git a/routers/web/auth/auth_test.go b/routers/web/auth/auth_test.go new file mode 100644 index 0000000000..c6afbf877c --- /dev/null +++ b/routers/web/auth/auth_test.go @@ -0,0 +1,43 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "net/http" + "net/url" + "testing" + + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/services/contexttest" + + "github.com/stretchr/testify/assert" +) + +func TestUserLogin(t *testing.T) { + ctx, resp := contexttest.MockContext(t, "/user/login") + SignIn(ctx) + assert.Equal(t, http.StatusOK, resp.Code) + + ctx, resp = contexttest.MockContext(t, "/user/login") + ctx.IsSigned = true + SignIn(ctx) + assert.Equal(t, http.StatusSeeOther, resp.Code) + assert.Equal(t, "/", test.RedirectURL(resp)) + + ctx, resp = contexttest.MockContext(t, "/user/login?redirect_to=/other") + ctx.IsSigned = true + SignIn(ctx) + assert.Equal(t, "/other", test.RedirectURL(resp)) + + ctx, resp = contexttest.MockContext(t, "/user/login") + ctx.Req.AddCookie(&http.Cookie{Name: "redirect_to", Value: "/other-cookie"}) + ctx.IsSigned = true + SignIn(ctx) + assert.Equal(t, "/other-cookie", test.RedirectURL(resp)) + + ctx, resp = contexttest.MockContext(t, "/user/login?redirect_to="+url.QueryEscape("https://example.com")) + ctx.IsSigned = true + SignIn(ctx) + assert.Equal(t, "/", test.RedirectURL(resp)) +} diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go index 2cde8b655e..a6bc71ac9c 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -35,7 +35,7 @@ func Code(ctx *context.Context) { keyword := ctx.FormTrim("q") queryType := ctx.FormTrim("t") - isMatch := queryType == "match" + isFuzzy := queryType != "match" ctx.Data["Keyword"] = keyword ctx.Data["Language"] = language @@ -77,7 +77,7 @@ func Code(ctx *context.Context) { ) if (len(repoIDs) > 0) || isAdmin { - total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch) + total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isFuzzy) if err != nil { if code_indexer.IsAvailable(ctx) { ctx.ServerError("SearchResults", err) diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 77aab8354b..57723e6a9a 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -35,6 +35,7 @@ import ( "code.gitea.io/gitea/services/forms" repo_service "code.gitea.io/gitea/services/repository" archiver_service "code.gitea.io/gitea/services/repository/archiver" + commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus" ) const ( @@ -630,30 +631,14 @@ func SearchRepo(ctx *context.Context) { return } - // collect the latest commit of each repo - // at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment - repoBranchNames := make(map[int64]string, len(repos)) - for _, repo := range repos { - repoBranchNames[repo.ID] = repo.DefaultBranch - } - - repoIDsToLatestCommitSHAs, err := git_model.FindBranchesByRepoAndBranchName(ctx, repoBranchNames) + latestCommitStatuses, err := commitstatus_service.FindReposLastestCommitStatuses(ctx, repos) if err != nil { - log.Error("FindBranchesByRepoAndBranchName: %v", err) - return - } - - // call the database O(1) times to get the commit statuses for all repos - repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoIDsToLatestCommitSHAs, db.ListOptionsAll) - if err != nil { - log.Error("GetLatestCommitStatusForPairs: %v", err) + log.Error("FindReposLastestCommitStatuses: %v", err) return } results := make([]*repo_service.WebSearchRepository, len(repos)) for i, repo := range repos { - latestCommitStatus := git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID]) - results[i] = &repo_service.WebSearchRepository{ Repository: &api.Repository{ ID: repo.ID, @@ -667,8 +652,11 @@ func SearchRepo(ctx *context.Context) { Link: repo.Link(), Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate, }, - LatestCommitStatus: latestCommitStatus, - LocaleLatestCommitStatus: latestCommitStatus.LocaleString(ctx.Locale), + } + + if latestCommitStatuses[i] != nil { + results[i].LatestCommitStatus = latestCommitStatuses[i] + results[i].LocaleLatestCommitStatus = latestCommitStatuses[i].LocaleString(ctx.Locale) } } diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go index 74970c02c9..550a3dc8be 100644 --- a/routers/web/repo/search.go +++ b/routers/web/repo/search.go @@ -21,7 +21,7 @@ func Search(ctx *context.Context) { keyword := ctx.FormTrim("q") queryType := ctx.FormTrim("t") - isMatch := queryType == "match" + isFuzzy := queryType != "match" ctx.Data["Keyword"] = keyword ctx.Data["Language"] = language @@ -44,7 +44,7 @@ func Search(ctx *context.Context) { ctx.Data["CodeIndexerEnabled"] = true total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID}, - language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch) + language, keyword, page, setting.UI.RepoSearchPagingNum, isFuzzy) if err != nil { if code_indexer.IsAvailable(ctx) { ctx.ServerError("SearchResults", err) diff --git a/routers/web/repo/setting/default_branch.go b/routers/web/repo/setting/default_branch.go index 610fc5bcdf..d0b32ef079 100644 --- a/routers/web/repo/setting/default_branch.go +++ b/routers/web/repo/setting/default_branch.go @@ -8,6 +8,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers/web/repo" @@ -40,7 +41,7 @@ func SetDefaultBranchPost(ctx *context.Context) { return } else if repo.DefaultBranch != branch { repo.DefaultBranch = branch - if err := ctx.Repo.GitRepo.SetDefaultBranch(branch); err != nil { + if err := gitrepo.SetDefaultBranch(ctx, repo, branch); err != nil { if !git.IsErrUnsupportedVersion(err) { ctx.ServerError("SetDefaultBranch", err) return diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 9f8d1fcdfd..842d14232e 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -36,6 +36,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" @@ -996,6 +997,8 @@ func renderCode(ctx *context.Context) { return } + checkOutdatedBranch(ctx) + checkCitationFile(ctx, entry) if ctx.Written() { return @@ -1071,7 +1074,7 @@ func renderCode(ctx *context.Context) { if err != nil { continue } - defaultBranch, err := gitRepo.GetDefaultBranch() + defaultBranch, err := gitrepo.GetDefaultBranch(ctx, repo) if err != nil { continue } @@ -1121,6 +1124,31 @@ PostRecentBranchCheck: ctx.HTML(http.StatusOK, tplRepoHome) } +func checkOutdatedBranch(ctx *context.Context) { + if !(ctx.Repo.IsAdmin() || ctx.Repo.IsOwner()) { + return + } + + // get the head commit of the branch since ctx.Repo.CommitID is not always the head commit of `ctx.Repo.BranchName` + commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName) + if err != nil { + log.Error("GetBranchCommitID: %v", err) + // Don't return an error page, as it can be rechecked the next time the user opens the page. + return + } + + dbBranch, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, ctx.Repo.BranchName) + if err != nil { + log.Error("GetBranch: %v", err) + // Don't return an error page, as it can be rechecked the next time the user opens the page. + return + } + + if dbBranch.CommitID != commit.ID.String() { + ctx.Flash.Warning(ctx.Tr("repo.error.broken_git_hook", "https://docs.gitea.com/help/faq#push-hook--webhook--actions-arent-running"), true) + } +} + // RenderUserCards render a page show users according the input template func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl base.TplName) { page := ctx.FormInt("page") diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index 49c83cfef5..719cca3049 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -79,7 +79,7 @@ func assertPagesMetas(t *testing.T, expectedNames []string, metas any) { func TestWiki(t *testing.T) { unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki/?action=_pages") + ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki") ctx.SetParams("*", "Home") contexttest.LoadRepo(t, ctx, 1) Wiki(ctx) diff --git a/routers/web/user/code.go b/routers/web/user/code.go index eb711b76eb..8613d38b65 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -40,7 +40,7 @@ func CodeSearch(ctx *context.Context) { keyword := ctx.FormTrim("q") queryType := ctx.FormTrim("t") - isMatch := queryType == "match" + isFuzzy := queryType != "match" ctx.Data["Keyword"] = keyword ctx.Data["Language"] = language @@ -75,7 +75,7 @@ func CodeSearch(ctx *context.Context) { ) if len(repoIDs) > 0 { - total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch) + total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isFuzzy) if err != nil { if code_indexer.IsAvailable(ctx) { ctx.ServerError("SearchResults", err) diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index ef02cd24a7..386309aa71 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -19,6 +19,8 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/auth" + "code.gitea.io/gitea/services/auth/source/db" + "code.gitea.io/gitea/services/auth/source/smtp" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/mailer" @@ -251,11 +253,24 @@ func DeleteAccount(ctx *context.Context) { ctx.Data["PageIsSettingsAccount"] = true if _, _, err := auth.UserSignIn(ctx, ctx.Doer.Name, ctx.FormString("password")); err != nil { - if user_model.IsErrUserNotExist(err) { + switch { + case user_model.IsErrUserNotExist(err): + loadAccountData(ctx) + + ctx.RenderWithErr(ctx.Tr("form.user_not_exist"), tplSettingsAccount, nil) + case errors.Is(err, smtp.ErrUnsupportedLoginType): + loadAccountData(ctx) + + ctx.RenderWithErr(ctx.Tr("form.unsupported_login_type"), tplSettingsAccount, nil) + case errors.As(err, &db.ErrUserPasswordNotSet{}): + loadAccountData(ctx) + + ctx.RenderWithErr(ctx.Tr("form.unset_password"), tplSettingsAccount, nil) + case errors.As(err, &db.ErrUserPasswordInvalid{}): loadAccountData(ctx) ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil) - } else { + default: ctx.ServerError("UserSignIn", err) } return diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go index cb01913bda..d2b60fc809 100644 --- a/routers/web/user/setting/keys.go +++ b/routers/web/user/setting/keys.go @@ -159,6 +159,11 @@ func KeysPost(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("settings.verify_gpg_key_success", keyID)) ctx.Redirect(setting.AppSubURL + "/user/settings/keys") case "ssh": + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) { + ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + return + } + content, err := asymkey_model.CheckPublicKeyString(form.Content) if err != nil { if db.IsErrSSHDisabled(err) { @@ -198,6 +203,11 @@ func KeysPost(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title)) ctx.Redirect(setting.AppSubURL + "/user/settings/keys") case "verify_ssh": + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) { + ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + return + } + token := asymkey_model.VerificationToken(ctx.Doer, 1) lastToken := asymkey_model.VerificationToken(ctx.Doer, 0) @@ -240,6 +250,11 @@ func DeleteKey(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success")) } case "ssh": + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) { + ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + return + } + keyID := ctx.FormInt64("id") external, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, keyID) if err != nil { @@ -318,4 +333,5 @@ func loadKeysData(ctx *context.Context) { ctx.Data["VerifyingID"] = ctx.FormString("verify_gpg") ctx.Data["VerifyingFingerprint"] = ctx.FormString("verify_ssh") + ctx.Data["UserDisabledFeatures"] = &setting.Admin.UserDisabledFeatures } diff --git a/services/actions/auth.go b/services/actions/auth.go index e0f9a9015d..8e934d89a8 100644 --- a/services/actions/auth.go +++ b/services/actions/auth.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -21,17 +22,41 @@ type actionsClaims struct { TaskID int64 RunID int64 JobID int64 + Ac string `json:"ac"` } +type actionsCacheScope struct { + Scope string + Permission actionsCachePermission +} + +type actionsCachePermission int + +const ( + actionsCachePermissionRead = 1 << iota + actionsCachePermissionWrite +) + func CreateAuthorizationToken(taskID, runID, jobID int64) (string, error) { now := time.Now() + ac, err := json.Marshal(&[]actionsCacheScope{ + { + Scope: "", + Permission: actionsCachePermissionWrite, + }, + }) + if err != nil { + return "", err + } + claims := actionsClaims{ RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)), NotBefore: jwt.NewNumericDate(now), }, Scp: fmt.Sprintf("Actions.Results:%d:%d", runID, jobID), + Ac: string(ac), TaskID: taskID, RunID: runID, JobID: jobID, diff --git a/services/actions/auth_test.go b/services/actions/auth_test.go index 1f62f17f52..f73ae8ae4c 100644 --- a/services/actions/auth_test.go +++ b/services/actions/auth_test.go @@ -7,6 +7,7 @@ import ( "net/http" "testing" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" "github.com/golang-jwt/jwt/v5" @@ -29,6 +30,14 @@ func TestCreateAuthorizationToken(t *testing.T) { taskIDClaim, ok := claims["TaskID"] assert.True(t, ok, "Has TaskID claim in jwt token") assert.Equal(t, float64(taskID), taskIDClaim, "Supplied taskid must match stored one") + acClaim, ok := claims["ac"] + assert.True(t, ok, "Has ac claim in jwt token") + ac, ok := acClaim.(string) + assert.True(t, ok, "ac claim is a string for buildx gha cache") + scopes := []actionsCacheScope{} + err = json.Unmarshal([]byte(ac), &scopes) + assert.NoError(t, err, "ac claim is a json list for buildx gha cache") + assert.GreaterOrEqual(t, len(scopes), 1, "Expected at least one action cache scope for buildx gha cache") } func TestParseAuthorizationToken(t *testing.T) { diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 19a5c66b71..6f0e8058a0 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -305,7 +305,18 @@ func handleWorkflows( run.NeedApproval = need } - jobs, err := jobparser.Parse(dwf.Content) + if err := run.LoadAttributes(ctx); err != nil { + log.Error("LoadAttributes: %v", err) + continue + } + + vars, err := actions_model.GetVariablesOfRun(ctx, run) + if err != nil { + log.Error("GetVariablesOfRun: %v", err) + continue + } + + jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars)) if err != nil { log.Error("jobparser.Parse: %v", err) continue diff --git a/services/auth/source/oauth2/providers.go b/services/auth/source/oauth2/providers.go index ac32647839..c3edae4ab6 100644 --- a/services/auth/source/oauth2/providers.go +++ b/services/auth/source/oauth2/providers.go @@ -59,7 +59,7 @@ func (p *AuthSourceProvider) DisplayName() string { func (p *AuthSourceProvider) IconHTML(size int) template.HTML { if p.iconURL != "" { - img := fmt.Sprintf(`%s`, + img := fmt.Sprintf(`%s`, size, size, html.EscapeString(p.iconURL), html.EscapeString(p.DisplayName()), diff --git a/services/context/base_test.go b/services/context/base_test.go new file mode 100644 index 0000000000..823f20e00b --- /dev/null +++ b/services/context/base_test.go @@ -0,0 +1,47 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "net/http" + "net/http/httptest" + "testing" + + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func TestRedirect(t *testing.T) { + req, _ := http.NewRequest("GET", "/", nil) + + cases := []struct { + url string + keep bool + }{ + {"http://test", false}, + {"https://test", false}, + {"//test", false}, + {"/://test", true}, + {"/test", true}, + } + for _, c := range cases { + resp := httptest.NewRecorder() + b, cleanup := NewBaseContext(resp, req) + resp.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "dummy"}).String()) + b.Redirect(c.url) + cleanup() + has := resp.Header().Get("Set-Cookie") == "i_like_gitea=dummy" + assert.Equal(t, c.keep, has, "url = %q", c.url) + } + + req, _ = http.NewRequest("GET", "/", nil) + resp := httptest.NewRecorder() + req.Header.Add("HX-Request", "true") + b, cleanup := NewBaseContext(resp, req) + b.Redirect("/other") + cleanup() + assert.Equal(t, "/other", resp.Header().Get("HX-Redirect")) + assert.Equal(t, http.StatusNoContent, resp.Code) +} diff --git a/services/context/repo.go b/services/context/repo.go index e78dfc585d..43eeab8098 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -702,7 +702,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { if len(ctx.Repo.Repository.DefaultBranch) > 0 && gitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) { ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch } else { - ctx.Repo.BranchName, _ = gitRepo.GetDefaultBranch() + ctx.Repo.BranchName, _ = gitrepo.GetDefaultBranch(ctx, ctx.Repo.Repository) if ctx.Repo.BranchName == "" { // If it still can't get a default branch, fall back to default branch from setting. // Something might be wrong. Either site admin should fix the repo sync or Gitea should fix a potential bug. diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go index 431017a30d..d3e6de7efe 100644 --- a/services/contexttest/context_tests.go +++ b/services/contexttest/context_tests.go @@ -7,6 +7,7 @@ package contexttest import ( gocontext "context" "io" + "maps" "net/http" "net/http/httptest" "net/url" @@ -36,7 +37,7 @@ func mockRequest(t *testing.T, reqPath string) *http.Request { } requestURL, err := url.Parse(path) assert.NoError(t, err) - req := &http.Request{Method: method, URL: requestURL, Form: url.Values{}} + req := &http.Request{Method: method, URL: requestURL, Form: maps.Clone(requestURL.Query()), Header: http.Header{}} req = req.WithContext(middleware.WithContextData(req.Context())) return req } diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 3418cf90df..de4a58f27b 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -593,7 +593,7 @@ func checkAndUpdateEmptyRepository(ctx context.Context, m *repo_model.Mirror, gi m.Repo.DefaultBranch = firstName } // Update the git repository default branch - if err := gitRepo.SetDefaultBranch(m.Repo.DefaultBranch); err != nil { + if err := gitrepo.SetDefaultBranch(ctx, m.Repo, m.Repo.DefaultBranch); err != nil { if !git.IsErrUnsupportedVersion(err) { log.Error("Failed to update default branch of underlying git repository %-v. Error: %v", m.Repo, err) desc := fmt.Sprintf("Failed to update default branch of underlying git repository '%s': %v", m.Repo.RepoPath(), err) diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go index 27ee572640..514bcee8f1 100644 --- a/services/pull/commit_status.go +++ b/services/pull/commit_status.go @@ -36,9 +36,9 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus, } } - for _, commitStatus := range commitStatuses { + for _, gp := range requiredContextsGlob { var targetStatus structs.CommitStatusState - for _, gp := range requiredContextsGlob { + for _, commitStatus := range commitStatuses { if gp.Match(commitStatus.Context) { targetStatus = commitStatus.State matchedCount++ @@ -46,17 +46,21 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus, } } - if targetStatus != "" && targetStatus.NoBetterThan(returnedStatus) { + // If required rule not match any action, then it is pending + if targetStatus == "" { + if structs.CommitStatusPending.NoBetterThan(returnedStatus) { + returnedStatus = structs.CommitStatusPending + } + break + } + + if targetStatus.NoBetterThan(returnedStatus) { returnedStatus = targetStatus } } } - if matchedCount != len(requiredContexts) { - return structs.CommitStatusPending - } - - if matchedCount == 0 { + if matchedCount == 0 && returnedStatus == structs.CommitStatusSuccess { status := git_model.CalcCommitStatus(commitStatuses) if status != nil { return status.State diff --git a/services/pull/commit_status_test.go b/services/pull/commit_status_test.go new file mode 100644 index 0000000000..592acdd55c --- /dev/null +++ b/services/pull/commit_status_test.go @@ -0,0 +1,65 @@ +// Copyright 2024 The Gitea Authors. +// All rights reserved. +// SPDX-License-Identifier: MIT + +package pull + +import ( + "testing" + + git_model "code.gitea.io/gitea/models/git" + "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" +) + +func TestMergeRequiredContextsCommitStatus(t *testing.T) { + testCases := [][]*git_model.CommitStatus{ + { + {Context: "Build 1", State: structs.CommitStatusSuccess}, + {Context: "Build 2", State: structs.CommitStatusSuccess}, + {Context: "Build 3", State: structs.CommitStatusSuccess}, + }, + { + {Context: "Build 1", State: structs.CommitStatusSuccess}, + {Context: "Build 2", State: structs.CommitStatusSuccess}, + {Context: "Build 2t", State: structs.CommitStatusPending}, + }, + { + {Context: "Build 1", State: structs.CommitStatusSuccess}, + {Context: "Build 2", State: structs.CommitStatusSuccess}, + {Context: "Build 2t", State: structs.CommitStatusFailure}, + }, + { + {Context: "Build 1", State: structs.CommitStatusSuccess}, + {Context: "Build 2", State: structs.CommitStatusSuccess}, + {Context: "Build 2t", State: structs.CommitStatusSuccess}, + }, + { + {Context: "Build 1", State: structs.CommitStatusSuccess}, + {Context: "Build 2", State: structs.CommitStatusSuccess}, + {Context: "Build 2t", State: structs.CommitStatusSuccess}, + }, + } + testCasesRequiredContexts := [][]string{ + {"Build*"}, + {"Build*", "Build 2t*"}, + {"Build*", "Build 2t*"}, + {"Build*", "Build 2t*", "Build 3*"}, + {"Build*", "Build *", "Build 2t*", "Build 1*"}, + } + + testCasesExpected := []structs.CommitStatusState{ + structs.CommitStatusSuccess, + structs.CommitStatusPending, + structs.CommitStatusFailure, + structs.CommitStatusPending, + structs.CommitStatusSuccess, + } + + for i, commitStatuses := range testCases { + if MergeRequiredContextsCommitStatus(commitStatuses, testCasesRequiredContexts[i]) != testCasesExpected[i] { + assert.Fail(t, "Test case failed", "Test case %d failed", i+1) + } + } +} diff --git a/services/repository/adopt.go b/services/repository/adopt.go index 7ca68776b5..0ac3c774b7 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -127,24 +127,17 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r repo.IsEmpty = false - // Don't bother looking this repo in the context it won't be there - gitRepo, err := gitrepo.OpenRepository(ctx, repo) - if err != nil { - return fmt.Errorf("openRepository: %w", err) - } - defer gitRepo.Close() - if len(defaultBranch) > 0 { repo.DefaultBranch = defaultBranch - if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { + if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil { return fmt.Errorf("setDefaultBranch: %w", err) } } else { - repo.DefaultBranch, err = gitRepo.GetDefaultBranch() + repo.DefaultBranch, err = gitrepo.GetDefaultBranch(ctx, repo) if err != nil { repo.DefaultBranch = setting.Repository.DefaultBranch - if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { + if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil { return fmt.Errorf("setDefaultBranch: %w", err) } } @@ -188,7 +181,7 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r repo.DefaultBranch = setting.Repository.DefaultBranch } - if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { + if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil { return fmt.Errorf("setDefaultBranch: %w", err) } } @@ -197,6 +190,13 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r return fmt.Errorf("updateRepository: %w", err) } + // Don't bother looking this repo in the context it won't be there + gitRepo, err := gitrepo.OpenRepository(ctx, repo) + if err != nil { + return fmt.Errorf("openRepository: %w", err) + } + defer gitRepo.Close() + if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil { return fmt.Errorf("SyncReleasesWithTags: %w", err) } diff --git a/services/repository/branch.go b/services/repository/branch.go index c3a7282f43..d349db7f74 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -317,7 +317,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m } if isDefault { - err2 = gitRepo.SetDefaultBranch(to) + err2 = gitrepo.SetDefaultBranch(ctx, repo, to) if err2 != nil { return err2 } diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go new file mode 100644 index 0000000000..145fc7d53c --- /dev/null +++ b/services/repository/commitstatus/commitstatus.go @@ -0,0 +1,135 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package commitstatus + +import ( + "context" + "crypto/sha256" + "fmt" + + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/log" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/services/automerge" +) + +func getCacheKey(repoID int64, brancheName string) string { + hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%d:%s", repoID, brancheName))) + return fmt.Sprintf("commit_status:%x", hashBytes) +} + +func updateCommitStatusCache(ctx context.Context, repoID int64, branchName string, status api.CommitStatusState) error { + c := cache.GetCache() + return c.Put(getCacheKey(repoID, branchName), string(status), 3*24*60) +} + +func deleteCommitStatusCache(ctx context.Context, repoID int64, branchName string) error { + c := cache.GetCache() + return c.Delete(getCacheKey(repoID, branchName)) +} + +// CreateCommitStatus creates a new CommitStatus given a bunch of parameters +// NOTE: All text-values will be trimmed from whitespaces. +// Requires: Repo, Creator, SHA +func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creator *user_model.User, sha string, status *git_model.CommitStatus) error { + repoPath := repo.RepoPath() + + // confirm that commit is exist + gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) + if err != nil { + return fmt.Errorf("OpenRepository[%s]: %w", repoPath, err) + } + defer closer.Close() + + objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) + + commit, err := gitRepo.GetCommit(sha) + if err != nil { + return fmt.Errorf("GetCommit[%s]: %w", sha, err) + } + if len(sha) != objectFormat.FullLength() { + // use complete commit sha + sha = commit.ID.String() + } + + if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{ + Repo: repo, + Creator: creator, + SHA: commit.ID, + CommitStatus: status, + }); err != nil { + return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err) + } + + defaultBranchCommit, err := gitRepo.GetBranchCommit(repo.DefaultBranch) + if err != nil { + return fmt.Errorf("GetBranchCommit[%s]: %w", repo.DefaultBranch, err) + } + + if commit.ID.String() == defaultBranchCommit.ID.String() { // since one commit status updated, the combined commit status should be invalid + if err := deleteCommitStatusCache(ctx, repo.ID, repo.DefaultBranch); err != nil { + log.Error("deleteCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err) + } + } + + if status.State.IsSuccess() { + if err := automerge.MergeScheduledPullRequest(ctx, sha, repo); err != nil { + return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err) + } + } + + return nil +} + +// FindReposLastestCommitStatuses loading repository default branch latest combinded commit status with cache +func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Repository) ([]*git_model.CommitStatus, error) { + results := make([]*git_model.CommitStatus, len(repos)) + c := cache.GetCache() + + for i, repo := range repos { + status, ok := c.Get(getCacheKey(repo.ID, repo.DefaultBranch)).(string) + if ok && status != "" { + results[i] = &git_model.CommitStatus{State: api.CommitStatusState(status)} + } + } + + // collect the latest commit of each repo + // at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment + repoBranchNames := make(map[int64]string, len(repos)) + for i, repo := range repos { + if results[i] == nil { + repoBranchNames[repo.ID] = repo.DefaultBranch + } + } + + repoIDsToLatestCommitSHAs, err := git_model.FindBranchesByRepoAndBranchName(ctx, repoBranchNames) + if err != nil { + return nil, fmt.Errorf("FindBranchesByRepoAndBranchName: %v", err) + } + + // call the database O(1) times to get the commit statuses for all repos + repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoIDsToLatestCommitSHAs, db.ListOptionsAll) + if err != nil { + return nil, fmt.Errorf("GetLatestCommitStatusForPairs: %v", err) + } + + for i, repo := range repos { + if results[i] == nil { + results[i] = git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID]) + if results[i].State != "" { + if err := updateCommitStatusCache(ctx, repo.ID, repo.DefaultBranch, results[i].State); err != nil { + log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err) + } + } + } + } + + return results, nil +} diff --git a/services/repository/create.go b/services/repository/create.go index 9bc0b93eff..d092d02a1f 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -177,12 +177,7 @@ func initRepository(ctx context.Context, repoPath string, u *user_model.User, re if len(opts.DefaultBranch) > 0 { repo.DefaultBranch = opts.DefaultBranch - gitRepo, err := gitrepo.OpenRepository(ctx, repo) - if err != nil { - return fmt.Errorf("openRepository: %w", err) - } - defer gitRepo.Close() - if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { + if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil { return fmt.Errorf("setDefaultBranch: %w", err) } diff --git a/services/repository/files/commit.go b/services/repository/files/commit.go index 512aec7c81..e0dad29273 100644 --- a/services/repository/files/commit.go +++ b/services/repository/files/commit.go @@ -5,61 +5,13 @@ package files import ( "context" - "fmt" asymkey_model "code.gitea.io/gitea/models/asymkey" - git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/services/automerge" ) -// CreateCommitStatus creates a new CommitStatus given a bunch of parameters -// NOTE: All text-values will be trimmed from whitespaces. -// Requires: Repo, Creator, SHA -func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creator *user_model.User, sha string, status *git_model.CommitStatus) error { - repoPath := repo.RepoPath() - - // confirm that commit is exist - gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) - if err != nil { - return fmt.Errorf("OpenRepository[%s]: %w", repoPath, err) - } - defer closer.Close() - - objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) - - commit, err := gitRepo.GetCommit(sha) - if err != nil { - gitRepo.Close() - return fmt.Errorf("GetCommit[%s]: %w", sha, err) - } else if len(sha) != objectFormat.FullLength() { - // use complete commit sha - sha = commit.ID.String() - } - gitRepo.Close() - - if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{ - Repo: repo, - Creator: creator, - SHA: commit.ID, - CommitStatus: status, - }); err != nil { - return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err) - } - - if status.State.IsSuccess() { - if err := automerge.MergeScheduledPullRequest(ctx, sha, repo); err != nil { - return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err) - } - } - - return nil -} - // CountDivergingCommits determines how many commits a branch is ahead or behind the repository's base branch func CountDivergingCommits(ctx context.Context, repo *repo_model.Repository, branch string) (*git.DivergeObject, error) { divergence, err := git.GetDivergingCommits(ctx, repo.RepoPath(), repo.DefaultBranch, branch) diff --git a/services/repository/generate.go b/services/repository/generate.go index c444b60b2c..9b09e271ab 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -272,12 +272,7 @@ func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *r repo.DefaultBranch = templateRepo.DefaultBranch } - gitRepo, err := gitrepo.OpenRepository(ctx, repo) - if err != nil { - return fmt.Errorf("openRepository: %w", err) - } - defer gitRepo.Close() - if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { + if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil { return fmt.Errorf("setDefaultBranch: %w", err) } if err = UpdateRepository(ctx, repo, false); err != nil { diff --git a/services/repository/migrate.go b/services/repository/migrate.go index b218a2ef46..5800f2b5cb 100644 --- a/services/repository/migrate.go +++ b/services/repository/migrate.go @@ -16,6 +16,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/migration" @@ -97,7 +98,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, } defer gitRepo.Close() - branch, err := gitRepo.GetDefaultBranch() + branch, err := gitrepo.GetDefaultBranch(ctx, repo) if err != nil { log.Warn("Failed to get the default branch of a migrated wiki repo: %v", err) if err := util.RemoveAll(wikiPath); err != nil { diff --git a/services/repository/push.go b/services/repository/push.go index bb080e30cc..5d10237845 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -183,7 +183,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { repo.DefaultBranch = refName repo.IsEmpty = false if repo.DefaultBranch != setting.Repository.DefaultBranch { - if err := gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { + if err := gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil { if !git.IsErrUnsupportedVersion(err) { return err } diff --git a/services/user/email.go b/services/user/email.go index 0b579cf792..9dc5270842 100644 --- a/services/user/email.go +++ b/services/user/email.go @@ -14,12 +14,13 @@ import ( "code.gitea.io/gitea/modules/util" ) -func AddOrSetPrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error { +// AdminAddOrSetPrimaryEmailAddress is used by admins to add or set a user's primary email address +func AdminAddOrSetPrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error { if strings.EqualFold(u.Email, emailStr) { return nil } - if err := user_model.ValidateEmail(emailStr); err != nil { + if err := user_model.ValidateEmailForAdmin(emailStr); err != nil { return err } diff --git a/services/user/email_test.go b/services/user/email_test.go index 66d4821346..0784b4f803 100644 --- a/services/user/email_test.go +++ b/services/user/email_test.go @@ -10,11 +10,13 @@ import ( organization_model "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + "github.com/gobwas/glob" "github.com/stretchr/testify/assert" ) -func TestAddOrSetPrimaryEmailAddress(t *testing.T) { +func TestAdminAddOrSetPrimaryEmailAddress(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 27}) @@ -28,7 +30,7 @@ func TestAddOrSetPrimaryEmailAddress(t *testing.T) { assert.NotEqual(t, "new-primary@example.com", primary.Email) assert.Equal(t, user.Email, primary.Email) - assert.NoError(t, AddOrSetPrimaryEmailAddress(db.DefaultContext, user, "new-primary@example.com")) + assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(db.DefaultContext, user, "new-primary@example.com")) primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID) assert.NoError(t, err) @@ -39,7 +41,19 @@ func TestAddOrSetPrimaryEmailAddress(t *testing.T) { assert.NoError(t, err) assert.Len(t, emails, 2) - assert.NoError(t, AddOrSetPrimaryEmailAddress(db.DefaultContext, user, "user27@example.com")) + setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("example.org")} + defer func() { + setting.Service.EmailDomainAllowList = []glob.Glob{} + }() + + assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(db.DefaultContext, user, "new-primary2@example2.com")) + + primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID) + assert.NoError(t, err) + assert.Equal(t, "new-primary2@example2.com", primary.Email) + assert.Equal(t, user.Email, primary.Email) + + assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(db.DefaultContext, user, "user27@example.com")) primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID) assert.NoError(t, err) @@ -48,7 +62,7 @@ func TestAddOrSetPrimaryEmailAddress(t *testing.T) { emails, err = user_model.GetEmailAddresses(db.DefaultContext, user.ID) assert.NoError(t, err) - assert.Len(t, emails, 2) + assert.Len(t, emails, 3) } func TestReplacePrimaryEmailAddress(t *testing.T) { diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index 16819f846a..32f1a3de45 100644 --- a/services/webhook/deliver.go +++ b/services/webhook/deliver.go @@ -32,36 +32,17 @@ import ( "github.com/gobwas/glob" ) -// Deliver deliver hook task -func Deliver(ctx context.Context, t *webhook_model.HookTask) error { - w, err := webhook_model.GetWebhookByID(ctx, t.HookID) - if err != nil { - return err - } - - defer func() { - err := recover() - if err == nil { - return - } - // There was a panic whilst delivering a hook... - log.Error("PANIC whilst trying to deliver webhook task[%d] to webhook %s Panic: %v\nStacktrace: %s", t.ID, w.URL, err, log.Stack(2)) - }() - - t.IsDelivered = true - - var req *http.Request - +func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) { switch w.HTTPMethod { case "": - log.Info("HTTP Method for webhook %s empty, setting to POST as default", w.URL) + log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID) fallthrough case http.MethodPost: switch w.ContentType { case webhook_model.ContentTypeJSON: req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent)) if err != nil { - return err + return nil, nil, err } req.Header.Set("Content-Type", "application/json") @@ -72,50 +53,58 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode())) if err != nil { - return err + return nil, nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + default: + return nil, nil, fmt.Errorf("invalid content type: %v", w.ContentType) } case http.MethodGet: u, err := url.Parse(w.URL) if err != nil { - return fmt.Errorf("unable to deliver webhook task[%d] as cannot parse webhook url %s: %w", t.ID, w.URL, err) + return nil, nil, fmt.Errorf("invalid URL: %w", err) } vals := u.Query() vals["payload"] = []string{t.PayloadContent} u.RawQuery = vals.Encode() req, err = http.NewRequest("GET", u.String(), nil) if err != nil { - return fmt.Errorf("unable to deliver webhook task[%d] as unable to create HTTP request for webhook url %s: %w", t.ID, w.URL, err) + return nil, nil, err } case http.MethodPut: switch w.Type { - case webhook_module.MATRIX: + case webhook_module.MATRIX: // used when t.Version == 1 txnID, err := getMatrixTxnID([]byte(t.PayloadContent)) if err != nil { - return err + return nil, nil, err } url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID)) req, err = http.NewRequest("PUT", url, strings.NewReader(t.PayloadContent)) if err != nil { - return fmt.Errorf("unable to deliver webhook task[%d] as cannot create matrix request for webhook url %s: %w", t.ID, w.URL, err) + return nil, nil, err } default: - return fmt.Errorf("invalid http method for webhook task[%d] in webhook %s: %v", t.ID, w.URL, w.HTTPMethod) + return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod) } default: - return fmt.Errorf("invalid http method for webhook task[%d] in webhook %s: %v", t.ID, w.URL, w.HTTPMethod) + return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod) } + body = []byte(t.PayloadContent) + return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) +} + +func addDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error { var signatureSHA1 string var signatureSHA256 string - if len(w.Secret) > 0 { - sig1 := hmac.New(sha1.New, []byte(w.Secret)) - sig256 := hmac.New(sha256.New, []byte(w.Secret)) - _, err = io.MultiWriter(sig1, sig256).Write([]byte(t.PayloadContent)) + if len(secret) > 0 { + sig1 := hmac.New(sha1.New, secret) + sig256 := hmac.New(sha256.New, secret) + _, err := io.MultiWriter(sig1, sig256).Write(payloadContent) if err != nil { - log.Error("prepareWebhooks.sigWrite: %v", err) + // this error should never happen, since the hashes are writing to []byte and always return a nil error. + return fmt.Errorf("prepareWebhooks.sigWrite: %w", err) } signatureSHA1 = hex.EncodeToString(sig1.Sum(nil)) signatureSHA256 = hex.EncodeToString(sig256.Sum(nil)) @@ -140,15 +129,36 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { req.Header["X-GitHub-Delivery"] = []string{t.UUID} req.Header["X-GitHub-Event"] = []string{event} req.Header["X-GitHub-Event-Type"] = []string{eventType} + return nil +} - // Add Authorization Header - authorization, err := w.HeaderAuthorization() +// Deliver creates the [http.Request] (depending on the webhook type), sends it +// and records the status and response. +func Deliver(ctx context.Context, t *webhook_model.HookTask) error { + w, err := webhook_model.GetWebhookByID(ctx, t.HookID) if err != nil { - log.Error("Webhook could not get Authorization header [%d]: %v", w.ID, err) return err } - if authorization != "" { - req.Header["Authorization"] = []string{authorization} + + defer func() { + err := recover() + if err == nil { + return + } + // There was a panic whilst delivering a hook... + log.Error("PANIC whilst trying to deliver webhook task[%d] to webhook %s Panic: %v\nStacktrace: %s", t.ID, w.URL, err, log.Stack(2)) + }() + + t.IsDelivered = true + + newRequest := webhookRequesters[w.Type] + if t.PayloadVersion == 1 || newRequest == nil { + newRequest = newDefaultRequest + } + + req, body, err := newRequest(ctx, w, t) + if err != nil { + return fmt.Errorf("cannot create http request for webhook %s[%d %s]: %w", w.Type, w.ID, w.URL, err) } // Record delivery information. @@ -156,11 +166,22 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { URL: req.URL.String(), HTTPMethod: req.Method, Headers: map[string]string{}, + Body: string(body), } for k, vals := range req.Header { t.RequestInfo.Headers[k] = strings.Join(vals, ",") } + // Add Authorization Header + authorization, err := w.HeaderAuthorization() + if err != nil { + return fmt.Errorf("cannot get Authorization header for webhook %s[%d %s]: %w", w.Type, w.ID, w.URL, err) + } + if authorization != "" { + req.Header.Set("Authorization", authorization) + t.RequestInfo.Headers["Authorization"] = "******" + } + t.ResponseInfo = &webhook_model.HookResponse{ Headers: map[string]string{}, } diff --git a/services/webhook/deliver_test.go b/services/webhook/deliver_test.go index eca2ba244b..bc06e43e03 100644 --- a/services/webhook/deliver_test.go +++ b/services/webhook/deliver_test.go @@ -5,10 +5,12 @@ package webhook import ( "context" + "io" "net/http" "net/http/httptest" "net/url" "os" + "strings" "testing" "time" @@ -17,7 +19,6 @@ import ( webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/hostmatcher" "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" "github.com/stretchr/testify/assert" @@ -114,13 +115,15 @@ func TestWebhookDeliverAuthorizationHeader(t *testing.T) { assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook)) db.GetEngine(db.DefaultContext).NoAutoTime().DB().Logger.ShowSQL(true) - hookTask := &webhook_model.HookTask{HookID: hook.ID, EventType: webhook_module.HookEventPush, Payloader: &api.PushPayload{}} + hookTask := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadVersion: 2, + } hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask) assert.NoError(t, err) - if !assert.NotNil(t, hookTask) { - return - } + assert.NotNil(t, hookTask) assert.NoError(t, Deliver(context.Background(), hookTask)) select { @@ -130,4 +133,202 @@ func TestWebhookDeliverAuthorizationHeader(t *testing.T) { } assert.True(t, hookTask.IsSucceed) + assert.Equal(t, "******", hookTask.RequestInfo.Headers["Authorization"]) +} + +func TestWebhookDeliverHookTask(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + done := make(chan struct{}, 1) + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + switch r.URL.Path { + case "/webhook/66d222a5d6349e1311f551e50722d837e30fce98": + // Version 1 + assert.Equal(t, "push", r.Header.Get("X-GitHub-Event")) + assert.Equal(t, "", r.Header.Get("Content-Type")) + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, `{"data": 42}`, string(body)) + + case "/webhook/6db5dc1e282529a8c162c7fe93dd2667494eeb51": + // Version 2 + assert.Equal(t, "push", r.Header.Get("X-GitHub-Event")) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.Len(t, body, 2147) + + default: + w.WriteHeader(404) + t.Fatalf("unexpected url path %s", r.URL.Path) + return + } + w.WriteHeader(200) + done <- struct{}{} + })) + t.Cleanup(s.Close) + + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.MATRIX, + URL: s.URL + "/webhook", + HTTPMethod: "PUT", + ContentType: webhook_model.ContentTypeJSON, + Meta: `{"message_type":0}`, // text + } + assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook)) + + t.Run("Version 1", func(t *testing.T) { + hookTask := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: `{"data": 42}`, + PayloadVersion: 1, + } + + hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask) + assert.NoError(t, err) + assert.NotNil(t, hookTask) + + assert.NoError(t, Deliver(context.Background(), hookTask)) + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("waited to long for request to happen") + } + + assert.True(t, hookTask.IsSucceed) + }) + + t.Run("Version 2", func(t *testing.T) { + p := pushTestPayload() + data, err := p.JSONPayload() + assert.NoError(t, err) + + hookTask := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask) + assert.NoError(t, err) + assert.NotNil(t, hookTask) + + assert.NoError(t, Deliver(context.Background(), hookTask)) + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("waited to long for request to happen") + } + + assert.True(t, hookTask.IsSucceed) + }) +} + +func TestWebhookDeliverSpecificTypes(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + type hookCase struct { + gotBody chan []byte + expectedMethod string + } + + cases := map[string]hookCase{ + webhook_module.SLACK: { + gotBody: make(chan []byte, 1), + }, + webhook_module.DISCORD: { + gotBody: make(chan []byte, 1), + }, + webhook_module.DINGTALK: { + gotBody: make(chan []byte, 1), + }, + webhook_module.TELEGRAM: { + gotBody: make(chan []byte, 1), + }, + webhook_module.MSTEAMS: { + gotBody: make(chan []byte, 1), + }, + webhook_module.FEISHU: { + gotBody: make(chan []byte, 1), + }, + webhook_module.MATRIX: { + gotBody: make(chan []byte, 1), + expectedMethod: "PUT", + }, + webhook_module.WECHATWORK: { + gotBody: make(chan []byte, 1), + }, + webhook_module.PACKAGIST: { + gotBody: make(chan []byte, 1), + }, + } + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "application/json", r.Header.Get("Content-Type"), r.URL.Path) + + typ := strings.Split(r.URL.Path, "/")[1] // take first segment (after skipping leading slash) + hc := cases[typ] + + if hc.expectedMethod != "" { + assert.Equal(t, hc.expectedMethod, r.Method, r.URL.Path) + } else { + assert.Equal(t, "POST", r.Method, r.URL.Path) + } + + require.NotNil(t, hc.gotBody, r.URL.Path) + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + w.WriteHeader(200) + hc.gotBody <- body + })) + t.Cleanup(s.Close) + + p := pushTestPayload() + data, err := p.JSONPayload() + assert.NoError(t, err) + + for typ, hc := range cases { + typ := typ + hc := hc + t.Run(typ, func(t *testing.T) { + t.Parallel() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: typ, + URL: s.URL + "/" + typ, + HTTPMethod: "", // should fallback to POST, when left unset by the specific hook + ContentType: 0, // set to 0 so that falling back to default request fails with "invalid content type" + Meta: "{}", + } + assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook)) + + hookTask := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask) + assert.NoError(t, err) + assert.NotNil(t, hookTask) + + assert.NoError(t, Deliver(context.Background(), hookTask)) + select { + case gotBody := <-hc.gotBody: + assert.NotEqual(t, string(data), string(gotBody), "request body must be different from the event payload") + assert.Equal(t, hookTask.RequestInfo.Body, string(gotBody), "request body was not saved") + case <-time.After(5 * time.Second): + t.Fatal("waited to long for request to happen") + } + + assert.True(t, hookTask.IsSucceed) + }) + } } diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go index d615e7254f..c57d04415a 100644 --- a/services/webhook/dingtalk.go +++ b/services/webhook/dingtalk.go @@ -4,12 +4,14 @@ package webhook import ( + "context" "fmt" + "net/http" "net/url" "strings" + webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -22,19 +24,8 @@ type ( DingtalkPayload dingtalk.Payload ) -var _ PayloadConvertor = &DingtalkPayload{} - -// JSONPayload Marshals the DingtalkPayload to json -func (d *DingtalkPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(d, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - // Create implements PayloadConvertor Create method -func (d *DingtalkPayload) Create(p *api.CreatePayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Create(p *api.CreatePayload) (DingtalkPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) @@ -43,7 +34,7 @@ func (d *DingtalkPayload) Create(p *api.CreatePayload) (api.Payloader, error) { } // Delete implements PayloadConvertor Delete method -func (d *DingtalkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Delete(p *api.DeletePayload) (DingtalkPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) @@ -52,14 +43,14 @@ func (d *DingtalkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { } // Fork implements PayloadConvertor Fork method -func (d *DingtalkPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Fork(p *api.ForkPayload) (DingtalkPayload, error) { title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) return createDingtalkPayload(title, title, fmt.Sprintf("view forked repo %s", p.Repo.FullName), p.Repo.HTMLURL), nil } // Push implements PayloadConvertor Push method -func (d *DingtalkPayload) Push(p *api.PushPayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Push(p *api.PushPayload) (DingtalkPayload, error) { var ( branchName = git.RefName(p.Ref).ShortName() commitDesc string @@ -100,14 +91,14 @@ func (d *DingtalkPayload) Push(p *api.PushPayload) (api.Payloader, error) { } // Issue implements PayloadConvertor Issue method -func (d *DingtalkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Issue(p *api.IssuePayload) (DingtalkPayload, error) { text, issueTitle, attachmentText, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true) return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+attachmentText, "view issue", p.Issue.HTMLURL), nil } // Wiki implements PayloadConvertor Wiki method -func (d *DingtalkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Wiki(p *api.WikiPayload) (DingtalkPayload, error) { text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true) url := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page) @@ -115,27 +106,27 @@ func (d *DingtalkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { } // IssueComment implements PayloadConvertor IssueComment method -func (d *DingtalkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { +func (dc dingtalkConvertor) IssueComment(p *api.IssueCommentPayload) (DingtalkPayload, error) { text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true) return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+p.Comment.Body, "view issue comment", p.Comment.HTMLURL), nil } // PullRequest implements PayloadConvertor PullRequest method -func (d *DingtalkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { +func (dc dingtalkConvertor) PullRequest(p *api.PullRequestPayload) (DingtalkPayload, error) { text, issueTitle, attachmentText, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true) return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+attachmentText, "view pull request", p.PullRequest.HTMLURL), nil } // Review implements PayloadConvertor Review method -func (d *DingtalkPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { +func (dc dingtalkConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (DingtalkPayload, error) { var text, title string switch p.Action { case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return DingtalkPayload{}, err } title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) @@ -146,14 +137,14 @@ func (d *DingtalkPayload) Review(p *api.PullRequestPayload, event webhook_module } // Repository implements PayloadConvertor Repository method -func (d *DingtalkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Repository(p *api.RepositoryPayload) (DingtalkPayload, error) { switch p.Action { case api.HookRepoCreated: title := fmt.Sprintf("[%s] Repository created", p.Repository.FullName) return createDingtalkPayload(title, title, "view repository", p.Repository.HTMLURL), nil case api.HookRepoDeleted: title := fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) - return &DingtalkPayload{ + return DingtalkPayload{ MsgType: "text", Text: struct { Content string `json:"content"` @@ -163,24 +154,24 @@ func (d *DingtalkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, e }, nil } - return nil, nil + return DingtalkPayload{}, nil } // Release implements PayloadConvertor Release method -func (d *DingtalkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Release(p *api.ReleasePayload) (DingtalkPayload, error) { text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true) return createDingtalkPayload(text, text, "view release", p.Release.HTMLURL), nil } -func (d *DingtalkPayload) Package(p *api.PackagePayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, error) { text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true) return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil } -func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload { - return &DingtalkPayload{ +func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload { + return DingtalkPayload{ MsgType: "actionCard", ActionCard: dingtalk.ActionCard{ Text: strings.TrimSpace(text), @@ -195,7 +186,10 @@ func createDingtalkPayload(title, text, singleTitle, singleURL string) *Dingtalk } } -// GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload -func GetDingtalkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) { - return convertPayloader(new(DingtalkPayload), p, event) +type dingtalkConvertor struct{} + +var _ payloadConvertor[DingtalkPayload] = dingtalkConvertor{} + +func newDingtalkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + return newJSONRequest(dingtalkConvertor{}, w, t, true) } diff --git a/services/webhook/dingtalk_test.go b/services/webhook/dingtalk_test.go index a03fa46f14..25f47347d0 100644 --- a/services/webhook/dingtalk_test.go +++ b/services/webhook/dingtalk_test.go @@ -4,9 +4,12 @@ package webhook import ( + "context" "net/url" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -24,248 +27,226 @@ func TestDingTalkPayload(t *testing.T) { } return "" } + dc := dingtalkConvertor{} t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(DingtalkPayload) - pl, err := d.Create(p) + pl, err := dc.Create(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] branch test created", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo] branch test created", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view ref test", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] branch test created", pl.ActionCard.Text) + assert.Equal(t, "[test/repo] branch test created", pl.ActionCard.Title) + assert.Equal(t, "view ref test", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(DingtalkPayload) - pl, err := d.Delete(p) + pl, err := dc.Delete(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] branch test deleted", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo] branch test deleted", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view ref test", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] branch test deleted", pl.ActionCard.Text) + assert.Equal(t, "[test/repo] branch test deleted", pl.ActionCard.Title) + assert.Equal(t, "view ref test", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(DingtalkPayload) - pl, err := d.Fork(p) + pl, err := dc.Fork(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view forked repo test/repo", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "test/repo2 is forked to test/repo", pl.ActionCard.Text) + assert.Equal(t, "test/repo2 is forked to test/repo", pl.ActionCard.Title) + assert.Equal(t, "view forked repo test/repo", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(DingtalkPayload) - pl, err := d.Push(p) + pl, err := dc.Push(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view commits", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.ActionCard.Text) + assert.Equal(t, "[test/repo:test] 2 new commits", pl.ActionCard.Title) + assert.Equal(t, "view commits", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(DingtalkPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := dc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\r\n\r\nissue body", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view issue", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\r\n\r\nissue body", pl.ActionCard.Text) + assert.Equal(t, "#2 crash", pl.ActionCard.Title) + assert.Equal(t, "view issue", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.ActionCard.SingleURL)) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = dc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view issue", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1", pl.ActionCard.Text) + assert.Equal(t, "#2 crash", pl.ActionCard.Title) + assert.Equal(t, "view issue", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(DingtalkPayload) - pl, err := d.IssueComment(p) + pl, err := dc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\r\n\r\nmore info needed", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view issue comment", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\r\n\r\nmore info needed", pl.ActionCard.Text) + assert.Equal(t, "#2 crash", pl.ActionCard.Title) + assert.Equal(t, "view issue comment", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(DingtalkPayload) - pl, err := d.PullRequest(p) + pl, err := dc.PullRequest(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\r\n\r\nfixes bug #2", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "#12 Fix bug", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view pull request", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\r\n\r\nfixes bug #2", pl.ActionCard.Text) + assert.Equal(t, "#12 Fix bug", pl.ActionCard.Title) + assert.Equal(t, "view pull request", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(DingtalkPayload) - pl, err := d.IssueComment(p) + pl, err := dc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\r\n\r\nchanges requested", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "#12 Fix bug", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view issue comment", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\r\n\r\nchanges requested", pl.ActionCard.Text) + assert.Equal(t, "#12 Fix bug", pl.ActionCard.Title) + assert.Equal(t, "view issue comment", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(DingtalkPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := dc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view pull request", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.ActionCard.Text) + assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug", pl.ActionCard.Title) + assert.Equal(t, "view pull request", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(DingtalkPayload) - pl, err := d.Repository(p) + pl, err := dc.Repository(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] Repository created", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo] Repository created", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view repository", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] Repository created", pl.ActionCard.Text) + assert.Equal(t, "[test/repo] Repository created", pl.ActionCard.Title) + assert.Equal(t, "view repository", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Package", func(t *testing.T) { p := packageTestPayload() - d := new(DingtalkPayload) - pl, err := d.Package(p) + pl, err := dc.Package(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view package", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.ActionCard.Text) + assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.ActionCard.Title) + assert.Equal(t, "view package", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(DingtalkPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := dc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.ActionCard.Text) + assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.ActionCard.Title) + assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL)) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = dc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.ActionCard.Text) + assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.ActionCard.Title) + assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL)) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = dc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.ActionCard.Text) + assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.ActionCard.Title) + assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(DingtalkPayload) - pl, err := d.Release(p) + pl, err := dc.Release(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view release", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.ActionCard.Text) + assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.ActionCard.Title) + assert.Equal(t, "view release", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", parseRealSingleURL(pl.ActionCard.SingleURL)) }) } func TestDingTalkJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(DingtalkPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.DINGTALK, + URL: "https://dingtalk.example.com/", + Meta: ``, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newDingtalkRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://dingtalk.example.com/", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body DingtalkPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.ActionCard.Text) } diff --git a/services/webhook/discord.go b/services/webhook/discord.go index e2ac1410b8..659754d5e0 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -4,8 +4,10 @@ package webhook import ( + "context" "errors" "fmt" + "net/http" "net/url" "strconv" "strings" @@ -98,19 +100,8 @@ var ( redColor = color("ff3232") ) -// JSONPayload Marshals the DiscordPayload to json -func (d *DiscordPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(d, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -var _ PayloadConvertor = &DiscordPayload{} - // Create implements PayloadConvertor Create method -func (d *DiscordPayload) Create(p *api.CreatePayload) (api.Payloader, error) { +func (d discordConvertor) Create(p *api.CreatePayload) (DiscordPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) @@ -119,7 +110,7 @@ func (d *DiscordPayload) Create(p *api.CreatePayload) (api.Payloader, error) { } // Delete implements PayloadConvertor Delete method -func (d *DiscordPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (d discordConvertor) Delete(p *api.DeletePayload) (DiscordPayload, error) { // deleted tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) @@ -128,14 +119,14 @@ func (d *DiscordPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { } // Fork implements PayloadConvertor Fork method -func (d *DiscordPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { +func (d discordConvertor) Fork(p *api.ForkPayload) (DiscordPayload, error) { title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL, greenColor), nil } // Push implements PayloadConvertor Push method -func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) { +func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) { var ( branchName = git.RefName(p.Ref).ShortName() commitDesc string @@ -170,35 +161,35 @@ func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) { } // Issue implements PayloadConvertor Issue method -func (d *DiscordPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { +func (d discordConvertor) Issue(p *api.IssuePayload) (DiscordPayload, error) { title, _, text, color := getIssuesPayloadInfo(p, noneLinkFormatter, false) return d.createPayload(p.Sender, title, text, p.Issue.HTMLURL, color), nil } // IssueComment implements PayloadConvertor IssueComment method -func (d *DiscordPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { +func (d discordConvertor) IssueComment(p *api.IssueCommentPayload) (DiscordPayload, error) { title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false) return d.createPayload(p.Sender, title, p.Comment.Body, p.Comment.HTMLURL, color), nil } // PullRequest implements PayloadConvertor PullRequest method -func (d *DiscordPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { +func (d discordConvertor) PullRequest(p *api.PullRequestPayload) (DiscordPayload, error) { title, _, text, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false) return d.createPayload(p.Sender, title, text, p.PullRequest.HTMLURL, color), nil } // Review implements PayloadConvertor Review method -func (d *DiscordPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { +func (d discordConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (DiscordPayload, error) { var text, title string var color int switch p.Action { case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return DiscordPayload{}, err } title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) @@ -220,7 +211,7 @@ func (d *DiscordPayload) Review(p *api.PullRequestPayload, event webhook_module. } // Repository implements PayloadConvertor Repository method -func (d *DiscordPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { +func (d discordConvertor) Repository(p *api.RepositoryPayload) (DiscordPayload, error) { var title, url string var color int switch p.Action { @@ -237,7 +228,7 @@ func (d *DiscordPayload) Repository(p *api.RepositoryPayload) (api.Payloader, er } // Wiki implements PayloadConvertor Wiki method -func (d *DiscordPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { +func (d discordConvertor) Wiki(p *api.WikiPayload) (DiscordPayload, error) { text, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false) htmlLink := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page) @@ -250,30 +241,35 @@ func (d *DiscordPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { } // Release implements PayloadConvertor Release method -func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { +func (d discordConvertor) Release(p *api.ReleasePayload) (DiscordPayload, error) { text, color := getReleasePayloadInfo(p, noneLinkFormatter, false) return d.createPayload(p.Sender, text, p.Release.Note, p.Release.HTMLURL, color), nil } -func (d *DiscordPayload) Package(p *api.PackagePayload) (api.Payloader, error) { +func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error) { text, color := getPackagePayloadInfo(p, noneLinkFormatter, false) return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil } -// GetDiscordPayload converts a discord webhook into a DiscordPayload -func GetDiscordPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) { - s := new(DiscordPayload) +type discordConvertor struct { + Username string + AvatarURL string +} - discord := &DiscordMeta{} - if err := json.Unmarshal([]byte(meta), &discord); err != nil { - return s, errors.New("GetDiscordPayload meta json:" + err.Error()) +var _ payloadConvertor[DiscordPayload] = discordConvertor{} + +func newDiscordRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + meta := &DiscordMeta{} + if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { + return nil, nil, fmt.Errorf("newDiscordRequest meta json: %w", err) } - s.Username = discord.Username - s.AvatarURL = discord.IconURL - - return convertPayloader(s, p, event) + sc := discordConvertor{ + Username: meta.Username, + AvatarURL: meta.IconURL, + } + return newJSONRequest(sc, w, t, true) } func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) { @@ -291,8 +287,8 @@ func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, } } -func (d *DiscordPayload) createPayload(s *api.User, title, text, url string, color int) *DiscordPayload { - return &DiscordPayload{ +func (d discordConvertor) createPayload(s *api.User, title, text, url string, color int) DiscordPayload { + return DiscordPayload{ Username: d.Username, AvatarURL: d.AvatarURL, Embeds: []DiscordEmbed{ diff --git a/services/webhook/discord_test.go b/services/webhook/discord_test.go index b567cbc395..c04b95383b 100644 --- a/services/webhook/discord_test.go +++ b/services/webhook/discord_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -15,295 +18,274 @@ import ( ) func TestDiscordPayload(t *testing.T) { + dc := discordConvertor{} + t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(DiscordPayload) - pl, err := d.Create(p) + pl, err := dc.Create(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] branch test created", pl.(*DiscordPayload).Embeds[0].Title) - assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] branch test created", pl.Embeds[0].Title) + assert.Empty(t, pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(DiscordPayload) - pl, err := d.Delete(p) + pl, err := dc.Delete(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] branch test deleted", pl.(*DiscordPayload).Embeds[0].Title) - assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] branch test deleted", pl.Embeds[0].Title) + assert.Empty(t, pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(DiscordPayload) - pl, err := d.Fork(p) + pl, err := dc.Fork(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*DiscordPayload).Embeds[0].Title) - assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "test/repo2 is forked to test/repo", pl.Embeds[0].Title) + assert.Empty(t, pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(DiscordPayload) - pl, err := d.Push(p) + pl, err := dc.Push(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title) + assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(DiscordPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := dc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "issue body", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Embeds[0].Title) + assert.Equal(t, "issue body", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = dc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*DiscordPayload).Embeds[0].Title) - assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Embeds[0].Title) + assert.Empty(t, pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(DiscordPayload) - pl, err := d.IssueComment(p) + pl, err := dc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "more info needed", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Embeds[0].Title) + assert.Equal(t, "more info needed", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(DiscordPayload) - pl, err := d.PullRequest(p) + pl, err := dc.PullRequest(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "fixes bug #2", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Embeds[0].Title) + assert.Equal(t, "fixes bug #2", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(DiscordPayload) - pl, err := d.IssueComment(p) + pl, err := dc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "changes requested", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Embeds[0].Title) + assert.Equal(t, "changes requested", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(DiscordPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := dc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "good job", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Embeds[0].Title) + assert.Equal(t, "good job", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(DiscordPayload) - pl, err := d.Repository(p) + pl, err := dc.Repository(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] Repository created", pl.(*DiscordPayload).Embeds[0].Title) - assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] Repository created", pl.Embeds[0].Title) + assert.Empty(t, pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Package", func(t *testing.T) { p := packageTestPayload() - d := new(DiscordPayload) - pl, err := d.Package(p) + pl, err := dc.Package(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "Package created: GiteaContainer:latest", pl.(*DiscordPayload).Embeds[0].Title) - assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "Package created: GiteaContainer:latest", pl.Embeds[0].Title) + assert.Empty(t, pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(DiscordPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := dc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "Wiki change comment", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Embeds[0].Title) + assert.Equal(t, "Wiki change comment", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = dc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "Wiki change comment", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Embeds[0].Title) + assert.Equal(t, "Wiki change comment", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = dc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*DiscordPayload).Embeds[0].Title) - assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Embeds[0].Title) + assert.Empty(t, pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(DiscordPayload) - pl, err := d.Release(p) + pl, err := dc.Release(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "Note of first stable release", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] Release created: v1.0", pl.Embeds[0].Title) + assert.Equal(t, "Note of first stable release", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) } func TestDiscordJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(DiscordPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.DISCORD, + URL: "https://discord.example.com/", + Meta: `{}`, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newDiscordRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://discord.example.com/", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body DiscordPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Embeds[0].Description) } diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go index 556443e70b..1ec436894b 100644 --- a/services/webhook/feishu.go +++ b/services/webhook/feishu.go @@ -4,11 +4,13 @@ package webhook import ( + "context" "fmt" + "net/http" "strings" + webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" ) @@ -23,8 +25,8 @@ type ( } ) -func newFeishuTextPayload(text string) *FeishuPayload { - return &FeishuPayload{ +func newFeishuTextPayload(text string) FeishuPayload { + return FeishuPayload{ MsgType: "text", Content: struct { Text string `json:"text"` @@ -34,19 +36,8 @@ func newFeishuTextPayload(text string) *FeishuPayload { } } -// JSONPayload Marshals the FeishuPayload to json -func (f *FeishuPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(f, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -var _ PayloadConvertor = &FeishuPayload{} - // Create implements PayloadConvertor Create method -func (f *FeishuPayload) Create(p *api.CreatePayload) (api.Payloader, error) { +func (fc feishuConvertor) Create(p *api.CreatePayload) (FeishuPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() text := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) @@ -55,7 +46,7 @@ func (f *FeishuPayload) Create(p *api.CreatePayload) (api.Payloader, error) { } // Delete implements PayloadConvertor Delete method -func (f *FeishuPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (fc feishuConvertor) Delete(p *api.DeletePayload) (FeishuPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() text := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) @@ -64,14 +55,14 @@ func (f *FeishuPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { } // Fork implements PayloadConvertor Fork method -func (f *FeishuPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { +func (fc feishuConvertor) Fork(p *api.ForkPayload) (FeishuPayload, error) { text := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) return newFeishuTextPayload(text), nil } // Push implements PayloadConvertor Push method -func (f *FeishuPayload) Push(p *api.PushPayload) (api.Payloader, error) { +func (fc feishuConvertor) Push(p *api.PushPayload) (FeishuPayload, error) { var ( branchName = git.RefName(p.Ref).ShortName() commitDesc string @@ -96,48 +87,40 @@ func (f *FeishuPayload) Push(p *api.PushPayload) (api.Payloader, error) { } // Issue implements PayloadConvertor Issue method -func (f *FeishuPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { +func (fc feishuConvertor) Issue(p *api.IssuePayload) (FeishuPayload, error) { title, link, by, operator, result, assignees := getIssuesInfo(p) - var res api.Payloader if assignees != "" { if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned { - res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.Issue.Body)) - } else { - res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.Issue.Body)) + return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.Issue.Body)), nil } - } else { - res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Issue.Body)) + return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.Issue.Body)), nil } - return res, nil + return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Issue.Body)), nil } // IssueComment implements PayloadConvertor IssueComment method -func (f *FeishuPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { +func (fc feishuConvertor) IssueComment(p *api.IssueCommentPayload) (FeishuPayload, error) { title, link, by, operator := getIssuesCommentInfo(p) return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Comment.Body)), nil } // PullRequest implements PayloadConvertor PullRequest method -func (f *FeishuPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { +func (fc feishuConvertor) PullRequest(p *api.PullRequestPayload) (FeishuPayload, error) { title, link, by, operator, result, assignees := getPullRequestInfo(p) - var res api.Payloader if assignees != "" { if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned { - res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.PullRequest.Body)) - } else { - res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.PullRequest.Body)) + return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.PullRequest.Body)), nil } - } else { - res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.PullRequest.Body)) + return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.PullRequest.Body)), nil } - return res, nil + return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.PullRequest.Body)), nil } // Review implements PayloadConvertor Review method -func (f *FeishuPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { +func (fc feishuConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (FeishuPayload, error) { action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return FeishuPayload{}, err } title := fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) @@ -147,7 +130,7 @@ func (f *FeishuPayload) Review(p *api.PullRequestPayload, event webhook_module.H } // Repository implements PayloadConvertor Repository method -func (f *FeishuPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { +func (fc feishuConvertor) Repository(p *api.RepositoryPayload) (FeishuPayload, error) { var text string switch p.Action { case api.HookRepoCreated: @@ -158,30 +141,33 @@ func (f *FeishuPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err return newFeishuTextPayload(text), nil } - return nil, nil + return FeishuPayload{}, nil } // Wiki implements PayloadConvertor Wiki method -func (f *FeishuPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { +func (fc feishuConvertor) Wiki(p *api.WikiPayload) (FeishuPayload, error) { text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true) return newFeishuTextPayload(text), nil } // Release implements PayloadConvertor Release method -func (f *FeishuPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { +func (fc feishuConvertor) Release(p *api.ReleasePayload) (FeishuPayload, error) { text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true) return newFeishuTextPayload(text), nil } -func (f *FeishuPayload) Package(p *api.PackagePayload) (api.Payloader, error) { +func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error) { text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true) return newFeishuTextPayload(text), nil } -// GetFeishuPayload converts a ding talk webhook into a FeishuPayload -func GetFeishuPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) { - return convertPayloader(new(FeishuPayload), p, event) +type feishuConvertor struct{} + +var _ payloadConvertor[FeishuPayload] = feishuConvertor{} + +func newFeishuRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + return newJSONRequest(feishuConvertor{}, w, t, true) } diff --git a/services/webhook/feishu_test.go b/services/webhook/feishu_test.go index 98bc50dede..ef18333fd4 100644 --- a/services/webhook/feishu_test.go +++ b/services/webhook/feishu_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -14,199 +17,177 @@ import ( ) func TestFeishuPayload(t *testing.T) { + fc := feishuConvertor{} t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(FeishuPayload) - pl, err := d.Create(p) + pl, err := fc.Create(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, `[test/repo] branch test created`, pl.(*FeishuPayload).Content.Text) + assert.Equal(t, `[test/repo] branch test created`, pl.Content.Text) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(FeishuPayload) - pl, err := d.Delete(p) + pl, err := fc.Delete(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, `[test/repo] branch test deleted`, pl.(*FeishuPayload).Content.Text) + assert.Equal(t, `[test/repo] branch test deleted`, pl.Content.Text) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(FeishuPayload) - pl, err := d.Fork(p) + pl, err := fc.Fork(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, `test/repo2 is forked to test/repo`, pl.(*FeishuPayload).Content.Text) + assert.Equal(t, `test/repo2 is forked to test/repo`, pl.Content.Text) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(FeishuPayload) - pl, err := d.Push(p) + pl, err := fc.Push(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Content.Text) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(FeishuPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := fc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[Issue-test/repo #2]: opened\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[Issue-test/repo #2]: opened\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.Content.Text) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = fc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[Issue-test/repo #2]: closed\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[Issue-test/repo #2]: closed\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.Content.Text) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(FeishuPayload) - pl, err := d.IssueComment(p) + pl, err := fc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[Comment-test/repo #2]: created\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\n\nmore info needed", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[Comment-test/repo #2]: created\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\n\nmore info needed", pl.Content.Text) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(FeishuPayload) - pl, err := d.PullRequest(p) + pl, err := fc.PullRequest(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[PullRequest-test/repo #12]: opened\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\nAssignees: user1\n\nfixes bug #2", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[PullRequest-test/repo #12]: opened\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\nAssignees: user1\n\nfixes bug #2", pl.Content.Text) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(FeishuPayload) - pl, err := d.IssueComment(p) + pl, err := fc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[Comment-test/repo #12]: created\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\n\nchanges requested", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[Comment-test/repo #12]: created\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\n\nchanges requested", pl.Content.Text) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(FeishuPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := fc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.Content.Text) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(FeishuPayload) - pl, err := d.Repository(p) + pl, err := fc.Repository(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[test/repo] Repository created", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[test/repo] Repository created", pl.Content.Text) }) t.Run("Package", func(t *testing.T) { p := packageTestPayload() - d := new(FeishuPayload) - pl, err := d.Package(p) + pl, err := fc.Package(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.Content.Text) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(FeishuPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := fc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.Content.Text) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = fc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.Content.Text) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = fc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.Content.Text) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(FeishuPayload) - pl, err := d.Release(p) + pl, err := fc.Release(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.Content.Text) }) } func TestFeishuJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(FeishuPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.FEISHU, + URL: "https://feishu.example.com/", + Meta: `{}`, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newFeishuRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://feishu.example.com/", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body FeishuPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Content.Text) } diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index 602d16ef39..0329804a8b 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -4,11 +4,12 @@ package webhook import ( + "bytes" + "context" "crypto/sha1" "encoding/hex" - "errors" "fmt" - "html" + "net/http" "net/url" "regexp" "strings" @@ -23,6 +24,37 @@ import ( webhook_module "code.gitea.io/gitea/modules/webhook" ) +func newMatrixRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + meta := &MatrixMeta{} + if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { + return nil, nil, fmt.Errorf("GetMatrixPayload meta json: %w", err) + } + mc := matrixConvertor{ + MsgType: messageTypeText[meta.MessageType], + } + payload, err := newPayload(mc, []byte(t.PayloadContent), t.EventType) + if err != nil { + return nil, nil, err + } + + body, err := json.MarshalIndent(payload, "", " ") + if err != nil { + return nil, nil, err + } + + txnID, err := getMatrixTxnID(body) + if err != nil { + return nil, nil, err + } + req, err := http.NewRequest(http.MethodPut, w.URL+"/"+txnID, bytes.NewReader(body)) + if err != nil { + return nil, nil, err + } + req.Header.Set("Content-Type", "application/json") + + return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) // likely useless, but has always been sent historially +} + const matrixPayloadSizeLimit = 1024 * 64 // MatrixMeta contains the Matrix metadata @@ -46,8 +78,6 @@ func GetMatrixHook(w *webhook_model.Webhook) *MatrixMeta { return s } -var _ PayloadConvertor = &MatrixPayload{} - // MatrixPayload contains payload for a Matrix room type MatrixPayload struct { Body string `json:"body"` @@ -57,90 +87,79 @@ type MatrixPayload struct { Commits []*api.PayloadCommit `json:"io.gitea.commits,omitempty"` } -// JSONPayload Marshals the MatrixPayload to json -func (m *MatrixPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(m, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil +var _ payloadConvertor[MatrixPayload] = matrixConvertor{} + +type matrixConvertor struct { + MsgType string } -// MatrixLinkFormatter creates a link compatible with Matrix -func MatrixLinkFormatter(url, text string) string { - return fmt.Sprintf(`%s`, html.EscapeString(url), html.EscapeString(text)) +func (m matrixConvertor) newPayload(text string, commits ...*api.PayloadCommit) (MatrixPayload, error) { + return MatrixPayload{ + Body: getMessageBody(text), + MsgType: m.MsgType, + Format: "org.matrix.custom.html", + FormattedBody: text, + Commits: commits, + }, nil } -// MatrixLinkToRef Matrix-formatter link to a repo ref -func MatrixLinkToRef(repoURL, ref string) string { - refName := git.RefName(ref).ShortName() - switch { - case strings.HasPrefix(ref, git.BranchPrefix): - return MatrixLinkFormatter(repoURL+"/src/branch/"+util.PathEscapeSegments(refName), refName) - case strings.HasPrefix(ref, git.TagPrefix): - return MatrixLinkFormatter(repoURL+"/src/tag/"+util.PathEscapeSegments(refName), refName) - default: - return MatrixLinkFormatter(repoURL+"/src/commit/"+util.PathEscapeSegments(refName), refName) - } -} - -// Create implements PayloadConvertor Create method -func (m *MatrixPayload) Create(p *api.CreatePayload) (api.Payloader, error) { - repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) +// Create implements payloadConvertor Create method +func (m matrixConvertor) Create(p *api.CreatePayload) (MatrixPayload, error) { + repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) refLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref) text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } // Delete composes Matrix payload for delete a branch or tag. -func (m *MatrixPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (m matrixConvertor) Delete(p *api.DeletePayload) (MatrixPayload, error) { refName := git.RefName(p.Ref).ShortName() - repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) + repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } // Fork composes Matrix payload for forked by a repository. -func (m *MatrixPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { - baseLink := MatrixLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName) - forkLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) +func (m matrixConvertor) Fork(p *api.ForkPayload) (MatrixPayload, error) { + baseLink := htmlLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName) + forkLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -// Issue implements PayloadConvertor Issue method -func (m *MatrixPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { - text, _, _, _ := getIssuesPayloadInfo(p, MatrixLinkFormatter, true) +// Issue implements payloadConvertor Issue method +func (m matrixConvertor) Issue(p *api.IssuePayload) (MatrixPayload, error) { + text, _, _, _ := getIssuesPayloadInfo(p, htmlLinkFormatter, true) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -// IssueComment implements PayloadConvertor IssueComment method -func (m *MatrixPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { - text, _, _ := getIssueCommentPayloadInfo(p, MatrixLinkFormatter, true) +// IssueComment implements payloadConvertor IssueComment method +func (m matrixConvertor) IssueComment(p *api.IssueCommentPayload) (MatrixPayload, error) { + text, _, _ := getIssueCommentPayloadInfo(p, htmlLinkFormatter, true) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -// Wiki implements PayloadConvertor Wiki method -func (m *MatrixPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { - text, _, _ := getWikiPayloadInfo(p, MatrixLinkFormatter, true) +// Wiki implements payloadConvertor Wiki method +func (m matrixConvertor) Wiki(p *api.WikiPayload) (MatrixPayload, error) { + text, _, _ := getWikiPayloadInfo(p, htmlLinkFormatter, true) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -// Release implements PayloadConvertor Release method -func (m *MatrixPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { - text, _ := getReleasePayloadInfo(p, MatrixLinkFormatter, true) +// Release implements payloadConvertor Release method +func (m matrixConvertor) Release(p *api.ReleasePayload) (MatrixPayload, error) { + text, _ := getReleasePayloadInfo(p, htmlLinkFormatter, true) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -// Push implements PayloadConvertor Push method -func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) { +// Push implements payloadConvertor Push method +func (m matrixConvertor) Push(p *api.PushPayload) (MatrixPayload, error) { var commitDesc string if p.TotalCommits == 1 { @@ -149,13 +168,13 @@ func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) { commitDesc = fmt.Sprintf("%d commits", p.TotalCommits) } - repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) + repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) branchLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref) text := fmt.Sprintf("[%s] %s pushed %s to %s:
", repoLink, p.Pusher.UserName, commitDesc, branchLink) // for each commit, generate a new line text for i, commit := range p.Commits { - text += fmt.Sprintf("%s: %s - %s", MatrixLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name) + text += fmt.Sprintf("%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name) // add linebreak to each commit but the last if i < len(p.Commits)-1 { text += "
" @@ -163,41 +182,41 @@ func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) { } - return getMatrixPayload(text, p.Commits, m.MsgType), nil + return m.newPayload(text, p.Commits...) } -// PullRequest implements PayloadConvertor PullRequest method -func (m *MatrixPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { - text, _, _, _ := getPullRequestPayloadInfo(p, MatrixLinkFormatter, true) +// PullRequest implements payloadConvertor PullRequest method +func (m matrixConvertor) PullRequest(p *api.PullRequestPayload) (MatrixPayload, error) { + text, _, _, _ := getPullRequestPayloadInfo(p, htmlLinkFormatter, true) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -// Review implements PayloadConvertor Review method -func (m *MatrixPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { - senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName) +// Review implements payloadConvertor Review method +func (m matrixConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (MatrixPayload, error) { + senderLink := htmlLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName) title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title) - titleLink := MatrixLinkFormatter(p.PullRequest.HTMLURL, title) - repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) + titleLink := htmlLinkFormatter(p.PullRequest.HTMLURL, title) + repoLink := htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) var text string switch p.Action { case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return MatrixPayload{}, err } text = fmt.Sprintf("[%s] Pull request review %s: %s by %s", repoLink, action, titleLink, senderLink) } - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -// Repository implements PayloadConvertor Repository method -func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { - senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) - repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) +// Repository implements payloadConvertor Repository method +func (m matrixConvertor) Repository(p *api.RepositoryPayload) (MatrixPayload, error) { + senderLink := htmlLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) + repoLink := htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) var text string switch p.Action { @@ -206,13 +225,12 @@ func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err case api.HookRepoDeleted: text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink) } - - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) { - senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) - packageLink := MatrixLinkFormatter(p.Package.HTMLURL, p.Package.Name) +func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) { + senderLink := htmlLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) + packageLink := htmlLinkFormatter(p.Package.HTMLURL, p.Package.Name) var text string switch p.Action { @@ -222,31 +240,7 @@ func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) { text = fmt.Sprintf("[%s] Package deleted by %s", packageLink, senderLink) } - return getMatrixPayload(text, nil, m.MsgType), nil -} - -// GetMatrixPayload converts a Matrix webhook into a MatrixPayload -func GetMatrixPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) { - s := new(MatrixPayload) - - matrix := &MatrixMeta{} - if err := json.Unmarshal([]byte(meta), &matrix); err != nil { - return s, errors.New("GetMatrixPayload meta json:" + err.Error()) - } - - s.MsgType = messageTypeText[matrix.MessageType] - - return convertPayloader(s, p, event) -} - -func getMatrixPayload(text string, commits []*api.PayloadCommit, msgType string) *MatrixPayload { - p := MatrixPayload{} - p.FormattedBody = text - p.Body = getMessageBody(text) - p.Format = "org.matrix.custom.html" - p.MsgType = msgType - p.Commits = commits - return &p + return m.newPayload(text) } var urlRegex = regexp.MustCompile(`]*?href="([^">]*?)">(.*?)`) @@ -271,3 +265,16 @@ func getMatrixTxnID(payload []byte) (string, error) { return hex.EncodeToString(h.Sum(nil)), nil } + +// MatrixLinkToRef Matrix-formatter link to a repo ref +func MatrixLinkToRef(repoURL, ref string) string { + refName := git.RefName(ref).ShortName() + switch { + case strings.HasPrefix(ref, git.BranchPrefix): + return htmlLinkFormatter(repoURL+"/src/branch/"+util.PathEscapeSegments(refName), refName) + case strings.HasPrefix(ref, git.TagPrefix): + return htmlLinkFormatter(repoURL+"/src/tag/"+util.PathEscapeSegments(refName), refName) + default: + return htmlLinkFormatter(repoURL+"/src/commit/"+util.PathEscapeSegments(refName), refName) + } +} diff --git a/services/webhook/matrix_test.go b/services/webhook/matrix_test.go index 99a22fbd7e..058f8e3c5f 100644 --- a/services/webhook/matrix_test.go +++ b/services/webhook/matrix_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -14,217 +17,213 @@ import ( ) func TestMatrixPayload(t *testing.T) { + mc := matrixConvertor{ + MsgType: "m.text", + } + t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(MatrixPayload) - pl, err := d.Create(p) + pl, err := mc.Create(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo:test] branch created by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.Body) + assert.Equal(t, `[test/repo:test] branch created by user1`, pl.FormattedBody) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(MatrixPayload) - pl, err := d.Delete(p) + pl, err := mc.Delete(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo:test] branch deleted by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.Body) + assert.Equal(t, `[test/repo:test] branch deleted by user1`, pl.FormattedBody) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(MatrixPayload) - pl, err := d.Fork(p) + pl, err := mc.Fork(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.(*MatrixPayload).Body) - assert.Equal(t, `test/repo2 is forked to test/repo`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.Body) + assert.Equal(t, `test/repo2 is forked to test/repo`, pl.FormattedBody) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(MatrixPayload) - pl, err := d.Push(p) + pl, err := mc.Push(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] user1 pushed 2 commits to test:
2020558: commit message - user1
2020558: commit message - user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", pl.Body) + assert.Equal(t, `[test/repo] user1 pushed 2 commits to test:
2020558: commit message - user1
2020558: commit message - user1`, pl.FormattedBody) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(MatrixPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := mc.Issue(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Issue opened: #2 crash by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Issue opened: #2 crash by user1`, pl.FormattedBody) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = mc.Issue(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.FormattedBody) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(MatrixPayload) - pl, err := d.IssueComment(p) + pl, err := mc.IssueComment(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] New comment on issue #2 crash by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] New comment on issue #2 crash by user1`, pl.FormattedBody) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(MatrixPayload) - pl, err := d.PullRequest(p) + pl, err := mc.PullRequest(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Pull request opened: #12 Fix bug by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Pull request opened: #12 Fix bug by user1`, pl.FormattedBody) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(MatrixPayload) - pl, err := d.IssueComment(p) + pl, err := mc.IssueComment(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] New comment on pull request #12 Fix bug by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] New comment on pull request #12 Fix bug by user1`, pl.FormattedBody) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(MatrixPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := mc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Pull request review approved: #12 Fix bug by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Pull request review approved: #12 Fix bug by user1`, pl.FormattedBody) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(MatrixPayload) - pl, err := d.Repository(p) + pl, err := mc.Repository(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Repository created by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.Body) + assert.Equal(t, `[test/repo] Repository created by user1`, pl.FormattedBody) }) t.Run("Package", func(t *testing.T) { p := packageTestPayload() - d := new(MatrixPayload) - pl, err := d.Package(p) + pl, err := mc.Package(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, `[[GiteaContainer](http://localhost:3000/user1/-/packages/container/GiteaContainer/latest)] Package published by [user1](https://try.gitea.io/user1)`, pl.(*MatrixPayload).Body) - assert.Equal(t, `[GiteaContainer] Package published by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, `[[GiteaContainer](http://localhost:3000/user1/-/packages/container/GiteaContainer/latest)] Package published by [user1](https://try.gitea.io/user1)`, pl.Body) + assert.Equal(t, `[GiteaContainer] Package published by user1`, pl.FormattedBody) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(MatrixPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := mc.Wiki(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New wiki page '[index](http://localhost:3000/test/repo/wiki/index)' (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New wiki page '[index](http://localhost:3000/test/repo/wiki/index)' (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.FormattedBody) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = mc.Wiki(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' edited (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' edited (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.FormattedBody) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = mc.Wiki(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' deleted by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' deleted by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.FormattedBody) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(MatrixPayload) - pl, err := d.Release(p) + pl, err := mc.Release(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/releases/tag/v1.0) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/releases/tag/v1.0) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.FormattedBody) }) } func TestMatrixJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(MatrixPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.MATRIX, + URL: "https://matrix.example.com/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message", + Meta: `{"message_type":0}`, // text + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newMatrixRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "PUT", req.Method) + assert.Equal(t, "/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message/6db5dc1e282529a8c162c7fe93dd2667494eeb51", req.URL.Path) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body MatrixPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", body.Body) } func Test_getTxnID(t *testing.T) { diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index 37810b4cd3..99d0106184 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -4,12 +4,14 @@ package webhook import ( + "context" "fmt" + "net/http" "net/url" "strings" + webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -56,19 +58,8 @@ type ( } ) -// JSONPayload Marshals the MSTeamsPayload to json -func (m *MSTeamsPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(m, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -var _ PayloadConvertor = &MSTeamsPayload{} - // Create implements PayloadConvertor Create method -func (m *MSTeamsPayload) Create(p *api.CreatePayload) (api.Payloader, error) { +func (m msteamsConvertor) Create(p *api.CreatePayload) (MSTeamsPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) @@ -85,7 +76,7 @@ func (m *MSTeamsPayload) Create(p *api.CreatePayload) (api.Payloader, error) { } // Delete implements PayloadConvertor Delete method -func (m *MSTeamsPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (m msteamsConvertor) Delete(p *api.DeletePayload) (MSTeamsPayload, error) { // deleted tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) @@ -102,7 +93,7 @@ func (m *MSTeamsPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { } // Fork implements PayloadConvertor Fork method -func (m *MSTeamsPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { +func (m msteamsConvertor) Fork(p *api.ForkPayload) (MSTeamsPayload, error) { title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) return createMSTeamsPayload( @@ -117,7 +108,7 @@ func (m *MSTeamsPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { } // Push implements PayloadConvertor Push method -func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) { +func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) { var ( branchName = git.RefName(p.Ref).ShortName() commitDesc string @@ -160,7 +151,7 @@ func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) { } // Issue implements PayloadConvertor Issue method -func (m *MSTeamsPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { +func (m msteamsConvertor) Issue(p *api.IssuePayload) (MSTeamsPayload, error) { title, _, attachmentText, color := getIssuesPayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -175,7 +166,7 @@ func (m *MSTeamsPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { } // IssueComment implements PayloadConvertor IssueComment method -func (m *MSTeamsPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { +func (m msteamsConvertor) IssueComment(p *api.IssueCommentPayload) (MSTeamsPayload, error) { title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -190,7 +181,7 @@ func (m *MSTeamsPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader } // PullRequest implements PayloadConvertor PullRequest method -func (m *MSTeamsPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { +func (m msteamsConvertor) PullRequest(p *api.PullRequestPayload) (MSTeamsPayload, error) { title, _, attachmentText, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -205,14 +196,14 @@ func (m *MSTeamsPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, } // Review implements PayloadConvertor Review method -func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { +func (m msteamsConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (MSTeamsPayload, error) { var text, title string var color int switch p.Action { case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return MSTeamsPayload{}, err } title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) @@ -242,7 +233,7 @@ func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_module. } // Repository implements PayloadConvertor Repository method -func (m *MSTeamsPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { +func (m msteamsConvertor) Repository(p *api.RepositoryPayload) (MSTeamsPayload, error) { var title, url string var color int switch p.Action { @@ -267,7 +258,7 @@ func (m *MSTeamsPayload) Repository(p *api.RepositoryPayload) (api.Payloader, er } // Wiki implements PayloadConvertor Wiki method -func (m *MSTeamsPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { +func (m msteamsConvertor) Wiki(p *api.WikiPayload) (MSTeamsPayload, error) { title, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -282,7 +273,7 @@ func (m *MSTeamsPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { } // Release implements PayloadConvertor Release method -func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { +func (m msteamsConvertor) Release(p *api.ReleasePayload) (MSTeamsPayload, error) { title, color := getReleasePayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -296,7 +287,7 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { ), nil } -func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) { +func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error) { title, color := getPackagePayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -310,12 +301,7 @@ func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) { ), nil } -// GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload -func GetMSTeamsPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) { - return convertPayloader(new(MSTeamsPayload), p, event) -} - -func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) *MSTeamsPayload { +func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload { facts := make([]MSTeamsFact, 0, 2) if r != nil { facts = append(facts, MSTeamsFact{ @@ -327,7 +313,7 @@ func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTar facts = append(facts, *fact) } - return &MSTeamsPayload{ + return MSTeamsPayload{ Type: "MessageCard", Context: "https://schema.org/extensions", ThemeColor: fmt.Sprintf("%x", color), @@ -356,3 +342,11 @@ func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTar }, } } + +type msteamsConvertor struct{} + +var _ payloadConvertor[MSTeamsPayload] = msteamsConvertor{} + +func newMSTeamsRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + return newJSONRequest(msteamsConvertor{}, w, t, true) +} diff --git a/services/webhook/msteams_test.go b/services/webhook/msteams_test.go index 8d1aed6040..01e08b918e 100644 --- a/services/webhook/msteams_test.go +++ b/services/webhook/msteams_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -14,22 +17,20 @@ import ( ) func TestMSTeamsPayload(t *testing.T) { + mc := msteamsConvertor{} t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Create(p) + pl, err := mc.Create(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] branch test created", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] branch test created", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] branch test created", pl.Title) + assert.Equal(t, "[test/repo] branch test created", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repo.FullName, fact.Value) } else if fact.Name == "branch:" { @@ -38,27 +39,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Delete(p) + pl, err := mc.Delete(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] branch test deleted", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] branch test deleted", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] branch test deleted", pl.Title) + assert.Equal(t, "[test/repo] branch test deleted", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repo.FullName, fact.Value) } else if fact.Name == "branch:" { @@ -67,27 +65,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Fork(p) + pl, err := mc.Fork(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "test/repo2 is forked to test/repo", pl.Title) + assert.Equal(t, "test/repo2 is forked to test/repo", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repo.FullName, fact.Value) } else if fact.Name == "Forkee:" { @@ -96,27 +91,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Push(p) + pl, err := mc.Push(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo:test] 2 new commits", pl.Title) + assert.Equal(t, "[test/repo:test] 2 new commits", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repo.FullName, fact.Value) } else if fact.Name == "Commit count:" { @@ -125,28 +117,25 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(MSTeamsPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := mc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "issue body", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Title) + assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "issue body", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Issue #:" { @@ -155,23 +144,21 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.PotentialAction[0].Targets[0].URI) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = mc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Title) + assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Issue #:" { @@ -180,27 +167,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.PotentialAction[0].Targets[0].URI) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(MSTeamsPayload) - pl, err := d.IssueComment(p) + pl, err := mc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "more info needed", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Title) + assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "more info needed", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Issue #:" { @@ -209,27 +193,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.PotentialAction[0].Targets[0].URI) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(MSTeamsPayload) - pl, err := d.PullRequest(p) + pl, err := mc.PullRequest(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "fixes bug #2", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Title) + assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "fixes bug #2", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Pull request #:" { @@ -238,27 +219,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(MSTeamsPayload) - pl, err := d.IssueComment(p) + pl, err := mc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "changes requested", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Title) + assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "changes requested", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Issue #:" { @@ -267,28 +245,25 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(MSTeamsPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := mc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "good job", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Title) + assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "good job", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Pull request #:" { @@ -297,155 +272,139 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Repository(p) + pl, err := mc.Repository(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] Repository created", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Repository created", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 1) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] Repository created", pl.Title) + assert.Equal(t, "[test/repo] Repository created", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 1) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Package", func(t *testing.T) { p := packageTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Package(p) + pl, err := mc.Package(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "Package created: GiteaContainer:latest", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "Package created: GiteaContainer:latest", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 1) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "Package created: GiteaContainer:latest", pl.Title) + assert.Equal(t, "Package created: GiteaContainer:latest", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 1) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Package:" { assert.Equal(t, p.Package.Name, fact.Value) } else { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(MSTeamsPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := mc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Title) + assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = mc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Title) + assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = mc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Title) + assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Release(p) + pl, err := mc.Release(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] Release created: v1.0", pl.Title) + assert.Equal(t, "[test/repo] Release created: v1.0", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Tag:" { @@ -454,21 +413,43 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.PotentialAction[0].Targets[0].URI) }) } func TestMSTeamsJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(MSTeamsPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.MSTEAMS, + URL: "https://msteams.example.com/", + Meta: ``, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newMSTeamsRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://msteams.example.com/", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body MSTeamsPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "[test/repo:test] 2 new commits", body.Summary) } diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go index 714a4c076e..7880d8b606 100644 --- a/services/webhook/packagist.go +++ b/services/webhook/packagist.go @@ -4,7 +4,9 @@ package webhook import ( - "errors" + "context" + "fmt" + "net/http" webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/json" @@ -38,84 +40,85 @@ func GetPackagistHook(w *webhook_model.Webhook) *PackagistMeta { return s } -// JSONPayload Marshals the PackagistPayload to json -func (f *PackagistPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(f, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -var _ PayloadConvertor = &PackagistPayload{} - // Create implements PayloadConvertor Create method -func (f *PackagistPayload) Create(_ *api.CreatePayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Create(_ *api.CreatePayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Delete implements PayloadConvertor Delete method -func (f *PackagistPayload) Delete(_ *api.DeletePayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Delete(_ *api.DeletePayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Fork implements PayloadConvertor Fork method -func (f *PackagistPayload) Fork(_ *api.ForkPayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Fork(_ *api.ForkPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Push implements PayloadConvertor Push method -func (f *PackagistPayload) Push(_ *api.PushPayload) (api.Payloader, error) { - return f, nil +// https://packagist.org/about +func (pc packagistConvertor) Push(_ *api.PushPayload) (PackagistPayload, error) { + return PackagistPayload{ + PackagistRepository: struct { + URL string `json:"url"` + }{ + URL: pc.PackageURL, + }, + }, nil } // Issue implements PayloadConvertor Issue method -func (f *PackagistPayload) Issue(_ *api.IssuePayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Issue(_ *api.IssuePayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // IssueComment implements PayloadConvertor IssueComment method -func (f *PackagistPayload) IssueComment(_ *api.IssueCommentPayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) IssueComment(_ *api.IssueCommentPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // PullRequest implements PayloadConvertor PullRequest method -func (f *PackagistPayload) PullRequest(_ *api.PullRequestPayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) PullRequest(_ *api.PullRequestPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Review implements PayloadConvertor Review method -func (f *PackagistPayload) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Repository implements PayloadConvertor Repository method -func (f *PackagistPayload) Repository(_ *api.RepositoryPayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Repository(_ *api.RepositoryPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Wiki implements PayloadConvertor Wiki method -func (f *PackagistPayload) Wiki(_ *api.WikiPayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Wiki(_ *api.WikiPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Release implements PayloadConvertor Release method -func (f *PackagistPayload) Release(_ *api.ReleasePayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Release(_ *api.ReleasePayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } -func (f *PackagistPayload) Package(_ *api.PackagePayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Package(_ *api.PackagePayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } -// GetPackagistPayload converts a packagist webhook into a PackagistPayload -func GetPackagistPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) { - s := new(PackagistPayload) +type packagistConvertor struct { + PackageURL string +} - packagist := &PackagistMeta{} - if err := json.Unmarshal([]byte(meta), &packagist); err != nil { - return s, errors.New("GetPackagistPayload meta json:" + err.Error()) +var _ payloadConvertor[PackagistPayload] = packagistConvertor{} + +func newPackagistRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + meta := &PackagistMeta{} + if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { + return nil, nil, fmt.Errorf("newpackagistRequest meta json: %w", err) } - s.PackagistRepository.URL = packagist.PackageURL - return convertPayloader(s, p, event) + pc := packagistConvertor{ + PackageURL: meta.PackageURL, + } + return newJSONRequest(pc, w, t, true) } diff --git a/services/webhook/packagist_test.go b/services/webhook/packagist_test.go index 26d01b0555..e9b0695baa 100644 --- a/services/webhook/packagist_test.go +++ b/services/webhook/packagist_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -14,155 +17,199 @@ import ( ) func TestPackagistPayload(t *testing.T) { + pc := packagistConvertor{ + PackageURL: "https://packagist.org/packages/example", + } t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(PackagistPayload) - pl, err := d.Create(p) + pl, err := pc.Create(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(PackagistPayload) - pl, err := d.Delete(p) + pl, err := pc.Delete(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(PackagistPayload) - pl, err := d.Fork(p) + pl, err := pc.Fork(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(PackagistPayload) - d.PackagistRepository.URL = "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN" - pl, err := d.Push(p) + pl, err := pc.Push(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &PackagistPayload{}, pl) - assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", pl.(*PackagistPayload).PackagistRepository.URL) + assert.Equal(t, "https://packagist.org/packages/example", pl.PackagistRepository.URL) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(PackagistPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := pc.Issue(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = pc.Issue(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(PackagistPayload) - pl, err := d.IssueComment(p) + pl, err := pc.IssueComment(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(PackagistPayload) - pl, err := d.PullRequest(p) + pl, err := pc.PullRequest(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(PackagistPayload) - pl, err := d.IssueComment(p) + pl, err := pc.IssueComment(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(PackagistPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := pc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(PackagistPayload) - pl, err := d.Repository(p) + pl, err := pc.Repository(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Package", func(t *testing.T) { p := packageTestPayload() - d := new(PackagistPayload) - pl, err := d.Package(p) + pl, err := pc.Package(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(PackagistPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := pc.Wiki(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = pc.Wiki(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = pc.Wiki(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(PackagistPayload) - pl, err := d.Release(p) + pl, err := pc.Release(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) } func TestPackagistJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(PackagistPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &PackagistPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.PACKAGIST, + URL: "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", + Meta: `{"package_url":"https://packagist.org/packages/example"}`, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newPackagistRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body PackagistPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "https://packagist.org/packages/example", body.PackagistRepository.URL) +} + +func TestPackagistEmptyPayload(t *testing.T) { + p := createTestPayload() + data, err := p.JSONPayload() + require.NoError(t, err) + + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.PACKAGIST, + URL: "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", + Meta: `{"package_url":"https://packagist.org/packages/example"}`, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventCreate, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newPackagistRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) + require.NoError(t, err) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body PackagistPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "", body.PackagistRepository.URL) } diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go index bd482c04ea..54a11a5868 100644 --- a/services/webhook/payloader.go +++ b/services/webhook/payloader.go @@ -4,58 +4,109 @@ package webhook import ( + "bytes" + "fmt" + "net/http" + + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" ) -// PayloadConvertor defines the interface to convert system webhook payload to external payload -type PayloadConvertor interface { - api.Payloader - Create(*api.CreatePayload) (api.Payloader, error) - Delete(*api.DeletePayload) (api.Payloader, error) - Fork(*api.ForkPayload) (api.Payloader, error) - Issue(*api.IssuePayload) (api.Payloader, error) - IssueComment(*api.IssueCommentPayload) (api.Payloader, error) - Push(*api.PushPayload) (api.Payloader, error) - PullRequest(*api.PullRequestPayload) (api.Payloader, error) - Review(*api.PullRequestPayload, webhook_module.HookEventType) (api.Payloader, error) - Repository(*api.RepositoryPayload) (api.Payloader, error) - Release(*api.ReleasePayload) (api.Payloader, error) - Wiki(*api.WikiPayload) (api.Payloader, error) - Package(*api.PackagePayload) (api.Payloader, error) +// payloadConvertor defines the interface to convert system payload to webhook payload +type payloadConvertor[T any] interface { + Create(*api.CreatePayload) (T, error) + Delete(*api.DeletePayload) (T, error) + Fork(*api.ForkPayload) (T, error) + Issue(*api.IssuePayload) (T, error) + IssueComment(*api.IssueCommentPayload) (T, error) + Push(*api.PushPayload) (T, error) + PullRequest(*api.PullRequestPayload) (T, error) + Review(*api.PullRequestPayload, webhook_module.HookEventType) (T, error) + Repository(*api.RepositoryPayload) (T, error) + Release(*api.ReleasePayload) (T, error) + Wiki(*api.WikiPayload) (T, error) + Package(*api.PackagePayload) (T, error) } -func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.HookEventType) (api.Payloader, error) { +func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (T, error) { + var p P + if err := json.Unmarshal(data, &p); err != nil { + var t T + return t, fmt.Errorf("could not unmarshal payload: %w", err) + } + return convert(p) +} + +func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module.HookEventType) (T, error) { switch event { case webhook_module.HookEventCreate: - return s.Create(p.(*api.CreatePayload)) + return convertUnmarshalledJSON(rc.Create, data) case webhook_module.HookEventDelete: - return s.Delete(p.(*api.DeletePayload)) + return convertUnmarshalledJSON(rc.Delete, data) case webhook_module.HookEventFork: - return s.Fork(p.(*api.ForkPayload)) + return convertUnmarshalledJSON(rc.Fork, data) case webhook_module.HookEventIssues, webhook_module.HookEventIssueAssign, webhook_module.HookEventIssueLabel, webhook_module.HookEventIssueMilestone: - return s.Issue(p.(*api.IssuePayload)) + return convertUnmarshalledJSON(rc.Issue, data) case webhook_module.HookEventIssueComment, webhook_module.HookEventPullRequestComment: - pl, ok := p.(*api.IssueCommentPayload) - if ok { - return s.IssueComment(pl) - } - return s.PullRequest(p.(*api.PullRequestPayload)) + // previous code sometimes sent s.PullRequest(p.(*api.PullRequestPayload)) + // however I couldn't find in notifier.go such a payload with an HookEvent***Comment event + + // History (most recent first): + // - refactored in https://github.com/go-gitea/gitea/pull/12310 + // - assertion added in https://github.com/go-gitea/gitea/pull/12046 + // - issue raised in https://github.com/go-gitea/gitea/issues/11940#issuecomment-645713996 + // > That's because for HookEventPullRequestComment event, some places use IssueCommentPayload and others use PullRequestPayload + + // In modules/actions/workflows.go:183 the type assertion is always payload.(*api.IssueCommentPayload) + return convertUnmarshalledJSON(rc.IssueComment, data) case webhook_module.HookEventPush: - return s.Push(p.(*api.PushPayload)) + return convertUnmarshalledJSON(rc.Push, data) case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestLabel, webhook_module.HookEventPullRequestMilestone, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestReviewRequest: - return s.PullRequest(p.(*api.PullRequestPayload)) + return convertUnmarshalledJSON(rc.PullRequest, data) case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment: - return s.Review(p.(*api.PullRequestPayload), event) + return convertUnmarshalledJSON(func(p *api.PullRequestPayload) (T, error) { + return rc.Review(p, event) + }, data) case webhook_module.HookEventRepository: - return s.Repository(p.(*api.RepositoryPayload)) + return convertUnmarshalledJSON(rc.Repository, data) case webhook_module.HookEventRelease: - return s.Release(p.(*api.ReleasePayload)) + return convertUnmarshalledJSON(rc.Release, data) case webhook_module.HookEventWiki: - return s.Wiki(p.(*api.WikiPayload)) + return convertUnmarshalledJSON(rc.Wiki, data) case webhook_module.HookEventPackage: - return s.Package(p.(*api.PackagePayload)) + return convertUnmarshalledJSON(rc.Package, data) } - return s, nil + var t T + return t, fmt.Errorf("newPayload unsupported event: %s", event) +} + +func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) { + payload, err := newPayload(pc, []byte(t.PayloadContent), t.EventType) + if err != nil { + return nil, nil, err + } + + body, err := json.MarshalIndent(payload, "", " ") + if err != nil { + return nil, nil, err + } + + method := w.HTTPMethod + if method == "" { + method = http.MethodPost + } + + req, err := http.NewRequest(method, w.URL, bytes.NewReader(body)) + if err != nil { + return nil, nil, err + } + req.Header.Set("Content-Type", "application/json") + + if withDefaultHeaders { + return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) + } + return req, body, nil } diff --git a/services/webhook/slack.go b/services/webhook/slack.go index 945b0662d8..ba8bac27d9 100644 --- a/services/webhook/slack.go +++ b/services/webhook/slack.go @@ -4,8 +4,9 @@ package webhook import ( - "errors" + "context" "fmt" + "net/http" "regexp" "strings" @@ -39,7 +40,6 @@ func GetSlackHook(w *webhook_model.Webhook) *SlackMeta { type SlackPayload struct { Channel string `json:"channel"` Text string `json:"text"` - Color string `json:"-"` Username string `json:"username"` IconURL string `json:"icon_url"` UnfurlLinks int `json:"unfurl_links"` @@ -56,15 +56,6 @@ type SlackAttachment struct { Text string `json:"text"` } -// JSONPayload Marshals the SlackPayload to json -func (s *SlackPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(s, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - // SlackTextFormatter replaces &, <, > with HTML characters // see: https://api.slack.com/docs/formatting func SlackTextFormatter(s string) string { @@ -98,10 +89,8 @@ func SlackLinkToRef(repoURL, ref string) string { return SlackLinkFormatter(url, refName) } -var _ PayloadConvertor = &SlackPayload{} - -// Create implements PayloadConvertor Create method -func (s *SlackPayload) Create(p *api.CreatePayload) (api.Payloader, error) { +// Create implements payloadConvertor Create method +func (s slackConvertor) Create(p *api.CreatePayload) (SlackPayload, error) { repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) refLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref) text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName) @@ -110,7 +99,7 @@ func (s *SlackPayload) Create(p *api.CreatePayload) (api.Payloader, error) { } // Delete composes Slack payload for delete a branch or tag. -func (s *SlackPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (s slackConvertor) Delete(p *api.DeletePayload) (SlackPayload, error) { refName := git.RefName(p.Ref).ShortName() repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName) @@ -119,7 +108,7 @@ func (s *SlackPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { } // Fork composes Slack payload for forked by a repository. -func (s *SlackPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { +func (s slackConvertor) Fork(p *api.ForkPayload) (SlackPayload, error) { baseLink := SlackLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName) forkLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink) @@ -127,8 +116,8 @@ func (s *SlackPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { return s.createPayload(text, nil), nil } -// Issue implements PayloadConvertor Issue method -func (s *SlackPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { +// Issue implements payloadConvertor Issue method +func (s slackConvertor) Issue(p *api.IssuePayload) (SlackPayload, error) { text, issueTitle, attachmentText, color := getIssuesPayloadInfo(p, SlackLinkFormatter, true) var attachments []SlackAttachment @@ -146,8 +135,8 @@ func (s *SlackPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { return s.createPayload(text, attachments), nil } -// IssueComment implements PayloadConvertor IssueComment method -func (s *SlackPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { +// IssueComment implements payloadConvertor IssueComment method +func (s slackConvertor) IssueComment(p *api.IssueCommentPayload) (SlackPayload, error) { text, issueTitle, color := getIssueCommentPayloadInfo(p, SlackLinkFormatter, true) return s.createPayload(text, []SlackAttachment{{ @@ -158,28 +147,28 @@ func (s *SlackPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, }}), nil } -// Wiki implements PayloadConvertor Wiki method -func (s *SlackPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { +// Wiki implements payloadConvertor Wiki method +func (s slackConvertor) Wiki(p *api.WikiPayload) (SlackPayload, error) { text, _, _ := getWikiPayloadInfo(p, SlackLinkFormatter, true) return s.createPayload(text, nil), nil } -// Release implements PayloadConvertor Release method -func (s *SlackPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { +// Release implements payloadConvertor Release method +func (s slackConvertor) Release(p *api.ReleasePayload) (SlackPayload, error) { text, _ := getReleasePayloadInfo(p, SlackLinkFormatter, true) return s.createPayload(text, nil), nil } -func (s *SlackPayload) Package(p *api.PackagePayload) (api.Payloader, error) { +func (s slackConvertor) Package(p *api.PackagePayload) (SlackPayload, error) { text, _ := getPackagePayloadInfo(p, SlackLinkFormatter, true) return s.createPayload(text, nil), nil } -// Push implements PayloadConvertor Push method -func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) { +// Push implements payloadConvertor Push method +func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) { // n new commits var ( commitDesc string @@ -219,8 +208,8 @@ func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) { }}), nil } -// PullRequest implements PayloadConvertor PullRequest method -func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { +// PullRequest implements payloadConvertor PullRequest method +func (s slackConvertor) PullRequest(p *api.PullRequestPayload) (SlackPayload, error) { text, issueTitle, attachmentText, color := getPullRequestPayloadInfo(p, SlackLinkFormatter, true) var attachments []SlackAttachment @@ -238,8 +227,8 @@ func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, er return s.createPayload(text, attachments), nil } -// Review implements PayloadConvertor Review method -func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { +// Review implements payloadConvertor Review method +func (s slackConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (SlackPayload, error) { senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title) titleLink := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index) @@ -250,7 +239,7 @@ func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.Ho case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return SlackPayload{}, err } text = fmt.Sprintf("[%s] Pull request review %s: [%s](%s) by %s", repoLink, action, title, titleLink, senderLink) @@ -259,8 +248,8 @@ func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.Ho return s.createPayload(text, nil), nil } -// Repository implements PayloadConvertor Repository method -func (s *SlackPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { +// Repository implements payloadConvertor Repository method +func (s slackConvertor) Repository(p *api.RepositoryPayload) (SlackPayload, error) { senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) repoLink := SlackLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) var text string @@ -275,8 +264,8 @@ func (s *SlackPayload) Repository(p *api.RepositoryPayload) (api.Payloader, erro return s.createPayload(text, nil), nil } -func (s *SlackPayload) createPayload(text string, attachments []SlackAttachment) *SlackPayload { - return &SlackPayload{ +func (s slackConvertor) createPayload(text string, attachments []SlackAttachment) SlackPayload { + return SlackPayload{ Channel: s.Channel, Text: text, Username: s.Username, @@ -285,21 +274,27 @@ func (s *SlackPayload) createPayload(text string, attachments []SlackAttachment) } } -// GetSlackPayload converts a slack webhook into a SlackPayload -func GetSlackPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) { - s := new(SlackPayload) +type slackConvertor struct { + Channel string + Username string + IconURL string + Color string +} - slack := &SlackMeta{} - if err := json.Unmarshal([]byte(meta), &slack); err != nil { - return s, errors.New("GetSlackPayload meta json:" + err.Error()) +var _ payloadConvertor[SlackPayload] = slackConvertor{} + +func newSlackRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + meta := &SlackMeta{} + if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { + return nil, nil, fmt.Errorf("newSlackRequest meta json: %w", err) } - - s.Channel = slack.Channel - s.Username = slack.Username - s.IconURL = slack.IconURL - s.Color = slack.Color - - return convertPayloader(s, p, event) + sc := slackConvertor{ + Channel: meta.Channel, + Username: meta.Username, + IconURL: meta.IconURL, + Color: meta.Color, + } + return newJSONRequest(sc, w, t, true) } var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`) diff --git a/services/webhook/slack_test.go b/services/webhook/slack_test.go index b1340963e2..7ebf16aba2 100644 --- a/services/webhook/slack_test.go +++ b/services/webhook/slack_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -14,201 +17,180 @@ import ( ) func TestSlackPayload(t *testing.T) { + sc := slackConvertor{} + t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(SlackPayload) - pl, err := d.Create(p) + pl, err := sc.Create(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[:] branch created by user1", pl.(*SlackPayload).Text) + assert.Equal(t, "[:] branch created by user1", pl.Text) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(SlackPayload) - pl, err := d.Delete(p) + pl, err := sc.Delete(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[:test] branch deleted by user1", pl.(*SlackPayload).Text) + assert.Equal(t, "[:test] branch deleted by user1", pl.Text) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(SlackPayload) - pl, err := d.Fork(p) + pl, err := sc.Fork(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, " is forked to ", pl.(*SlackPayload).Text) + assert.Equal(t, " is forked to ", pl.Text) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(SlackPayload) - pl, err := d.Push(p) + pl, err := sc.Push(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[:] 2 new commits pushed by user1", pl.(*SlackPayload).Text) + assert.Equal(t, "[:] 2 new commits pushed by user1", pl.Text) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(SlackPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := sc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] Issue opened: by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] Issue opened: by ", pl.Text) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = sc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] Issue closed: by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] Issue closed: by ", pl.Text) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(SlackPayload) - pl, err := d.IssueComment(p) + pl, err := sc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] New comment on issue by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] New comment on issue by ", pl.Text) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(SlackPayload) - pl, err := d.PullRequest(p) + pl, err := sc.PullRequest(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] Pull request opened: by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] Pull request opened: by ", pl.Text) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(SlackPayload) - pl, err := d.IssueComment(p) + pl, err := sc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] New comment on pull request by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] New comment on pull request by ", pl.Text) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(SlackPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := sc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by ", pl.Text) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(SlackPayload) - pl, err := d.Repository(p) + pl, err := sc.Repository(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] Repository created by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] Repository created by ", pl.Text) }) t.Run("Package", func(t *testing.T) { p := packageTestPayload() - d := new(SlackPayload) - pl, err := d.Package(p) + pl, err := sc.Package(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "Package created: by ", pl.(*SlackPayload).Text) + assert.Equal(t, "Package created: by ", pl.Text) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(SlackPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := sc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] New wiki page '' (Wiki change comment) by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] New wiki page '' (Wiki change comment) by ", pl.Text) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = sc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] Wiki page '' edited (Wiki change comment) by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] Wiki page '' edited (Wiki change comment) by ", pl.Text) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = sc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] Wiki page '' deleted by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] Wiki page '' deleted by ", pl.Text) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(SlackPayload) - pl, err := d.Release(p) + pl, err := sc.Release(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] Release created: by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] Release created: by ", pl.Text) }) } func TestSlackJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(SlackPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.SLACK, + URL: "https://slack.example.com/", + Meta: `{}`, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newSlackRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://slack.example.com/", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body SlackPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "[:] 2 new commits pushed by user1", body.Text) } func TestIsValidSlackChannel(t *testing.T) { diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index 1bdc74e183..e4a5b5a424 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -4,14 +4,15 @@ package webhook import ( + "context" "fmt" + "net/http" "strings" webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/markup" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" ) @@ -41,22 +42,8 @@ func GetTelegramHook(w *webhook_model.Webhook) *TelegramMeta { return s } -var _ PayloadConvertor = &TelegramPayload{} - -// JSONPayload Marshals the TelegramPayload to json -func (t *TelegramPayload) JSONPayload() ([]byte, error) { - t.ParseMode = "HTML" - t.DisableWebPreview = true - t.Message = markup.Sanitize(t.Message) - data, err := json.MarshalIndent(t, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - // Create implements PayloadConvertor Create method -func (t *TelegramPayload) Create(p *api.CreatePayload) (api.Payloader, error) { +func (t telegramConvertor) Create(p *api.CreatePayload) (TelegramPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf(`[%s] %s %s created`, p.Repo.HTMLURL, p.Repo.FullName, p.RefType, @@ -66,7 +53,7 @@ func (t *TelegramPayload) Create(p *api.CreatePayload) (api.Payloader, error) { } // Delete implements PayloadConvertor Delete method -func (t *TelegramPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (t telegramConvertor) Delete(p *api.DeletePayload) (TelegramPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf(`[%s] %s %s deleted`, p.Repo.HTMLURL, p.Repo.FullName, p.RefType, @@ -76,14 +63,14 @@ func (t *TelegramPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { } // Fork implements PayloadConvertor Fork method -func (t *TelegramPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { +func (t telegramConvertor) Fork(p *api.ForkPayload) (TelegramPayload, error) { title := fmt.Sprintf(`%s is forked to %s`, p.Forkee.FullName, p.Repo.HTMLURL, p.Repo.FullName) return createTelegramPayload(title), nil } // Push implements PayloadConvertor Push method -func (t *TelegramPayload) Push(p *api.PushPayload) (api.Payloader, error) { +func (t telegramConvertor) Push(p *api.PushPayload) (TelegramPayload, error) { var ( branchName = git.RefName(p.Ref).ShortName() commitDesc string @@ -121,34 +108,34 @@ func (t *TelegramPayload) Push(p *api.PushPayload) (api.Payloader, error) { } // Issue implements PayloadConvertor Issue method -func (t *TelegramPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { +func (t telegramConvertor) Issue(p *api.IssuePayload) (TelegramPayload, error) { text, _, attachmentText, _ := getIssuesPayloadInfo(p, htmlLinkFormatter, true) return createTelegramPayload(text + "\n\n" + attachmentText), nil } // IssueComment implements PayloadConvertor IssueComment method -func (t *TelegramPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { +func (t telegramConvertor) IssueComment(p *api.IssueCommentPayload) (TelegramPayload, error) { text, _, _ := getIssueCommentPayloadInfo(p, htmlLinkFormatter, true) return createTelegramPayload(text + "\n" + p.Comment.Body), nil } // PullRequest implements PayloadConvertor PullRequest method -func (t *TelegramPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { +func (t telegramConvertor) PullRequest(p *api.PullRequestPayload) (TelegramPayload, error) { text, _, attachmentText, _ := getPullRequestPayloadInfo(p, htmlLinkFormatter, true) return createTelegramPayload(text + "\n" + attachmentText), nil } // Review implements PayloadConvertor Review method -func (t *TelegramPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { +func (t telegramConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (TelegramPayload, error) { var text, attachmentText string switch p.Action { case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return TelegramPayload{}, err } text = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) @@ -159,7 +146,7 @@ func (t *TelegramPayload) Review(p *api.PullRequestPayload, event webhook_module } // Repository implements PayloadConvertor Repository method -func (t *TelegramPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { +func (t telegramConvertor) Repository(p *api.RepositoryPayload) (TelegramPayload, error) { var title string switch p.Action { case api.HookRepoCreated: @@ -169,36 +156,39 @@ func (t *TelegramPayload) Repository(p *api.RepositoryPayload) (api.Payloader, e title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) return createTelegramPayload(title), nil } - return nil, nil + return TelegramPayload{}, nil } // Wiki implements PayloadConvertor Wiki method -func (t *TelegramPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { +func (t telegramConvertor) Wiki(p *api.WikiPayload) (TelegramPayload, error) { text, _, _ := getWikiPayloadInfo(p, htmlLinkFormatter, true) return createTelegramPayload(text), nil } // Release implements PayloadConvertor Release method -func (t *TelegramPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { +func (t telegramConvertor) Release(p *api.ReleasePayload) (TelegramPayload, error) { text, _ := getReleasePayloadInfo(p, htmlLinkFormatter, true) return createTelegramPayload(text), nil } -func (t *TelegramPayload) Package(p *api.PackagePayload) (api.Payloader, error) { +func (t telegramConvertor) Package(p *api.PackagePayload) (TelegramPayload, error) { text, _ := getPackagePayloadInfo(p, htmlLinkFormatter, true) return createTelegramPayload(text), nil } -// GetTelegramPayload converts a telegram webhook into a TelegramPayload -func GetTelegramPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) { - return convertPayloader(new(TelegramPayload), p, event) -} - -func createTelegramPayload(message string) *TelegramPayload { - return &TelegramPayload{ +func createTelegramPayload(message string) TelegramPayload { + return TelegramPayload{ Message: strings.TrimSpace(message), } } + +type telegramConvertor struct{} + +var _ payloadConvertor[TelegramPayload] = telegramConvertor{} + +func newTelegramRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + return newJSONRequest(telegramConvertor{}, w, t, true) +} diff --git a/services/webhook/telegram_test.go b/services/webhook/telegram_test.go index 5b9927d057..27ab96cd09 100644 --- a/services/webhook/telegram_test.go +++ b/services/webhook/telegram_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -14,199 +17,177 @@ import ( ) func TestTelegramPayload(t *testing.T) { + tc := telegramConvertor{} t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(TelegramPayload) - pl, err := d.Create(p) + pl, err := tc.Create(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `[test/repo] branch test created`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[test/repo] branch test created`, pl.Message) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(TelegramPayload) - pl, err := d.Delete(p) + pl, err := tc.Delete(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `[test/repo] branch test deleted`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[test/repo] branch test deleted`, pl.Message) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(TelegramPayload) - pl, err := d.Fork(p) + pl, err := tc.Fork(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `test/repo2 is forked to test/repo`, pl.(*TelegramPayload).Message) + assert.Equal(t, `test/repo2 is forked to test/repo`, pl.Message) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(TelegramPayload) - pl, err := d.Push(p) + pl, err := tc.Push(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, "[test/repo:test] 2 new commits\n[2020558] commit message - user1\n[2020558] commit message - user1", pl.(*TelegramPayload).Message) + assert.Equal(t, "[test/repo:test] 2 new commits\n[2020558] commit message - user1\n[2020558] commit message - user1", pl.Message) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(TelegramPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := tc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\n\nissue body", pl.(*TelegramPayload).Message) + assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\n\nissue body", pl.Message) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = tc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.Message) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(TelegramPayload) - pl, err := d.IssueComment(p) + pl, err := tc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\nmore info needed", pl.(*TelegramPayload).Message) + assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\nmore info needed", pl.Message) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(TelegramPayload) - pl, err := d.PullRequest(p) + pl, err := tc.PullRequest(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\nfixes bug #2", pl.(*TelegramPayload).Message) + assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\nfixes bug #2", pl.Message) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(TelegramPayload) - pl, err := d.IssueComment(p) + pl, err := tc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\nchanges requested", pl.(*TelegramPayload).Message) + assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\nchanges requested", pl.Message) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(TelegramPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := tc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug\ngood job", pl.(*TelegramPayload).Message) + assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug\ngood job", pl.Message) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(TelegramPayload) - pl, err := d.Repository(p) + pl, err := tc.Repository(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `[test/repo] Repository created`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[test/repo] Repository created`, pl.Message) }) t.Run("Package", func(t *testing.T) { p := packageTestPayload() - d := new(TelegramPayload) - pl, err := d.Package(p) + pl, err := tc.Package(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `Package created: GiteaContainer:latest by user1`, pl.(*TelegramPayload).Message) + assert.Equal(t, `Package created: GiteaContainer:latest by user1`, pl.Message) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(TelegramPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := tc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.Message) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = tc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.Message) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = tc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.Message) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(TelegramPayload) - pl, err := d.Release(p) + pl, err := tc.Release(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.Message) }) } func TestTelegramJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(TelegramPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.TELEGRAM, + URL: "https://telegram.example.com/", + Meta: ``, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newTelegramRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://telegram.example.com/", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body TelegramPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "[test/repo:test] 2 new commits\n[2020558] commit message - user1\n[2020558] commit message - user1", body.Message) } diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index 996942d1e5..e6646501da 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "net/http" "strings" "code.gitea.io/gitea/models/db" @@ -26,48 +27,16 @@ import ( "github.com/gobwas/glob" ) -type webhook struct { - name webhook_module.HookType - payloadCreator func(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) -} - -var webhooks = map[webhook_module.HookType]*webhook{ - webhook_module.SLACK: { - name: webhook_module.SLACK, - payloadCreator: GetSlackPayload, - }, - webhook_module.DISCORD: { - name: webhook_module.DISCORD, - payloadCreator: GetDiscordPayload, - }, - webhook_module.DINGTALK: { - name: webhook_module.DINGTALK, - payloadCreator: GetDingtalkPayload, - }, - webhook_module.TELEGRAM: { - name: webhook_module.TELEGRAM, - payloadCreator: GetTelegramPayload, - }, - webhook_module.MSTEAMS: { - name: webhook_module.MSTEAMS, - payloadCreator: GetMSTeamsPayload, - }, - webhook_module.FEISHU: { - name: webhook_module.FEISHU, - payloadCreator: GetFeishuPayload, - }, - webhook_module.MATRIX: { - name: webhook_module.MATRIX, - payloadCreator: GetMatrixPayload, - }, - webhook_module.WECHATWORK: { - name: webhook_module.WECHATWORK, - payloadCreator: GetWechatworkPayload, - }, - webhook_module.PACKAGIST: { - name: webhook_module.PACKAGIST, - payloadCreator: GetPackagistPayload, - }, +var webhookRequesters = map[webhook_module.HookType]func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error){ + webhook_module.SLACK: newSlackRequest, + webhook_module.DISCORD: newDiscordRequest, + webhook_module.DINGTALK: newDingtalkRequest, + webhook_module.TELEGRAM: newTelegramRequest, + webhook_module.MSTEAMS: newMSTeamsRequest, + webhook_module.FEISHU: newFeishuRequest, + webhook_module.MATRIX: newMatrixRequest, + webhook_module.WECHATWORK: newWechatworkRequest, + webhook_module.PACKAGIST: newPackagistRequest, } // IsValidHookTaskType returns true if a webhook registered @@ -75,7 +44,7 @@ func IsValidHookTaskType(name string) bool { if name == webhook_module.FORGEJO || name == webhook_module.GITEA || name == webhook_module.GOGS { return true } - _, ok := webhooks[name] + _, ok := webhookRequesters[name] return ok } @@ -159,7 +128,9 @@ func checkBranch(w *webhook_model.Webhook, branch string) bool { return g.Match(branch) } -// PrepareWebhook creates a hook task and enqueues it for processing +// PrepareWebhook creates a hook task and enqueues it for processing. +// The payload is saved as-is. The adjustments depending on the webhook type happen +// right before delivery, in the [Deliver] method. func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook_module.HookEventType, p api.Payloader) error { // Skip sending if webhooks are disabled. if setting.DisableWebhooks { @@ -193,25 +164,19 @@ func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook } } - var payloader api.Payloader - var err error - webhook, ok := webhooks[w.Type] - if ok { - payloader, err = webhook.payloadCreator(p, event, w.Meta) - if err != nil { - return fmt.Errorf("create payload for %s[%s]: %w", w.Type, event, err) - } - } else { - payloader = p + payload, err := p.JSONPayload() + if err != nil { + return fmt.Errorf("JSONPayload for %s: %w", event, err) } task, err := webhook_model.CreateHookTask(ctx, &webhook_model.HookTask{ - HookID: w.ID, - Payloader: payloader, - EventType: event, + HookID: w.ID, + PayloadContent: string(payload), + EventType: event, + PayloadVersion: 2, }) if err != nil { - return fmt.Errorf("CreateHookTask: %w", err) + return fmt.Errorf("CreateHookTask for %s: %w", event, err) } return enqueueHookTask(task.ID) diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go index 338b94360b..5f5c146232 100644 --- a/services/webhook/webhook_test.go +++ b/services/webhook/webhook_test.go @@ -77,7 +77,3 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) { unittest.AssertNotExistsBean(t, hookTask) } } - -// TODO TestHookTask_deliver - -// TODO TestDeliverHooks diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go index 80245c7e77..46e7856ecf 100644 --- a/services/webhook/wechatwork.go +++ b/services/webhook/wechatwork.go @@ -4,11 +4,13 @@ package webhook import ( + "context" "fmt" + "net/http" "strings" + webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" ) @@ -28,20 +30,8 @@ type ( } ) -// SetSecret sets the Wechatwork secret -func (f *WechatworkPayload) SetSecret(_ string) {} - -// JSONPayload Marshals the WechatworkPayload to json -func (f *WechatworkPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(f, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -func newWechatworkMarkdownPayload(title string) *WechatworkPayload { - return &WechatworkPayload{ +func newWechatworkMarkdownPayload(title string) WechatworkPayload { + return WechatworkPayload{ Msgtype: "markdown", Markdown: struct { Content string `json:"content"` @@ -51,10 +41,8 @@ func newWechatworkMarkdownPayload(title string) *WechatworkPayload { } } -var _ PayloadConvertor = &WechatworkPayload{} - // Create implements PayloadConvertor Create method -func (f *WechatworkPayload) Create(p *api.CreatePayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Create(p *api.CreatePayload) (WechatworkPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) @@ -63,7 +51,7 @@ func (f *WechatworkPayload) Create(p *api.CreatePayload) (api.Payloader, error) } // Delete implements PayloadConvertor Delete method -func (f *WechatworkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Delete(p *api.DeletePayload) (WechatworkPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) @@ -72,14 +60,14 @@ func (f *WechatworkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) } // Fork implements PayloadConvertor Fork method -func (f *WechatworkPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Fork(p *api.ForkPayload) (WechatworkPayload, error) { title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) return newWechatworkMarkdownPayload(title), nil } // Push implements PayloadConvertor Push method -func (f *WechatworkPayload) Push(p *api.PushPayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Push(p *api.PushPayload) (WechatworkPayload, error) { var ( branchName = git.RefName(p.Ref).ShortName() commitDesc string @@ -108,7 +96,7 @@ func (f *WechatworkPayload) Push(p *api.PushPayload) (api.Payloader, error) { } // Issue implements PayloadConvertor Issue method -func (f *WechatworkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Issue(p *api.IssuePayload) (WechatworkPayload, error) { text, issueTitle, attachmentText, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true) var content string content += fmt.Sprintf(" >%s\n >%s \n > %s \n [%s](%s)", text, attachmentText, issueTitle, p.Issue.HTMLURL, p.Issue.HTMLURL) @@ -117,7 +105,7 @@ func (f *WechatworkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { } // IssueComment implements PayloadConvertor IssueComment method -func (f *WechatworkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { +func (wc wechatworkConvertor) IssueComment(p *api.IssueCommentPayload) (WechatworkPayload, error) { text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true) var content string content += fmt.Sprintf(" >%s\n >%s \n >%s \n [%s](%s)", text, p.Comment.Body, issueTitle, p.Comment.HTMLURL, p.Comment.HTMLURL) @@ -126,7 +114,7 @@ func (f *WechatworkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloa } // PullRequest implements PayloadConvertor PullRequest method -func (f *WechatworkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { +func (wc wechatworkConvertor) PullRequest(p *api.PullRequestPayload) (WechatworkPayload, error) { text, issueTitle, attachmentText, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true) pr := fmt.Sprintf("> %s \r\n > %s \r\n > %s \r\n", text, issueTitle, attachmentText) @@ -135,13 +123,13 @@ func (f *WechatworkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloade } // Review implements PayloadConvertor Review method -func (f *WechatworkPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { +func (wc wechatworkConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (WechatworkPayload, error) { var text, title string switch p.Action { case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return WechatworkPayload{}, err } title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) text = p.Review.Content @@ -151,7 +139,7 @@ func (f *WechatworkPayload) Review(p *api.PullRequestPayload, event webhook_modu } // Repository implements PayloadConvertor Repository method -func (f *WechatworkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Repository(p *api.RepositoryPayload) (WechatworkPayload, error) { var title string switch p.Action { case api.HookRepoCreated: @@ -162,30 +150,33 @@ func (f *WechatworkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, return newWechatworkMarkdownPayload(title), nil } - return nil, nil + return WechatworkPayload{}, nil } // Wiki implements PayloadConvertor Wiki method -func (f *WechatworkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Wiki(p *api.WikiPayload) (WechatworkPayload, error) { text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true) return newWechatworkMarkdownPayload(text), nil } // Release implements PayloadConvertor Release method -func (f *WechatworkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Release(p *api.ReleasePayload) (WechatworkPayload, error) { text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true) return newWechatworkMarkdownPayload(text), nil } -func (f *WechatworkPayload) Package(p *api.PackagePayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload, error) { text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true) return newWechatworkMarkdownPayload(text), nil } -// GetWechatworkPayload GetWechatworkPayload converts a ding talk webhook into a WechatworkPayload -func GetWechatworkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) { - return convertPayloader(new(WechatworkPayload), p, event) +type wechatworkConvertor struct{} + +var _ payloadConvertor[WechatworkPayload] = wechatworkConvertor{} + +func newWechatworkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + return newJSONRequest(wechatworkConvertor{}, w, t, true) } diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index 81e0b84ea8..01c8cf987f 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/sync" @@ -87,7 +88,7 @@ func NormalizeWikiBranch(ctx context.Context, repo *repo_model.Repository, to st return err } - if err := gitRepo.SetDefaultBranch(to); err != nil { + if err := gitrepo.SetDefaultBranch(ctx, repo, to); err != nil { return err } diff --git a/tailwind.config.js b/tailwind.config.js index 7f36822001..d783268bd7 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,17 +1,41 @@ import {readFileSync} from 'node:fs'; import {env} from 'node:process'; -import {parse} from 'css-variables-parser'; +import {parse} from 'postcss'; const isProduction = env.NODE_ENV !== 'development'; +function extractRootVars(css) { + const root = parse(css); + const vars = new Set(); + root.walkRules((rule) => { + if (rule.selector !== ':root') return; + rule.each((decl) => { + if (decl.value && decl.prop.startsWith('--')) { + vars.add(decl.prop.substring(2)); + } + }); + }); + return Array.from(vars); +} + +const vars = extractRootVars([ + readFileSync(new URL('web_src/css/themes/theme-gitea-light.css', import.meta.url), 'utf8'), + readFileSync(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url), 'utf8'), +].join('\n')); + export default { prefix: 'tw-', important: true, // the frameworks are mixed together, so tailwind needs to override other framework's styles content: [ isProduction && '!./templates/devtest/**/*', isProduction && '!./web_src/js/standalone/devtest.js', + '!./templates/swagger/v1_json.tmpl', + '!./templates/user/auth/oidc_wellknown.tmpl', + '!**/*_test.go', + '!./modules/{public,options,templates}/bindata.go', + './{build,models,modules,routers,services}/**/*.go', './templates/**/*.tmpl', - './web_src/**/*.{js,vue}', + './web_src/js/**/*.{js,vue}', ].filter(Boolean), blocklist: [ // classes that don't work without CSS variables from "@tailwind base" which we don't use @@ -23,15 +47,10 @@ export default { theme: { colors: { // make `tw-bg-red` etc work with our CSS variables - ...Object.fromEntries( - Object.keys(parse([ - readFileSync(new URL('web_src/css/themes/theme-gitea-light.css', import.meta.url), 'utf8'), - readFileSync(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url), 'utf8'), - ].join('\n'), {})).filter((prop) => prop.startsWith('color-')).map((prop) => { - const color = prop.substring(6); - return [color, `var(--color-${color})`]; - }) - ), + ...Object.fromEntries(vars.filter((prop) => prop.startsWith('color-')).map((prop) => { + const color = prop.substring(6); + return [color, `var(--color-${color})`]; + })), inherit: 'inherit', current: 'currentcolor', transparent: 'transparent', diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index 53a12cbe27..cc7d338589 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -2,7 +2,7 @@
{{if .NeedUpdate}}
-

{{(ctx.Locale.Tr "admin.dashboard.new_version_hint" .RemoteVersion AppVer) | SanitizeHTML}}

+

{{ctx.Locale.Tr "admin.dashboard.new_version_hint" .RemoteVersion AppVer}}

{{end}}

diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl index bcd80368e6..29fbb5f039 100644 --- a/templates/admin/emails/list.tmpl +++ b/templates/admin/emails/list.tmpl @@ -47,8 +47,8 @@ {{range .Emails}} {{.Name}} - {{.FullName}} - {{.Email}} + {{.FullName}} + {{.Email}} {{if .IsPrimary}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} {{if .CanChange}} diff --git a/templates/admin/layout_head.tmpl b/templates/admin/layout_head.tmpl index 0067f336e0..b326c82a6c 100644 --- a/templates/admin/layout_head.tmpl +++ b/templates/admin/layout_head.tmpl @@ -3,7 +3,7 @@
{{template "base/alert" .ctxData}}
-
+
{{template "admin/navbar" .ctxData}}
{{/* block: admin-setting-content */}} diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl index ed410425b5..e0abe4f8c0 100644 --- a/templates/admin/notice.tmpl +++ b/templates/admin/notice.tmpl @@ -31,7 +31,7 @@ -
+ {{.CsrfTokenHtml}}
diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index cf860dab2a..aef4815424 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -62,8 +62,8 @@ {{end}} {{.Package.Type.Name}} - {{.Package.Name}} - {{.Version.Version}} + {{.Package.Name}} + {{.Version.Version}} {{.Creator.Name}} {{if .Repository}} diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl index 8fdc80fc70..e9ce17ac90 100644 --- a/templates/admin/user/list.tmpl +++ b/templates/admin/user/list.tmpl @@ -96,7 +96,7 @@ {{ctx.Locale.Tr "admin.users.remote"}} {{end}} - {{.Email}} + {{.Email}} {{if .IsActive}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} {{if .IsRestricted}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} {{if index $.UsersTwoFaStatus .ID}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 06fc5913d0..e755775985 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -14,7 +14,7 @@

- {{ctx.Locale.Tr "startpage.install_desc" | SanitizeHTML}} + {{ctx.Locale.Tr "startpage.install_desc"}}

@@ -25,7 +25,7 @@ {{svg "octicon-device-desktop"}} {{ctx.Locale.Tr "startpage.platform"}}

- {{ctx.Locale.Tr "startpage.platform_desc" | SanitizeHTML}} + {{ctx.Locale.Tr "startpage.platform_desc"}}

@@ -35,7 +35,7 @@ {{svg "octicon-rocket"}} {{ctx.Locale.Tr "startpage.lightweight"}}

- {{ctx.Locale.Tr "startpage.lightweight_desc" | SanitizeHTML}} + {{ctx.Locale.Tr "startpage.lightweight_desc"}}

@@ -43,7 +43,7 @@ {{svg "octicon-code"}} {{ctx.Locale.Tr "startpage.license"}}

- {{ctx.Locale.Tr "startpage.license_desc" | SanitizeHTML}} + {{ctx.Locale.Tr "startpage.license_desc"}}

diff --git a/templates/mail/auth/activate.tmpl b/templates/mail/auth/activate.tmpl index c50717d315..b1bb4cb463 100644 --- a/templates/mail/auth/activate.tmpl +++ b/templates/mail/auth/activate.tmpl @@ -8,8 +8,8 @@ {{$activate_url := printf "%suser/activate?code=%s" AppUrl (QueryEscape .Code)}} -

{{.locale.Tr "mail.activate_account.text_1" (.DisplayName|DotEscape) AppName | SanitizeHTML}}


-

{{.locale.Tr "mail.activate_account.text_2" .ActiveCodeLives | SanitizeHTML}}

{{$activate_url}}


+

{{.locale.Tr "mail.activate_account.text_1" (.DisplayName|DotEscape) AppName}}


+

{{.locale.Tr "mail.activate_account.text_2" .ActiveCodeLives}}

{{$activate_url}}


{{.locale.Tr "mail.link_not_working_do_paste"}}

© {{AppName}}

diff --git a/templates/mail/auth/activate_email.tmpl b/templates/mail/auth/activate_email.tmpl index 30fcb99ab8..3d32f80a4e 100644 --- a/templates/mail/auth/activate_email.tmpl +++ b/templates/mail/auth/activate_email.tmpl @@ -8,8 +8,8 @@ {{$activate_url := printf "%suser/activate_email?code=%s&email=%s" AppUrl (QueryEscape .Code) (QueryEscape .Email)}} -

{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape) | SanitizeHTML}}


-

{{.locale.Tr "mail.activate_email.text" .ActiveCodeLives | SanitizeHTML}}

{{$activate_url}}


+

{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}


+

{{.locale.Tr "mail.activate_email.text" .ActiveCodeLives}}

{{$activate_url}}


{{.locale.Tr "mail.link_not_working_do_paste"}}

© {{AppName}}

diff --git a/templates/mail/auth/register_notify.tmpl b/templates/mail/auth/register_notify.tmpl index 27c685e58f..62dbf7d927 100644 --- a/templates/mail/auth/register_notify.tmpl +++ b/templates/mail/auth/register_notify.tmpl @@ -8,7 +8,7 @@ {{$set_pwd_url := printf "%[1]suser/forgot_password" AppUrl}} -

{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape) | SanitizeHTML}}


+

{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}


{{.locale.Tr "mail.register_notify.text_1" AppName}}


{{.locale.Tr "mail.register_notify.text_2" .Username}}

{{AppUrl}}user/login


{{.locale.Tr "mail.register_notify.text_3" $set_pwd_url}}


diff --git a/templates/mail/auth/reset_passwd.tmpl b/templates/mail/auth/reset_passwd.tmpl index e1af5b483c..55b1ecec3f 100644 --- a/templates/mail/auth/reset_passwd.tmpl +++ b/templates/mail/auth/reset_passwd.tmpl @@ -8,8 +8,8 @@ {{$recover_url := printf "%suser/recover_account?code=%s" AppUrl (QueryEscape .Code)}} -

{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape) | SanitizeHTML}}


-

{{.locale.Tr "mail.reset_password.text" .ResetPwdCodeLives | SanitizeHTML}}

{{$recover_url}}


+

{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}


+

{{.locale.Tr "mail.reset_password.text" .ResetPwdCodeLives}}

{{$recover_url}}


{{.locale.Tr "mail.link_not_working_do_paste"}}

© {{AppName}}

diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl index 796dc403b7..4e83dbcfdb 100644 --- a/templates/mail/issue/default.tmpl +++ b/templates/mail/issue/default.tmpl @@ -16,7 +16,7 @@ - {{if .IsMention}}

{{.locale.Tr "mail.issue.x_mentioned_you" .Doer.Name | SanitizeHTML}}

{{end}} + {{if .IsMention}}

{{.locale.Tr "mail.issue.x_mentioned_you" .Doer.Name}}

{{end}} {{if eq .ActionName "push"}}

{{if .Comment.IsForcePush}} @@ -58,7 +58,7 @@ {{.locale.Tr "mail.issue.action.new" .Doer.Name .Issue.Index}} {{end}} {{else}} - {{.Body | SanitizeHTML}} + {{.Body}} {{end -}} {{- range .ReviewComments}}


diff --git a/templates/mail/notify/admin_new_user.tmpl b/templates/mail/notify/admin_new_user.tmpl index 04f90b61ab..613c58194a 100644 --- a/templates/mail/notify/admin_new_user.tmpl +++ b/templates/mail/notify/admin_new_user.tmpl @@ -13,8 +13,8 @@
    -

    {{.Locale.Tr "mail.admin.new_user.user_info" | SanitizeHTML}}: @{{.NewUser.Name}}

    -
  • {{.Locale.Tr "admin.users.created" | SanitizeHTML}}: {{DateTime "full" .NewUser.CreatedUnix}}
  • +

    {{.Locale.Tr "mail.admin.new_user.user_info"}}: @{{.NewUser.Name}}

    +
  • {{.Locale.Tr "admin.users.created"}}: {{DateTime "full" .NewUser.CreatedUnix}}

{{.Body | SanitizeHTML}}

diff --git a/templates/mail/team_invite.tmpl b/templates/mail/team_invite.tmpl index 67b368f348..cb0c0c0a50 100644 --- a/templates/mail/team_invite.tmpl +++ b/templates/mail/team_invite.tmpl @@ -5,7 +5,7 @@ -

{{.locale.Tr "mail.team_invite.text_1" (DotEscape .Inviter.DisplayName) (DotEscape .Team.Name) (DotEscape .Organization.DisplayName) | SanitizeHTML}}

+

{{.locale.Tr "mail.team_invite.text_1" (DotEscape .Inviter.DisplayName) (DotEscape .Team.Name) (DotEscape .Organization.DisplayName)}}

{{.locale.Tr "mail.team_invite.text_2"}}

{{.InviteURL}}

{{.locale.Tr "mail.link_not_working_do_paste"}}

{{.locale.Tr "mail.team_invite.text_3" .Invite.Email}}

diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl index efbbc43b1d..943557b1ca 100644 --- a/templates/org/header.tmpl +++ b/templates/org/header.tmpl @@ -7,7 +7,7 @@ {{if .Org.Visibility.IsLimited}}{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}{{end}} {{if .Org.Visibility.IsPrivate}}{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}{{end}} - + {{if .EnableFeed}} {{svg "octicon-rss" 24}} diff --git a/templates/org/settings/delete.tmpl b/templates/org/settings/delete.tmpl index 8c93e7548d..e1ef471e34 100644 --- a/templates/org/settings/delete.tmpl +++ b/templates/org/settings/delete.tmpl @@ -6,7 +6,7 @@
-

{{svg "octicon-alert"}} {{ctx.Locale.Tr "org.settings.delete_prompt" | SanitizeHTML}}

+

{{svg "octicon-alert"}} {{ctx.Locale.Tr "org.settings.delete_prompt"}}

{{.CsrfTokenHtml}} diff --git a/templates/org/settings/labels.tmpl b/templates/org/settings/labels.tmpl index 56931def82..8eb7b4584e 100644 --- a/templates/org/settings/labels.tmpl +++ b/templates/org/settings/labels.tmpl @@ -2,7 +2,7 @@
- {{ctx.Locale.Tr "org.settings.labels_desc" | SanitizeHTML}} + {{ctx.Locale.Tr "org.settings.labels_desc"}}
diff --git a/templates/org/team/invite.tmpl b/templates/org/team/invite.tmpl index 5a8780c205..1167828d14 100644 --- a/templates/org/team/invite.tmpl +++ b/templates/org/team/invite.tmpl @@ -7,7 +7,7 @@ {{ctx.AvatarUtils.Avatar .Organization 140}}
-
{{ctx.Locale.Tr "org.teams.invite.title" .Team.Name .Organization.Name | SanitizeHTML}}
+
{{ctx.Locale.Tr "org.teams.invite.title" .Team.Name .Organization.Name}}
{{ctx.Locale.Tr "org.teams.invite.by" .Inviter.Name}}
{{ctx.Locale.Tr "org.teams.invite.description"}}
diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl index 99b16ee22d..50ef53b91b 100644 --- a/templates/org/team/new.tmpl +++ b/templates/org/team/new.tmpl @@ -32,14 +32,14 @@
- {{ctx.Locale.Tr "org.teams.specific_repositories_helper" | SanitizeHTML}} + {{ctx.Locale.Tr "org.teams.specific_repositories_helper"}}
- {{ctx.Locale.Tr "org.teams.all_repositories_helper" | SanitizeHTML}} + {{ctx.Locale.Tr "org.teams.all_repositories_helper"}}
diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl index e59c0e5613..9311a46e38 100644 --- a/templates/org/team/sidebar.tmpl +++ b/templates/org/team/sidebar.tmpl @@ -27,16 +27,16 @@ {{if eq .Team.LowerName "owners"}}
- {{ctx.Locale.Tr "org.teams.owners_permission_desc" | SanitizeHTML}} + {{ctx.Locale.Tr "org.teams.owners_permission_desc"}}
{{else}}

{{ctx.Locale.Tr "org.team_access_desc"}}

    {{if .Team.IncludesAllRepositories}} -
  • {{ctx.Locale.Tr "org.teams.all_repositories" | SanitizeHTML}}
  • +
  • {{ctx.Locale.Tr "org.teams.all_repositories"}}
  • {{else}} -
  • {{ctx.Locale.Tr "org.teams.specific_repositories" | SanitizeHTML}}
  • +
  • {{ctx.Locale.Tr "org.teams.specific_repositories"}}
  • {{end}} {{if .Team.CanCreateOrgRepo}}
  • {{ctx.Locale.Tr "org.teams.can_create_org_repo"}}
  • @@ -44,10 +44,10 @@
{{if (eq .Team.AccessMode 2)}}

{{ctx.Locale.Tr "org.settings.permission"}}

- {{ctx.Locale.Tr "org.teams.write_permission_desc" | SanitizeHTML}} + {{ctx.Locale.Tr "org.teams.write_permission_desc"}} {{else if (eq .Team.AccessMode 3)}}

{{ctx.Locale.Tr "org.settings.permission"}}

- {{ctx.Locale.Tr "org.teams.admin_permission_desc" | SanitizeHTML}} + {{ctx.Locale.Tr "org.teams.admin_permission_desc"}} {{else}} diff --git a/templates/package/settings.tmpl b/templates/package/settings.tmpl index 10e26c7010..9424baf493 100644 --- a/templates/package/settings.tmpl +++ b/templates/package/settings.tmpl @@ -10,7 +10,7 @@ {{template "user/overview/header" .}} {{end}} {{template "base/alert" .}} -

{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}}) / {{ctx.Locale.Tr "repo.settings"}}

+

{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}}) / {{ctx.Locale.Tr "repo.settings"}}

{{ctx.Locale.Tr "packages.settings.link"}}

diff --git a/templates/package/shared/cleanup_rules/preview.tmpl b/templates/package/shared/cleanup_rules/preview.tmpl index 7a50d5ccca..cff8e8249f 100644 --- a/templates/package/shared/cleanup_rules/preview.tmpl +++ b/templates/package/shared/cleanup_rules/preview.tmpl @@ -19,7 +19,7 @@ - + diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl index 67c686675c..7b10e52ff7 100644 --- a/templates/package/shared/list.tmpl +++ b/templates/package/shared/list.tmpl @@ -20,7 +20,7 @@
- {{.Package.Name}} + {{.Package.Name}} {{svg .Package.Type.SVGName 16}} {{.Package.Type.Name}}
diff --git a/templates/package/shared/versionlist.tmpl b/templates/package/shared/versionlist.tmpl index eee952c096..59d6d89b53 100644 --- a/templates/package/shared/versionlist.tmpl +++ b/templates/package/shared/versionlist.tmpl @@ -23,7 +23,7 @@
- {{.Version.LowerVersion}} + {{.Version.LowerVersion}}
{{ctx.Locale.Tr "packages.published_by" (TimeSinceUnix .Version.CreatedUnix ctx.Locale) .Creator.HomeLink .Creator.GetDisplayName}}
diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index 0fa23d67fd..54af71126f 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -87,7 +87,7 @@ {{end}}
{{ctx.Locale.Tr "packages.versions"}} ({{.TotalVersionCount}}) - {{ctx.Locale.Tr "packages.versions.view_all"}} + {{ctx.Locale.Tr "packages.versions.view_all"}}
{{range .LatestVersions}}
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index 30fbd498a4..54a41221bf 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -10,7 +10,7 @@ {{ctx.Locale.PrettyNumber .ClosedCount}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
- diff --git a/templates/projects/new.tmpl b/templates/projects/new.tmpl index 711dbe842a..92ee36c1c4 100644 --- a/templates/projects/new.tmpl +++ b/templates/projects/new.tmpl @@ -55,7 +55,7 @@
-
+
{{ctx.Locale.Tr "repo.milestones.cancel"}} diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 3792ccca0e..1d03477a9f 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -165,9 +165,9 @@
-
+
{{range (index $.IssuesMap .ID)}} -
+
{{template "repo/issue/card" (dict "Issue" . "Page" $)}}
{{end}} diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl index 75104cdcd2..6e0d0d1a5e 100644 --- a/templates/repo/blame.tmpl +++ b/templates/repo/blame.tmpl @@ -2,11 +2,11 @@ {{$revsFileLink := URLJoin .RepoLink "src" .BranchNameSubURL "/.git-blame-ignore-revs"}} {{if .UsesIgnoreRevs}}
-

{{ctx.Locale.Tr "repo.blame.ignore_revs" $revsFileLink (print $revsFileLink "?bypass-blame-ignore=true") | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.blame.ignore_revs" $revsFileLink (print $revsFileLink "?bypass-blame-ignore=true")}}

{{else}}
-

{{ctx.Locale.Tr "repo.blame.ignore_revs.failed" $revsFileLink | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.blame.ignore_revs.failed" $revsFileLink}}

{{end}} {{end}} diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 4aa0e22b78..916111faca 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -72,7 +72,7 @@
{{ctx.Locale.Tr "repo.branches"}}
-
+
-

{{ctx.Locale.Tr "repo.branch.delete_desc" | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.branch.delete_desc"}}

{{template "base/modal_actions_confirm" .}}
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 7892a57163..80af73ce48 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -17,7 +17,7 @@ {{$class = (print $class " isWarning")}} {{end}} {{end}} -
+

{{RenderCommitMessage $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}

{{if not $.PageIsWiki}} @@ -184,7 +184,7 @@
{{if .Commit.Signature}} -
+
{{if .Verification.Verified}} {{if ne .Verification.SigningUser.ID 0}} diff --git a/templates/repo/commit_statuses.tmpl b/templates/repo/commit_statuses.tmpl index 74c20a6a2c..b035e74c2f 100644 --- a/templates/repo/commit_statuses.tmpl +++ b/templates/repo/commit_statuses.tmpl @@ -1,6 +1,6 @@ {{if .Statuses}} {{if and (eq (len .Statuses) 1) .Status.TargetURL}} - + {{template "repo/commit_status" .Status}} {{else}} diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl index 79e1bd6309..86e6b7225e 100644 --- a/templates/repo/commits_list_small.tmpl +++ b/templates/repo/commits_list_small.tmpl @@ -13,7 +13,7 @@ {{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}} - + {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} {{$class := "ui sha label"}} {{if .Signature}} diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl index 054a3f6bec..70f673e27e 100644 --- a/templates/repo/commits_table.tmpl +++ b/templates/repo/commits_table.tmpl @@ -8,7 +8,7 @@ {{ctx.Locale.Tr "repo.commits.no_commits" $.BaseBranch $.HeadBranch}} {{end}}
-
+
{{if .PageIsCommits}}
- {{ctx.Locale.Tr "repo.license_helper_desc" "https://choosealicense.com/" | SanitizeHTML}} + {{ctx.Locale.Tr "repo.license_helper_desc" "https://choosealicense.com/"}}
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index f9f18208e5..886004ea65 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -19,13 +19,13 @@ {{end}} {{if not .DiffNotAvailable}}
- {{svg "octicon-diff" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion | SanitizeHTML}} + {{svg "octicon-diff" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion}}
{{end}}
{{if and .PageIsPullFiles $.SignedUserID (not .IsArchived) (not .DiffNotAvailable)}} -
+
@@ -112,7 +112,7 @@

-
{{if $showFileViewToggle}} {{/* for image or CSV, it can have a horizontal scroll bar, there won't be review comment context menu (position absolute) which would be clipped by "overflow" */}} -
+

{{.Package.Type.Name}} {{.Package.Name}}{{.Version.Version}}{{.Version.Version}} {{.Creator.Name}} {{FileSize .CalculateBlobSize}} {{DateTime "short" .Version.CreatedUnix}}
{{if $isImage}} {{template "repo/diff/image_diff" dict "file" . "root" $ "blobBase" $blobBase "blobHead" $blobHead "sniffedTypeBase" $sniffedTypeBase "sniffedTypeHead" $sniffedTypeHead}} diff --git a/templates/repo/diff/comment_form.tmpl b/templates/repo/diff/comment_form.tmpl index 54817d4740..6005ea28ef 100644 --- a/templates/repo/diff/comment_form.tmpl +++ b/templates/repo/diff/comment_form.tmpl @@ -27,7 +27,7 @@ - diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 2c4a924ba7..99d75b8a84 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -80,7 +80,7 @@
{{if .Repository.IsArchived}} -
+
{{ctx.Locale.Tr "repo.settings.archive.mirrors_unavailable"}}
{{else}} @@ -191,7 +191,7 @@
-

{{ctx.Locale.Tr "repo.mirror_lfs_endpoint_desc" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery" | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.mirror_lfs_endpoint_desc" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery"}}

{{end}}
diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl index e4fcf2ee6b..31fb59e5e3 100644 --- a/templates/repo/settings/tags.tmpl +++ b/templates/repo/settings/tags.tmpl @@ -1,7 +1,7 @@ {{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings edit")}}
{{if .Repository.IsArchived}} -
+
{{ctx.Locale.Tr "repo.settings.archive.tagsettings_unavailable"}}
{{else}} diff --git a/templates/repo/settings/units/issues.tmpl b/templates/repo/settings/units/issues.tmpl index 7ddafd1c6a..77f5782151 100644 --- a/templates/repo/settings/units/issues.tmpl +++ b/templates/repo/settings/units/issues.tmpl @@ -61,7 +61,7 @@
-

{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc" | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc"}}

@@ -89,7 +89,7 @@
-

{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc" | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc"}}

diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl index 00f9a48ba7..e56929b70f 100644 --- a/templates/repo/settings/webhook/base_list.tmpl +++ b/templates/repo/settings/webhook/base_list.tmpl @@ -10,7 +10,7 @@
- {{.Description | SanitizeHTML}} + {{.Description}}
{{range .Webhooks}}
diff --git a/templates/repo/settings/webhook/dingtalk.tmpl b/templates/repo/settings/webhook/dingtalk.tmpl index a620d9e241..0ba99e98ee 100644 --- a/templates/repo/settings/webhook/dingtalk.tmpl +++ b/templates/repo/settings/webhook/dingtalk.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "dingtalk"}} -

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://dingtalk.com" (ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk") | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://dingtalk.com" (ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk")}}

{{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/discord.tmpl b/templates/repo/settings/webhook/discord.tmpl index ff1ca75d3f..b623a6d8d3 100644 --- a/templates/repo/settings/webhook/discord.tmpl +++ b/templates/repo/settings/webhook/discord.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "discord"}} -

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://discord.com" (ctx.Locale.Tr "repo.settings.web_hook_name_discord") | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://discord.com" (ctx.Locale.Tr "repo.settings.web_hook_name_discord")}}

{{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/feishu.tmpl b/templates/repo/settings/webhook/feishu.tmpl index 1acb42c76c..d80deab26f 100644 --- a/templates/repo/settings/webhook/feishu.tmpl +++ b/templates/repo/settings/webhook/feishu.tmpl @@ -1,6 +1,6 @@ {{if eq .HookType "feishu"}} -

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://feishu.cn" (ctx.Locale.Tr "repo.settings.web_hook_name_feishu") | SanitizeHTML}}

-

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://larksuite.com" (ctx.Locale.Tr "repo.settings.web_hook_name_larksuite") | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://feishu.cn" (ctx.Locale.Tr "repo.settings.web_hook_name_feishu")}}

+

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://larksuite.com" (ctx.Locale.Tr "repo.settings.web_hook_name_larksuite")}}

{{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/forgejo.tmpl b/templates/repo/settings/webhook/forgejo.tmpl index 0045f27616..0bfb99115c 100644 --- a/templates/repo/settings/webhook/forgejo.tmpl +++ b/templates/repo/settings/webhook/forgejo.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "forgejo"}} -

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://forgejo.org/docs/latest/user/webhooks/" (ctx.Locale.Tr "repo.settings.web_hook_name_forgejo") | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://forgejo.org/docs/latest/user/webhooks/" (ctx.Locale.Tr "repo.settings.web_hook_name_forgejo")}}

{{template "base/disable_form_autofill"}} {{.CsrfTokenHtml}} diff --git a/templates/repo/settings/webhook/gitea.tmpl b/templates/repo/settings/webhook/gitea.tmpl index cb2e789459..38ec29c784 100644 --- a/templates/repo/settings/webhook/gitea.tmpl +++ b/templates/repo/settings/webhook/gitea.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "gitea"}} -

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://forgejo.org/docs/latest/user/webhooks/" (ctx.Locale.Tr "repo.settings.web_hook_name_gitea") | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://forgejo.org/docs/latest/user/webhooks/" (ctx.Locale.Tr "repo.settings.web_hook_name_gitea")}}

{{template "base/disable_form_autofill"}} {{.CsrfTokenHtml}} diff --git a/templates/repo/settings/webhook/gogs.tmpl b/templates/repo/settings/webhook/gogs.tmpl index a81b15fd5b..ff1742aa18 100644 --- a/templates/repo/settings/webhook/gogs.tmpl +++ b/templates/repo/settings/webhook/gogs.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "gogs"}} -

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://forgejo.org/docs/latest/user/webhooks/" (ctx.Locale.Tr "repo.settings.web_hook_name_gogs") | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://forgejo.org/docs/latest/user/webhooks/" (ctx.Locale.Tr "repo.settings.web_hook_name_gogs")}}

{{template "base/disable_form_autofill"}} {{.CsrfTokenHtml}} diff --git a/templates/repo/settings/webhook/history.tmpl b/templates/repo/settings/webhook/history.tmpl index 3c21a42421..4e0f0e9c3e 100644 --- a/templates/repo/settings/webhook/history.tmpl +++ b/templates/repo/settings/webhook/history.tmpl @@ -19,6 +19,8 @@
{{if .IsSucceed}} {{svg "octicon-check"}} + {{else if not .IsDelivered}} + {{svg "octicon-stopwatch"}} {{else}} {{svg "octicon-alert"}} {{end}} @@ -62,7 +64,7 @@ {{range $key, $val := .RequestInfo.Headers}}{{$key}}: {{$val}} {{end}}
{{ctx.Locale.Tr "repo.settings.webhook.payload"}}
-
{{.PayloadContent}}
+
{{or .RequestInfo.Body .PayloadContent}}
{{else}} - {{end}} diff --git a/templates/repo/settings/webhook/matrix.tmpl b/templates/repo/settings/webhook/matrix.tmpl index 662beeaccb..7f1c9f08e6 100644 --- a/templates/repo/settings/webhook/matrix.tmpl +++ b/templates/repo/settings/webhook/matrix.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "matrix"}} -

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://matrix.org/" (ctx.Locale.Tr "repo.settings.web_hook_name_matrix") | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://matrix.org/" (ctx.Locale.Tr "repo.settings.web_hook_name_matrix")}}

{{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/msteams.tmpl b/templates/repo/settings/webhook/msteams.tmpl index 08e381de38..62ea24e763 100644 --- a/templates/repo/settings/webhook/msteams.tmpl +++ b/templates/repo/settings/webhook/msteams.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "msteams"}} -

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://teams.microsoft.com" (ctx.Locale.Tr "repo.settings.web_hook_name_msteams") | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://teams.microsoft.com" (ctx.Locale.Tr "repo.settings.web_hook_name_msteams")}}

{{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/packagist.tmpl b/templates/repo/settings/webhook/packagist.tmpl index 48a4ecce6d..5330daf339 100644 --- a/templates/repo/settings/webhook/packagist.tmpl +++ b/templates/repo/settings/webhook/packagist.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "packagist"}} -

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://packagist.org" (ctx.Locale.Tr "repo.settings.web_hook_name_packagist") | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://packagist.org" (ctx.Locale.Tr "repo.settings.web_hook_name_packagist")}}

{{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl index a3764371e2..3ef8894444 100644 --- a/templates/repo/settings/webhook/settings.tmpl +++ b/templates/repo/settings/webhook/settings.tmpl @@ -5,19 +5,19 @@
- +
- +
- +
@@ -255,7 +255,7 @@
- {{ctx.Locale.Tr "repo.settings.branch_filter_desc" | SanitizeHTML}} + {{ctx.Locale.Tr "repo.settings.branch_filter_desc"}}
diff --git a/templates/repo/settings/webhook/slack.tmpl b/templates/repo/settings/webhook/slack.tmpl index 5dfc7a67b7..78fc25b9f4 100644 --- a/templates/repo/settings/webhook/slack.tmpl +++ b/templates/repo/settings/webhook/slack.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "slack"}} -

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://slack.com" (ctx.Locale.Tr "repo.settings.web_hook_name_slack") | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://slack.com" (ctx.Locale.Tr "repo.settings.web_hook_name_slack")}}

{{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/telegram.tmpl b/templates/repo/settings/webhook/telegram.tmpl index 62cac464b6..f92c2be0db 100644 --- a/templates/repo/settings/webhook/telegram.tmpl +++ b/templates/repo/settings/webhook/telegram.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "telegram"}} -

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://core.telegram.org/bots" (ctx.Locale.Tr "repo.settings.web_hook_name_telegram") | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://core.telegram.org/bots" (ctx.Locale.Tr "repo.settings.web_hook_name_telegram")}}

{{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/wechatwork.tmpl b/templates/repo/settings/webhook/wechatwork.tmpl index 95c4cefe64..78a1617123 100644 --- a/templates/repo/settings/webhook/wechatwork.tmpl +++ b/templates/repo/settings/webhook/wechatwork.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "wechatwork"}} -

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://work.weixin.qq.com" (ctx.Locale.Tr "repo.settings.web_hook_name_wechatwork") | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://work.weixin.qq.com" (ctx.Locale.Tr "repo.settings.web_hook_name_wechatwork")}}

{{.CsrfTokenHtml}}
diff --git a/templates/repo/unicode_escape_prompt.tmpl b/templates/repo/unicode_escape_prompt.tmpl index 9fe458f97b..8bceafa8bb 100644 --- a/templates/repo/unicode_escape_prompt.tmpl +++ b/templates/repo/unicode_escape_prompt.tmpl @@ -1,22 +1,22 @@ {{if .EscapeStatus}} {{if .EscapeStatus.HasInvisible}} -
+
{{ctx.Locale.Tr "repo.invisible_runes_header"}}
-

{{ctx.Locale.Tr "repo.invisible_runes_description" | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.invisible_runes_description"}}

{{if .EscapeStatus.HasAmbiguous}} -

{{ctx.Locale.Tr "repo.ambiguous_runes_description" | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}

{{end}}
{{else if .EscapeStatus.HasAmbiguous}} -
+
{{ctx.Locale.Tr "repo.ambiguous_runes_header"}}
-

{{ctx.Locale.Tr "repo.ambiguous_runes_description" | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}

{{end}} {{end}} diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 91b10f744a..f8665b5d2c 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -1,12 +1,12 @@
{{- if .FileError}}
-
{{.FileError}}
+
{{.FileError}}
{{end}} {{- if .FileWarning}}
-
{{.FileWarning}}
+
{{.FileWarning}}
{{end}} diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index 9fa05b5b5c..541b1e9b42 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -79,19 +79,19 @@ {{if .sidebarPresent}} {{end}} -
+
{{if .footerPresent}}
diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index e8a0079c1c..a90188297f 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -13,7 +13,7 @@
- {{RenderEmoji $.Context .Title | RenderCodeBlock}} + {{RenderEmoji $.Context .Title | RenderCodeBlock}} {{if .IsPull}} {{if (index $.CommitStatuses .PullRequest.ID)}} {{template "repo/commit_statuses" dict "Status" (index $.CommitLastStatus .PullRequest.ID) "Statuses" (index $.CommitStatuses .PullRequest.ID)}} @@ -36,7 +36,7 @@ {{if .Assignees}}
{{range .Assignees}} - + {{ctx.AvatarUtils.Avatar . 20}} {{end}} @@ -44,7 +44,7 @@ {{end}} {{if .NumComments}} diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl index c04b4af805..98e3044db2 100644 --- a/templates/status/500.tmpl +++ b/templates/status/500.tmpl @@ -1,5 +1,5 @@ {{/* This page should only depend the minimal template functions/variables, to avoid triggering new panics. -* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl, ThemeName, SanitizeHTML +* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl, ThemeName * ctx.Locale * .Flash * .ErrorMsg @@ -41,11 +41,11 @@
{{if .ErrorMsg}}

{{ctx.Locale.Tr "error.occurred"}}:

-
{{.ErrorMsg}}
+
{{.ErrorMsg}}
{{end}}
{{if or .SignedUser.IsAdmin .ShowFooterVersion}}

{{ctx.Locale.Tr "admin.config.app_ver"}}: {{AppVer}}

{{end}} - {{if .SignedUser.IsAdmin}}

{{ctx.Locale.Tr "error.report_message" | SanitizeHTML}}

{{end}} + {{if .SignedUser.IsAdmin}}

{{ctx.Locale.Tr "error.report_message"}}

{{end}}
diff --git a/templates/user/auth/captcha.tmpl b/templates/user/auth/captcha.tmpl index 1c3379e629..d4d1a82418 100644 --- a/templates/user/auth/captcha.tmpl +++ b/templates/user/auth/captcha.tmpl @@ -24,7 +24,7 @@
{{else if eq .CaptchaType "cfturnstile"}} -
+
diff --git a/templates/user/auth/finalize_openid.tmpl b/templates/user/auth/finalize_openid.tmpl index f7da41d16c..1c1dcdb825 100644 --- a/templates/user/auth/finalize_openid.tmpl +++ b/templates/user/auth/finalize_openid.tmpl @@ -35,7 +35,7 @@ {{if .ShowRegistrationButton}} {{end}} diff --git a/templates/user/auth/grant.tmpl b/templates/user/auth/grant.tmpl index d56692ccce..cb9bba8749 100644 --- a/templates/user/auth/grant.tmpl +++ b/templates/user/auth/grant.tmpl @@ -9,11 +9,11 @@ {{template "base/alert" .}}

{{ctx.Locale.Tr "auth.authorize_application_description"}}
- {{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML | SanitizeHTML}} + {{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}}

-

{{ctx.Locale.Tr "auth.authorize_redirect_notice" .ApplicationRedirectDomainHTML | SanitizeHTML}}

+

{{ctx.Locale.Tr "auth.authorize_redirect_notice" .ApplicationRedirectDomainHTML}}

diff --git a/templates/user/auth/reset_passwd.tmpl b/templates/user/auth/reset_passwd.tmpl index c0bf693f46..9fee30f554 100644 --- a/templates/user/auth/reset_passwd.tmpl +++ b/templates/user/auth/reset_passwd.tmpl @@ -34,7 +34,7 @@

{{ctx.Locale.Tr "twofa"}}

-
{{ctx.Locale.Tr "settings.twofa_is_enrolled" | SanitizeHTML}}
+
{{ctx.Locale.Tr "settings.twofa_is_enrolled"}}
{{if .scratch_code}}
@@ -53,11 +53,11 @@ {{if and .has_two_factor (not .scratch_code)}} - {{ctx.Locale.Tr "auth.use_scratch_code" | SanitizeHTML}} + {{ctx.Locale.Tr "auth.use_scratch_code"}} {{end}}
{{else}} -

{{ctx.Locale.Tr "auth.invalid_code_forgot_password" (printf "%s/user/forgot_password" AppSubUrl) | SanitizeHTML}}

+

{{ctx.Locale.Tr "auth.invalid_code_forgot_password" (printf "%s/user/forgot_password" AppSubUrl)}}

{{end}}
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index 15733eb9bf..0d0064b02a 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -48,7 +48,7 @@ {{if .ShowRegistrationButton}} {{end}} diff --git a/templates/user/auth/twofa.tmpl b/templates/user/auth/twofa.tmpl index 8a898c7dd8..5260178d13 100644 --- a/templates/user/auth/twofa.tmpl +++ b/templates/user/auth/twofa.tmpl @@ -17,7 +17,7 @@
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index 82622366e7..7b7023cfaa 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -4,7 +4,7 @@
-
{{/* */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/* - */}}{{/* */}}{{end}}{{/* @@ -62,7 +62,7 @@ {{if $match.RightIdx}}{{end}} {{/* */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/* - */}}{{/* */}}{{end}}{{/* @@ -79,7 +79,7 @@ {{if $line.LeftIdx}}{{end}} {{/* */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 2))}}{{/* - */}}{{/* */}}{{end}}{{/* @@ -94,7 +94,7 @@ {{if $line.RightIdx}}{{end}} {{/* */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 3))}}{{/* - */}}{{/* */}}{{end}}{{/* diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl index ae97dc6db6..e3249c26d5 100644 --- a/templates/repo/diff/section_unified.tmpl +++ b/templates/repo/diff/section_unified.tmpl @@ -52,7 +52,7 @@ {{else}} {{/* */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/* - */}}{{/* */}}{{end}}{{/* diff --git a/templates/repo/diff/stats.tmpl b/templates/repo/diff/stats.tmpl index 64ee17a7c5..b7acb3d49b 100644 --- a/templates/repo/diff/stats.tmpl +++ b/templates/repo/diff/stats.tmpl @@ -1,5 +1,5 @@ {{Eval .file.Addition "+" .file.Deletion}} - + {{/* if the denominator is zero, then the float result is "width: NaNpx", as before, it just works */}}
diff --git a/templates/repo/diff/whitespace_dropdown.tmpl b/templates/repo/diff/whitespace_dropdown.tmpl index 7bf2ac9aec..cfabf836d6 100644 --- a/templates/repo/diff/whitespace_dropdown.tmpl +++ b/templates/repo/diff/whitespace_dropdown.tmpl @@ -2,26 +2,26 @@ {{svg "gitea-whitespace"}}
@@ -45,7 +45,7 @@ {{ctx.Locale.Tr "loading"}}
- {{ctx.Locale.Tr "loading"}} +
{{template "repo/editor/commit_form" .}} diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl index 7a0e6926c0..a858a728e9 100644 --- a/templates/repo/empty.tmpl +++ b/templates/repo/empty.tmpl @@ -6,7 +6,7 @@
{{template "base/alert" .}} {{if .Repository.IsArchived}} -
+
{{if .Repository.ArchivedUnix.IsZero}} {{ctx.Locale.Tr "repo.archive.title"}} {{else}} @@ -24,7 +24,7 @@
-

{{ctx.Locale.Tr "repo.clone_this_repo"}} {{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/Git-Basics-Getting-a-Git-Repository" | SanitizeHTML}}

+

{{ctx.Locale.Tr "repo.clone_this_repo"}} {{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/Git-Basics-Getting-a-Git-Repository"}}

{{if and .CanWriteCode (not .Repository.IsArchived)}} diff --git a/templates/repo/header_fork.tmpl b/templates/repo/header_fork.tmpl index e248a77850..3ac8889615 100644 --- a/templates/repo/header_fork.tmpl +++ b/templates/repo/header_fork.tmpl @@ -30,7 +30,7 @@
{{ctx.Locale.Tr "repo.already_forked" .Name}}
-
+
{{range $.UserAndOrgForks}}
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index f9a4599b92..6f0a996841 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -39,7 +39,7 @@ {{range .Topics}} {{/* keey the same layout as Fomantic UI generated labels */}} - {{.Name}}{{svg "octicon-x" 16 "delete icon"}} + {{.Name}}{{svg "octicon-x" 16 "delete icon"}} {{end}}
@@ -59,7 +59,7 @@ {{end}} {{if .Repository.IsArchived}} -
+
{{if .Repository.ArchivedUnix.IsZero}} {{ctx.Locale.Tr "repo.archive.title"}} {{else}} diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl index 5e524079c8..ff635c736a 100644 --- a/templates/repo/issue/card.tmpl +++ b/templates/repo/issue/card.tmpl @@ -7,7 +7,7 @@
{{end}}
-
+
{{template "shared/issueicon" .}}
diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl index 252b5816b3..9b0061b60e 100644 --- a/templates/repo/issue/labels/label_list.tmpl +++ b/templates/repo/issue/labels/label_list.tmpl @@ -61,7 +61,7 @@
  • - {{ctx.Locale.Tr "repo.org_labels_desc" | SanitizeHTML}} + {{ctx.Locale.Tr "repo.org_labels_desc"}} {{if .IsOrganizationOwner}} ({{ctx.Locale.Tr "repo.org_labels_desc_manage"}}): {{end}} diff --git a/templates/repo/issue/labels/labels_selector_field.tmpl b/templates/repo/issue/labels/labels_selector_field.tmpl index d24dac46eb..e42a1de895 100644 --- a/templates/repo/issue/labels/labels_selector_field.tmpl +++ b/templates/repo/issue/labels/labels_selector_field.tmpl @@ -21,7 +21,7 @@
    {{end}} {{$previousExclusiveScope = $exclusiveScope}} - {{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}  {{RenderLabel $.Context .}} + {{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}  {{RenderLabel $.Context .}} {{if .Description}}
    {{.Description | RenderEmoji $.Context}}{{end}}

    {{template "repo/issue/labels/label_archived" .}}

    @@ -34,7 +34,7 @@
    {{end}} {{$previousExclusiveScope = $exclusiveScope}} - {{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}  {{RenderLabel $.Context .}} + {{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}  {{RenderLabel $.Context .}} {{if .Description}}
    {{.Description | RenderEmoji $.Context}}{{end}}

    {{template "repo/issue/labels/label_archived" .}}

    diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl index 012b613fbf..62c1d00f00 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -6,7 +6,7 @@ {{if .PinnedIssues}}
    {{range .PinnedIssues}} -
    +
    {{template "repo/issue/card" (dict "Issue" . "Page" $ "isPinnedIssueCard" true)}}
    {{end}} diff --git a/templates/repo/issue/milestone_new.tmpl b/templates/repo/issue/milestone_new.tmpl index 3e79ee7ee9..7a56d73ac9 100644 --- a/templates/repo/issue/milestone_new.tmpl +++ b/templates/repo/issue/milestone_new.tmpl @@ -39,7 +39,7 @@
    -
    {{else if .Repository.IsArchived}} -
    +
    {{if .Issue.IsPull}} {{ctx.Locale.Tr "repo.archive.pull.nocomment"}} {{else}} @@ -124,7 +124,7 @@ {{end}} {{else}} {{/* not .IsSigned */}} {{if .Repository.IsArchived}} -
    +
    {{if .Issue.IsPull}} {{ctx.Locale.Tr "repo.archive.pull.nocomment"}} {{else}} @@ -181,7 +181,7 @@ {{ctx.Locale.Tr "repo.branch.delete" .HeadTarget}}
    -

    {{ctx.Locale.Tr "repo.branch.delete_desc" | SanitizeHTML}}

    +

    {{ctx.Locale.Tr "repo.branch.delete_desc"}}

    {{template "base/modal_actions_confirm" .}}
    diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 45cc65478f..37ec98a3de 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -69,7 +69,7 @@
    {{.Content}}
    {{if .Attachments}} - {{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "RenderedContent" .RenderedContent}} + {{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}} {{end}}
    {{$reactions := .Reactions.GroupByType}} @@ -80,7 +80,7 @@
    {{else if eq .Type 1}}
    - {{svg "octicon-dot-fill"}} + {{svg "octicon-dot-fill"}} {{if not .OriginalAuthor}} {{template "shared/user/avatarlink" dict "user" .Poster}} {{end}} @@ -95,7 +95,7 @@
    {{else if eq .Type 2}}
    - {{svg "octicon-circle-slash"}} + {{svg "octicon-circle-slash"}} {{if not .OriginalAuthor}} {{template "shared/user/avatarlink" dict "user" .Poster}} {{end}} @@ -110,7 +110,7 @@
    {{else if eq .Type 28}}
    - {{svg "octicon-git-merge"}} + {{svg "octicon-git-merge"}} {{if not .OriginalAuthor}} {{template "shared/user/avatarlink" dict "user" .Poster}} {{end}} @@ -383,7 +383,7 @@ {{ctx.AvatarUtils.Avatar .Poster 40}} {{end}} - {{svg (printf "octicon-%s" .Review.Type.Icon)}} + {{svg (printf "octicon-%s" .Review.Type.Icon)}} {{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}} {{if eq .Review.Type 1}} @@ -444,7 +444,7 @@
    {{.Content}}
    {{if .Attachments}} - {{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "RenderedContent" .RenderedContent}} + {{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}} {{end}}
    {{$reactions := .Reactions.GroupByType}} @@ -566,7 +566,7 @@ {{end}} {{if and .IsForcePush $.Issue.PullRequest.BaseRepo.Name}} - + {{ctx.Locale.Tr "repo.issues.force_push_compare"}} {{end}} diff --git a/templates/repo/issue/view_content/comments_delete_time.tmpl b/templates/repo/issue/view_content/comments_delete_time.tmpl index 95121b0dc7..2377e7c4f0 100644 --- a/templates/repo/issue/view_content/comments_delete_time.tmpl +++ b/templates/repo/issue/view_content/comments_delete_time.tmpl @@ -1,7 +1,7 @@ {{if and .comment.Time (.ctxData.Repository.IsTimetrackerEnabled ctx)}} {{/* compatibility with time comments made before v1.14 */}} {{if (not .comment.Time.Deleted)}} {{if (or .ctxData.IsAdmin (and .ctxData.IsSigned (eq .ctxData.SignedUserID .comment.PosterID)))}} - + {{$reactions := .Reactions.GroupByType}} diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index f5b6751d6d..329a39dd69 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -20,7 +20,7 @@ {{range .Reviewers}} {{if .User}} - {{svg "octicon-check"}} + {{svg "octicon-check"}} {{ctx.AvatarUtils.Avatar .User 28 "gt-mr-3"}}{{template "repo/search_name" .User}} @@ -35,7 +35,7 @@ {{range .TeamReviewers}} {{if .Team}} - {{svg "octicon-check" 16}} + {{svg "octicon-check" 16}} {{svg "octicon-people" 16 "gt-ml-4 gt-mr-2"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}} @@ -231,7 +231,7 @@ {{$checked = true}} {{end}} {{end}} - {{svg "octicon-check"}} + {{svg "octicon-check"}} {{ctx.AvatarUtils.Avatar . 20 "gt-mr-3"}}{{template "repo/search_name" .}} diff --git a/templates/repo/migrate/migrate.tmpl b/templates/repo/migrate/migrate.tmpl index d1abb15374..8ba567ee6b 100644 --- a/templates/repo/migrate/migrate.tmpl +++ b/templates/repo/migrate/migrate.tmpl @@ -16,10 +16,10 @@ {{svg (printf "gitea-%s" .Name) 184}} {{end}}
    -
    - {{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery" | SanitizeHTML}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description.local"}}{{end}} + {{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description.local"}}{{end}}
    diff --git a/templates/repo/pulse.tmpl b/templates/repo/pulse.tmpl index e6a59ea8c6..5943ae0434 100644 --- a/templates/repo/pulse.tmpl +++ b/templates/repo/pulse.tmpl @@ -108,7 +108,7 @@ {{end}} {{if gt .Activity.PublishedReleaseCount 0}} -

    +

    {{svg "octicon-tag" 16 "gt-mr-3"}} {{ctx.Locale.Tr "repo.activity.title.releases_published_by" (ctx.Locale.TrN .Activity.PublishedReleaseCount "repo.activity.title.releases_1" "repo.activity.title.releases_n" .Activity.PublishedReleaseCount) @@ -130,7 +130,7 @@ {{end}} {{if gt .Activity.MergedPRCount 0}} -

    +

    {{svg "octicon-git-pull-request" 16 "gt-mr-3"}} {{ctx.Locale.Tr "repo.activity.title.prs_merged_by" (ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.MergedPRCount) @@ -149,7 +149,7 @@ {{end}} {{if gt .Activity.OpenedPRCount 0}} -

    +

    {{svg "octicon-git-branch" 16 "gt-mr-3"}} {{ctx.Locale.Tr "repo.activity.title.prs_opened_by" (ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.OpenedPRCount) @@ -168,7 +168,7 @@ {{end}} {{if gt .Activity.ClosedIssueCount 0}} -

    +

    {{svg "octicon-issue-closed" 16 "gt-mr-3"}} {{ctx.Locale.Tr "repo.activity.title.issues_closed_from" (ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.ClosedIssueCount) @@ -187,7 +187,7 @@ {{end}} {{if gt .Activity.OpenedIssueCount 0}} -

    +

    {{svg "octicon-issue-opened" 16 "gt-mr-3"}} {{ctx.Locale.Tr "repo.activity.title.issues_created_by" (ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.OpenedIssueCount) @@ -206,7 +206,7 @@ {{end}} {{if gt .Activity.UnresolvedIssueCount 0}} -

    +

    {{svg "octicon-comment-discussion" 16 "gt-mr-3"}} {{ctx.Locale.TrN .Activity.UnresolvedIssueCount "repo.activity.title.unresolved_conv_1" "repo.activity.title.unresolved_conv_n" .Activity.UnresolvedIssueCount}}

    diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl index 541af86ff5..f1934493d8 100644 --- a/templates/repo/release/list.tmpl +++ b/templates/repo/release/list.tmpl @@ -54,7 +54,7 @@ {{TimeSinceUnix $release.CreatedUnix ctx.Locale}} {{end}} {{if and (not $release.IsDraft) ($.Permission.CanRead $.UnitTypeCode)}} - | {{ctx.Locale.Tr "repo.release.ahead.commits" $release.NumCommitsBehind | SanitizeHTML}} {{ctx.Locale.Tr "repo.release.ahead.target" $release.TargetBehind}} + | {{ctx.Locale.Tr "repo.release.ahead.commits" $release.NumCommitsBehind}} {{ctx.Locale.Tr "repo.release.ahead.target" $release.TargetBehind}} {{end}}

    diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl index 46b1c9b291..30e783167c 100644 --- a/templates/repo/release/new.tmpl +++ b/templates/repo/release/new.tmpl @@ -64,7 +64,7 @@
    - {{.Size | FileSize}} + {{.Size | FileSize}} {{svg "octicon-info"}} diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl index fbdc12defb..73aff887f3 100644 --- a/templates/repo/settings/branches.tmpl +++ b/templates/repo/settings/branches.tmpl @@ -1,7 +1,7 @@ {{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings edit")}}
    {{if .Repository.IsArchived}} -
    +
    {{ctx.Locale.Tr "repo.settings.archive.branchsettings_unavailable"}}
    {{else}} @@ -16,7 +16,7 @@ {{.CsrfTokenHtml}} {{if not .Repository.IsEmpty}} -
  • {{if .InRepo}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} {{if .Exists}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} {{if .Accessible}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} + {{ctx.Locale.Tr "repo.settings.lfs_findcommits"}}
    - {{.StatusLocaleName ctx.Locale}} + {{.StatusLocaleName ctx.Locale}} {{.ID}}

    {{.Name}}