diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index eef7bfbfef..bf9791eeb9 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -246,6 +246,15 @@ importers: '@hcengineering/github-resources': specifier: workspace:^0.7.0 version: link:../services/github/github-resources + '@hcengineering/gitlab': + specifier: workspace:^0.7.0 + version: link:../services/gitlab/gitlab + '@hcengineering/gitlab-assets': + specifier: workspace:^0.7.0 + version: link:../services/gitlab/gitlab-assets + '@hcengineering/gitlab-resources': + specifier: workspace:^0.7.0 + version: link:../services/gitlab/gitlab-resources '@hcengineering/global-profile': specifier: workspace:^0.7.0 version: link:../plugins/global-profile @@ -1315,6 +1324,15 @@ importers: '@hcengineering/github-resources': specifier: workspace:^0.7.0 version: link:../../services/github/github-resources + '@hcengineering/gitlab': + specifier: workspace:^0.7.0 + version: link:../../services/gitlab/gitlab + '@hcengineering/gitlab-assets': + specifier: workspace:^0.7.0 + version: link:../../services/gitlab/gitlab-assets + '@hcengineering/gitlab-resources': + specifier: workspace:^0.7.0 + version: link:../../services/gitlab/gitlab-resources '@hcengineering/global-profile': specifier: workspace:^0.7.0 version: link:../../plugins/global-profile @@ -5973,6 +5991,9 @@ importers: '@hcengineering/model-github': specifier: workspace:^0.7.0 version: link:../../services/github/model-github + '@hcengineering/model-gitlab': + specifier: workspace:^0.7.0 + version: link:../../services/gitlab/model-gitlab '@hcengineering/model-gmail': specifier: workspace:^0.7.0 version: link:../gmail @@ -6189,6 +6210,9 @@ importers: '@hcengineering/server-github-model': specifier: workspace:^0.7.0 version: link:../../services/github/server-github-model + '@hcengineering/server-gitlab-model': + specifier: workspace:^0.7.0 + version: link:../../services/gitlab/server-gitlab-model devDependencies: '@hcengineering/platform-rig': specifier: workspace:^0.7.21 @@ -36561,6 +36585,12 @@ importers: '@hcengineering/github-assets': specifier: workspace:^0.7.0 version: link:../../services/github/github-assets + '@hcengineering/gitlab': + specifier: workspace:^0.7.0 + version: link:../../services/gitlab/gitlab + '@hcengineering/gitlab-assets': + specifier: workspace:^0.7.0 + version: link:../../services/gitlab/gitlab-assets '@hcengineering/gmail': specifier: workspace:^0.7.0 version: link:../../plugins/gmail @@ -36726,6 +36756,12 @@ importers: '@hcengineering/server-github-resources': specifier: workspace:^0.7.0 version: link:../../services/github/server-github-resources + '@hcengineering/server-gitlab': + specifier: workspace:^0.7.0 + version: link:../../services/gitlab/server-gitlab + '@hcengineering/server-gitlab-resources': + specifier: workspace:^0.7.0 + version: link:../../services/gitlab/server-gitlab-resources '@hcengineering/server-gmail': specifier: workspace:^0.7.0 version: link:../../server-plugins/gmail @@ -39356,6 +39392,722 @@ importers: specifier: ^5.9.3 version: 5.9.3 + ../../services/gitlab/gitlab: + dependencies: + '@hcengineering/activity': + specifier: workspace:^0.7.0 + version: link:../../../plugins/activity + '@hcengineering/contact': + specifier: workspace:^0.7.0 + version: link:../../../plugins/contact + '@hcengineering/core': + specifier: workspace:^0.7.26 + version: link:../../../foundations/core/packages/core + '@hcengineering/platform': + specifier: workspace:^0.7.20 + version: link:../../../foundations/core/packages/platform + '@hcengineering/preference': + specifier: workspace:^0.7.0 + version: link:../../../plugins/preference + '@hcengineering/task': + specifier: workspace:^0.7.0 + version: link:../../../plugins/task + '@hcengineering/time': + specifier: workspace:^0.7.0 + version: link:../../../plugins/time + '@hcengineering/tracker': + specifier: workspace:^0.7.0 + version: link:../../../plugins/tracker + '@hcengineering/ui': + specifier: workspace:^0.7.0 + version: link:../../../packages/ui + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.21 + version: link:../../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.67.0 + version: 8.71.1(@typescript-eslint/parser@8.71.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.67.0 + version: 8.71.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint: + specifier: ^9.39.5 + version: 9.39.5(jiti@2.7.0) + eslint-config-love: + specifier: ^157.0.0 + version: 157.0.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-n: + specifier: ^18.3.0 + version: 18.4.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-promise: + specifier: ^7.3.0 + version: 7.3.0(eslint@9.39.5(jiti@2.7.0)) + eslint-plugin-svelte: + specifier: ^3.23.0 + version: 3.23.0(eslint@9.39.5(jiti@2.7.0))(svelte@4.2.20)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)) + jest: + specifier: ^30.4.2 + version: 30.5.2(@types/node@24.13.6)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.4.12 + version: 29.4.14(@babel/core@7.28.5)(@jest/transform@30.5.2)(@jest/types@30.5.1)(babel-jest@30.5.2(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.5.1)(jest@30.5.2(@types/node@24.13.6)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../services/gitlab/gitlab-assets: + dependencies: + '@hcengineering/gitlab': + specifier: workspace:^0.7.0 + version: link:../gitlab + '@hcengineering/platform': + specifier: workspace:^0.7.20 + version: link:../../../foundations/core/packages/platform + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.21 + version: link:../../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@types/node': + specifier: ^24.13.3 + version: 24.13.6 + '@typescript-eslint/eslint-plugin': + specifier: ^8.67.0 + version: 8.71.1(@typescript-eslint/parser@8.71.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.67.0 + version: 8.71.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint: + specifier: ^9.39.5 + version: 9.39.5(jiti@2.7.0) + eslint-config-love: + specifier: ^157.0.0 + version: 157.0.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-n: + specifier: ^18.3.0 + version: 18.4.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-promise: + specifier: ^7.3.0 + version: 7.3.0(eslint@9.39.5(jiti@2.7.0)) + eslint-plugin-svelte: + specifier: ^3.23.0 + version: 3.23.0(eslint@9.39.5(jiti@2.7.0))(svelte@4.2.20)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)) + jest: + specifier: ^30.4.2 + version: 30.5.2(@types/node@24.13.6)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.4.12 + version: 29.4.14(@babel/core@7.28.5)(@jest/transform@30.5.2)(@jest/types@30.5.1)(babel-jest@30.5.2(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.5.1)(jest@30.5.2(@types/node@24.13.6)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../services/gitlab/gitlab-resources: + dependencies: + '@hcengineering/account-client': + specifier: workspace:^0.7.25 + version: link:../../../foundations/core/packages/account-client + '@hcengineering/activity-resources': + specifier: workspace:^0.7.0 + version: link:../../../plugins/activity-resources + '@hcengineering/analytics': + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/analytics + '@hcengineering/contact': + specifier: workspace:^0.7.0 + version: link:../../../plugins/contact + '@hcengineering/contact-resources': + specifier: workspace:^0.7.0 + version: link:../../../plugins/contact-resources + '@hcengineering/core': + specifier: workspace:^0.7.26 + version: link:../../../foundations/core/packages/core + '@hcengineering/diffview': + specifier: workspace:^0.7.0 + version: link:../../../plugins/diffview + '@hcengineering/diffview-resources': + specifier: workspace:^0.7.0 + version: link:../../../plugins/diffview-resources + '@hcengineering/gitlab': + specifier: workspace:^0.7.0 + version: link:../gitlab + '@hcengineering/platform': + specifier: workspace:^0.7.20 + version: link:../../../foundations/core/packages/platform + '@hcengineering/presentation': + specifier: workspace:^0.7.0 + version: link:../../../packages/presentation + '@hcengineering/setting': + specifier: workspace:^0.7.0 + version: link:../../../plugins/setting + '@hcengineering/setting-resources': + specifier: workspace:^0.7.0 + version: link:../../../plugins/setting-resources + '@hcengineering/task': + specifier: workspace:^0.7.0 + version: link:../../../plugins/task + '@hcengineering/text-editor-resources': + specifier: workspace:^0.7.0 + version: link:../../../plugins/text-editor-resources + '@hcengineering/tracker': + specifier: workspace:^0.7.0 + version: link:../../../plugins/tracker + '@hcengineering/tracker-resources': + specifier: workspace:^0.7.0 + version: link:../../../plugins/tracker-resources + '@hcengineering/ui': + specifier: workspace:^0.7.0 + version: link:../../../packages/ui + '@hcengineering/view': + specifier: workspace:^0.7.0 + version: link:../../../plugins/view + '@hcengineering/view-resources': + specifier: workspace:^0.7.0 + version: link:../../../plugins/view-resources + lru-cache: + specifier: ^11.1.0 + version: 11.2.2 + svelte: + specifier: ^4.2.20 + version: 4.2.20 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.21 + version: link:../../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.67.0 + version: 8.71.1(@typescript-eslint/parser@8.71.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.67.0 + version: 8.71.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint: + specifier: ^9.39.5 + version: 9.39.5(jiti@2.7.0) + eslint-config-love: + specifier: ^157.0.0 + version: 157.0.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-n: + specifier: ^18.3.0 + version: 18.4.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-promise: + specifier: ^7.3.0 + version: 7.3.0(eslint@9.39.5(jiti@2.7.0)) + eslint-plugin-svelte: + specifier: ^3.23.0 + version: 3.23.0(eslint@9.39.5(jiti@2.7.0))(svelte@4.2.20)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)) + jest: + specifier: ^30.4.2 + version: 30.5.2(@types/node@24.13.6)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + prettier-plugin-svelte: + specifier: ^3.4.0 + version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) + sass: + specifier: ^1.80.0 + version: 1.93.3 + svelte-check: + specifier: ^3.6.9 + version: 3.8.6(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.28)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)))(postcss@8.5.28)(sass@1.93.3)(svelte@4.2.20) + svelte-eslint-parser: + specifier: ^1.8.1 + version: 1.8.1(svelte@4.2.20) + svelte-loader: + specifier: ^3.2.0 + version: 3.2.4(svelte@4.2.20) + svelte-preprocess: + specifier: ^5.1.4 + version: 5.1.4(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.28)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)))(postcss@8.5.28)(sass@1.93.3)(svelte@4.2.20)(typescript@5.9.3) + ts-jest: + specifier: ^29.4.12 + version: 29.4.14(@babel/core@7.28.5)(@jest/transform@30.5.2)(@jest/types@30.5.1)(babel-jest@30.5.2(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.5.1)(jest@30.5.2(@types/node@24.13.6)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../services/gitlab/model-gitlab: + dependencies: + '@hcengineering/activity': + specifier: workspace:^0.7.0 + version: link:../../../plugins/activity + '@hcengineering/chunter': + specifier: workspace:^0.7.0 + version: link:../../../plugins/chunter + '@hcengineering/contact': + specifier: workspace:^0.7.0 + version: link:../../../plugins/contact + '@hcengineering/core': + specifier: workspace:^0.7.26 + version: link:../../../foundations/core/packages/core + '@hcengineering/gitlab': + specifier: workspace:^0.7.0 + version: link:../gitlab + '@hcengineering/gitlab-resources': + specifier: workspace:^0.7.0 + version: link:../gitlab-resources + '@hcengineering/model': + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/model + '@hcengineering/model-activity': + specifier: workspace:^0.7.0 + version: link:../../../models/activity + '@hcengineering/model-chunter': + specifier: workspace:^0.7.0 + version: link:../../../models/chunter + '@hcengineering/model-contact': + specifier: workspace:^0.7.0 + version: link:../../../models/contact + '@hcengineering/model-core': + specifier: workspace:^0.7.0 + version: link:../../../models/core + '@hcengineering/model-preference': + specifier: workspace:^0.7.0 + version: link:../../../models/preference + '@hcengineering/model-presentation': + specifier: workspace:^0.7.0 + version: link:../../../models/presentation + '@hcengineering/model-time': + specifier: workspace:^0.7.0 + version: link:../../../models/time + '@hcengineering/model-tracker': + specifier: workspace:^0.7.0 + version: link:../../../models/tracker + '@hcengineering/model-view': + specifier: workspace:^0.7.0 + version: link:../../../models/view + '@hcengineering/model-workbench': + specifier: workspace:^0.7.0 + version: link:../../../models/workbench + '@hcengineering/platform': + specifier: workspace:^0.7.20 + version: link:../../../foundations/core/packages/platform + '@hcengineering/preference': + specifier: workspace:^0.7.0 + version: link:../../../plugins/preference + '@hcengineering/setting': + specifier: workspace:^0.7.0 + version: link:../../../plugins/setting + '@hcengineering/tags': + specifier: workspace:^0.7.0 + version: link:../../../plugins/tags + '@hcengineering/task': + specifier: workspace:^0.7.0 + version: link:../../../plugins/task + '@hcengineering/time': + specifier: workspace:^0.7.0 + version: link:../../../plugins/time + '@hcengineering/ui': + specifier: workspace:^0.7.0 + version: link:../../../packages/ui + '@hcengineering/view': + specifier: workspace:^0.7.0 + version: link:../../../plugins/view + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.21 + version: link:../../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@types/node': + specifier: ^24.13.3 + version: 24.13.6 + '@typescript-eslint/eslint-plugin': + specifier: ^8.67.0 + version: 8.71.1(@typescript-eslint/parser@8.71.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.67.0 + version: 8.71.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint: + specifier: ^9.39.5 + version: 9.39.5(jiti@2.7.0) + eslint-config-love: + specifier: ^157.0.0 + version: 157.0.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-n: + specifier: ^18.3.0 + version: 18.4.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-promise: + specifier: ^7.3.0 + version: 7.3.0(eslint@9.39.5(jiti@2.7.0)) + eslint-plugin-svelte: + specifier: ^3.23.0 + version: 3.23.0(eslint@9.39.5(jiti@2.7.0))(svelte@4.2.20)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)) + jest: + specifier: ^30.4.2 + version: 30.5.2(@types/node@24.13.6)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.4.12 + version: 29.4.14(@babel/core@7.28.5)(@jest/transform@30.5.2)(@jest/types@30.5.1)(babel-jest@30.5.2(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.5.1)(jest@30.5.2(@types/node@24.13.6)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../services/gitlab/pod-gitlab: + dependencies: + '@hcengineering/account-client': + specifier: workspace:^0.7.25 + version: link:../../../foundations/core/packages/account-client + '@hcengineering/activity': + specifier: workspace:^0.7.0 + version: link:../../../plugins/activity + '@hcengineering/analytics': + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/analytics + '@hcengineering/analytics-service': + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/analytics-service + '@hcengineering/attachment': + specifier: workspace:^0.7.0 + version: link:../../../plugins/attachment + '@hcengineering/chunter': + specifier: workspace:^0.7.0 + version: link:../../../plugins/chunter + '@hcengineering/client': + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/client + '@hcengineering/client-resources': + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/client-resources + '@hcengineering/collaborator-client': + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/collaborator-client + '@hcengineering/contact': + specifier: workspace:^0.7.0 + version: link:../../../plugins/contact + '@hcengineering/core': + specifier: workspace:^0.7.26 + version: link:../../../foundations/core/packages/core + '@hcengineering/gitlab': + specifier: workspace:^0.7.0 + version: link:../gitlab + '@hcengineering/platform': + specifier: workspace:^0.7.20 + version: link:../../../foundations/core/packages/platform + '@hcengineering/safe-fetch': + specifier: workspace:^0.7.0 + version: link:../../../packages/safe-fetch + '@hcengineering/server-client': + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/client + '@hcengineering/server-core': + specifier: workspace:^0.7.19 + version: link:../../../foundations/server/packages/core + '@hcengineering/server-storage': + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/server-storage + '@hcengineering/server-token': + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/token + '@hcengineering/task': + specifier: workspace:^0.7.0 + version: link:../../../plugins/task + '@hcengineering/text': + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/text + '@hcengineering/text-markdown': + specifier: workspace:^0.7.21 + version: link:../../../foundations/core/packages/text-markdown + '@hcengineering/time': + specifier: workspace:^0.7.0 + version: link:../../../plugins/time + '@hcengineering/tracker': + specifier: workspace:^0.7.0 + version: link:../../../plugins/tracker + body-parser: + specifier: ^1.20.3 + version: 1.20.3 + cors: + specifier: ^2.8.6 + version: 2.8.6 + express: + specifier: ^4.21.2 + version: 4.21.2 + lru-cache: + specifier: ^11.1.0 + version: 11.2.2 + ws: + specifier: ^8.21.0 + version: 8.21.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.21 + version: link:../../../foundations/utils/packages/platform-rig + '@types/body-parser': + specifier: ~1.19.2 + version: 1.19.6 + '@types/cors': + specifier: ^2.8.19 + version: 2.8.19 + '@types/express': + specifier: ^4.17.13 + version: 4.17.25 + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@types/node': + specifier: ^24.13.3 + version: 24.13.6 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + '@typescript-eslint/eslint-plugin': + specifier: ^8.67.0 + version: 8.71.1(@typescript-eslint/parser@8.71.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.67.0 + version: 8.71.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + cross-env: + specifier: ~7.0.3 + version: 7.0.3 + esbuild: + specifier: ^0.25.10 + version: 0.25.12 + eslint: + specifier: ^9.39.5 + version: 9.39.5(jiti@2.7.0) + eslint-config-love: + specifier: ^157.0.0 + version: 157.0.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-n: + specifier: ^18.3.0 + version: 18.4.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-promise: + specifier: ^7.3.0 + version: 7.3.0(eslint@9.39.5(jiti@2.7.0)) + eslint-plugin-svelte: + specifier: ^3.23.0 + version: 3.23.0(eslint@9.39.5(jiti@2.7.0))(svelte@4.2.20)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)) + jest: + specifier: ^30.4.2 + version: 30.5.2(@types/node@24.13.6)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.4.12 + version: 29.4.14(@babel/core@7.28.5)(@jest/transform@30.5.2)(@jest/types@30.5.1)(babel-jest@30.5.2(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.5.1)(jest@30.5.2(@types/node@24.13.6)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)))(typescript@5.9.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@24.13.6)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../services/gitlab/server-gitlab: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.26 + version: link:../../../foundations/core/packages/core + '@hcengineering/gitlab': + specifier: workspace:^0.7.0 + version: link:../gitlab + '@hcengineering/platform': + specifier: workspace:^0.7.20 + version: link:../../../foundations/core/packages/platform + '@hcengineering/server-core': + specifier: workspace:^0.7.19 + version: link:../../../foundations/server/packages/core + '@hcengineering/time': + specifier: workspace:^0.7.0 + version: link:../../../plugins/time + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.21 + version: link:../../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@types/node': + specifier: ^24.13.3 + version: 24.13.6 + '@typescript-eslint/eslint-plugin': + specifier: ^8.67.0 + version: 8.71.1(@typescript-eslint/parser@8.71.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.67.0 + version: 8.71.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint: + specifier: ^9.39.5 + version: 9.39.5(jiti@2.7.0) + eslint-config-love: + specifier: ^157.0.0 + version: 157.0.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-n: + specifier: ^18.3.0 + version: 18.4.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-promise: + specifier: ^7.3.0 + version: 7.3.0(eslint@9.39.5(jiti@2.7.0)) + eslint-plugin-svelte: + specifier: ^3.23.0 + version: 3.23.0(eslint@9.39.5(jiti@2.7.0))(svelte@4.2.20)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)) + jest: + specifier: ^30.4.2 + version: 30.5.2(@types/node@24.13.6)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.4.12 + version: 29.4.14(@babel/core@7.28.5)(@jest/transform@30.5.2)(@jest/types@30.5.1)(babel-jest@30.5.2(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.5.1)(jest@30.5.2(@types/node@24.13.6)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../services/gitlab/server-gitlab-model: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.26 + version: link:../../../foundations/core/packages/core + '@hcengineering/gitlab': + specifier: workspace:^0.7.0 + version: link:../gitlab + '@hcengineering/model': + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/model + '@hcengineering/server-core': + specifier: workspace:^0.7.19 + version: link:../../../foundations/server/packages/core + '@hcengineering/server-gitlab': + specifier: workspace:^0.7.0 + version: link:../server-gitlab + '@hcengineering/time': + specifier: workspace:^0.7.0 + version: link:../../../plugins/time + '@hcengineering/tracker': + specifier: workspace:^0.7.0 + version: link:../../../plugins/tracker + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.21 + version: link:../../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@types/node': + specifier: ^24.13.3 + version: 24.13.6 + '@typescript-eslint/eslint-plugin': + specifier: ^8.67.0 + version: 8.71.1(@typescript-eslint/parser@8.71.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.67.0 + version: 8.71.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint: + specifier: ^9.39.5 + version: 9.39.5(jiti@2.7.0) + eslint-config-love: + specifier: ^157.0.0 + version: 157.0.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-n: + specifier: ^18.3.0 + version: 18.4.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-promise: + specifier: ^7.3.0 + version: 7.3.0(eslint@9.39.5(jiti@2.7.0)) + eslint-plugin-svelte: + specifier: ^3.23.0 + version: 3.23.0(eslint@9.39.5(jiti@2.7.0))(svelte@4.2.20)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)) + jest: + specifier: ^30.4.2 + version: 30.5.2(@types/node@24.13.6)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.4.12 + version: 29.4.14(@babel/core@7.28.5)(@jest/transform@30.5.2)(@jest/types@30.5.1)(babel-jest@30.5.2(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.5.1)(jest@30.5.2(@types/node@24.13.6)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../services/gitlab/server-gitlab-resources: + dependencies: + '@hcengineering/attachment': + specifier: workspace:^0.7.0 + version: link:../../../plugins/attachment + '@hcengineering/chunter': + specifier: workspace:^0.7.0 + version: link:../../../plugins/chunter + '@hcengineering/core': + specifier: workspace:^0.7.26 + version: link:../../../foundations/core/packages/core + '@hcengineering/gitlab': + specifier: workspace:^0.7.0 + version: link:../gitlab + '@hcengineering/server-core': + specifier: workspace:^0.7.19 + version: link:../../../foundations/server/packages/core + '@hcengineering/server-gitlab': + specifier: workspace:^0.7.0 + version: link:../server-gitlab + '@hcengineering/time': + specifier: workspace:^0.7.0 + version: link:../../../plugins/time + '@hcengineering/tracker': + specifier: workspace:^0.7.0 + version: link:../../../plugins/tracker + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.21 + version: link:../../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.67.0 + version: 8.71.1(@typescript-eslint/parser@8.71.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.67.0 + version: 8.71.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint: + specifier: ^9.39.5 + version: 9.39.5(jiti@2.7.0) + eslint-config-love: + specifier: ^157.0.0 + version: 157.0.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-n: + specifier: ^18.3.0 + version: 18.4.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-promise: + specifier: ^7.3.0 + version: 7.3.0(eslint@9.39.5(jiti@2.7.0)) + eslint-plugin-svelte: + specifier: ^3.23.0 + version: 3.23.0(eslint@9.39.5(jiti@2.7.0))(svelte@4.2.20)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)) + jest: + specifier: ^30.4.2 + version: 30.5.2(@types/node@24.13.6)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.4.12 + version: 29.4.14(@babel/core@7.28.5)(@jest/transform@30.5.2)(@jest/types@30.5.1)(babel-jest@30.5.2(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.5.1)(jest@30.5.2(@types/node@24.13.6)(ts-node@10.9.2(@types/node@24.13.6)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + ../../services/gmail/pod-gmail: dependencies: '@hcengineering/account-client': diff --git a/desktop/package.json b/desktop/package.json index 65d8217788..07a8cb28c1 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -186,6 +186,9 @@ "@hcengineering/github": "workspace:^0.7.0", "@hcengineering/github-assets": "workspace:^0.7.0", "@hcengineering/github-resources": "workspace:^0.7.0", + "@hcengineering/gitlab": "workspace:^0.7.0", + "@hcengineering/gitlab-assets": "workspace:^0.7.0", + "@hcengineering/gitlab-resources": "workspace:^0.7.0", "@hcengineering/desktop-preferences": "workspace:^0.7.0", "@hcengineering/desktop-preferences-assets": "workspace:^0.7.0", "@hcengineering/desktop-preferences-resources": "workspace:^0.7.0", diff --git a/desktop/src/ui/platform.ts b/desktop/src/ui/platform.ts index ef6171df34..3a6433b426 100644 --- a/desktop/src/ui/platform.ts +++ b/desktop/src/ui/platform.ts @@ -226,6 +226,9 @@ import { ipcMainExposed } from './typesUtils' import github, { githubId } from '@hcengineering/github' import '@hcengineering/github-assets' import * as GithubLang from '@hcengineering/github-assets/lang' +import gitlab, { gitlabId } from '@hcengineering/gitlab' +import '@hcengineering/gitlab-assets' +import * as GitlabLang from '@hcengineering/gitlab-assets/lang' import { preferenceId } from '@hcengineering/preference' import { uiId } from '@hcengineering/ui/src/plugin' @@ -276,6 +279,7 @@ function configureI18n (): void { addStringsLoader(documentId, DocumentLang.loadLang) addStringsLoader(timeId, TimeLang.loadLang) addStringsLoader(githubId, GithubLang.loadLang) + addStringsLoader(gitlabId, GitlabLang.loadLang) addStringsLoader(documentsId, ControlledDocumentsLang.loadLang) addStringsLoader(productsId, ProductsLang.loadLang) addStringsLoader(questionsId, QuestionsLang.loadLang) @@ -363,6 +367,7 @@ export async function configurePlatform (onWorkbenchConnect?: () => Promise Promise await import('@hcengineering/recorder-resources')) addLocation(presenceId, async () => await import('@hcengineering/presence-resources')) addLocation(githubId, async () => await import(/* webpackChunkName: "github" */ '@hcengineering/github-resources')) + addLocation(gitlabId, async () => await import(/* webpackChunkName: "gitlab" */ '@hcengineering/gitlab-resources')) addLocation( desktopPreferencesId, async () => diff --git a/desktop/src/ui/types.ts b/desktop/src/ui/types.ts index 74a2e143b2..c72c91994c 100644 --- a/desktop/src/ui/types.ts +++ b/desktop/src/ui/types.ts @@ -40,6 +40,7 @@ export interface Config { GITHUB_APP: string GITHUB_CLIENTID: string GITHUB_URL: string + GITLAB_URL?: string GMAIL_URL: string INITIAL_URL: string LINK_PREVIEW_URL?: string diff --git a/dev/docker-compose.gitlab.yaml b/dev/docker-compose.gitlab.yaml new file mode 100644 index 0000000000..d90c1ac2be --- /dev/null +++ b/dev/docker-compose.gitlab.yaml @@ -0,0 +1,36 @@ +# Dev overlay to run the GitLab integration service. +# +# Usage from the dev/ directory: +# docker compose -f docker-compose.yaml -f docker-compose.gitlab.yaml up -d +# +# Build the image first: rush docker:build --to @hcengineering/pod-gitlab +# front already points GITLAB_URL at http://huly.local:3600 in docker-compose.yaml. +# GitLab.com cannot reach huly.local: set GITLAB_WEBHOOK_BASE_URL to a tunnel URL (smee, ngrok) to receive webhooks. +# +services: + gitlab: + image: platformcollective/gitlab + extra_hosts: + - 'huly.local:host-gateway' + restart: unless-stopped + ports: + - 3600:3600 + environment: + - PORT=3600 + - SERVER_SECRET=secret + - ACCOUNTS_URL=http://huly.local:3000 + - FRONT_URL=http://huly.local:8087 + - COLLABORATOR_URL=ws://huly.local:3078 + # Blob storage for merge request diffs (same as the other services); unset = no diffs + - STORAGE_CONFIG=${STORAGE_CONFIG} + # OAuth callback; register the same URL in the GitLab application. Empty = {FRONT_URL}/gitlab + - GITLAB_REDIRECT_URI=${GITLAB_REDIRECT_URI:-} + - WEBHOOK_BASE_URL=${GITLAB_WEBHOOK_BASE_URL:-http://huly.local:3600} + - WEBHOOK_SECRET=${GITLAB_WEBHOOK_SECRET:-secret} + # Dev only: lets a self-managed GitLab on http://localhost or *.local be used + - GITLAB_ALLOW_INSECURE_HOSTS=true + - GITLAB_READONLY=${GITLAB_READONLY:-false} + # Part B: comma list of trusted GitLab hosts (private addresses allowed); per-request timeout + - GITLAB_ALLOWED_HOSTS=${GITLAB_ALLOWED_HOSTS:-} + - GITLAB_REQUEST_TIMEOUT_MS=${GITLAB_REQUEST_TIMEOUT_MS:-30000} + - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 7bd59d7873..c46b75638c 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -282,6 +282,7 @@ services: - COLLABORATOR_URL=ws://huly.local:3078 - STORAGE_CONFIG=${STORAGE_CONFIG} - GITHUB_URL=http://huly.local:3500 + - GITLAB_URL=http://huly.local:3600 - PRINT_URL=http://huly.local:4005 - SIGN_URL=http://huly.local:4006 # - ANALYTICS_COLLECTOR_URL=http://huly.local:4017 diff --git a/dev/prod/package.json b/dev/prod/package.json index 09a9e440ec..201923ebfb 100644 --- a/dev/prod/package.json +++ b/dev/prod/package.json @@ -132,6 +132,9 @@ "@hcengineering/github": "workspace:^0.7.0", "@hcengineering/github-assets": "workspace:^0.7.0", "@hcengineering/github-resources": "workspace:^0.7.0", + "@hcengineering/gitlab": "workspace:^0.7.0", + "@hcengineering/gitlab-assets": "workspace:^0.7.0", + "@hcengineering/gitlab-resources": "workspace:^0.7.0", "@hcengineering/gmail": "workspace:^0.7.0", "@hcengineering/gmail-assets": "workspace:^0.7.0", "@hcengineering/gmail-resources": "workspace:^0.7.0", diff --git a/dev/prod/public/config.json b/dev/prod/public/config.json index 8a3b72d286..26ac455fb5 100644 --- a/dev/prod/public/config.json +++ b/dev/prod/public/config.json @@ -12,6 +12,7 @@ "GITHUB_APP": "uberflow-dev", "GITHUB_CLIENTID": "Iv1.43f9cac43bd68617", "GITHUB_URL": "http://huly.local:3500", + "GITLAB_URL": "http://huly.local:3600", "LAST_NAME_FIRST": "true", "PRINT_URL": "http://huly.local:4005", "SIGN_URL": "http://huly.local:4006", diff --git a/dev/prod/src/platform.ts b/dev/prod/src/platform.ts index 6b410e07b8..323b95a0e3 100644 --- a/dev/prod/src/platform.ts +++ b/dev/prod/src/platform.ts @@ -75,6 +75,7 @@ import workbench, { workbenchId } from '@hcengineering/workbench' import { mailId } from '@hcengineering/mail' import { chatId } from '@hcengineering/chat' import github, { githubId } from '@hcengineering/github' +import gitlab, { gitlabId } from '@hcengineering/gitlab' import { bitrixId } from '@hcengineering/bitrix' import { inboxId } from '@hcengineering/inbox' import { achievementId } from '@hcengineering/achievement' @@ -191,6 +192,8 @@ import '@hcengineering/mail-assets' import * as MailLang from '@hcengineering/mail-assets/lang' import '@hcengineering/github-assets' import * as GithubLang from '@hcengineering/github-assets/lang' +import '@hcengineering/gitlab-assets' +import * as GitlabLang from '@hcengineering/gitlab-assets/lang' import '@hcengineering/achievement-assets' import * as AchievementLang from '@hcengineering/achievement-assets/lang' import '@hcengineering/communication-assets' @@ -235,6 +238,7 @@ export interface Config { GITHUB_APP?: string GITHUB_CLIENTID?: string GITHUB_URL: string + GITLAB_URL?: string LOVE_ENDPOINT?: string LIVEKIT_WS?: string SIGN_URL?: string @@ -384,6 +388,7 @@ function configureI18n(): void { addStringsLoader(documentId, DocumentLang.loadLang) addStringsLoader(timeId, TimeLang.loadLang) addStringsLoader(githubId, GithubLang.loadLang) + addStringsLoader(gitlabId, GitlabLang.loadLang) addStringsLoader(documentsId, ControlledDocumentsLang.loadLang) addStringsLoader(productsId, ProductsLang.loadLang) addStringsLoader(questionsId, QuestionsLang.loadLang) @@ -523,6 +528,7 @@ export async function configurePlatform() { setMetadata(github.metadata.GithubApplication, config.GITHUB_APP ?? '') setMetadata(github.metadata.GithubClientID, config.GITHUB_CLIENTID ?? '') setMetadata(github.metadata.GithubURL, config.GITHUB_URL) + setMetadata(gitlab.metadata.GitlabURL, config.GITLAB_URL ?? '') setMetadata(rekoni.metadata.RekoniUrl, config.REKONI_URL) @@ -558,6 +564,7 @@ export async function configurePlatform() { [loginId, login.component.LoginApp], [onboardId, onboard.component.OnboardApp], [githubId, github.component.ConnectApp], + [gitlabId, gitlab.component.ConnectApp], [calendarId, calendar.component.ConnectApp], [guestId, guest.component.GuestApp], [globalProfileRoute, globalProfile.component.GlobalProfileApp], @@ -643,6 +650,7 @@ export async function configurePlatform() { async () => await import(/* webpackChunkName: "document" */ '@hcengineering/document-resources') ) addLocation(githubId, async () => await import(/* webpackChunkName: "github" */ '@hcengineering/github-resources')) + addLocation(gitlabId, async () => await import(/* webpackChunkName: "gitlab" */ '@hcengineering/gitlab-resources')) addLocation( questionsId, async () => await import(/* webpackChunkName: "training" */ '@hcengineering/questions-resources') diff --git a/foundations/core/packages/core/src/classes.ts b/foundations/core/packages/core/src/classes.ts index 11049aa3fe..c16df47941 100644 --- a/foundations/core/packages/core/src/classes.ts +++ b/foundations/core/packages/core/src/classes.ts @@ -1010,6 +1010,7 @@ export interface WorkspaceMemberInfo { export enum SocialIdType { EMAIL = 'email', GITHUB = 'github', + GITLAB = 'gitlab', GOOGLE = 'google', PHONE = 'phone', OIDC = 'oidc', diff --git a/models/all/package.json b/models/all/package.json index 2234c1a6a5..f974f08bd3 100644 --- a/models/all/package.json +++ b/models/all/package.json @@ -105,6 +105,8 @@ "@hcengineering/model-server-document": "workspace:^0.7.0", "@hcengineering/model-github": "workspace:^0.7.0", "@hcengineering/server-github-model": "workspace:^0.7.0", + "@hcengineering/server-gitlab-model": "workspace:^0.7.0", + "@hcengineering/model-gitlab": "workspace:^0.7.0", "@hcengineering/model-love": "workspace:^0.7.0", "@hcengineering/model-server-love": "workspace:^0.7.0", "@hcengineering/model-print": "workspace:^0.7.0", diff --git a/models/all/src/index.ts b/models/all/src/index.ts index e483fe8848..2dd62d39a5 100644 --- a/models/all/src/index.ts +++ b/models/all/src/index.ts @@ -89,6 +89,8 @@ import { serverDocumentId, createModel as serverDocumentModel } from '@hcenginee import github, { githubId, createModel as githubModel } from '@hcengineering/model-github' import { serverGithubId, createModel as serverGithubModel } from '@hcengineering/server-github-model' +import { serverGitlabId, createModel as serverGitlabModel } from '@hcengineering/server-gitlab-model' +import gitlab, { gitlabId, createModel as gitlabModel } from '@hcengineering/model-gitlab' import { analyticsCollectorId, createModel as analyticsCollectorModel } from '@hcengineering/model-analytics-collector' import { exportId, createModel as exportModel } from '@hcengineering/model-export' @@ -387,6 +389,18 @@ export default function buildModel (): Builder { classFilter: defaultFilter } ], + [ + gitlabModel, + gitlabId, + { + label: gitlab.string.ConfigLabel, + description: gitlab.string.ConfigDescription, + enabled: false, + beta: true, + icon: gitlab.icon.Gitlab, + classFilter: defaultFilter + } + ], [ loveModel, loveId, @@ -561,6 +575,7 @@ export default function buildModel (): Builder { [serverActivityModel, serverActivityId], [serverDocumentModel, serverDocumentId], [serverGithubModel, serverGithubId], + [serverGitlabModel, serverGitlabId], [serverLoveModel, serverLoveId], [serverTimeModel, serverTimeId], [serverGuestModel, serverGuestId], diff --git a/packages/safe-fetch/src/__tests__/ranges.test.ts b/packages/safe-fetch/src/__tests__/ranges.test.ts index 75f1f01835..dffd2d7a18 100644 --- a/packages/safe-fetch/src/__tests__/ranges.test.ts +++ b/packages/safe-fetch/src/__tests__/ranges.test.ts @@ -36,6 +36,7 @@ describe('isBlockedAddress', () => { '64:ff9b::8.8.8.8', '64:ff9b:1::a00:1', '64:ff9b:1:ffff::808:808', + '100::1', '::ffff:0:a00:1', '2002:0a00:0001::', '2002:0808:0808::', diff --git a/packages/safe-fetch/src/ranges.ts b/packages/safe-fetch/src/ranges.ts index f2117131cf..98dbd72b40 100644 --- a/packages/safe-fetch/src/ranges.ts +++ b/packages/safe-fetch/src/ranges.ts @@ -30,6 +30,7 @@ export const BLOCKED_IPV6_RANGES: readonly string[] = [ '::1/128', // loopback '64:ff9b::/96', // NAT64 well-known prefix, blocked as a whole '64:ff9b:1::/48', // NAT64 local-use prefix (RFC 8215), blocked as a whole + '100::/64', // discard-only (RFC 6666) '2001::/32', // Teredo, blocked as a whole '2002::/16', // 6to4, blocked as a whole 'fc00::/7', // unique local diff --git a/plugins/diffview-resources/src/components/FileDiffView.svelte b/plugins/diffview-resources/src/components/FileDiffView.svelte index cb78566253..64882a9fc4 100644 --- a/plugins/diffview-resources/src/components/FileDiffView.svelte +++ b/plugins/diffview-resources/src/components/FileDiffView.svelte @@ -40,7 +40,7 @@ let hideDiffLabel = getHideDiffLabel(file) function getHideDiffLabel(file: DiffFile): IntlString | undefined { - if (diffRenderLimit >= 0 && file.stats.addedLines + file.stats.addedLines > diffRenderLimit) { + if (diffRenderLimit >= 0 && file.stats.addedLines + file.stats.deletedLines > diffRenderLimit) { return diffview.string.LargeDiffsAreHidden } if (file.diffType === 'delete') { diff --git a/plugins/diffview-resources/src/index.ts b/plugins/diffview-resources/src/index.ts index d8b26ad4b7..9d213bbf1d 100644 --- a/plugins/diffview-resources/src/index.ts +++ b/plugins/diffview-resources/src/index.ts @@ -19,6 +19,8 @@ import DiffView from './components/DiffView.svelte' import Highlight from './components/Highlight.svelte' import InlineDiffView from './components/InlineDiffView.svelte' +export { parseDiff, type ParseDiffOptions } from './parser' + export default async (): Promise => ({ component: { DiffView, diff --git a/plugins/diffview-resources/src/parser.ts b/plugins/diffview-resources/src/parser.ts index c94c82d8f5..56b0932f8d 100644 --- a/plugins/diffview-resources/src/parser.ts +++ b/plugins/diffview-resources/src/parser.ts @@ -36,8 +36,19 @@ const diff2htmlConfig: Diff2HtmlConfig = { /** * @public */ -export function parseDiff (diff: Diff): DiffFile[] { - const files = parse(diff, diff2htmlConfig) +export interface ParseDiffOptions { + /** Files with more changed lines come back without hunks (isTooBig); null: no limit. Default 10000 */ + maxChanges?: number | null +} + +/** + * @public + */ +export function parseDiff (diff: Diff, options: ParseDiffOptions = {}): DiffFile[] { + // diff2html reads an undefined diffMaxChanges as no limit + const diffMaxChanges = + options.maxChanges === null ? undefined : (options.maxChanges ?? diff2htmlConfig.diffMaxChanges) + const files = parse(diff, { ...diff2htmlConfig, diffMaxChanges }) return files.map(mapFile) } diff --git a/pods/front/src/__start.ts b/pods/front/src/__start.ts index fc8cd76377..60a6072172 100644 --- a/pods/front/src/__start.ts +++ b/pods/front/src/__start.ts @@ -32,6 +32,7 @@ startFront(metricsContext, { INTERCOM_APP_ID: process.env.INTERCOM_APP_ID ?? '', INTERCOM_API_URL: process.env.INTERCOM_API_URL ?? '', GITHUB_URL: process.env.GITHUB_URL ?? '', + GITLAB_URL: process.env.GITLAB_URL ?? '', LIVEKIT_WS: process.env.LIVEKIT_WS ?? '', LOVE_ENDPOINT: process.env.LOVE_ENDPOINT ?? '', SIGN_URL: process.env.SIGN_URL ?? '', diff --git a/rush.json b/rush.json index e5cd93a1f9..9a818338d1 100644 --- a/rush.json +++ b/rush.json @@ -2176,6 +2176,46 @@ "projectFolder": "services/github/server-github-model", "shouldPublish": false }, + { + "packageName": "@hcengineering/gitlab", + "projectFolder": "services/gitlab/gitlab", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/gitlab-assets", + "projectFolder": "services/gitlab/gitlab-assets", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/pod-gitlab", + "projectFolder": "services/gitlab/pod-gitlab", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/model-gitlab", + "projectFolder": "services/gitlab/model-gitlab", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/gitlab-resources", + "projectFolder": "services/gitlab/gitlab-resources", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/server-gitlab", + "projectFolder": "services/gitlab/server-gitlab", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/server-gitlab-resources", + "projectFolder": "services/gitlab/server-gitlab-resources", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/server-gitlab-model", + "projectFolder": "services/gitlab/server-gitlab-model", + "shouldPublish": false + }, { "packageName": "@hcengineering/desktop-preferences", "projectFolder": "plugins/desktop-preferences", diff --git a/server/account/src/__tests__/operations.test.ts b/server/account/src/__tests__/operations.test.ts index 6faf413e23..705ff099b4 100644 --- a/server/account/src/__tests__/operations.test.ts +++ b/server/account/src/__tests__/operations.test.ts @@ -61,7 +61,8 @@ import { leaveWorkspace, checkJoin, mergeSpecifiedPersons, - canMergeSpecifiedPersons + canMergeSpecifiedPersons, + ensurePerson } from '../operations' import { accountPlugin } from '../plugin' @@ -3831,3 +3832,30 @@ describe('merge specified persons', () => { }) }) }) + +describe('ensurePerson', () => { + const mockCtx = { + error: jest.fn(), + info: jest.fn(), + warn: jest.fn() + } as unknown as MeasureContext + + test('should let the gitlab service ensure a person without a workspace role check', async () => { + ;(decodeTokenVerbose as jest.Mock).mockReturnValue({ account: 'svc', extra: { service: 'gitlab' } }) + const roleSpy = jest.spyOn(utils, 'getWorkspaceRole') + const mockDb = { + socialId: { findOne: jest.fn(async () => ({ _id: 'sid-1' as PersonId, personUuid: 'p-1' as PersonUuid })) } + } as unknown as AccountDB + + const result = await ensurePerson(mockCtx, mockDb, null, 'test-token', { + socialType: SocialIdType.GITLAB, + socialValue: '42@gitlab.com', + firstName: 'Alice', + lastName: '' + }) + + expect(result).toEqual({ uuid: 'p-1', socialId: 'sid-1' }) + expect(roleSpy).not.toHaveBeenCalled() + roleSpy.mockRestore() + }) +}) diff --git a/server/account/src/__tests__/postgres.test.ts b/server/account/src/__tests__/postgres.test.ts index 68ea6caa8a..b5abec9afc 100644 --- a/server/account/src/__tests__/postgres.test.ts +++ b/server/account/src/__tests__/postgres.test.ts @@ -21,6 +21,7 @@ import { type WorkspaceUuid } from '@hcengineering/core' import { AccountPostgresDbCollection, PostgresAccountDB, PostgresDbCollection } from '../collections/postgres/postgres' +import { getMigrations } from '../collections/postgres/migrations' import { type Sql } from 'postgres' interface TestWorkspace { @@ -1030,6 +1031,16 @@ describe('PostgresAccountDB', () => { }) }) +describe('getMigrations', () => { + it.each(['postgres', 'cockroach'] as const)('adds the gitlab social id type on %s', (flavor) => { + const migration = getMigrations('global_account', flavor).find( + ([key]) => key === 'account_db_v30_add_gitlab_social_id_type' + ) + expect(migration).toBeDefined() + expect(migration?.[1]).toContain("'gitlab'") + }) +}) + describe('PostgresAccountDB.hasWorkspacePermission', () => { function createDb (rows: any[]): PostgresAccountDB { const mock: any = Object.assign(jest.fn(), { unsafe: jest.fn().mockResolvedValue(rows) }) diff --git a/server/account/src/__tests__/serviceOperations.test.ts b/server/account/src/__tests__/serviceOperations.test.ts index 7d11125a32..05fc86b31b 100644 --- a/server/account/src/__tests__/serviceOperations.test.ts +++ b/server/account/src/__tests__/serviceOperations.test.ts @@ -116,6 +116,34 @@ describe('addSocialIdToPerson', () => { ) }) + test('should allow gitlab service to add social id', async () => { + ;(decodeTokenVerbose as jest.Mock).mockReturnValue({ + extra: { service: 'gitlab' } + }) + const newSocialId = 'new-social-id' as PersonId + addSocialIdSpy.mockResolvedValue(newSocialId) + + const params = { + person: 'test-person' as PersonUuid, + type: SocialIdType.GITLAB, + value: '42@gitlab.com', + confirmed: true, + displayValue: 'alice' + } + + const result = await addSocialIdToPerson(mockCtx, mockDb, mockBranding, mockToken, params) + + expect(result).toBe(newSocialId) + expect(addSocialIdSpy).toHaveBeenCalledWith( + mockDb, + params.person, + params.type, + params.value, + params.confirmed, + params.displayValue + ) + }) + test('should allow admin to add social id', async () => { ;(decodeTokenVerbose as jest.Mock).mockReturnValue({ extra: { admin: 'true' } diff --git a/server/account/src/__tests__/utils.test.ts b/server/account/src/__tests__/utils.test.ts index 591b662146..cc2142175c 100644 --- a/server/account/src/__tests__/utils.test.ts +++ b/server/account/src/__tests__/utils.test.ts @@ -56,6 +56,7 @@ import { cleanExpiredOtp, getWorkspaces, verifyAllowedServices, + integrationServices, getPersonName, getInviteEmail, getFrontUrl, @@ -2541,6 +2542,12 @@ describe('account utils', () => { }) }) }) + + describe('integrationServices', () => { + it('allows the gitlab service to manage integrations', () => { + expect(integrationServices).toContain('gitlab') + }) + }) }) describe('token log redaction', () => { diff --git a/server/account/src/collections/postgres/migrations.ts b/server/account/src/collections/postgres/migrations.ts index e46c18af01..7a7c206af0 100644 --- a/server/account/src/collections/postgres/migrations.ts +++ b/server/account/src/collections/postgres/migrations.ts @@ -86,7 +86,8 @@ export function getMigrations (ns: string, flavor: DBFlavor): [string, string][] getV26Migration(ns, flavor), getV27Migration(ns, flavor), getV28Migration(ns, flavor), - getV29Migration(ns, flavor) + getV29Migration(ns, flavor), + getV30Migration(ns, flavor) ] } @@ -885,3 +886,28 @@ function getV29Migration (ns: string, flavor: DBFlavor): [string, string] { ` ] } + +function getV30Migration (ns: string, flavor: DBFlavor): [string, string] { + // For PostgreSQL, we need to check if the value exists before adding it + const addValueSql = + flavor === 'postgres' + ? ` + -- Add gitlab value to social_id_type enum (PostgreSQL) + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum + WHERE enumlabel = 'gitlab' + AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'social_id_type' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = '${ns}')) + ) THEN + ALTER TYPE ${ns}.social_id_type ADD VALUE 'gitlab'; + END IF; + END $$; + ` + : ` + -- Add gitlab value to social_id_type enum (CockroachDB) + ALTER TYPE ${ns}.social_id_type ADD VALUE IF NOT EXISTS 'gitlab'; + ` + + return ['account_db_v30_add_gitlab_social_id_type', addValueSql] +} diff --git a/server/account/src/operations.ts b/server/account/src/operations.ts index b31e4af595..dcaf1d6058 100644 --- a/server/account/src/operations.ts +++ b/server/account/src/operations.ts @@ -2577,7 +2577,7 @@ export async function ensurePerson ( ): Promise<{ uuid: PersonUuid, socialId: PersonId }> { const { account, workspace, extra } = decodeTokenVerbose(ctx, token) const allowedService = verifyAllowedServices( - ['tool', 'workspace', 'schedule', 'mail', 'github', 'hulygram'], + ['tool', 'workspace', 'schedule', 'mail', 'github', 'gitlab', 'hulygram'], extra, false ) diff --git a/server/account/src/serviceOperations.ts b/server/account/src/serviceOperations.ts index f2c201212b..8f30bc33ef 100644 --- a/server/account/src/serviceOperations.ts +++ b/server/account/src/serviceOperations.ts @@ -618,7 +618,7 @@ export async function addSocialIdToPerson ( if (extra?.admin !== 'true') { verifyAllowedServices( - ['github', 'telegram-bot', 'gmail', 'tool', 'workspace', 'hulygram', 'google-calendar', 'ai-assistant'], + ['github', 'gitlab', 'telegram-bot', 'gmail', 'tool', 'workspace', 'hulygram', 'google-calendar', 'ai-assistant'], extra ) } diff --git a/server/account/src/utils.ts b/server/account/src/utils.ts index 6f7b817f90..45d85e1149 100644 --- a/server/account/src/utils.ts +++ b/server/account/src/utils.ts @@ -2132,6 +2132,7 @@ export async function setTimezone ( // Move to config? export const integrationServices = [ 'github', + 'gitlab', 'telegram-bot', 'hulygram', 'mailbox', diff --git a/server/server-pipeline/package.json b/server/server-pipeline/package.json index edffb9ed81..f054fc92ec 100644 --- a/server/server-pipeline/package.json +++ b/server/server-pipeline/package.json @@ -71,6 +71,8 @@ "@hcengineering/server-calendar-resources": "workspace:^0.7.0", "@hcengineering/server-github": "workspace:^0.7.0", "@hcengineering/server-github-resources": "workspace:^0.7.0", + "@hcengineering/server-gitlab": "workspace:^0.7.0", + "@hcengineering/server-gitlab-resources": "workspace:^0.7.0", "@hcengineering/server-gmail": "workspace:^0.7.0", "@hcengineering/server-gmail-resources": "workspace:^0.7.0", "@hcengineering/server-preference": "workspace:^0.7.0", @@ -170,6 +172,8 @@ "@hcengineering/training": "workspace:^0.7.0", "@hcengineering/github": "workspace:^0.7.0", "@hcengineering/github-assets": "workspace:^0.7.0", + "@hcengineering/gitlab": "workspace:^0.7.0", + "@hcengineering/gitlab-assets": "workspace:^0.7.0", "@hcengineering/server-ai-bot": "workspace:^0.7.0", "@hcengineering/server-ai-bot-resources": "workspace:^0.7.0", "@hcengineering/time": "workspace:^0.7.0", diff --git a/server/server-pipeline/src/internationalization.ts b/server/server-pipeline/src/internationalization.ts index e24082a7c4..160dc4b95e 100644 --- a/server/server-pipeline/src/internationalization.ts +++ b/server/server-pipeline/src/internationalization.ts @@ -11,6 +11,7 @@ import { documentId } from '@hcengineering/document' import { exportId } from '@hcengineering/export' import { driveId } from '@hcengineering/drive' import { githubId } from '@hcengineering/github' +import { gitlabId } from '@hcengineering/gitlab' import { gmailId } from '@hcengineering/gmail' import { hrId } from '@hcengineering/hr' import { inventoryId } from '@hcengineering/inventory' @@ -54,6 +55,7 @@ import documentEn from '@hcengineering/document-assets/lang/en.json' import exportEn from '@hcengineering/export-assets/lang/en.json' import driveEn from '@hcengineering/drive-assets/lang/en.json' import githubEn from '@hcengineering/github-assets/lang/en.json' +import gitlabEn from '@hcengineering/gitlab-assets/lang/en.json' import gmailEn from '@hcengineering/gmail-assets/lang/en.json' import hrEn from '@hcengineering/hr-assets/lang/en.json' import inventoryEn from '@hcengineering/inventory-assets/lang/en.json' @@ -119,6 +121,7 @@ export function registerStringLoaders (): void { addStringsLoader(productsId, async (lang: string) => productsEn) addStringsLoader(trainingId, async (lang: string) => trainingEn) addStringsLoader(githubId, async (lang: string) => githubEn) + addStringsLoader(gitlabId, async (lang: string) => gitlabEn) addStringsLoader(timeId, async (lang: string) => timeEn) addStringsLoader(surveyId, async (lang: string) => surveyEn) addStringsLoader(chatId, async (lang: string) => chatEn) diff --git a/server/server-pipeline/src/pipeline.ts b/server/server-pipeline/src/pipeline.ts index b3666df7ab..3481948688 100644 --- a/server/server-pipeline/src/pipeline.ts +++ b/server/server-pipeline/src/pipeline.ts @@ -74,6 +74,7 @@ import { createStorageDataAdapter } from './blobStorage' import { CommunicationMiddleware, type CommunicationApiFactory } from './communication' import { RatingMiddleware } from '@hcengineering/server-rating' +import { GitlabIntegrationMiddleware } from '@hcengineering/server-gitlab' /** * @public @@ -164,6 +165,7 @@ export function createServerPipeline ( VersioningMiddleware.create, IdentifierMiddleware.create, // After ApplyTx to ensure that it pass RatingMiddleware.create, // Rating editing restrictions + GitlabIntegrationMiddleware.create, // GitLab connection documents: service-only fields TransientMiddleware.create, TxMiddleware.create, // Store tx into transaction domain ...(opt.disableTriggers === true ? [] : [TriggersMiddleware.create]), diff --git a/server/server-pipeline/src/serverPlugins.ts b/server/server-pipeline/src/serverPlugins.ts index 36298ce9d5..fca468e5f0 100644 --- a/server/server-pipeline/src/serverPlugins.ts +++ b/server/server-pipeline/src/serverPlugins.ts @@ -10,6 +10,7 @@ import { serverDocumentsId } from '@hcengineering/server-controlled-documents' import { serverDocumentId } from '@hcengineering/server-document' import { serverDriveId } from '@hcengineering/server-drive' import { serverGithubId } from '@hcengineering/server-github' +import { serverGitlabId } from '@hcengineering/server-gitlab' import { serverGmailId } from '@hcengineering/server-gmail' import { serverGuestId } from '@hcengineering/server-guest' import { serverHrId } from '@hcengineering/server-hr' @@ -59,6 +60,7 @@ export function registerServerPlugins (): void { addLocation(serverDocumentsId, () => import('@hcengineering/server-controlled-documents-resources')) addLocation(serverTrainingId, () => import('@hcengineering/server-training-resources')) addLocation(serverGithubId, () => import('@hcengineering/server-github-resources')) + addLocation(serverGitlabId, () => import('@hcengineering/server-gitlab-resources')) addLocation(serverAiBotId, () => import('@hcengineering/server-ai-bot-resources')) addLocation(serverProcessId, () => import('@hcengineering/server-process-resources')) } diff --git a/services/gitlab/gitlab-assets/assets/icons.svg b/services/gitlab/gitlab-assets/assets/icons.svg new file mode 100644 index 0000000000..85bee04a59 --- /dev/null +++ b/services/gitlab/gitlab-assets/assets/icons.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/services/gitlab/gitlab-assets/config/rig.json b/services/gitlab/gitlab-assets/config/rig.json new file mode 100644 index 0000000000..b75800b9b7 --- /dev/null +++ b/services/gitlab/gitlab-assets/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "assets" +} diff --git a/services/gitlab/gitlab-assets/eslint.config.mjs b/services/gitlab/gitlab-assets/eslint.config.mjs new file mode 100644 index 0000000000..f7b605c3e9 --- /dev/null +++ b/services/gitlab/gitlab-assets/eslint.config.mjs @@ -0,0 +1,3 @@ +import config from '@hcengineering/platform-rig/profiles/assets/eslint.config.mjs' + +export default config diff --git a/services/gitlab/gitlab-assets/jest.config.js b/services/gitlab/gitlab-assets/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/services/gitlab/gitlab-assets/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/gitlab/gitlab-assets/lang/cs.json b/services/gitlab/gitlab-assets/lang/cs.json new file mode 100644 index 0000000000..a06f7aae15 --- /dev/null +++ b/services/gitlab/gitlab-assets/lang/cs.json @@ -0,0 +1,121 @@ +{ + "string": { + "Gitlab": "GitLab", + "GitlabDesc": "Připojte své projekty GitLab pro synchronizaci úkolů, žádostí o sloučení a aktualizací projektů", + "ConfigLabel": "GitLab", + "ConfigDescription": "Rozšíření pro integraci s GitLab", + "Authorize": "Autorizovat {title} v GitLab", + "ReAuthorize": "Znovu autorizovat {title} v GitLab", + "Authorized": "Připojeno jako {login}", + "NotAuthorized": "Autorizujte {title} pro přístup k vašemu účtu GitLab. Zobrazí se zde projekty, ve kterých máte roli Maintainer.", + "NoRepositories": "Pro tento účet nebyly nalezeny žádné projekty s rolí Maintainer.", + "RefreshRepositories": "Obnovit projekty", + "ImageMode": "Obrázky z GitLabu", + "ImageModeLink": "Odkazovat na GitLab", + "ImageModeCopy": "Kopírovat do pracovního prostoru", + "ImageModeLinkHint": "Obrázky zůstávají na GitLabu. Vidí je jen lidé, kteří mohou otevřít projekt v GitLabu.", + "ImageModeCopyHint": "Obrázky se zkopírují do pracovního prostoru. Vidí je každý, kdo má přístup k propojenému projektu.", + "ImageNoAccess": "K zobrazení tohoto obrázku potřebujete přístup k tomuto projektu v GitLabu.", + "ImageNotConnected": "Připojte svůj účet GitLab, abyste viděli obrázky z GitLabu.", + "ImageUnavailable": "Tento obrázek teď nelze zobrazit.", + "GitlabImages": "Obrázky z GitLabu ({count})", + "OpenInGitlab": "Otevřít v GitLabu", + "LinkToProject": "Propojit s projektem {title}", + "Unlink": "Odpojit", + "LinkedTo": "Propojeno s", + "LinkRequiresConnector": "Propojit může jen člen, který tento účet připojil, nebo správce", + "Unavailable": "Nedostupné", + "Processing": "Zpracování...", + "AutoClose": "Tato záložka se zavře za {time} s", + "RequestFailed": "Požadavek selhal. Zkuste to prosím znovu.", + "CloseTab": "Zavřít záložku", + "KeepSettingsOpen": "Nechte záložku nastavení {title} otevřenou během připojování GitLab a zkuste to znovu", + "Repository": "Projekt GitLab", + "SetupTitle": "Aplikace GitLab", + "SetupWhere": "Vyberte, kde v GitLab aplikaci zaregistrovat:", + "SetupUserOwned": "Uživatelská: v nastavení vašeho uživatelského účtu", + "SetupGroupOwned": "Skupinová (doporučeno pro týmy): v části Settings > Applications dané skupiny", + "SetupInstanceWide": "Pro celou instanci (správci self-managed): v oblasti Admin area", + "SetupTrustedHint": "Zaškrtněte Trusted, aby se uživatelům nezobrazovala obrazovka se souhlasem.", + "SetupNameAndRedirect": "Název: {title}. Redirect URI:", + "SetupConfidentialScopes": "Zaškrtněte Confidential, vyberte scopes {scopes} a aplikaci uložte.", + "SetupCopyCredentials": "Zkopírujte Application ID a Secret níže. GitLab zobrazí secret pouze jednou.", + "GitlabUrl": "URL GitLab", + "SelfManaged": "Vlastní server GitLab (self-managed)", + "ApplicationId": "ID aplikace", + "ApplicationSecret": "Tajný klíč", + "KeepSecretHint": "Nechte prázdné, chcete-li ponechat stávající secret", + "Save": "Uložit", + "Change": "Změnit", + "Remove": "Odebrat", + "Copy": "Kopírovat", + "Copied": "Zkopírováno", + "ConfiguredApp": "Aplikace GitLab {clientId} na {host}", + "OwnerMustConfigure": "Nejprve musí vlastník pracovního prostoru nastavit aplikaci GitLab.", + "InvalidGitlabUrl": "Zadejte URL s https:// (http:// pouze pro localhost nebo hostitele *.local)", + "SetupEnterUrlFirst": "Níže zadejte URL své instance GitLab, abyste viděli, kde aplikaci zaregistrovat.", + "DisconnectEveryone": "Odpojit všechny", + "DisconnectEveryoneConfirm": "Odpojit účty GitLab všech členů od tohoto pracovního prostoru? Jejich propojené projekty budou odpojeny a webhooky budou podle možností odstraněny. Členové se poté mohou připojit znovu.", + "GitlabIssue": "Úkol GitLab", + "IssueConnectedActivityInfo": "Propojeno s úkolem GitLab #{number} v projektu {repoName}", + "MergeRequest": "Žádost o sloučení", + "MergeRequests": "Žádosti o sloučení", + "MergeRequestConnectedActivityInfo": "Propojeno se žádostí o sloučení GitLab !{number} v projektu {repoName}", + "Draft": "Koncept", + "MergedAt": "Datum sloučení", + "ClosedAt": "Datum zavření", + "Commits": "Commity", + "Files": "Soubory", + "SourceBranch": "Zdrojová větev", + "TargetBranch": "Cílová větev", + "MergeStatus": "Stav sloučení", + "MergeRequestState": "Stav", + "Reviewers": "Recenzenti", + "StateOpened": "Otevřeno", + "StateMerged": "Sloučeno", + "StateClosed": "Zavřeno", + "StateLocked": "Uzamčeno", + "ReadyToMerge": "Připraveno ke sloučení", + "Conflict": "Konflikt", + "Checking": "Kontroluje se", + "PipelinePending": "Pipeline musí projít", + "UnresolvedDiscussions": "Nevyřešené diskuse", + "NeedsApproval": "Vyžaduje schválení", + "All": "Vše", + "Active": "Aktivní", + "Closed": "Zavřeno", + "ChangedFiles": "Změněné soubory: {files}", + "WithoutRepository": "Bez repozitáře", + "CreateInGitlab": "Vytvořit v GitLab", + "DiffUnavailable": "Rozdíly se nepodařilo načíst", + "DiffTooLarge": "Příliš mnoho změn pro zobrazení zde.", + "ApprovedBy": "Schválili", + "Approve": "Schválit", + "RevokeApproval": "Odvolat schválení", + "ConnectToApprove": "Pro schvalování připojte svůj účet GitLab", + "Review": "Recenze", + "ReviewThread": "Vlákno recenze", + "ReviewComment": "Komentář k recenzi", + "ReviewComments": "Komentáře k recenzi", + "ReviewApproved": "schválil(a)", + "ReviewUnapproved": "odvolal(a) schválení", + "ReviewRequestedChanges": "požádal(a) o změny", + "ReviewReviewed": "zrecenzoval(a)", + "CommentedOnDiff": "okomentoval(a) {path}", + "ResolveThread": "Vyřešit vlákno", + "UnresolveThread": "Znovu otevřít vlákno", + "ResolvedBy": "Vyřešil(a)", + "Outdated": "Zastaralé", + "SyncError": "Chyba synchronizace s GitLab", + "Retry": "Zkusit znovu", + "ReviewNotSent": "Neodesláno do GitLab", + "LinkFailed": "Projekt GitLab se nepodařilo propojit. Zkuste to znovu.", + "UnlinkFailed": "Projekt GitLab se nepodařilo odpojit. Zkuste to znovu.", + "NotConfigured": "Integrace GitLab není na tomto serveru nastavena.", + "AuthorizeLinkInvalid": "GitLab vrátil neplatný přihlašovací odkaz. Zkuste to znovu.", + "IntegrationRepositories": "Projekty GitLab — {host} / {login}", + "SyncErrorMessage": "Chyba synchronizace s GitLab: {message}", + "UnlinkConfirm": "Odpojit {repository} od {project}? Změny se přestanou synchronizovat, dokud jej znovu nepropojíte.", + "RemoveAppConfirm": "Odebrat aplikaci GitLab z tohoto pracovního prostoru? Členové se ke GitLabu nepřipojí, dokud nebude nastavena nová." + } +} diff --git a/services/gitlab/gitlab-assets/lang/de.json b/services/gitlab/gitlab-assets/lang/de.json new file mode 100644 index 0000000000..83124da7fb --- /dev/null +++ b/services/gitlab/gitlab-assets/lang/de.json @@ -0,0 +1,121 @@ +{ + "string": { + "Gitlab": "GitLab", + "GitlabDesc": "Verbinden Sie Ihre GitLab-Projekte, um Issues, Merge Requests und Projekt-Updates zu synchronisieren", + "ConfigLabel": "GitLab", + "ConfigDescription": "Erweiterung für die GitLab-Integration", + "Authorize": "{title} bei GitLab autorisieren", + "ReAuthorize": "{title} bei GitLab erneut autorisieren", + "Authorized": "Verbunden als {login}", + "NotAuthorized": "Autorisieren Sie {title} für den Zugriff auf Ihr GitLab-Konto. Projekte, in denen Sie die Rolle Maintainer haben, werden hier aufgelistet.", + "NoRepositories": "Für dieses Konto wurden keine Projekte mit der Rolle Maintainer gefunden.", + "RefreshRepositories": "Projekte aktualisieren", + "ImageMode": "Bilder aus GitLab", + "ImageModeLink": "Auf GitLab verlinken", + "ImageModeCopy": "In den Arbeitsbereich kopieren", + "ImageModeLinkHint": "Bilder bleiben auf GitLab. Nur Personen, die das GitLab-Projekt öffnen können, sehen sie.", + "ImageModeCopyHint": "Bilder werden in den Arbeitsbereich kopiert. Alle mit Zugriff auf das verknüpfte Projekt sehen sie.", + "ImageNoAccess": "Sie benötigen Zugriff auf dieses GitLab-Projekt, um dieses Bild zu sehen.", + "ImageNotConnected": "Verbinden Sie Ihr GitLab-Konto, um Bilder aus GitLab zu sehen.", + "ImageUnavailable": "Dieses Bild kann gerade nicht angezeigt werden.", + "GitlabImages": "GitLab-Bilder ({count})", + "OpenInGitlab": "In GitLab öffnen", + "LinkToProject": "Mit {title}-Projekt verknüpfen", + "Unlink": "Verknüpfung aufheben", + "LinkedTo": "Verknüpft mit", + "LinkRequiresConnector": "Nur das Mitglied, das dieses Konto verbunden hat, oder ein Maintainer kann es verknüpfen", + "Unavailable": "Nicht verfügbar", + "Processing": "Wird verarbeitet...", + "AutoClose": "Dieser Tab wird in {time} Sekunden geschlossen", + "RequestFailed": "Anfrage fehlgeschlagen. Bitte versuchen Sie es erneut.", + "CloseTab": "Tab schließen", + "KeepSettingsOpen": "Lassen Sie den Einstellungs-Tab von {title} geöffnet, während Sie GitLab verbinden, und versuchen Sie es dann erneut", + "Repository": "GitLab-Projekt", + "SetupTitle": "GitLab-Anwendung", + "SetupWhere": "Wählen Sie aus, wo die Anwendung in GitLab registriert werden soll:", + "SetupUserOwned": "Benutzereigen: in Ihren Benutzereinstellungen", + "SetupGroupOwned": "Gruppeneigen (empfohlen für Teams): unter Settings > Applications der Gruppe", + "SetupInstanceWide": "Instanzweit (Administratoren von Self-Managed-Instanzen): im Admin-Bereich", + "SetupTrustedHint": "Aktivieren Sie Trusted, um den Zustimmungsbildschirm für Benutzer zu überspringen.", + "SetupNameAndRedirect": "Name: {title}. Weiterleitungs-URI:", + "SetupConfidentialScopes": "Aktivieren Sie Confidential, wählen Sie die Scopes {scopes} aus und speichern Sie dann die Anwendung.", + "SetupCopyCredentials": "Kopieren Sie die Application ID und das Secret unten. GitLab zeigt das Secret nur einmal an.", + "GitlabUrl": "GitLab-URL", + "SelfManaged": "Self-Managed GitLab", + "ApplicationId": "Anwendungs-ID", + "ApplicationSecret": "Geheimer Schlüssel", + "KeepSecretHint": "Leer lassen, um das aktuelle Secret beizubehalten", + "Save": "Speichern", + "Change": "Ändern", + "Remove": "Entfernen", + "Copy": "Kopieren", + "Copied": "Kopiert", + "ConfiguredApp": "GitLab-Anwendung {clientId} auf {host}", + "OwnerMustConfigure": "Ein Eigentümer des Arbeitsbereichs muss zuerst die GitLab-Anwendung einrichten.", + "InvalidGitlabUrl": "Geben Sie eine https://-URL ein (http:// nur für localhost oder *.local-Hosts)", + "SetupEnterUrlFirst": "Geben Sie unten die URL Ihrer GitLab-Instanz ein, um zu sehen, wo die Anwendung registriert werden soll.", + "DisconnectEveryone": "Alle trennen", + "DisconnectEveryoneConfirm": "GitLab-Konten aller Mitglieder von diesem Arbeitsbereich trennen? Ihre verknüpften Projekte werden getrennt und Webhooks nach Möglichkeit entfernt. Mitglieder können sich anschließend erneut verbinden.", + "GitlabIssue": "GitLab-Issue", + "IssueConnectedActivityInfo": "Verbunden mit GitLab-Issue #{number} in {repoName}", + "MergeRequest": "Merge Request", + "MergeRequests": "Merge Requests", + "MergeRequestConnectedActivityInfo": "Verbunden mit GitLab-Merge-Request !{number} in {repoName}", + "Draft": "Entwurf", + "MergedAt": "Zusammengeführt am", + "ClosedAt": "Geschlossen am", + "Commits": "Commits", + "Files": "Dateien", + "SourceBranch": "Quell-Branch", + "TargetBranch": "Ziel-Branch", + "MergeStatus": "Merge-Status", + "MergeRequestState": "Status", + "Reviewers": "Reviewer", + "StateOpened": "Offen", + "StateMerged": "Zusammengeführt", + "StateClosed": "Geschlossen", + "StateLocked": "Gesperrt", + "ReadyToMerge": "Bereit zum Zusammenführen", + "Conflict": "Konflikt", + "Checking": "Wird geprüft", + "PipelinePending": "Pipeline muss erfolgreich sein", + "UnresolvedDiscussions": "Ungelöste Diskussionen", + "NeedsApproval": "Genehmigung erforderlich", + "All": "Alle", + "Active": "Aktiv", + "Closed": "Geschlossen", + "ChangedFiles": "{files} geänderte Dateien", + "WithoutRepository": "Ohne Repository", + "CreateInGitlab": "In GitLab erstellen", + "DiffUnavailable": "Der Diff konnte nicht geladen werden", + "DiffTooLarge": "Zu viele Änderungen, um sie hier anzuzeigen.", + "ApprovedBy": "Genehmigt von", + "Approve": "Genehmigen", + "RevokeApproval": "Genehmigung widerrufen", + "ConnectToApprove": "Verbinden Sie Ihr GitLab-Konto, um zu genehmigen", + "Review": "Review", + "ReviewThread": "Review-Thread", + "ReviewComment": "Review-Kommentar", + "ReviewComments": "Review-Kommentare", + "ReviewApproved": "hat genehmigt", + "ReviewUnapproved": "hat die Genehmigung widerrufen", + "ReviewRequestedChanges": "hat Änderungen angefordert", + "ReviewReviewed": "hat geprüft", + "CommentedOnDiff": "hat {path} kommentiert", + "ResolveThread": "Thread auflösen", + "UnresolveThread": "Thread wieder öffnen", + "ResolvedBy": "Aufgelöst von", + "Outdated": "Veraltet", + "SyncError": "GitLab-Synchronisierungsfehler", + "Retry": "Erneut versuchen", + "ReviewNotSent": "Nicht an GitLab gesendet", + "LinkFailed": "Das GitLab-Projekt konnte nicht verknüpft werden. Bitte erneut versuchen.", + "UnlinkFailed": "Die Verknüpfung des GitLab-Projekts konnte nicht aufgehoben werden. Bitte erneut versuchen.", + "NotConfigured": "Die GitLab-Integration ist auf diesem Server nicht eingerichtet.", + "AuthorizeLinkInvalid": "GitLab hat einen ungültigen Anmeldelink zurückgegeben. Bitte erneut versuchen.", + "IntegrationRepositories": "GitLab-Projekte — {host} / {login}", + "SyncErrorMessage": "GitLab-Synchronisierungsfehler: {message}", + "UnlinkConfirm": "Verknüpfung von {repository} mit {project} aufheben? Änderungen werden erst wieder synchronisiert, wenn es erneut verknüpft wird.", + "RemoveAppConfirm": "GitLab-Anwendung aus diesem Arbeitsbereich entfernen? Mitglieder können GitLab erst wieder verbinden, wenn eine neue eingerichtet ist." + } +} diff --git a/services/gitlab/gitlab-assets/lang/en.json b/services/gitlab/gitlab-assets/lang/en.json new file mode 100644 index 0000000000..06f9b9d941 --- /dev/null +++ b/services/gitlab/gitlab-assets/lang/en.json @@ -0,0 +1,121 @@ +{ + "string": { + "Gitlab": "GitLab", + "GitlabDesc": "Connect your GitLab projects to sync issues, merge requests, and project updates", + "ConfigLabel": "GitLab", + "ConfigDescription": "Extension for GitLab integration", + "Authorize": "Authorize {title} with GitLab", + "ReAuthorize": "Re-authorize {title} with GitLab", + "Authorized": "Connected as {login}", + "NotAuthorized": "Authorize {title} to access your GitLab account. Projects where you have the Maintainer role will be listed here.", + "NoRepositories": "No projects with the Maintainer role were found for this account.", + "RefreshRepositories": "Refresh projects", + "ImageMode": "Images from GitLab", + "ImageModeLink": "Link to GitLab", + "ImageModeCopy": "Copy into the workspace", + "ImageModeLinkHint": "Images stay on GitLab. Only people who can open the GitLab project see them.", + "ImageModeCopyHint": "Images are copied into the workspace. Everyone with access to the linked project sees them.", + "ImageNoAccess": "You need access to this GitLab project to see this image.", + "ImageNotConnected": "Connect your GitLab account to see images from GitLab.", + "ImageUnavailable": "This image cannot be shown right now.", + "GitlabImages": "GitLab images ({count})", + "OpenInGitlab": "Open in GitLab", + "LinkToProject": "Link to {title} project", + "Unlink": "Unlink", + "LinkedTo": "Linked to", + "LinkRequiresConnector": "Only the member who connected this account or a maintainer can link it", + "Unavailable": "Unavailable", + "Processing": "Processing...", + "AutoClose": "This tab will close in {time} seconds", + "RequestFailed": "Request failed. Please try again.", + "CloseTab": "Close tab", + "KeepSettingsOpen": "Keep the {title} settings tab open while connecting GitLab, then try again", + "Repository": "GitLab project", + "SetupTitle": "GitLab application", + "SetupWhere": "Choose where to register the application in GitLab:", + "SetupUserOwned": "User-owned: in your user settings", + "SetupGroupOwned": "Group-owned (recommended for teams): in the group's Settings > Applications", + "SetupInstanceWide": "Instance-wide (self-managed administrators): in the Admin area", + "SetupTrustedHint": "Tick Trusted to skip the consent screen for users.", + "SetupNameAndRedirect": "Name: {title}. Redirect URI:", + "SetupConfidentialScopes": "Tick Confidential, select the scopes {scopes}, then save the application.", + "SetupCopyCredentials": "Copy the Application ID and Secret below. GitLab shows the secret only once.", + "GitlabUrl": "GitLab URL", + "SelfManaged": "Self-managed GitLab", + "ApplicationId": "Application ID", + "ApplicationSecret": "Secret", + "KeepSecretHint": "Leave empty to keep the current secret", + "Save": "Save", + "Change": "Change", + "Remove": "Remove", + "Copy": "Copy", + "Copied": "Copied", + "ConfiguredApp": "GitLab application {clientId} on {host}", + "OwnerMustConfigure": "A workspace owner needs to set up the GitLab application first.", + "InvalidGitlabUrl": "Enter an https:// URL (http:// only for localhost or *.local hosts)", + "SetupEnterUrlFirst": "Enter the URL of your GitLab instance below to see where to register the application.", + "DisconnectEveryone": "Disconnect everyone", + "DisconnectEveryoneConfirm": "Disconnect every member's GitLab account from this workspace? Their linked projects are unlinked and webhooks are removed where possible. Members can connect again afterwards.", + "GitlabIssue": "GitLab issue", + "IssueConnectedActivityInfo": "Connected to GitLab issue #{number} in {repoName}", + "MergeRequest": "Merge request", + "MergeRequests": "Merge requests", + "MergeRequestConnectedActivityInfo": "Connected to GitLab merge request !{number} in {repoName}", + "Draft": "Draft", + "MergedAt": "Merged at", + "ClosedAt": "Closed at", + "Commits": "Commits", + "Files": "Files", + "SourceBranch": "Source branch", + "TargetBranch": "Target branch", + "MergeStatus": "Merge status", + "MergeRequestState": "State", + "Reviewers": "Reviewers", + "StateOpened": "Open", + "StateMerged": "Merged", + "StateClosed": "Closed", + "StateLocked": "Locked", + "ReadyToMerge": "Ready to merge", + "Conflict": "Conflict", + "Checking": "Checking", + "PipelinePending": "Pipeline must pass", + "UnresolvedDiscussions": "Unresolved discussions", + "NeedsApproval": "Needs approval", + "All": "All", + "Active": "Active", + "Closed": "Closed", + "ChangedFiles": "{files} changed files", + "WithoutRepository": "Without repository", + "CreateInGitlab": "Create in GitLab", + "DiffUnavailable": "The diff could not be loaded", + "DiffTooLarge": "Too many changes to show here.", + "ApprovedBy": "Approved by", + "Approve": "Approve", + "RevokeApproval": "Revoke approval", + "ConnectToApprove": "Connect your GitLab account to approve", + "Review": "Review", + "ReviewThread": "Review thread", + "ReviewComment": "Review comment", + "ReviewComments": "Review comments", + "ReviewApproved": "approved", + "ReviewUnapproved": "revoked approval", + "ReviewRequestedChanges": "requested changes", + "ReviewReviewed": "reviewed", + "CommentedOnDiff": "commented on {path}", + "ResolveThread": "Resolve thread", + "UnresolveThread": "Reopen thread", + "ResolvedBy": "Resolved by", + "Outdated": "Outdated", + "SyncError": "GitLab sync error", + "Retry": "Retry", + "ReviewNotSent": "Not sent to GitLab", + "LinkFailed": "Could not link the GitLab project. Try again.", + "UnlinkFailed": "Could not unlink the GitLab project. Try again.", + "NotConfigured": "The GitLab integration is not configured on this server.", + "AuthorizeLinkInvalid": "GitLab returned an invalid sign-in link. Try again.", + "IntegrationRepositories": "GitLab projects — {host} / {login}", + "SyncErrorMessage": "GitLab sync error: {message}", + "UnlinkConfirm": "Unlink {repository} from {project}? Changes stop syncing until it is linked again.", + "RemoveAppConfirm": "Remove the GitLab application from this workspace? Members cannot connect GitLab until a new one is set up." + } +} diff --git a/services/gitlab/gitlab-assets/lang/es.json b/services/gitlab/gitlab-assets/lang/es.json new file mode 100644 index 0000000000..34f5e1b57c --- /dev/null +++ b/services/gitlab/gitlab-assets/lang/es.json @@ -0,0 +1,121 @@ +{ + "string": { + "Gitlab": "GitLab", + "GitlabDesc": "Conecta tus proyectos de GitLab para sincronizar issues, solicitudes de fusión y actualizaciones del proyecto", + "ConfigLabel": "GitLab", + "ConfigDescription": "Extensión para la integración con GitLab", + "Authorize": "Autorizar {title} con GitLab", + "ReAuthorize": "Volver a autorizar {title} con GitLab", + "Authorized": "Conectado como {login}", + "NotAuthorized": "Autoriza {title} para acceder a tu cuenta de GitLab. Aquí se mostrarán los proyectos en los que tengas el rol de Mantenedor.", + "NoRepositories": "No se encontraron proyectos con el rol de Mantenedor para esta cuenta.", + "RefreshRepositories": "Actualizar proyectos", + "ImageMode": "Imágenes de GitLab", + "ImageModeLink": "Enlazar a GitLab", + "ImageModeCopy": "Copiar al espacio de trabajo", + "ImageModeLinkHint": "Las imágenes permanecen en GitLab. Solo las ven quienes pueden abrir el proyecto de GitLab.", + "ImageModeCopyHint": "Las imágenes se copian al espacio de trabajo. Las ve cualquiera con acceso al proyecto vinculado.", + "ImageNoAccess": "Necesitas acceso a este proyecto de GitLab para ver esta imagen.", + "ImageNotConnected": "Conecta tu cuenta de GitLab para ver imágenes de GitLab.", + "ImageUnavailable": "Esta imagen no se puede mostrar ahora.", + "GitlabImages": "Imágenes de GitLab ({count})", + "OpenInGitlab": "Abrir en GitLab", + "LinkToProject": "Vincular con el proyecto {title}", + "Unlink": "Desvincular", + "LinkedTo": "Vinculado a", + "LinkRequiresConnector": "Solo el miembro que conectó esta cuenta o un mantenedor puede vincularlo", + "Unavailable": "No disponible", + "Processing": "Procesando...", + "AutoClose": "Esta pestaña se cerrará en {time} segundos", + "RequestFailed": "La solicitud falló. Inténtalo de nuevo.", + "CloseTab": "Cerrar pestaña", + "KeepSettingsOpen": "Mantén abierta la pestaña de configuración de {title} mientras conectas GitLab y vuelve a intentarlo", + "Repository": "Proyecto de GitLab", + "SetupTitle": "Aplicación de GitLab", + "SetupWhere": "Elige dónde registrar la aplicación en GitLab:", + "SetupUserOwned": "De usuario: en la configuración de tu usuario", + "SetupGroupOwned": "De grupo (recomendado para equipos): en Settings > Applications del grupo", + "SetupInstanceWide": "De toda la instancia (administradores de instancias autogestionadas): en el área de administración", + "SetupTrustedHint": "Marca Trusted para omitir la pantalla de consentimiento a los usuarios.", + "SetupNameAndRedirect": "Nombre: {title}. Redirect URI:", + "SetupConfidentialScopes": "Marca Confidential, selecciona los scopes {scopes} y guarda la aplicación.", + "SetupCopyCredentials": "Copia el Application ID y el Secret a continuación. GitLab muestra el secreto solo una vez.", + "GitlabUrl": "URL de GitLab", + "SelfManaged": "GitLab autogestionado", + "ApplicationId": "ID de la aplicación", + "ApplicationSecret": "Secreto", + "KeepSecretHint": "Déjalo vacío para conservar el secreto actual", + "Save": "Guardar", + "Change": "Cambiar", + "Remove": "Eliminar", + "Copy": "Copiar", + "Copied": "Copiado", + "ConfiguredApp": "Aplicación de GitLab {clientId} en {host}", + "OwnerMustConfigure": "Primero, un propietario del espacio de trabajo debe configurar la aplicación de GitLab.", + "InvalidGitlabUrl": "Introduce una URL https:// (http:// solo para localhost o hosts *.local)", + "SetupEnterUrlFirst": "Introduce abajo la URL de tu instancia de GitLab para ver dónde registrar la aplicación.", + "DisconnectEveryone": "Desconectar a todos", + "DisconnectEveryoneConfirm": "¿Desconectar la cuenta de GitLab de todos los miembros de este espacio de trabajo? Sus proyectos vinculados se desvincularán y los webhooks se eliminarán cuando sea posible. Los miembros podrán volver a conectarse después.", + "GitlabIssue": "Issue de GitLab", + "IssueConnectedActivityInfo": "Conectado al issue de GitLab #{number} en {repoName}", + "MergeRequest": "Solicitud de fusión", + "MergeRequests": "Solicitudes de fusión", + "MergeRequestConnectedActivityInfo": "Conectado a la solicitud de fusión de GitLab !{number} en {repoName}", + "Draft": "Borrador", + "MergedAt": "Fusionado en", + "ClosedAt": "Cerrado en", + "Commits": "Commits", + "Files": "Archivos", + "SourceBranch": "Rama de origen", + "TargetBranch": "Rama de destino", + "MergeStatus": "Estado de fusión", + "MergeRequestState": "Estado", + "Reviewers": "Revisores", + "StateOpened": "Abierta", + "StateMerged": "Fusionada", + "StateClosed": "Cerrada", + "StateLocked": "Bloqueada", + "ReadyToMerge": "Lista para fusionar", + "Conflict": "Conflicto", + "Checking": "Comprobando", + "PipelinePending": "El pipeline debe pasar", + "UnresolvedDiscussions": "Discusiones sin resolver", + "NeedsApproval": "Requiere aprobación", + "All": "Todos", + "Active": "Activos", + "Closed": "Cerrados", + "ChangedFiles": "{files} archivos modificados", + "WithoutRepository": "Sin proyecto", + "CreateInGitlab": "Crear en GitLab", + "DiffUnavailable": "No se pudo cargar el diff", + "DiffTooLarge": "Demasiados cambios para mostrarlos aquí.", + "ApprovedBy": "Aprobado por", + "Approve": "Aprobar", + "RevokeApproval": "Revocar aprobación", + "ConnectToApprove": "Conecta tu cuenta de GitLab para aprobar", + "Review": "Revisión", + "ReviewThread": "Hilo de revisión", + "ReviewComment": "Comentario de revisión", + "ReviewComments": "Comentarios de revisión", + "ReviewApproved": "aprobó", + "ReviewUnapproved": "revocó la aprobación", + "ReviewRequestedChanges": "solicitó cambios", + "ReviewReviewed": "revisó", + "CommentedOnDiff": "comentó en {path}", + "ResolveThread": "Resolver hilo", + "UnresolveThread": "Reabrir hilo", + "ResolvedBy": "Resuelto por", + "Outdated": "Obsoleto", + "SyncError": "Error de sincronización de GitLab", + "Retry": "Reintentar", + "ReviewNotSent": "No enviado a GitLab", + "LinkFailed": "No se pudo vincular el proyecto de GitLab. Inténtalo de nuevo.", + "UnlinkFailed": "No se pudo desvincular el proyecto de GitLab. Inténtalo de nuevo.", + "NotConfigured": "La integración con GitLab no está configurada en este servidor.", + "AuthorizeLinkInvalid": "GitLab devolvió un enlace de inicio de sesión no válido. Inténtalo de nuevo.", + "IntegrationRepositories": "Proyectos de GitLab — {host} / {login}", + "SyncErrorMessage": "Error de sincronización de GitLab: {message}", + "UnlinkConfirm": "¿Desvincular {repository} de {project}? Los cambios dejarán de sincronizarse hasta que se vuelva a vincular.", + "RemoveAppConfirm": "¿Eliminar la aplicación de GitLab de este espacio de trabajo? Los miembros no podrán conectar GitLab hasta que se configure una nueva." + } +} diff --git a/services/gitlab/gitlab-assets/lang/fr.json b/services/gitlab/gitlab-assets/lang/fr.json new file mode 100644 index 0000000000..5f945575bc --- /dev/null +++ b/services/gitlab/gitlab-assets/lang/fr.json @@ -0,0 +1,121 @@ +{ + "string": { + "Gitlab": "GitLab", + "GitlabDesc": "Connectez vos projets GitLab pour synchroniser les problèmes, les demandes de fusion et les mises à jour de projet", + "ConfigLabel": "GitLab", + "ConfigDescription": "Extension pour l'intégration GitLab", + "Authorize": "Autoriser {title} avec GitLab", + "ReAuthorize": "Autoriser à nouveau {title} avec GitLab", + "Authorized": "Connecté en tant que {login}", + "NotAuthorized": "Autorisez {title} à accéder à votre compte GitLab. Les projets pour lesquels vous avez le rôle Mainteneur seront listés ici.", + "NoRepositories": "Aucun projet avec le rôle Mainteneur n'a été trouvé pour ce compte.", + "RefreshRepositories": "Actualiser les projets", + "ImageMode": "Images de GitLab", + "ImageModeLink": "Lier à GitLab", + "ImageModeCopy": "Copier dans l'espace de travail", + "ImageModeLinkHint": "Les images restent sur GitLab. Seules les personnes pouvant ouvrir le projet GitLab les voient.", + "ImageModeCopyHint": "Les images sont copiées dans l'espace de travail. Toute personne ayant accès au projet lié les voit.", + "ImageNoAccess": "Vous devez avoir accès à ce projet GitLab pour voir cette image.", + "ImageNotConnected": "Connectez votre compte GitLab pour voir les images de GitLab.", + "ImageUnavailable": "Cette image ne peut pas être affichée pour le moment.", + "GitlabImages": "Images GitLab ({count})", + "OpenInGitlab": "Ouvrir dans GitLab", + "LinkToProject": "Lier au projet {title}", + "Unlink": "Dissocier", + "LinkedTo": "Lié à", + "LinkRequiresConnector": "Seul le membre qui a connecté ce compte ou un mainteneur peut le lier", + "Unavailable": "Indisponible", + "Processing": "Traitement en cours...", + "AutoClose": "Cet onglet se fermera dans {time} secondes", + "RequestFailed": "La demande a échoué. Veuillez réessayer.", + "CloseTab": "Fermer l'onglet", + "KeepSettingsOpen": "Gardez l'onglet des paramètres de {title} ouvert pendant la connexion à GitLab, puis réessayez", + "Repository": "Projet GitLab", + "SetupTitle": "Application GitLab", + "SetupWhere": "Choisissez où enregistrer l'application dans GitLab :", + "SetupUserOwned": "Appartenant à un utilisateur : dans vos paramètres utilisateur", + "SetupGroupOwned": "Appartenant à un groupe (recommandé pour les équipes) : dans Paramètres > Applications du groupe", + "SetupInstanceWide": "À l'échelle de l'instance (administrateurs d'instances auto-gérées) : dans la zone d'administration", + "SetupTrustedHint": "Cochez Trusted pour que les utilisateurs n'aient pas à passer par l'écran de consentement.", + "SetupNameAndRedirect": "Nom : {title}. Redirect URI :", + "SetupConfidentialScopes": "Cochez Confidential, sélectionnez les scopes {scopes}, puis enregistrez l'application.", + "SetupCopyCredentials": "Copiez l'Application ID et le Secret ci-dessous. GitLab n'affiche le secret qu'une seule fois.", + "GitlabUrl": "URL GitLab", + "SelfManaged": "GitLab auto-géré", + "ApplicationId": "ID de l'application", + "ApplicationSecret": "Clé secrète", + "KeepSecretHint": "Laisser vide pour conserver le secret actuel", + "Save": "Enregistrer", + "Change": "Modifier", + "Remove": "Supprimer", + "Copy": "Copier", + "Copied": "Copié", + "ConfiguredApp": "Application GitLab {clientId} sur {host}", + "OwnerMustConfigure": "Un propriétaire de l'espace de travail doit d'abord configurer l'application GitLab.", + "InvalidGitlabUrl": "Saisissez une URL https:// (http:// uniquement pour localhost ou les hôtes *.local)", + "SetupEnterUrlFirst": "Saisissez ci-dessous l'URL de votre instance GitLab pour voir où enregistrer l'application.", + "DisconnectEveryone": "Déconnecter tout le monde", + "DisconnectEveryoneConfirm": "Déconnecter les comptes GitLab de tous les membres de cet espace de travail ? Leurs projets liés seront dissociés et les webhooks supprimés si possible. Les membres pourront ensuite se reconnecter.", + "GitlabIssue": "Ticket GitLab", + "IssueConnectedActivityInfo": "Connecté au ticket GitLab #{number} dans {repoName}", + "MergeRequest": "Demande de fusion", + "MergeRequests": "Demandes de fusion", + "MergeRequestConnectedActivityInfo": "Connecté à la demande de fusion GitLab !{number} dans {repoName}", + "Draft": "Brouillon", + "MergedAt": "Fusionné le", + "ClosedAt": "Fermé le", + "Commits": "Commits", + "Files": "Fichiers", + "SourceBranch": "Branche source", + "TargetBranch": "Branche cible", + "MergeStatus": "État de la fusion", + "MergeRequestState": "État", + "Reviewers": "Relecteurs", + "StateOpened": "Ouvert", + "StateMerged": "Fusionné", + "StateClosed": "Fermé", + "StateLocked": "Verrouillé", + "ReadyToMerge": "Prêt à fusionner", + "Conflict": "Conflit", + "Checking": "Vérification", + "PipelinePending": "Le pipeline doit réussir", + "UnresolvedDiscussions": "Discussions non résolues", + "NeedsApproval": "Approbation requise", + "All": "Tous", + "Active": "Actif", + "Closed": "Fermé", + "ChangedFiles": "{files} fichiers modifiés", + "WithoutRepository": "Sans dépôt", + "CreateInGitlab": "Créer dans GitLab", + "DiffUnavailable": "Impossible de charger le diff", + "DiffTooLarge": "Trop de modifications pour les afficher ici.", + "ApprovedBy": "Approuvé par", + "Approve": "Approuver", + "RevokeApproval": "Révoquer l'approbation", + "ConnectToApprove": "Connectez votre compte GitLab pour approuver", + "Review": "Révision", + "ReviewThread": "Fil de révision", + "ReviewComment": "Commentaire de révision", + "ReviewComments": "Commentaires de révision", + "ReviewApproved": "a approuvé", + "ReviewUnapproved": "a révoqué l'approbation", + "ReviewRequestedChanges": "a demandé des modifications", + "ReviewReviewed": "a relu", + "CommentedOnDiff": "a commenté {path}", + "ResolveThread": "Résoudre le fil", + "UnresolveThread": "Rouvrir le fil", + "ResolvedBy": "Résolu par", + "Outdated": "Obsolète", + "SyncError": "Erreur de synchronisation GitLab", + "Retry": "Réessayer", + "ReviewNotSent": "Non envoyé à GitLab", + "LinkFailed": "Impossible d'associer le projet GitLab. Réessayez.", + "UnlinkFailed": "Impossible de dissocier le projet GitLab. Réessayez.", + "NotConfigured": "L'intégration GitLab n'est pas configurée sur ce serveur.", + "AuthorizeLinkInvalid": "GitLab a renvoyé un lien de connexion non valide. Réessayez.", + "IntegrationRepositories": "Projets GitLab — {host} / {login}", + "SyncErrorMessage": "Erreur de synchronisation GitLab : {message}", + "UnlinkConfirm": "Dissocier {repository} de {project} ? Les modifications ne seront plus synchronisées tant qu'il ne sera pas associé à nouveau.", + "RemoveAppConfirm": "Supprimer l'application GitLab de cet espace de travail ? Les membres ne pourront plus connecter GitLab tant qu'une nouvelle application n'aura pas été configurée." + } +} diff --git a/services/gitlab/gitlab-assets/lang/it.json b/services/gitlab/gitlab-assets/lang/it.json new file mode 100644 index 0000000000..889fac472e --- /dev/null +++ b/services/gitlab/gitlab-assets/lang/it.json @@ -0,0 +1,121 @@ +{ + "string": { + "Gitlab": "GitLab", + "GitlabDesc": "Collega i tuoi progetti GitLab per sincronizzare issue, merge request e aggiornamenti dei progetti", + "ConfigLabel": "GitLab", + "ConfigDescription": "Estensione per l'integrazione con GitLab", + "Authorize": "Autorizza {title} con GitLab", + "ReAuthorize": "Autorizza di nuovo {title} con GitLab", + "Authorized": "Connesso come {login}", + "NotAuthorized": "Autorizza {title} ad accedere al tuo account GitLab. Qui verranno elencati i progetti in cui hai il ruolo Maintainer.", + "NoRepositories": "Nessun progetto con il ruolo Maintainer trovato per questo account.", + "RefreshRepositories": "Aggiorna progetti", + "ImageMode": "Immagini da GitLab", + "ImageModeLink": "Collega a GitLab", + "ImageModeCopy": "Copia nello spazio di lavoro", + "ImageModeLinkHint": "Le immagini restano su GitLab. Le vede solo chi può aprire il progetto GitLab.", + "ImageModeCopyHint": "Le immagini vengono copiate nello spazio di lavoro. Le vede chiunque abbia accesso al progetto collegato.", + "ImageNoAccess": "Serve l'accesso a questo progetto GitLab per vedere questa immagine.", + "ImageNotConnected": "Collega il tuo account GitLab per vedere le immagini da GitLab.", + "ImageUnavailable": "Questa immagine non può essere mostrata al momento.", + "GitlabImages": "Immagini GitLab ({count})", + "OpenInGitlab": "Apri in GitLab", + "LinkToProject": "Collega al progetto {title}", + "Unlink": "Scollega", + "LinkedTo": "Collegato a", + "LinkRequiresConnector": "Solo il membro che ha connesso questo account o un maintainer può collegarlo", + "Unavailable": "Non disponibile", + "Processing": "Elaborazione in corso...", + "AutoClose": "Questa scheda verrà chiusa tra {time} secondi", + "RequestFailed": "Richiesta fallita. Riprova.", + "CloseTab": "Chiudi scheda", + "KeepSettingsOpen": "Mantieni aperta la scheda delle impostazioni di {title} durante la connessione a GitLab, quindi riprova", + "Repository": "Progetto GitLab", + "SetupTitle": "Applicazione GitLab", + "SetupWhere": "Scegli dove registrare l'applicazione in GitLab:", + "SetupUserOwned": "Di proprietà dell'utente: nelle impostazioni del tuo utente", + "SetupGroupOwned": "Di proprietà del gruppo (consigliato per i team): in Settings > Applications del gruppo", + "SetupInstanceWide": "A livello di istanza (amministratori di istanze self-managed): nell'area Admin", + "SetupTrustedHint": "Seleziona Trusted per saltare la schermata di consenso per gli utenti.", + "SetupNameAndRedirect": "Nome: {title}. Redirect URI:", + "SetupConfidentialScopes": "Seleziona Confidential, scegli gli scope {scopes}, quindi salva l'applicazione.", + "SetupCopyCredentials": "Copia l'Application ID e il Secret qui sotto. GitLab mostra il secret una sola volta.", + "GitlabUrl": "URL di GitLab", + "SelfManaged": "GitLab self-managed", + "ApplicationId": "ID applicazione", + "ApplicationSecret": "Chiave segreta", + "KeepSecretHint": "Lascia vuoto per mantenere il secret attuale", + "Save": "Salva", + "Change": "Modifica", + "Remove": "Rimuovi", + "Copy": "Copia", + "Copied": "Copiato", + "ConfiguredApp": "Applicazione GitLab {clientId} su {host}", + "OwnerMustConfigure": "Un proprietario del workspace deve prima configurare l'applicazione GitLab.", + "InvalidGitlabUrl": "Inserisci un URL https:// (http:// solo per localhost o host *.local)", + "SetupEnterUrlFirst": "Inserisci qui sotto l'URL della tua istanza GitLab per vedere dove registrare l'applicazione.", + "DisconnectEveryone": "Disconnetti tutti", + "DisconnectEveryoneConfirm": "Disconnettere gli account GitLab di tutti i membri da questo workspace? I loro progetti collegati verranno scollegati e i webhook rimossi, ove possibile. I membri potranno ricollegarsi in seguito.", + "GitlabIssue": "Issue GitLab", + "IssueConnectedActivityInfo": "Collegato all'issue GitLab #{number} in {repoName}", + "MergeRequest": "Merge request", + "MergeRequests": "Merge request", + "MergeRequestConnectedActivityInfo": "Collegato alla merge request GitLab !{number} in {repoName}", + "Draft": "Bozza", + "MergedAt": "Unito il", + "ClosedAt": "Chiuso il", + "Commits": "Commit", + "Files": "File", + "SourceBranch": "Branch di origine", + "TargetBranch": "Branch di destinazione", + "MergeStatus": "Stato dell'unione", + "MergeRequestState": "Stato", + "Reviewers": "Revisori", + "StateOpened": "Aperto", + "StateMerged": "Unito", + "StateClosed": "Chiuso", + "StateLocked": "Bloccato", + "ReadyToMerge": "Pronto per l'unione", + "Conflict": "Conflitto", + "Checking": "Verifica in corso", + "PipelinePending": "La pipeline deve passare", + "UnresolvedDiscussions": "Discussioni non risolte", + "NeedsApproval": "Richiede approvazione", + "All": "Tutti", + "Active": "Attivi", + "Closed": "Chiusi", + "ChangedFiles": "{files} file modificati", + "WithoutRepository": "Senza repository", + "CreateInGitlab": "Crea in GitLab", + "DiffUnavailable": "Impossibile caricare il diff", + "DiffTooLarge": "Troppe modifiche da mostrare qui.", + "ApprovedBy": "Approvato da", + "Approve": "Approva", + "RevokeApproval": "Revoca approvazione", + "ConnectToApprove": "Collega il tuo account GitLab per approvare", + "Review": "Revisione", + "ReviewThread": "Thread di revisione", + "ReviewComment": "Commento di revisione", + "ReviewComments": "Commenti di revisione", + "ReviewApproved": "ha approvato", + "ReviewUnapproved": "ha revocato l'approvazione", + "ReviewRequestedChanges": "ha richiesto modifiche", + "ReviewReviewed": "ha revisionato", + "CommentedOnDiff": "ha commentato {path}", + "ResolveThread": "Risolvi thread", + "UnresolveThread": "Riapri thread", + "ResolvedBy": "Risolto da", + "Outdated": "Obsoleto", + "SyncError": "Errore di sincronizzazione GitLab", + "Retry": "Riprova", + "ReviewNotSent": "Non inviato a GitLab", + "LinkFailed": "Impossibile collegare il progetto GitLab. Riprova.", + "UnlinkFailed": "Impossibile scollegare il progetto GitLab. Riprova.", + "NotConfigured": "L'integrazione GitLab non è configurata su questo server.", + "AuthorizeLinkInvalid": "GitLab ha restituito un link di accesso non valido. Riprova.", + "IntegrationRepositories": "Progetti GitLab — {host} / {login}", + "SyncErrorMessage": "Errore di sincronizzazione GitLab: {message}", + "UnlinkConfirm": "Scollegare {repository} da {project}? Le modifiche non verranno sincronizzate finché non sarà collegato di nuovo.", + "RemoveAppConfirm": "Rimuovere l'applicazione GitLab da questo spazio di lavoro? I membri non potranno collegare GitLab finché non ne verrà configurata una nuova." + } +} diff --git a/services/gitlab/gitlab-assets/lang/ko.json b/services/gitlab/gitlab-assets/lang/ko.json new file mode 100644 index 0000000000..1b95295a6f --- /dev/null +++ b/services/gitlab/gitlab-assets/lang/ko.json @@ -0,0 +1,121 @@ +{ + "string": { + "Gitlab": "GitLab", + "GitlabDesc": "GitLab 프로젝트를 연결하여 이슈, 병합 요청, 프로젝트 업데이트를 동기화합니다", + "ConfigLabel": "GitLab", + "ConfigDescription": "GitLab 통합 확장 기능", + "Authorize": "GitLab으로 {title} 인증", + "ReAuthorize": "GitLab으로 {title} 다시 인증", + "Authorized": "{login}(으)로 연결됨", + "NotAuthorized": "{title}이(가) GitLab 계정에 액세스할 수 있도록 인증하세요. 메인테이너 역할이 있는 프로젝트가 여기에 표시됩니다.", + "NoRepositories": "이 계정에서 메인테이너 역할이 있는 프로젝트를 찾을 수 없습니다.", + "RefreshRepositories": "프로젝트 새로고침", + "ImageMode": "GitLab 이미지", + "ImageModeLink": "GitLab에 링크", + "ImageModeCopy": "워크스페이스에 복사", + "ImageModeLinkHint": "이미지는 GitLab에 남습니다. GitLab 프로젝트를 열 수 있는 사람만 볼 수 있습니다.", + "ImageModeCopyHint": "이미지가 워크스페이스에 복사됩니다. 연결된 프로젝트에 접근할 수 있는 모든 사람이 볼 수 있습니다.", + "ImageNoAccess": "이 이미지를 보려면 이 GitLab 프로젝트에 대한 접근 권한이 필요합니다.", + "ImageNotConnected": "GitLab 이미지를 보려면 GitLab 계정을 연결하세요.", + "ImageUnavailable": "지금은 이 이미지를 표시할 수 없습니다.", + "GitlabImages": "GitLab 이미지 ({count})", + "OpenInGitlab": "GitLab에서 열기", + "LinkToProject": "{title} 프로젝트에 연결", + "Unlink": "연결 해제", + "LinkedTo": "연결 대상", + "LinkRequiresConnector": "이 계정을 연결한 멤버 또는 관리자만 연결할 수 있습니다", + "Unavailable": "사용할 수 없음", + "Processing": "처리 중...", + "AutoClose": "이 탭은 {time}초 후에 닫힙니다", + "RequestFailed": "요청에 실패했습니다. 다시 시도하세요.", + "CloseTab": "탭 닫기", + "KeepSettingsOpen": "GitLab을 연결하는 동안 {title} 설정 탭을 열어 둔 채 다시 시도하세요", + "Repository": "GitLab 프로젝트", + "SetupTitle": "GitLab 애플리케이션", + "SetupWhere": "GitLab에서 애플리케이션을 등록할 위치를 선택하세요.", + "SetupUserOwned": "사용자 소유: 사용자 설정에서", + "SetupGroupOwned": "그룹 소유(팀에 권장): 그룹의 설정 > 애플리케이션에서", + "SetupInstanceWide": "인스턴스 전체(자체 관리형 관리자): 관리자 영역에서", + "SetupTrustedHint": "신뢰함을 선택하면 사용자에게 동의 화면이 표시되지 않습니다.", + "SetupNameAndRedirect": "이름: {title}. 리디렉션 URI:", + "SetupConfidentialScopes": "기밀을 선택하고 스코프 {scopes}을(를) 선택한 다음 애플리케이션을 저장하세요.", + "SetupCopyCredentials": "아래의 Application ID와 시크릿을 복사하세요. GitLab은 시크릿을 한 번만 표시합니다.", + "GitlabUrl": "GitLab URL", + "SelfManaged": "자체 관리형 GitLab", + "ApplicationId": "애플리케이션 ID", + "ApplicationSecret": "시크릿", + "KeepSecretHint": "현재 시크릿을 유지하려면 비워 두세요", + "Save": "저장", + "Change": "변경", + "Remove": "제거", + "Copy": "복사", + "Copied": "복사됨", + "ConfiguredApp": "{host}의 GitLab 애플리케이션 {clientId}", + "OwnerMustConfigure": "먼저 워크스페이스 소유자가 GitLab 애플리케이션을 설정해야 합니다.", + "InvalidGitlabUrl": "https:// URL을 입력하세요(http://는 localhost 또는 *.local 호스트에서만 허용)", + "SetupEnterUrlFirst": "애플리케이션을 등록할 위치를 보려면 아래에 GitLab 인스턴스의 URL을 입력하세요.", + "DisconnectEveryone": "모두 연결 끊기", + "DisconnectEveryoneConfirm": "이 워크스페이스에서 모든 멤버의 GitLab 계정 연결을 끊으시겠습니까? 연결된 프로젝트는 연결이 해제되고 가능한 경우 웹훅이 제거됩니다. 멤버는 이후에 다시 연결할 수 있습니다.", + "GitlabIssue": "GitLab 이슈", + "IssueConnectedActivityInfo": "{repoName}의 GitLab 이슈 #{number}에 연결됨", + "MergeRequest": "병합 요청", + "MergeRequests": "병합 요청", + "MergeRequestConnectedActivityInfo": "{repoName}의 GitLab 병합 요청 !{number}에 연결됨", + "Draft": "초안", + "MergedAt": "병합 시각", + "ClosedAt": "닫은 시각", + "Commits": "커밋", + "Files": "파일", + "SourceBranch": "소스 브랜치", + "TargetBranch": "대상 브랜치", + "MergeStatus": "병합 상태", + "MergeRequestState": "상태", + "Reviewers": "리뷰어", + "StateOpened": "열림", + "StateMerged": "병합됨", + "StateClosed": "닫힘", + "StateLocked": "잠김", + "ReadyToMerge": "병합 준비 완료", + "Conflict": "충돌", + "Checking": "확인 중", + "PipelinePending": "파이프라인 통과 필요", + "UnresolvedDiscussions": "해결되지 않은 토론", + "NeedsApproval": "승인 필요", + "All": "전체", + "Active": "활성", + "Closed": "닫힘", + "ChangedFiles": "변경된 파일 {files}개", + "WithoutRepository": "저장소 없음", + "CreateInGitlab": "GitLab에서 만들기", + "DiffUnavailable": "차이점을 불러올 수 없습니다", + "DiffTooLarge": "변경 사항이 너무 많아 여기에 표시할 수 없습니다.", + "ApprovedBy": "승인자", + "Approve": "승인", + "RevokeApproval": "승인 취소", + "ConnectToApprove": "승인하려면 GitLab 계정을 연결하세요", + "Review": "리뷰", + "ReviewThread": "리뷰 스레드", + "ReviewComment": "리뷰 댓글", + "ReviewComments": "리뷰 댓글", + "ReviewApproved": "승인함", + "ReviewUnapproved": "승인을 취소함", + "ReviewRequestedChanges": "변경을 요청함", + "ReviewReviewed": "리뷰함", + "CommentedOnDiff": "{path}에 댓글을 남김", + "ResolveThread": "스레드 해결", + "UnresolveThread": "스레드 다시 열기", + "ResolvedBy": "해결자", + "Outdated": "오래됨", + "SyncError": "GitLab 동기화 오류", + "Retry": "다시 시도", + "ReviewNotSent": "GitLab으로 전송되지 않음", + "LinkFailed": "GitLab 프로젝트를 연결하지 못했습니다. 다시 시도하세요.", + "UnlinkFailed": "GitLab 프로젝트 연결을 해제하지 못했습니다. 다시 시도하세요.", + "NotConfigured": "이 서버에 GitLab 통합이 설정되어 있지 않습니다.", + "AuthorizeLinkInvalid": "GitLab이 잘못된 로그인 링크를 반환했습니다. 다시 시도하세요.", + "IntegrationRepositories": "GitLab 프로젝트 — {host} / {login}", + "SyncErrorMessage": "GitLab 동기화 오류: {message}", + "UnlinkConfirm": "{project}에서 {repository} 연결을 해제할까요? 다시 연결할 때까지 변경 사항이 동기화되지 않습니다.", + "RemoveAppConfirm": "이 워크스페이스에서 GitLab 애플리케이션을 제거할까요? 새 애플리케이션이 설정될 때까지 멤버는 GitLab을 연결할 수 없습니다." + } +} diff --git a/services/gitlab/gitlab-assets/lang/pl.json b/services/gitlab/gitlab-assets/lang/pl.json new file mode 100644 index 0000000000..a5def4d232 --- /dev/null +++ b/services/gitlab/gitlab-assets/lang/pl.json @@ -0,0 +1,121 @@ +{ + "string": { + "Gitlab": "GitLab", + "GitlabDesc": "Połącz swoje projekty GitLab, aby synchronizować zadania, prośby o scalenie i aktualizacje projektów", + "ConfigLabel": "GitLab", + "ConfigDescription": "Rozszerzenie integracji z GitLab", + "Authorize": "Autoryzuj {title} w GitLab", + "ReAuthorize": "Autoryzuj ponownie {title} w GitLab", + "Authorized": "Połączono jako {login}", + "NotAuthorized": "Autoryzuj {title}, aby uzyskać dostęp do swojego konta GitLab. Zostaną tu wyświetlone projekty, w których masz rolę Maintainer.", + "NoRepositories": "Nie znaleziono projektów z rolą Maintainer dla tego konta.", + "RefreshRepositories": "Odśwież projekty", + "ImageMode": "Obrazy z GitLaba", + "ImageModeLink": "Linkuj do GitLaba", + "ImageModeCopy": "Kopiuj do obszaru roboczego", + "ImageModeLinkHint": "Obrazy pozostają w GitLabie. Widzą je tylko osoby, które mogą otworzyć projekt w GitLabie.", + "ImageModeCopyHint": "Obrazy są kopiowane do obszaru roboczego. Widzi je każdy, kto ma dostęp do połączonego projektu.", + "ImageNoAccess": "Aby zobaczyć ten obraz, potrzebujesz dostępu do tego projektu w GitLabie.", + "ImageNotConnected": "Połącz swoje konto GitLab, aby widzieć obrazy z GitLaba.", + "ImageUnavailable": "Tego obrazu nie można teraz wyświetlić.", + "GitlabImages": "Obrazy z GitLaba ({count})", + "OpenInGitlab": "Otwórz w GitLabie", + "LinkToProject": "Połącz z projektem {title}", + "Unlink": "Odłącz", + "LinkedTo": "Połączono z", + "LinkRequiresConnector": "Połączyć może tylko członek, który podłączył to konto, lub opiekun", + "Unavailable": "Niedostępne", + "Processing": "Przetwarzanie...", + "AutoClose": "Ta karta zostanie zamknięta za {time} s", + "RequestFailed": "Zapytanie nie powiodło się. Spróbuj ponownie.", + "CloseTab": "Zamknij kartę", + "KeepSettingsOpen": "Nie zamykaj karty ustawień {title} podczas łączenia z GitLab, a następnie spróbuj ponownie", + "Repository": "Projekt GitLab", + "SetupTitle": "Aplikacja GitLab", + "SetupWhere": "Wybierz, gdzie zarejestrować aplikację w GitLab:", + "SetupUserOwned": "Użytkownika: w ustawieniach Twojego konta", + "SetupGroupOwned": "Grupy (zalecane dla zespołów): w sekcji Settings > Applications grupy", + "SetupInstanceWide": "Całej instancji (administratorzy self-managed): w sekcji Admin area", + "SetupTrustedHint": "Zaznacz Trusted, aby użytkownicy nie widzieli ekranu zgody.", + "SetupNameAndRedirect": "Nazwa: {title}. Redirect URI:", + "SetupConfidentialScopes": "Zaznacz Confidential, wybierz scopes {scopes} i zapisz aplikację.", + "SetupCopyCredentials": "Skopiuj poniżej Application ID i Secret. GitLab pokazuje secret tylko raz.", + "GitlabUrl": "URL GitLab", + "SelfManaged": "Własny serwer GitLab (self-managed)", + "ApplicationId": "Identyfikator aplikacji", + "ApplicationSecret": "Klucz tajny", + "KeepSecretHint": "Pozostaw puste, aby zachować bieżący secret", + "Save": "Zapisz", + "Change": "Zmień", + "Remove": "Usuń", + "Copy": "Kopiuj", + "Copied": "Skopiowano", + "ConfiguredApp": "Aplikacja GitLab {clientId} na {host}", + "OwnerMustConfigure": "Najpierw właściciel przestrzeni roboczej musi skonfigurować aplikację GitLab.", + "InvalidGitlabUrl": "Wpisz adres URL z https:// (http:// tylko dla localhost lub hostów *.local)", + "SetupEnterUrlFirst": "Wpisz poniżej adres URL swojej instancji GitLab, aby zobaczyć, gdzie zarejestrować aplikację.", + "DisconnectEveryone": "Rozłącz wszystkich", + "DisconnectEveryoneConfirm": "Rozłączyć konta GitLab wszystkich członków z tej przestrzeni roboczej? Ich połączone projekty zostaną odłączone, a webhooki usunięte, jeśli to możliwe. Członkowie będą mogli połączyć się ponownie.", + "GitlabIssue": "Zadanie GitLab", + "IssueConnectedActivityInfo": "Powiązane z zadaniem GitLab #{number} w projekcie {repoName}", + "MergeRequest": "Prośba o scalenie", + "MergeRequests": "Prośby o scalenie", + "MergeRequestConnectedActivityInfo": "Powiązane z prośbą o scalenie GitLab !{number} w projekcie {repoName}", + "Draft": "Wersja robocza", + "MergedAt": "Data scalenia", + "ClosedAt": "Data zamknięcia", + "Commits": "Commity", + "Files": "Pliki", + "SourceBranch": "Gałąź źródłowa", + "TargetBranch": "Gałąź docelowa", + "MergeStatus": "Stan scalania", + "MergeRequestState": "Stan", + "Reviewers": "Recenzenci", + "StateOpened": "Otwarte", + "StateMerged": "Scalone", + "StateClosed": "Zamknięte", + "StateLocked": "Zablokowane", + "ReadyToMerge": "Gotowe do scalenia", + "Conflict": "Konflikt", + "Checking": "Sprawdzanie", + "PipelinePending": "Pipeline musi zakończyć się powodzeniem", + "UnresolvedDiscussions": "Nierozwiązane dyskusje", + "NeedsApproval": "Wymaga zatwierdzenia", + "All": "Wszystko", + "Active": "Aktywne", + "Closed": "Zamknięte", + "ChangedFiles": "Zmienione pliki: {files}", + "WithoutRepository": "Bez repozytorium", + "CreateInGitlab": "Utwórz w GitLab", + "DiffUnavailable": "Nie udało się załadować zmian", + "DiffTooLarge": "Zbyt wiele zmian, aby je tu wyświetlić.", + "ApprovedBy": "Zatwierdzili", + "Approve": "Zatwierdź", + "RevokeApproval": "Cofnij zatwierdzenie", + "ConnectToApprove": "Połącz swoje konto GitLab, aby zatwierdzać", + "Review": "Recenzja", + "ReviewThread": "Wątek recenzji", + "ReviewComment": "Komentarz do recenzji", + "ReviewComments": "Komentarze do recenzji", + "ReviewApproved": "zatwierdził(a)", + "ReviewUnapproved": "cofnął(-ęła) zatwierdzenie", + "ReviewRequestedChanges": "poprosił(a) o zmiany", + "ReviewReviewed": "zrecenzował(a)", + "CommentedOnDiff": "skomentował(a) {path}", + "ResolveThread": "Rozwiąż wątek", + "UnresolveThread": "Otwórz wątek ponownie", + "ResolvedBy": "Rozwiązał(a)", + "Outdated": "Nieaktualne", + "SyncError": "Błąd synchronizacji z GitLab", + "Retry": "Ponów", + "ReviewNotSent": "Nie wysłano do GitLab", + "LinkFailed": "Nie udało się połączyć projektu GitLab. Spróbuj ponownie.", + "UnlinkFailed": "Nie udało się odłączyć projektu GitLab. Spróbuj ponownie.", + "NotConfigured": "Integracja z GitLab nie jest skonfigurowana na tym serwerze.", + "AuthorizeLinkInvalid": "GitLab zwrócił nieprawidłowy link logowania. Spróbuj ponownie.", + "IntegrationRepositories": "Projekty GitLab — {host} / {login}", + "SyncErrorMessage": "Błąd synchronizacji z GitLab: {message}", + "UnlinkConfirm": "Odłączyć {repository} od {project}? Zmiany nie będą synchronizowane, dopóki nie zostanie ponownie połączony.", + "RemoveAppConfirm": "Usunąć aplikację GitLab z tego obszaru roboczego? Członkowie nie będą mogli połączyć GitLab, dopóki nie zostanie skonfigurowana nowa." + } +} diff --git a/services/gitlab/gitlab-assets/lang/pt-br.json b/services/gitlab/gitlab-assets/lang/pt-br.json new file mode 100644 index 0000000000..df7a82282c --- /dev/null +++ b/services/gitlab/gitlab-assets/lang/pt-br.json @@ -0,0 +1,121 @@ +{ + "string": { + "Gitlab": "GitLab", + "GitlabDesc": "Conecte seus projetos do GitLab para sincronizar issues, merge requests e atualizações do projeto", + "ConfigLabel": "GitLab", + "ConfigDescription": "Extensão para integração com o GitLab", + "Authorize": "Autorizar {title} com o GitLab", + "ReAuthorize": "Reautorizar {title} com o GitLab", + "Authorized": "Conectado como {login}", + "NotAuthorized": "Autorize {title} a acessar sua conta do GitLab. Os projetos em que você tem a função Mantenedor serão listados aqui.", + "NoRepositories": "Nenhum projeto com a função Mantenedor foi encontrado para esta conta.", + "RefreshRepositories": "Atualizar projetos", + "ImageMode": "Imagens do GitLab", + "ImageModeLink": "Vincular ao GitLab", + "ImageModeCopy": "Copiar para o espaço de trabalho", + "ImageModeLinkHint": "As imagens ficam no GitLab. Apenas quem pode abrir o projeto do GitLab as vê.", + "ImageModeCopyHint": "As imagens são copiadas para o espaço de trabalho. Todos com acesso ao projeto vinculado as veem.", + "ImageNoAccess": "Você precisa de acesso a este projeto do GitLab para ver esta imagem.", + "ImageNotConnected": "Conecte sua conta do GitLab para ver imagens do GitLab.", + "ImageUnavailable": "Não é possível exibir esta imagem agora.", + "GitlabImages": "Imagens do GitLab ({count})", + "OpenInGitlab": "Abrir no GitLab", + "LinkToProject": "Vincular ao projeto {title}", + "Unlink": "Desvincular", + "LinkedTo": "Vinculado a", + "LinkRequiresConnector": "Somente o membro que conectou esta conta ou um mantenedor pode vinculá-lo", + "Unavailable": "Indisponível", + "Processing": "Processando...", + "AutoClose": "Esta guia será fechada em {time} segundos", + "RequestFailed": "A solicitação falhou. Tente novamente.", + "CloseTab": "Fechar guia", + "KeepSettingsOpen": "Mantenha a guia de configurações de {title} aberta enquanto conecta o GitLab e tente novamente", + "Repository": "Projeto do GitLab", + "SetupTitle": "Aplicativo do GitLab", + "SetupWhere": "Escolha onde registrar o aplicativo no GitLab:", + "SetupUserOwned": "Do usuário: nas configurações do seu usuário", + "SetupGroupOwned": "Do grupo (recomendado para equipes): em Settings > Applications do grupo", + "SetupInstanceWide": "Para toda a instância (administradores de instâncias auto-gerenciadas): na Área de administração", + "SetupTrustedHint": "Marque Trusted para ocultar a tela de consentimento dos usuários.", + "SetupNameAndRedirect": "Nome: {title}. Redirect URI:", + "SetupConfidentialScopes": "Marque Confidential, selecione os scopes {scopes} e salve o aplicativo.", + "SetupCopyCredentials": "Copie o Application ID e o Secret abaixo. O GitLab mostra o secret apenas uma vez.", + "GitlabUrl": "URL do GitLab", + "SelfManaged": "GitLab auto-gerenciado", + "ApplicationId": "ID do aplicativo", + "ApplicationSecret": "Chave secreta", + "KeepSecretHint": "Deixe em branco para manter o secret atual", + "Save": "Salvar", + "Change": "Alterar", + "Remove": "Remover", + "Copy": "Copiar", + "Copied": "Copiado", + "ConfiguredApp": "Aplicativo do GitLab {clientId} em {host}", + "OwnerMustConfigure": "Um proprietário do espaço de trabalho precisa configurar o aplicativo do GitLab primeiro.", + "InvalidGitlabUrl": "Informe uma URL https:// (http:// somente para localhost ou hosts *.local)", + "SetupEnterUrlFirst": "Informe abaixo a URL da sua instância do GitLab para ver onde registrar o aplicativo.", + "DisconnectEveryone": "Desconectar todos", + "DisconnectEveryoneConfirm": "Desconectar a conta do GitLab de todos os membros deste espaço de trabalho? Os projetos vinculados serão desvinculados e os webhooks serão removidos quando possível. Os membros poderão se conectar novamente depois.", + "GitlabIssue": "Issue do GitLab", + "IssueConnectedActivityInfo": "Conectado à issue do GitLab #{number} em {repoName}", + "MergeRequest": "Merge request", + "MergeRequests": "Merge requests", + "MergeRequestConnectedActivityInfo": "Conectado ao merge request do GitLab !{number} em {repoName}", + "Draft": "Rascunho", + "MergedAt": "Mesclado em", + "ClosedAt": "Fechado em", + "Commits": "Commits", + "Files": "Arquivos", + "SourceBranch": "Branch de origem", + "TargetBranch": "Branch de destino", + "MergeStatus": "Status do merge", + "MergeRequestState": "Estado", + "Reviewers": "Revisores", + "StateOpened": "Aberto", + "StateMerged": "Mesclado", + "StateClosed": "Fechado", + "StateLocked": "Bloqueado", + "ReadyToMerge": "Pronto para merge", + "Conflict": "Conflito", + "Checking": "Verificando", + "PipelinePending": "O pipeline precisa passar", + "UnresolvedDiscussions": "Discussões não resolvidas", + "NeedsApproval": "Requer aprovação", + "All": "Todos", + "Active": "Ativos", + "Closed": "Fechados", + "ChangedFiles": "{files} arquivos alterados", + "WithoutRepository": "Sem repositório", + "CreateInGitlab": "Criar no GitLab", + "DiffUnavailable": "Não foi possível carregar o diff", + "DiffTooLarge": "Alterações demais para mostrar aqui.", + "ApprovedBy": "Aprovado por", + "Approve": "Aprovar", + "RevokeApproval": "Revogar aprovação", + "ConnectToApprove": "Conecte sua conta do GitLab para aprovar", + "Review": "Revisão", + "ReviewThread": "Thread de revisão", + "ReviewComment": "Comentário de revisão", + "ReviewComments": "Comentários de revisão", + "ReviewApproved": "aprovou", + "ReviewUnapproved": "revogou a aprovação", + "ReviewRequestedChanges": "solicitou alterações", + "ReviewReviewed": "revisou", + "CommentedOnDiff": "comentou em {path}", + "ResolveThread": "Resolver thread", + "UnresolveThread": "Reabrir thread", + "ResolvedBy": "Resolvido por", + "Outdated": "Desatualizado", + "SyncError": "Erro de sincronização do GitLab", + "Retry": "Tentar novamente", + "ReviewNotSent": "Não enviado ao GitLab", + "LinkFailed": "Não foi possível vincular o projeto do GitLab. Tente novamente.", + "UnlinkFailed": "Não foi possível desvincular o projeto do GitLab. Tente novamente.", + "NotConfigured": "A integração com o GitLab não está configurada neste servidor.", + "AuthorizeLinkInvalid": "O GitLab retornou um link de login inválido. Tente novamente.", + "IntegrationRepositories": "Projetos do GitLab — {host} / {login}", + "SyncErrorMessage": "Erro de sincronização do GitLab: {message}", + "UnlinkConfirm": "Desvincular {repository} de {project}? As alterações deixarão de ser sincronizadas até que seja vinculado novamente.", + "RemoveAppConfirm": "Remover o aplicativo do GitLab deste espaço de trabalho? Os membros não poderão conectar o GitLab até que um novo seja configurado." + } +} diff --git a/services/gitlab/gitlab-assets/lang/pt.json b/services/gitlab/gitlab-assets/lang/pt.json new file mode 100644 index 0000000000..4017086918 --- /dev/null +++ b/services/gitlab/gitlab-assets/lang/pt.json @@ -0,0 +1,121 @@ +{ + "string": { + "Gitlab": "GitLab", + "GitlabDesc": "Ligue os seus projetos do GitLab para sincronizar issues, merge requests e atualizações do projeto", + "ConfigLabel": "GitLab", + "ConfigDescription": "Extensão para integração com o GitLab", + "Authorize": "Autorizar {title} com o GitLab", + "ReAuthorize": "Reautorizar {title} com o GitLab", + "Authorized": "Ligado como {login}", + "NotAuthorized": "Autorize {title} a aceder à sua conta do GitLab. Os projetos em que tem a função Mantenedor serão listados aqui.", + "NoRepositories": "Não foram encontrados projetos com a função Mantenedor para esta conta.", + "RefreshRepositories": "Atualizar projetos", + "ImageMode": "Imagens do GitLab", + "ImageModeLink": "Vincular ao GitLab", + "ImageModeCopy": "Copiar para o espaço de trabalho", + "ImageModeLinkHint": "As imagens ficam no GitLab. Apenas quem pode abrir o projeto do GitLab as vê.", + "ImageModeCopyHint": "As imagens são copiadas para o espaço de trabalho. Todos com acesso ao projeto vinculado as veem.", + "ImageNoAccess": "Precisa de acesso a este projeto do GitLab para ver esta imagem.", + "ImageNotConnected": "Ligue a sua conta do GitLab para ver imagens do GitLab.", + "ImageUnavailable": "Não é possível mostrar esta imagem de momento.", + "GitlabImages": "Imagens do GitLab ({count})", + "OpenInGitlab": "Abrir no GitLab", + "LinkToProject": "Associar ao projeto {title}", + "Unlink": "Desassociar", + "LinkedTo": "Associado a", + "LinkRequiresConnector": "Apenas o membro que ligou esta conta ou um responsável pode associá-lo", + "Unavailable": "Indisponível", + "Processing": "A processar...", + "AutoClose": "Este separador será fechado dentro de {time} segundos", + "RequestFailed": "O pedido falhou. Tente novamente.", + "CloseTab": "Fechar separador", + "KeepSettingsOpen": "Mantenha o separador de definições de {title} aberto enquanto liga o GitLab e tente novamente", + "Repository": "Projeto do GitLab", + "SetupTitle": "Aplicação do GitLab", + "SetupWhere": "Escolha onde registar a aplicação no GitLab:", + "SetupUserOwned": "Do utilizador: nas definições do seu utilizador", + "SetupGroupOwned": "Do grupo (recomendado para equipas): em Settings > Applications do grupo", + "SetupInstanceWide": "Para toda a instância (administradores de instâncias autogeridas): na Área de administração", + "SetupTrustedHint": "Marque Trusted para ocultar o ecrã de consentimento aos utilizadores.", + "SetupNameAndRedirect": "Nome: {title}. Redirect URI:", + "SetupConfidentialScopes": "Marque Confidential, selecione os scopes {scopes} e guarde a aplicação.", + "SetupCopyCredentials": "Copie o Application ID e o Secret abaixo. O GitLab mostra o secret apenas uma vez.", + "GitlabUrl": "URL do GitLab", + "SelfManaged": "GitLab autogerido", + "ApplicationId": "ID da aplicação", + "ApplicationSecret": "Chave secreta", + "KeepSecretHint": "Deixe em branco para manter o secret atual", + "Save": "Guardar", + "Change": "Alterar", + "Remove": "Remover", + "Copy": "Copiar", + "Copied": "Copiado", + "ConfiguredApp": "Aplicação do GitLab {clientId} em {host}", + "OwnerMustConfigure": "Um proprietário do espaço de trabalho tem de configurar primeiro a aplicação do GitLab.", + "InvalidGitlabUrl": "Introduza um URL https:// (http:// apenas para localhost ou hosts *.local)", + "SetupEnterUrlFirst": "Introduza abaixo o URL da sua instância do GitLab para ver onde registar a aplicação.", + "DisconnectEveryone": "Desligar todos", + "DisconnectEveryoneConfirm": "Desligar a conta do GitLab de todos os membros deste espaço de trabalho? Os projetos associados serão desassociados e os webhooks serão removidos sempre que possível. Os membros poderão voltar a ligar-se depois.", + "GitlabIssue": "Issue do GitLab", + "IssueConnectedActivityInfo": "Ligado à issue do GitLab #{number} em {repoName}", + "MergeRequest": "Merge request", + "MergeRequests": "Merge requests", + "MergeRequestConnectedActivityInfo": "Ligado ao merge request do GitLab !{number} em {repoName}", + "Draft": "Rascunho", + "MergedAt": "Integrado em", + "ClosedAt": "Fechado em", + "Commits": "Commits", + "Files": "Ficheiros", + "SourceBranch": "Branch de origem", + "TargetBranch": "Branch de destino", + "MergeStatus": "Estado do merge", + "MergeRequestState": "Estado", + "Reviewers": "Revisores", + "StateOpened": "Aberto", + "StateMerged": "Integrado", + "StateClosed": "Fechado", + "StateLocked": "Bloqueado", + "ReadyToMerge": "Pronto para merge", + "Conflict": "Conflito", + "Checking": "A verificar", + "PipelinePending": "O pipeline tem de passar", + "UnresolvedDiscussions": "Discussões por resolver", + "NeedsApproval": "Requer aprovação", + "All": "Todos", + "Active": "Ativos", + "Closed": "Fechados", + "ChangedFiles": "{files} ficheiros alterados", + "WithoutRepository": "Sem repositório", + "CreateInGitlab": "Criar no GitLab", + "DiffUnavailable": "Não foi possível carregar o diff", + "DiffTooLarge": "Demasiadas alterações para mostrar aqui.", + "ApprovedBy": "Aprovado por", + "Approve": "Aprovar", + "RevokeApproval": "Revogar aprovação", + "ConnectToApprove": "Ligue a sua conta do GitLab para aprovar", + "Review": "Revisão", + "ReviewThread": "Thread de revisão", + "ReviewComment": "Comentário de revisão", + "ReviewComments": "Comentários de revisão", + "ReviewApproved": "aprovou", + "ReviewUnapproved": "revogou a aprovação", + "ReviewRequestedChanges": "solicitou alterações", + "ReviewReviewed": "revisou", + "CommentedOnDiff": "comentou em {path}", + "ResolveThread": "Resolver thread", + "UnresolveThread": "Reabrir thread", + "ResolvedBy": "Resolvido por", + "Outdated": "Desatualizado", + "SyncError": "Erro de sincronização do GitLab", + "Retry": "Tentar novamente", + "ReviewNotSent": "Não enviado ao GitLab", + "LinkFailed": "Não foi possível associar o projeto do GitLab. Tente novamente.", + "UnlinkFailed": "Não foi possível desassociar o projeto do GitLab. Tente novamente.", + "NotConfigured": "A integração com o GitLab não está configurada neste servidor.", + "AuthorizeLinkInvalid": "O GitLab devolveu uma ligação de início de sessão inválida. Tente novamente.", + "IntegrationRepositories": "Projetos do GitLab — {host} / {login}", + "SyncErrorMessage": "Erro de sincronização do GitLab: {message}", + "UnlinkConfirm": "Desassociar {repository} de {project}? As alterações deixam de ser sincronizadas até voltar a ser associado.", + "RemoveAppConfirm": "Remover a aplicação do GitLab deste espaço de trabalho? Os membros não podem ligar o GitLab até ser configurada uma nova." + } +} diff --git a/services/gitlab/gitlab-assets/lang/ru.json b/services/gitlab/gitlab-assets/lang/ru.json new file mode 100644 index 0000000000..4e90f9fc24 --- /dev/null +++ b/services/gitlab/gitlab-assets/lang/ru.json @@ -0,0 +1,121 @@ +{ + "string": { + "Gitlab": "GitLab", + "GitlabDesc": "Подключите проекты GitLab для синхронизации задач, merge request и обновлений проектов", + "ConfigLabel": "GitLab", + "ConfigDescription": "Расширение для интеграции с GitLab", + "Authorize": "Авторизовать {title} в GitLab", + "ReAuthorize": "Повторно авторизовать {title} в GitLab", + "Authorized": "Подключено как {login}", + "NotAuthorized": "Авторизуйте {title} для доступа к вашему аккаунту GitLab. Здесь будут показаны проекты, в которых у вас роль Maintainer.", + "NoRepositories": "Для этого аккаунта не найдено проектов с ролью Maintainer.", + "RefreshRepositories": "Обновить проекты", + "ImageMode": "Изображения из GitLab", + "ImageModeLink": "Ссылаться на GitLab", + "ImageModeCopy": "Копировать в рабочее пространство", + "ImageModeLinkHint": "Изображения остаются в GitLab. Их видят только те, кто может открыть проект в GitLab.", + "ImageModeCopyHint": "Изображения копируются в рабочее пространство. Их видят все, у кого есть доступ к связанному проекту.", + "ImageNoAccess": "Чтобы увидеть это изображение, нужен доступ к этому проекту в GitLab.", + "ImageNotConnected": "Подключите свою учётную запись GitLab, чтобы видеть изображения из GitLab.", + "ImageUnavailable": "Сейчас это изображение нельзя показать.", + "GitlabImages": "Изображения GitLab ({count})", + "OpenInGitlab": "Открыть в GitLab", + "LinkToProject": "Связать с проектом {title}", + "Unlink": "Отвязать", + "LinkedTo": "Связан с", + "LinkRequiresConnector": "Связать может только участник, подключивший эту учётную запись, или мейнтейнер", + "Unavailable": "Недоступен", + "Processing": "Обработка...", + "AutoClose": "Вкладка закроется через {time} сек.", + "RequestFailed": "Запрос не выполнен. Попробуйте ещё раз.", + "CloseTab": "Закрыть вкладку", + "KeepSettingsOpen": "Не закрывайте вкладку настроек {title} во время подключения GitLab и попробуйте снова", + "Repository": "Проект GitLab", + "SetupTitle": "Приложение GitLab", + "SetupWhere": "Выберите, где зарегистрировать приложение в GitLab:", + "SetupUserOwned": "Пользовательское: в настройках вашего профиля", + "SetupGroupOwned": "Групповое (рекомендуется для команд): в разделе Settings > Applications группы", + "SetupInstanceWide": "Для всего инстанса (администраторы self-managed): в разделе Admin area", + "SetupTrustedHint": "Отметьте Trusted, чтобы пользователи не видели экран подтверждения доступа.", + "SetupNameAndRedirect": "Имя: {title}. Redirect URI:", + "SetupConfidentialScopes": "Отметьте Confidential, выберите scopes {scopes} и сохраните приложение.", + "SetupCopyCredentials": "Скопируйте Application ID и Secret в поля ниже. GitLab показывает секрет только один раз.", + "GitlabUrl": "URL GitLab", + "SelfManaged": "Собственный сервер GitLab (self-managed)", + "ApplicationId": "ID приложения", + "ApplicationSecret": "Секрет", + "KeepSecretHint": "Оставьте пустым, чтобы сохранить текущий секрет", + "Save": "Сохранить", + "Change": "Изменить", + "Remove": "Удалить", + "Copy": "Копировать", + "Copied": "Скопировано", + "ConfiguredApp": "Приложение GitLab {clientId} на {host}", + "OwnerMustConfigure": "Сначала владелец рабочего пространства должен настроить приложение GitLab.", + "InvalidGitlabUrl": "Введите URL с https:// (http:// только для localhost или хостов *.local)", + "SetupEnterUrlFirst": "Введите ниже URL вашего экземпляра GitLab, чтобы увидеть, где зарегистрировать приложение.", + "DisconnectEveryone": "Отключить всех", + "DisconnectEveryoneConfirm": "Отключить аккаунты GitLab всех участников от этого рабочего пространства? Их связанные проекты будут отвязаны, а вебхуки по возможности удалены. Участники смогут подключиться снова.", + "GitlabIssue": "Задача GitLab", + "IssueConnectedActivityInfo": "Связано с задачей GitLab #{number} в проекте {repoName}", + "MergeRequest": "Запрос на слияние", + "MergeRequests": "Запросы на слияние", + "MergeRequestConnectedActivityInfo": "Связано с запросом на слияние GitLab !{number} в проекте {repoName}", + "Draft": "Черновик", + "MergedAt": "Дата слияния", + "ClosedAt": "Дата закрытия", + "Commits": "Коммиты", + "Files": "Файлы", + "SourceBranch": "Исходная ветка", + "TargetBranch": "Целевая ветка", + "MergeStatus": "Статус слияния", + "MergeRequestState": "Состояние", + "Reviewers": "Ревьюеры", + "StateOpened": "Открыт", + "StateMerged": "Слит", + "StateClosed": "Закрыт", + "StateLocked": "Заблокирован", + "ReadyToMerge": "Готов к слиянию", + "Conflict": "Конфликт", + "Checking": "Проверяется", + "PipelinePending": "Ожидает пайплайн", + "UnresolvedDiscussions": "Есть нерешённые обсуждения", + "NeedsApproval": "Требуется одобрение", + "All": "Все", + "Active": "Активные", + "Closed": "Закрытые", + "ChangedFiles": "Изменено файлов: {files}", + "WithoutRepository": "Без репозитория", + "CreateInGitlab": "Создать в GitLab", + "DiffUnavailable": "Не удалось загрузить изменения", + "DiffTooLarge": "Слишком много изменений для показа здесь.", + "ApprovedBy": "Одобрили", + "Approve": "Одобрить", + "RevokeApproval": "Отозвать одобрение", + "ConnectToApprove": "Подключите свой аккаунт GitLab, чтобы одобрять", + "Review": "Ревью", + "ReviewThread": "Обсуждение ревью", + "ReviewComment": "Комментарий ревью", + "ReviewComments": "Комментарии ревью", + "ReviewApproved": "одобрил(а)", + "ReviewUnapproved": "отозвал(а) одобрение", + "ReviewRequestedChanges": "запросил(а) изменения", + "ReviewReviewed": "проверил(а)", + "CommentedOnDiff": "прокомментировал(а) {path}", + "ResolveThread": "Закрыть обсуждение", + "UnresolveThread": "Открыть обсуждение снова", + "ResolvedBy": "Закрыл(а)", + "Outdated": "Устарело", + "SyncError": "Ошибка синхронизации с GitLab", + "Retry": "Повторить", + "ReviewNotSent": "Не отправлено в GitLab", + "LinkFailed": "Не удалось привязать проект GitLab. Попробуйте ещё раз.", + "UnlinkFailed": "Не удалось отвязать проект GitLab. Попробуйте ещё раз.", + "NotConfigured": "Интеграция с GitLab не настроена на этом сервере.", + "AuthorizeLinkInvalid": "GitLab вернул недействительную ссылку для входа. Попробуйте ещё раз.", + "IntegrationRepositories": "Проекты GitLab — {host} / {login}", + "SyncErrorMessage": "Ошибка синхронизации с GitLab: {message}", + "UnlinkConfirm": "Отвязать {repository} от {project}? Изменения не будут синхронизироваться, пока он не будет привязан снова.", + "RemoveAppConfirm": "Удалить приложение GitLab из этого рабочего пространства? Участники не смогут подключить GitLab, пока не будет настроено новое." + } +} diff --git a/services/gitlab/gitlab-assets/lang/sp.json b/services/gitlab/gitlab-assets/lang/sp.json new file mode 100644 index 0000000000..78de18d1be --- /dev/null +++ b/services/gitlab/gitlab-assets/lang/sp.json @@ -0,0 +1,121 @@ +{ + "string": { + "Gitlab": "GitLab", + "GitlabDesc": "Conecta tus proyectos de GitLab para sincronizar problemas, solicitudes de fusión y actualizaciones del proyecto", + "ConfigLabel": "GitLab", + "ConfigDescription": "Extensión para la integración con GitLab", + "Authorize": "Autorizar {title} con GitLab", + "ReAuthorize": "Volver a autorizar {title} con GitLab", + "Authorized": "Conectado como {login}", + "NotAuthorized": "Autoriza {title} para acceder a tu cuenta de GitLab. Aquí se mostrarán los proyectos en los que tengas el rol de Mantenedor.", + "NoRepositories": "No se encontraron proyectos con el rol de Mantenedor para esta cuenta.", + "RefreshRepositories": "Actualizar proyectos", + "ImageMode": "Imágenes de GitLab", + "ImageModeLink": "Enlazar a GitLab", + "ImageModeCopy": "Copiar al espacio de trabajo", + "ImageModeLinkHint": "Las imágenes permanecen en GitLab. Solo las ven quienes pueden abrir el proyecto de GitLab.", + "ImageModeCopyHint": "Las imágenes se copian al espacio de trabajo. Las ve cualquiera con acceso al proyecto vinculado.", + "ImageNoAccess": "Necesitas acceso a este proyecto de GitLab para ver esta imagen.", + "ImageNotConnected": "Conecta tu cuenta de GitLab para ver imágenes de GitLab.", + "ImageUnavailable": "Esta imagen no se puede mostrar ahora.", + "GitlabImages": "Imágenes de GitLab ({count})", + "OpenInGitlab": "Abrir en GitLab", + "LinkToProject": "Vincular con el proyecto {title}", + "Unlink": "Desvincular", + "LinkedTo": "Vinculado a", + "LinkRequiresConnector": "Solo el miembro que conectó esta cuenta o un mantenedor puede vincularlo", + "Unavailable": "No disponible", + "Processing": "Procesando...", + "AutoClose": "Esta pestaña se cerrará en {time} segundos", + "RequestFailed": "La solicitud ha fallado. Inténtalo de nuevo.", + "CloseTab": "Cerrar pestaña", + "KeepSettingsOpen": "Mantén abierta la pestaña de configuración de {title} mientras conectas GitLab y vuelve a intentarlo", + "Repository": "Proyecto de GitLab", + "SetupTitle": "Aplicación de GitLab", + "SetupWhere": "Elige dónde registrar la aplicación en GitLab:", + "SetupUserOwned": "De usuario: en la configuración de tu usuario", + "SetupGroupOwned": "De grupo (recomendado para equipos): en Settings > Applications del grupo", + "SetupInstanceWide": "De toda la instancia (administradores de instancias autogestionadas): en el área de administración", + "SetupTrustedHint": "Marca Trusted para omitir la pantalla de consentimiento de los usuarios.", + "SetupNameAndRedirect": "Nombre: {title}. Redirect URI:", + "SetupConfidentialScopes": "Marca Confidential, selecciona los scopes {scopes} y guarda la aplicación.", + "SetupCopyCredentials": "Copia el Application ID y el Secret a continuación. GitLab muestra el secreto solo una vez.", + "GitlabUrl": "URL de GitLab", + "SelfManaged": "GitLab autogestionado", + "ApplicationId": "ID de la aplicación", + "ApplicationSecret": "Secreto", + "KeepSecretHint": "Déjalo vacío para conservar el secreto actual", + "Save": "Guardar", + "Change": "Cambiar", + "Remove": "Eliminar", + "Copy": "Copiar", + "Copied": "Copiado", + "ConfiguredApp": "Aplicación de GitLab {clientId} en {host}", + "OwnerMustConfigure": "Primero, un propietario del espacio de trabajo debe configurar la aplicación de GitLab.", + "InvalidGitlabUrl": "Introduce una URL https:// (http:// solo para localhost o hosts *.local)", + "SetupEnterUrlFirst": "Introduce abajo la URL de tu instancia de GitLab para ver dónde registrar la aplicación.", + "DisconnectEveryone": "Desconectar a todos", + "DisconnectEveryoneConfirm": "¿Desconectar la cuenta de GitLab de todos los miembros de este espacio de trabajo? Sus proyectos vinculados se desvincularán y los webhooks se eliminarán cuando sea posible. Los miembros podrán volver a conectarse después.", + "GitlabIssue": "Problema de GitLab", + "IssueConnectedActivityInfo": "Conectado al problema de GitLab #{number} en {repoName}", + "MergeRequest": "Solicitud de fusión", + "MergeRequests": "Solicitudes de fusión", + "MergeRequestConnectedActivityInfo": "Conectado a la solicitud de fusión de GitLab !{number} en {repoName}", + "Draft": "Borrador", + "MergedAt": "Combinado en", + "ClosedAt": "Cerrado en", + "Commits": "Confirmaciones", + "Files": "Archivos", + "SourceBranch": "Rama de origen", + "TargetBranch": "Rama de destino", + "MergeStatus": "Estado de la combinación", + "MergeRequestState": "Estado", + "Reviewers": "Revisores", + "StateOpened": "Abierta", + "StateMerged": "Combinada", + "StateClosed": "Cerrada", + "StateLocked": "Bloqueada", + "ReadyToMerge": "Lista para combinar", + "Conflict": "Conflicto", + "Checking": "Comprobando", + "PipelinePending": "El pipeline debe pasar", + "UnresolvedDiscussions": "Discusiones sin resolver", + "NeedsApproval": "Requiere aprobación", + "All": "Todos", + "Active": "Activos", + "Closed": "Cerrados", + "ChangedFiles": "{files} archivos modificados", + "WithoutRepository": "Sin repositorio", + "CreateInGitlab": "Crear en GitLab", + "DiffUnavailable": "No se pudo cargar el diff", + "DiffTooLarge": "Demasiados cambios para mostrarlos aquí.", + "ApprovedBy": "Aprobado por", + "Approve": "Aprobar", + "RevokeApproval": "Revocar aprobación", + "ConnectToApprove": "Conecta tu cuenta de GitLab para aprobar", + "Review": "Revisión", + "ReviewThread": "Hilo de revisión", + "ReviewComment": "Comentario de revisión", + "ReviewComments": "Comentarios de revisión", + "ReviewApproved": "aprobó", + "ReviewUnapproved": "revocó la aprobación", + "ReviewRequestedChanges": "solicitó cambios", + "ReviewReviewed": "revisó", + "CommentedOnDiff": "comentó en {path}", + "ResolveThread": "Resolver hilo", + "UnresolveThread": "Reabrir hilo", + "ResolvedBy": "Resuelto por", + "Outdated": "Obsoleto", + "SyncError": "Error de sincronización de GitLab", + "Retry": "Reintentar", + "ReviewNotSent": "No enviado a GitLab", + "LinkFailed": "No se pudo vincular el proyecto de GitLab. Inténtalo de nuevo.", + "UnlinkFailed": "No se pudo desvincular el proyecto de GitLab. Inténtalo de nuevo.", + "NotConfigured": "La integración con GitLab no está configurada en este servidor.", + "AuthorizeLinkInvalid": "GitLab devolvió un enlace de inicio de sesión no válido. Inténtalo de nuevo.", + "IntegrationRepositories": "Proyectos de GitLab — {host} / {login}", + "SyncErrorMessage": "Error de sincronización de GitLab: {message}", + "UnlinkConfirm": "¿Desvincular {repository} de {project}? Los cambios dejarán de sincronizarse hasta que se vuelva a vincular.", + "RemoveAppConfirm": "¿Eliminar la aplicación de GitLab de este espacio de trabajo? Los miembros no podrán conectar GitLab hasta que se configure una nueva." + } +} diff --git a/services/gitlab/gitlab-assets/lang/tr.json b/services/gitlab/gitlab-assets/lang/tr.json new file mode 100644 index 0000000000..58394ef411 --- /dev/null +++ b/services/gitlab/gitlab-assets/lang/tr.json @@ -0,0 +1,121 @@ +{ + "string": { + "Gitlab": "GitLab", + "GitlabDesc": "GitLab projelerinizi bağlayarak sorunları, birleştirme isteklerini ve proje güncellemelerini senkronize edin", + "ConfigLabel": "GitLab", + "ConfigDescription": "GitLab entegrasyonu için uzantı", + "Authorize": "{title} uygulamasını GitLab ile yetkilendir", + "ReAuthorize": "{title} uygulamasını GitLab ile yeniden yetkilendir", + "Authorized": "{login} olarak bağlandı", + "NotAuthorized": "GitLab hesabınıza erişmek için {title} uygulamasını yetkilendirin. Maintainer rolüne sahip olduğunuz projeler burada listelenir.", + "NoRepositories": "Bu hesap için Maintainer rolüne sahip proje bulunamadı.", + "RefreshRepositories": "Projeleri yenile", + "ImageMode": "GitLab görselleri", + "ImageModeLink": "GitLab'e bağlantı ver", + "ImageModeCopy": "Çalışma alanına kopyala", + "ImageModeLinkHint": "Görseller GitLab'de kalır. Yalnızca GitLab projesini açabilenler görür.", + "ImageModeCopyHint": "Görseller çalışma alanına kopyalanır. Bağlı projeye erişimi olan herkes görür.", + "ImageNoAccess": "Bu görseli görmek için bu GitLab projesine erişiminiz olmalı.", + "ImageNotConnected": "GitLab görsellerini görmek için GitLab hesabınızı bağlayın.", + "ImageUnavailable": "Bu görsel şu anda gösterilemiyor.", + "GitlabImages": "GitLab görselleri ({count})", + "OpenInGitlab": "GitLab'de aç", + "LinkToProject": "{title} projesine bağla", + "Unlink": "Bağlantıyı kaldır", + "LinkedTo": "Bağlı olduğu proje", + "LinkRequiresConnector": "Yalnızca bu hesabı bağlayan üye veya bir yönetici bağlayabilir", + "Unavailable": "Kullanılamıyor", + "Processing": "İşleniyor...", + "AutoClose": "Bu sekme {time} saniye içinde kapanacak", + "RequestFailed": "İstek başarısız. Lütfen tekrar deneyin.", + "CloseTab": "Sekmeyi kapat", + "KeepSettingsOpen": "GitLab bağlanırken {title} ayarları sekmesini açık tutun, ardından tekrar deneyin", + "Repository": "GitLab projesi", + "SetupTitle": "GitLab uygulaması", + "SetupWhere": "Uygulamanın GitLab'da nerede kaydedileceğini seçin:", + "SetupUserOwned": "Kullanıcıya ait: kullanıcı ayarlarınızda", + "SetupGroupOwned": "Gruba ait (ekipler için önerilir): grubun Settings > Applications bölümünde", + "SetupInstanceWide": "Tüm örnek için (self-managed yöneticileri): Admin area içinde", + "SetupTrustedHint": "Kullanıcılara onay ekranını göstermemek için Trusted seçeneğini işaretleyin.", + "SetupNameAndRedirect": "Ad: {title}. Redirect URI:", + "SetupConfidentialScopes": "Confidential seçeneğini işaretleyin, {scopes} scopes değerlerini seçin ve uygulamayı kaydedin.", + "SetupCopyCredentials": "Aşağıdaki Application ID ve Secret değerlerini kopyalayın. GitLab secret değerini yalnızca bir kez gösterir.", + "GitlabUrl": "GitLab URL'si", + "SelfManaged": "Kendi sunucunuzdaki GitLab (self-managed)", + "ApplicationId": "Uygulama Kimliği", + "ApplicationSecret": "Gizli anahtar", + "KeepSecretHint": "Mevcut secret'ı korumak için boş bırakın", + "Save": "Kaydet", + "Change": "Değiştir", + "Remove": "Kaldır", + "Copy": "Kopyala", + "Copied": "Kopyalandı", + "ConfiguredApp": "{host} üzerindeki GitLab uygulaması {clientId}", + "OwnerMustConfigure": "Önce bir çalışma alanı sahibinin GitLab uygulamasını kurması gerekir.", + "InvalidGitlabUrl": "https:// ile başlayan bir URL girin (http:// yalnızca localhost veya *.local ana bilgisayarları için)", + "SetupEnterUrlFirst": "Uygulamanın nerede kaydedileceğini görmek için aşağıya GitLab örneğinizin URL'sini girin.", + "DisconnectEveryone": "Herkesin bağlantısını kes", + "DisconnectEveryoneConfirm": "Tüm üyelerin GitLab hesaplarının bu çalışma alanıyla bağlantısı kesilsin mi? Bağlı projelerinin bağlantısı kaldırılır ve mümkünse webhook'lar silinir. Üyeler daha sonra tekrar bağlanabilir.", + "GitlabIssue": "GitLab sorunu", + "IssueConnectedActivityInfo": "{repoName} içindeki GitLab sorunu #{number} ile bağlandı", + "MergeRequest": "Birleştirme isteği", + "MergeRequests": "Birleştirme istekleri", + "MergeRequestConnectedActivityInfo": "{repoName} içindeki GitLab birleştirme isteği !{number} ile bağlandı", + "Draft": "Taslak", + "MergedAt": "Birleştirilme tarihi", + "ClosedAt": "Kapatılma tarihi", + "Commits": "Commit'ler", + "Files": "Dosyalar", + "SourceBranch": "Kaynak dal", + "TargetBranch": "Hedef dal", + "MergeStatus": "Birleştirme durumu", + "MergeRequestState": "Durum", + "Reviewers": "İnceleyenler", + "StateOpened": "Açık", + "StateMerged": "Birleştirildi", + "StateClosed": "Kapalı", + "StateLocked": "Kilitli", + "ReadyToMerge": "Birleştirmeye hazır", + "Conflict": "Çakışma", + "Checking": "Denetleniyor", + "PipelinePending": "Pipeline başarılı olmalı", + "UnresolvedDiscussions": "Çözülmemiş tartışmalar", + "NeedsApproval": "Onay gerekli", + "All": "Tümü", + "Active": "Aktif", + "Closed": "Kapalı", + "ChangedFiles": "Değiştirilen dosyalar: {files}", + "WithoutRepository": "Depo olmadan", + "CreateInGitlab": "GitLab'da oluştur", + "DiffUnavailable": "Değişiklikler yüklenemedi", + "DiffTooLarge": "Burada gösterilemeyecek kadar çok değişiklik var.", + "ApprovedBy": "Onaylayanlar", + "Approve": "Onayla", + "RevokeApproval": "Onayı geri al", + "ConnectToApprove": "Onaylamak için GitLab hesabınızı bağlayın", + "Review": "İnceleme", + "ReviewThread": "İnceleme konusu", + "ReviewComment": "İnceleme yorumu", + "ReviewComments": "İnceleme yorumları", + "ReviewApproved": "onayladı", + "ReviewUnapproved": "onayı geri aldı", + "ReviewRequestedChanges": "değişiklik istedi", + "ReviewReviewed": "inceledi", + "CommentedOnDiff": "{path} üzerine yorum yaptı", + "ResolveThread": "Konuyu çöz", + "UnresolveThread": "Konuyu yeniden aç", + "ResolvedBy": "Çözen", + "Outdated": "Güncel değil", + "SyncError": "GitLab senkronizasyon hatası", + "Retry": "Yeniden dene", + "ReviewNotSent": "GitLab'a gönderilmedi", + "LinkFailed": "GitLab projesi bağlanamadı. Tekrar deneyin.", + "UnlinkFailed": "GitLab projesinin bağlantısı kaldırılamadı. Tekrar deneyin.", + "NotConfigured": "GitLab entegrasyonu bu sunucuda yapılandırılmamış.", + "AuthorizeLinkInvalid": "GitLab geçersiz bir oturum açma bağlantısı döndürdü. Tekrar deneyin.", + "IntegrationRepositories": "GitLab projeleri — {host} / {login}", + "SyncErrorMessage": "GitLab senkronizasyon hatası: {message}", + "UnlinkConfirm": "{repository} projesinin {project} ile bağlantısı kaldırılsın mı? Yeniden bağlanana kadar değişiklikler senkronize edilmez.", + "RemoveAppConfirm": "GitLab uygulaması bu çalışma alanından kaldırılsın mı? Yeni bir uygulama ayarlanana kadar üyeler GitLab'ı bağlayamaz." + } +} diff --git a/services/gitlab/gitlab-assets/lang/zh.json b/services/gitlab/gitlab-assets/lang/zh.json new file mode 100644 index 0000000000..aba5dba715 --- /dev/null +++ b/services/gitlab/gitlab-assets/lang/zh.json @@ -0,0 +1,121 @@ +{ + "string": { + "Gitlab": "GitLab", + "GitlabDesc": "连接您的 GitLab 项目,同步议题、合并请求和项目更新", + "ConfigLabel": "GitLab", + "ConfigDescription": "GitLab 集成扩展", + "Authorize": "通过 GitLab 授权 {title}", + "ReAuthorize": "通过 GitLab 重新授权 {title}", + "Authorized": "已以 {login} 身份连接", + "NotAuthorized": "授权 {title} 访问您的 GitLab 账户。此处将列出您拥有维护者角色的项目。", + "NoRepositories": "未找到此账户拥有维护者角色的项目。", + "RefreshRepositories": "刷新项目", + "ImageMode": "来自 GitLab 的图片", + "ImageModeLink": "链接到 GitLab", + "ImageModeCopy": "复制到工作区", + "ImageModeLinkHint": "图片保留在 GitLab 上。只有能打开该 GitLab 项目的人才能看到。", + "ImageModeCopyHint": "图片会复制到工作区。所有能访问已关联项目的人都能看到。", + "ImageNoAccess": "您需要此 GitLab 项目的访问权限才能查看此图片。", + "ImageNotConnected": "连接您的 GitLab 账户以查看来自 GitLab 的图片。", + "ImageUnavailable": "暂时无法显示此图片。", + "GitlabImages": "GitLab 图片 ({count})", + "OpenInGitlab": "在 GitLab 中打开", + "LinkToProject": "关联到 {title} 项目", + "Unlink": "取消关联", + "LinkedTo": "已关联到", + "LinkRequiresConnector": "只有连接此账户的成员或维护者可以关联", + "Unavailable": "不可用", + "Processing": "处理中...", + "AutoClose": "此标签页将在 {time} 秒后关闭", + "RequestFailed": "请求失败,请重试。", + "CloseTab": "关闭标签页", + "KeepSettingsOpen": "连接 GitLab 时请保持 {title} 设置标签页打开,然后重试", + "Repository": "GitLab 项目", + "SetupTitle": "GitLab 应用程序", + "SetupWhere": "选择在 GitLab 中注册应用程序的位置:", + "SetupUserOwned": "用户所有:在您的用户设置中", + "SetupGroupOwned": "群组所有(推荐团队使用):在群组的 设置 > 应用程序 中", + "SetupInstanceWide": "实例范围(自助管理的管理员):在管理中心", + "SetupTrustedHint": "勾选“受信任”可为用户跳过授权确认页面。", + "SetupNameAndRedirect": "名称:{title}。重定向 URI:", + "SetupConfidentialScopes": "勾选“机密”,选择范围 {scopes},然后保存应用程序。", + "SetupCopyCredentials": "复制下方的 Application ID 和密钥。GitLab 只显示一次密钥。", + "GitlabUrl": "GitLab URL", + "SelfManaged": "自助管理的 GitLab", + "ApplicationId": "应用程序 ID", + "ApplicationSecret": "密钥", + "KeepSecretHint": "留空以保留当前密钥", + "Save": "保存", + "Change": "更改", + "Remove": "移除", + "Copy": "复制", + "Copied": "已复制", + "ConfiguredApp": "{host} 上的 GitLab 应用程序 {clientId}", + "OwnerMustConfigure": "需要先由工作区所有者设置 GitLab 应用程序。", + "InvalidGitlabUrl": "请输入 https:// URL(http:// 仅限 localhost 或 *.local 主机)", + "SetupEnterUrlFirst": "请在下方输入您的 GitLab 实例 URL,以查看注册应用程序的位置。", + "DisconnectEveryone": "断开所有人的连接", + "DisconnectEveryoneConfirm": "要断开此工作区中所有成员的 GitLab 账户连接吗?他们已关联的项目将被取消关联,并在可能的情况下移除 Webhook。之后成员可以重新连接。", + "GitlabIssue": "GitLab 议题", + "IssueConnectedActivityInfo": "已连接到 {repoName} 中的 GitLab 议题 #{number}", + "MergeRequest": "合并请求", + "MergeRequests": "合并请求", + "MergeRequestConnectedActivityInfo": "已连接到 {repoName} 中的 GitLab 合并请求 !{number}", + "Draft": "草稿", + "MergedAt": "合并于", + "ClosedAt": "关闭于", + "Commits": "提交", + "Files": "文件", + "SourceBranch": "源分支", + "TargetBranch": "目标分支", + "MergeStatus": "合并状态", + "MergeRequestState": "状态", + "Reviewers": "审核者", + "StateOpened": "开放", + "StateMerged": "已合并", + "StateClosed": "已关闭", + "StateLocked": "已锁定", + "ReadyToMerge": "可以合并", + "Conflict": "冲突", + "Checking": "检查中", + "PipelinePending": "流水线必须通过", + "UnresolvedDiscussions": "未解决的讨论", + "NeedsApproval": "需要批准", + "All": "全部", + "Active": "活跃", + "Closed": "已关闭", + "ChangedFiles": "{files} 个更改的文件", + "WithoutRepository": "无仓库", + "CreateInGitlab": "在 GitLab 中创建", + "DiffUnavailable": "无法加载差异", + "DiffTooLarge": "更改过多,无法在此显示。", + "ApprovedBy": "批准者", + "Approve": "批准", + "RevokeApproval": "撤销批准", + "ConnectToApprove": "连接您的 GitLab 账户以进行批准", + "Review": "审核", + "ReviewThread": "审核主题", + "ReviewComment": "审核评论", + "ReviewComments": "审核评论", + "ReviewApproved": "已批准", + "ReviewUnapproved": "已撤销批准", + "ReviewRequestedChanges": "请求更改", + "ReviewReviewed": "已审核", + "CommentedOnDiff": "评论了 {path}", + "ResolveThread": "解决主题", + "UnresolveThread": "重新打开主题", + "ResolvedBy": "解决者", + "Outdated": "已过时", + "SyncError": "GitLab 同步错误", + "Retry": "重试", + "ReviewNotSent": "未发送到 GitLab", + "LinkFailed": "无法关联 GitLab 项目,请重试。", + "UnlinkFailed": "无法取消关联 GitLab 项目,请重试。", + "NotConfigured": "此服务器未配置 GitLab 集成。", + "AuthorizeLinkInvalid": "GitLab 返回了无效的登录链接,请重试。", + "IntegrationRepositories": "GitLab 项目 — {host} / {login}", + "SyncErrorMessage": "GitLab 同步错误:{message}", + "UnlinkConfirm": "要取消 {repository} 与 {project} 的关联吗?在重新关联之前,更改将不再同步。", + "RemoveAppConfirm": "要从此工作区移除 GitLab 应用程序吗?在设置新的应用程序之前,成员无法连接 GitLab。" + } +} diff --git a/services/gitlab/gitlab-assets/package.json b/services/gitlab/gitlab-assets/package.json new file mode 100644 index 0000000000..5ce157ad1f --- /dev/null +++ b/services/gitlab/gitlab-assets/package.json @@ -0,0 +1,67 @@ +{ + "name": "@hcengineering/gitlab-assets", + "version": "0.7.0", + "main": "./lib/index.js", + "types": "./types/index.d.ts", + "files": [ + "lib", + "types", + "src", + "assets", + "lang" + ], + "template": "@hcengineering/assets-package", + "scripts": { + "build": "compile", + "test": "jest --passWithNoTests --silent", + "build:docs": "", + "format": "format src", + "build:watch": "compile", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.21", + "@typescript-eslint/eslint-plugin": "^8.67.0", + "@typescript-eslint/parser": "^8.67.0", + "eslint-plugin-n": "^18.3.0", + "eslint-plugin-promise": "^7.3.0", + "eslint": "^9.39.5", + "prettier": "^3.6.2", + "@types/node": "^24.13.3", + "jest": "^30.4.2", + "ts-jest": "^29.4.12", + "@types/jest": "^30.0.0", + "typescript": "^5.9.3", + "eslint-plugin-svelte": "^3.23.0", + "eslint-config-love": "^157.0.0" + }, + "dependencies": { + "@hcengineering/platform": "workspace:^0.7.20", + "@hcengineering/gitlab": "workspace:^0.7.0" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "default": "./lib/index.js" + }, + "./lang/*.json": { + "require": "./lang/*.json", + "import": "./lang/*.json" + }, + "./lang": { + "types": "./types/lang.d.ts", + "default": "./lib/lang.js" + }, + "./assets": { + "require": "./assets", + "import": "./assets" + }, + "./assets/*.svg": { + "require": "./assets/*.svg", + "import": "./assets/*.svg" + } + } +} diff --git a/services/gitlab/gitlab-assets/src/__tests__/lang.test.ts b/services/gitlab/gitlab-assets/src/__tests__/lang.test.ts new file mode 100644 index 0000000000..1dec62ad7c --- /dev/null +++ b/services/gitlab/gitlab-assets/src/__tests__/lang.test.ts @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: EPL-2.0 +import { makeLocalesTest } from '@hcengineering/platform' + +it( + 'Locales are equal', + makeLocalesTest((lang) => import(`../../lang/${lang}.json`)) +) + +const locales = ['cs', 'de', 'en', 'es', 'fr', 'it', 'ko', 'pl', 'pt', 'pt-br', 'ru', 'sp', 'tr', 'zh'] + +type Strings = Record + +async function stringsOf (lang: string): Promise { + return (await import(`../../lang/${lang}.json`)).string +} + +let english: Strings +beforeAll(async () => { + english = await stringsOf('en') +}) + +// The settings integration card renders the description without params; a placeholder would fail formatting. +it.each(locales)('GitlabDesc has no placeholders (%s)', async (lang) => { + const strings = await stringsOf(lang) + expect(strings.GitlabDesc).not.toMatch(/[{}]/) +}) + +// SetupApp always passes these params; a hard-coded product name or scope list would drift from the pod. +it.each(locales)('setup strings take the title and scopes params (%s)', async (lang) => { + const strings = await stringsOf(lang) + expect(strings.SetupNameAndRedirect).toContain('{title}') + expect(strings.SetupNameAndRedirect).not.toContain('Huly') + expect(strings.SetupConfidentialScopes).toContain('{scopes}') + expect(strings.DisconnectEveryone).toBeTruthy() + expect(strings.DisconnectEveryoneConfirm).not.toMatch(/[{}]/) +}) + +// The pod renders this activity message with exactly these params. +it.each(locales)('issue strings exist with their params (%s)', async (lang) => { + const strings = await stringsOf(lang) + expect(strings.GitlabIssue).toBeTruthy() + for (const param of ['{url}', '{number}', '{repository}', '{repoName}']) { + expect(strings.IssueConnectedActivityInfo).toContain(param) + } +}) + +const MERGE_REQUEST_KEYS = [ + 'MergeRequest', + 'MergeRequests', + 'MergeRequestConnectedActivityInfo', + 'Draft', + 'MergedAt', + 'ClosedAt', + 'Commits', + 'Files', + 'SourceBranch', + 'TargetBranch', + 'MergeStatus', + 'MergeRequestState', + 'Reviewers', + 'StateOpened', + 'StateMerged', + 'StateClosed', + 'StateLocked', + 'ReadyToMerge', + 'Conflict', + 'Checking', + 'PipelinePending', + 'UnresolvedDiscussions', + 'NeedsApproval', + 'All', + 'Active', + 'Closed', + 'ChangedFiles', + 'WithoutRepository', + 'CreateInGitlab', + 'DiffUnavailable', + 'DiffTooLarge' +] + +// The pod renders the activity message with exactly these params; the diff panel passes {files}. +it.each(locales)('merge request strings exist with their params (%s)', async (lang) => { + const strings = await stringsOf(lang) + for (const key of MERGE_REQUEST_KEYS) { + expect(strings[key]).toBeTruthy() + } + for (const param of ['{url}', '{number}', '{repository}', '{repoName}']) { + expect(strings.MergeRequestConnectedActivityInfo).toContain(param) + } + expect(strings.ChangedFiles).toContain('{files}') +}) + +const REVIEW_KEYS = [ + 'ApprovedBy', + 'Approve', + 'RevokeApproval', + 'ConnectToApprove', + 'Review', + 'ReviewThread', + 'ReviewComment', + 'ReviewComments', + 'ReviewApproved', + 'ReviewUnapproved', + 'ReviewRequestedChanges', + 'ReviewReviewed', + 'CommentedOnDiff', + 'ResolveThread', + 'UnresolveThread', + 'ResolvedBy', + 'Outdated' +] + +// The thread presenter always passes {path} +it.each(locales)('review strings exist with their params (%s)', async (lang) => { + const strings = await stringsOf(lang) + for (const key of REVIEW_KEYS) expect(strings[key]).toBeTruthy() + expect(strings.CommentedOnDiff).toContain('{path}') +}) + +const SYNC_ERROR_KEYS = ['SyncError', 'Retry', 'ReviewNotSent'] + +it.each(locales)('sync error strings exist (%s)', async (lang) => { + const strings = await stringsOf(lang) + for (const key of SYNC_ERROR_KEYS) expect(strings[key]).toBeTruthy() +}) + +const IMAGE_MODE_KEYS = ['ImageMode', 'ImageModeLink', 'ImageModeCopy', 'ImageModeLinkHint', 'ImageModeCopyHint'] + +// The image mode setting is translated in every locale, never left in English +it.each(locales.filter((it) => it !== 'en'))('image mode strings are translated (%s)', async (lang) => { + const strings = await stringsOf(lang) + for (const key of IMAGE_MODE_KEYS) { + expect(strings[key]).toBeTruthy() + expect(strings[key]).not.toBe(english[key]) + } +}) + +const IMAGE_VIEWER_KEYS = ['ImageNoAccess', 'ImageNotConnected', 'ImageUnavailable', 'GitlabImages'] + +// The image viewer placeholders are translated in every locale, never left in English +it.each(locales.filter((it) => it !== 'en'))('image viewer strings are translated (%s)', async (lang) => { + const strings = await stringsOf(lang) + for (const key of [...IMAGE_VIEWER_KEYS, 'OpenInGitlab']) expect(strings[key]).toBeTruthy() + for (const key of IMAGE_VIEWER_KEYS) expect(strings[key]).not.toBe(english[key]) +}) + +// The issue header passes {count} +it.each(locales)('GitlabImages takes the count param (%s)', async (lang) => { + const strings = await stringsOf(lang) + expect(strings.GitlabImages).toContain('{count}') +}) + +const CLIENT_ERROR_KEYS = ['LinkFailed', 'UnlinkFailed', 'NotConfigured', 'AuthorizeLinkInvalid', 'RemoveAppConfirm'] + +// Setup and error strings are real translations, never English copies +it.each(locales.filter((it) => it !== 'en'))('setup and error strings are translated (%s)', async (lang) => { + const strings = await stringsOf(lang) + for (const key of [ + ...CLIENT_ERROR_KEYS, + 'ApplicationId', + 'ApplicationSecret', + 'SetupNameAndRedirect', + 'SyncErrorMessage', + 'UnlinkConfirm', + 'IntegrationRepositories' + ]) { + expect(strings[key]).toBeTruthy() + expect(strings[key]).not.toBe(english[key]) + } +}) + +// The components always pass these params +it.each(locales)('error and header strings take their params (%s)', async (lang) => { + const strings = await stringsOf(lang) + expect(strings.SyncErrorMessage).toContain('{message}') + for (const param of ['{host}', '{login}']) expect(strings.IntegrationRepositories).toContain(param) + for (const param of ['{repository}', '{project}']) expect(strings.UnlinkConfirm).toContain(param) + for (const key of CLIENT_ERROR_KEYS) expect(strings[key]).not.toMatch(/[{}]/) +}) diff --git a/services/gitlab/gitlab-assets/src/index.ts b/services/gitlab/gitlab-assets/src/index.ts new file mode 100644 index 0000000000..5e95b31385 --- /dev/null +++ b/services/gitlab/gitlab-assets/src/index.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: EPL-2.0 + +import { loadMetadata } from '@hcengineering/platform' +import gitlab from '@hcengineering/gitlab' + +const icons = require('../assets/icons.svg') as string // eslint-disable-line +loadMetadata(gitlab.icon, { + Gitlab: `${icons}#gitlab`, + GitlabRepository: `${icons}#repository`, + MergeRequest: `${icons}#mergeRequest`, + MergeRequestMerged: `${icons}#mergeRequestMerged`, + MergeRequestClosed: `${icons}#mergeRequestClosed`, + Image: `${icons}#image` +}) diff --git a/services/gitlab/gitlab-assets/src/lang.ts b/services/gitlab/gitlab-assets/src/lang.ts new file mode 100644 index 0000000000..58055d1565 --- /dev/null +++ b/services/gitlab/gitlab-assets/src/lang.ts @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: EPL-2.0 + +import type { Loader } from '@hcengineering/platform' + +export const loadLang: Loader = async (lang) => await import(`../lang/${lang}.json`) diff --git a/services/gitlab/gitlab-assets/tsconfig.json b/services/gitlab/gitlab-assets/tsconfig.json new file mode 100644 index 0000000000..2857a41ad2 --- /dev/null +++ b/services/gitlab/gitlab-assets/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/assets/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "types": ["node", "jest"], + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/services/gitlab/gitlab-resources/.prettierrc b/services/gitlab/gitlab-resources/.prettierrc new file mode 100644 index 0000000000..792942803a --- /dev/null +++ b/services/gitlab/gitlab-resources/.prettierrc @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "trailingComma": "none", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "printWidth": 120, + "useTabs": false, + "bracketSpacing": true, + "proseWrap": "preserve", + "plugins": [ + "prettier-plugin-svelte" + ], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} \ No newline at end of file diff --git a/services/gitlab/gitlab-resources/config/rig.json b/services/gitlab/gitlab-resources/config/rig.json new file mode 100644 index 0000000000..bcad6f7c33 --- /dev/null +++ b/services/gitlab/gitlab-resources/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "ui" +} diff --git a/services/gitlab/gitlab-resources/eslint.config.mjs b/services/gitlab/gitlab-resources/eslint.config.mjs new file mode 100644 index 0000000000..f1ad4ff026 --- /dev/null +++ b/services/gitlab/gitlab-resources/eslint.config.mjs @@ -0,0 +1,3 @@ +import config from '@hcengineering/platform-rig/profiles/ui/eslint.config.mjs' + +export default config diff --git a/services/gitlab/gitlab-resources/jest.config.js b/services/gitlab/gitlab-resources/jest.config.js new file mode 100644 index 0000000000..8edf78f000 --- /dev/null +++ b/services/gitlab/gitlab-resources/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + // svelte/store ships as ESM, which the CommonJS test runtime cannot require + moduleNameMapper: { + '^svelte/store$': '/src/__mocks__/svelte-store.ts', + // Components of imported resource packages (diffview-resources) are never rendered in these tests + '\\.svelte$': '/src/__mocks__/svelte-component.ts' + }, + coverageReporters: ["text-summary", "html"] +} diff --git a/services/gitlab/gitlab-resources/package.json b/services/gitlab/gitlab-resources/package.json new file mode 100644 index 0000000000..067cbcca8c --- /dev/null +++ b/services/gitlab/gitlab-resources/package.json @@ -0,0 +1,63 @@ +{ + "name": "@hcengineering/gitlab-resources", + "version": "0.7.0", + "main": "src/index.ts", + "scripts": { + "build": "compile ui", + "build:docs": "api-extractor run --local", + "format": "format src", + "build:watch": "compile ui", + "_phase:build": "compile ui", + "_phase:format": "format src", + "_phase:validate": "compile validate", + "svelte-check": "do-svelte-check", + "_phase:svelte-check": "do-svelte-check", + "test": "jest --passWithNoTests --silent", + "_phase:test": "jest --passWithNoTests --silent" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.21", + "svelte-loader": "^3.2.0", + "sass": "^1.80.0", + "svelte-preprocess": "^5.1.4", + "@typescript-eslint/eslint-plugin": "^8.67.0", + "eslint-plugin-promise": "^7.3.0", + "eslint-plugin-n": "^18.3.0", + "eslint": "^9.39.5", + "@typescript-eslint/parser": "^8.67.0", + "prettier-plugin-svelte": "^3.4.0", + "prettier": "^3.6.2", + "svelte-check": "^3.6.9", + "typescript": "^5.9.3", + "jest": "^30.4.2", + "ts-jest": "^29.4.12", + "@types/jest": "^30.0.0", + "eslint-plugin-svelte": "^3.23.0", + "svelte-eslint-parser": "^1.8.1", + "eslint-config-love": "^157.0.0" + }, + "dependencies": { + "@hcengineering/core": "workspace:^0.7.26", + "@hcengineering/platform": "workspace:^0.7.20", + "@hcengineering/ui": "workspace:^0.7.0", + "@hcengineering/presentation": "workspace:^0.7.0", + "@hcengineering/tracker": "workspace:^0.7.0", + "@hcengineering/setting": "workspace:^0.7.0", + "@hcengineering/setting-resources": "workspace:^0.7.0", + "@hcengineering/account-client": "workspace:^0.7.25", + "@hcengineering/analytics": "workspace:^0.7.19", + "@hcengineering/view-resources": "workspace:^0.7.0", + "@hcengineering/gitlab": "workspace:^0.7.0", + "@hcengineering/diffview": "workspace:^0.7.0", + "@hcengineering/diffview-resources": "workspace:^0.7.0", + "@hcengineering/task": "workspace:^0.7.0", + "@hcengineering/tracker-resources": "workspace:^0.7.0", + "@hcengineering/view": "workspace:^0.7.0", + "@hcengineering/activity-resources": "workspace:^0.7.0", + "@hcengineering/contact": "workspace:^0.7.0", + "@hcengineering/contact-resources": "workspace:^0.7.0", + "@hcengineering/text-editor-resources": "workspace:^0.7.0", + "lru-cache": "^11.1.0", + "svelte": "^4.2.20" + } +} diff --git a/services/gitlab/gitlab-resources/postcss.config.js b/services/gitlab/gitlab-resources/postcss.config.js new file mode 100644 index 0000000000..88752c6cb0 --- /dev/null +++ b/services/gitlab/gitlab-resources/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: [ + require('autoprefixer') + ] +} diff --git a/services/gitlab/gitlab-resources/src/__mocks__/svelte-component.ts b/services/gitlab/gitlab-resources/src/__mocks__/svelte-component.ts new file mode 100644 index 0000000000..cccc90329b --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__mocks__/svelte-component.ts @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: EPL-2.0 +// +// `.svelte` stand-in for Jest: resource packages imported for their helpers (diffview-resources' parseDiff) also +// import their components, which the ts-jest runtime cannot compile. The tests never render them. + +const component = {} +export default component diff --git a/services/gitlab/gitlab-resources/src/__mocks__/svelte-store.ts b/services/gitlab/gitlab-resources/src/__mocks__/svelte-store.ts new file mode 100644 index 0000000000..61163c3a57 --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__mocks__/svelte-store.ts @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: EPL-2.0 +// +// `svelte/store` stand-in for Jest: the real module ships as ESM, which the ts-jest CommonJS runtime cannot +// require (plugins/view-resources maps it the same way). It keeps Svelte's store contract: a subscriber gets +// the current value at once, `start` runs on the first subscriber and its stop function after the last leaves. + +export type Subscriber = (value: T) => void +export type Unsubscriber = () => void +export type Updater = (value: T) => T +export type StartStopNotifier = (set: Subscriber) => Unsubscriber | undefined + +export interface Readable { + subscribe: (run: Subscriber) => Unsubscriber +} + +export interface Writable extends Readable { + set: (value: T) => void + update: (updater: Updater) => void +} + +export function writable (value: T, start?: StartStopNotifier): Writable { + const subscribers = new Set>() + let stop: Unsubscriber | undefined + + function set (next: T): void { + value = next + for (const run of subscribers) run(value) + } + + return { + set, + update: (updater) => { + set(updater(value)) + }, + subscribe: (run) => { + subscribers.add(run) + if (subscribers.size === 1 && start !== undefined) stop = start(set) + run(value) + return () => { + subscribers.delete(run) + if (subscribers.size === 0) { + stop?.() + stop = undefined + } + } + } + } +} + +export function readable (value: T, start?: StartStopNotifier): Readable { + return { subscribe: writable(value, start).subscribe } +} + +export function get (store: Readable): T { + let value: T | undefined + store.subscribe((current) => { + value = current + })() + return value as T +} diff --git a/services/gitlab/gitlab-resources/src/__tests__/app-status.test.ts b/services/gitlab/gitlab-resources/src/__tests__/app-status.test.ts new file mode 100644 index 0000000000..fc18bb955e --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/app-status.test.ts @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: EPL-2.0 +import { appConfigArgs, appStatusOf, setupFormOf } from '../app-status' + +describe('appStatusOf', () => { + it('reads a configured application', () => { + expect( + appStatusOf({ + configured: true, + host: 'https://git.corp.local', + clientId: 'id', + redirectUri: 'https://huly/gitlab', + scopes: 'api read_user' + }) + ).toEqual({ + configured: true, + host: 'https://git.corp.local', + clientId: 'id', + redirectUri: 'https://huly/gitlab', + scopes: 'api read_user' + }) + }) + + it('defaults fields the service did not send or sent with another type', () => { + expect(appStatusOf({ configured: 'yes', host: 3 })).toEqual({ + configured: false, + host: undefined, + clientId: undefined, + redirectUri: '', + scopes: '' + }) + }) +}) + +describe('setupFormOf', () => { + it('treats gitlab.com and an unconfigured app as not self-managed', () => { + const status = { configured: true, host: 'https://gitlab.com', clientId: 'id', redirectUri: '', scopes: '' } + expect(setupFormOf(status)).toEqual({ selfManaged: false, host: '', clientId: 'id', clientSecret: '' }) + expect(setupFormOf(undefined)).toEqual({ selfManaged: false, host: '', clientId: '', clientSecret: '' }) + }) + + it('keeps a self-managed host', () => { + const status = { configured: true, host: 'https://git.corp.local', clientId: 'id', redirectUri: '', scopes: '' } + expect(setupFormOf(status).host).toBe('https://git.corp.local') + expect(setupFormOf(status).selfManaged).toBe(true) + }) +}) + +describe('appConfigArgs', () => { + it('sends no host for gitlab.com and no secret when it is left empty', () => { + expect(appConfigArgs({ selfManaged: false, host: 'ignored', clientId: ' id ', clientSecret: ' ' })).toEqual({ + clientId: 'id' + }) + }) + + it('sends the trimmed host and secret when given', () => { + expect( + appConfigArgs({ selfManaged: true, host: ' https://git.corp.local ', clientId: 'id', clientSecret: ' s ' }) + ).toEqual({ clientId: 'id', host: 'https://git.corp.local', clientSecret: 's' }) + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/authentication.test.ts b/services/gitlab/gitlab-resources/src/__tests__/authentication.test.ts new file mode 100644 index 0000000000..59bd33e5e8 --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/authentication.test.ts @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: EPL-2.0 + +import { type Account } from '@hcengineering/core' +import { type GitlabAuthentication } from '@hcengineering/gitlab' + +interface FakeQuery { + query: jest.Mock + unsubscribe: jest.Mock +} + +const queries: FakeQuery[] = [] + +jest.mock('@hcengineering/presentation', () => ({ + createQuery: () => { + const query: FakeQuery = { query: jest.fn(), unsubscribe: jest.fn() } + queries.push(query) + return query + } +})) + +describe('gitlabAuthentication', () => { + beforeEach(() => { + queries.length = 0 + jest.resetModules() + }) + + it('loads without a signed-in account (the OAuth landing tab has no session)', async () => { + await expect(import('../components/authentication')).resolves.toBeDefined() + expect(queries).toHaveLength(0) + }) + + it("queries the current account's authentication while subscribed", async () => { + // resetModules gives the module under test a fresh core, so the account is set on that instance + const core: { setCurrentAccount: (account: Account) => void } = await import('@hcengineering/core') + core.setCurrentAccount({ primarySocialId: 'social-1' } as unknown as Account) + const { gitlabAuthentication } = await import('../components/authentication') + const seen: Array = [] + + const unsubscribe = gitlabAuthentication.subscribe((value) => seen.push(value)) + expect(queries).toHaveLength(1) + const [, filter, onResult] = queries[0].query.mock.calls[0] + expect(filter).toEqual({ attachedTo: 'social-1' }) + + const authentication = { login: 'octocat' } as unknown as GitlabAuthentication + onResult([authentication]) + expect(seen).toEqual([undefined, authentication]) + + unsubscribe() + expect(queries[0].unsubscribe).toHaveBeenCalled() + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/errors.test.ts b/services/gitlab/gitlab-resources/src/__tests__/errors.test.ts new file mode 100644 index 0000000000..1b59f4daed --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/errors.test.ts @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: EPL-2.0 +import { + addStringsLoader, + PlatformError, + Severity, + Status, + getEmbeddedLabel, + unknownError, + type IntlString, + type Plugin +} from '@hcengineering/platform' +import { Analytics } from '@hcengineering/analytics' +import { + APP_IN_USE_CODE, + GitlabError, + GitlabServiceError, + errorLabel, + errorText, + isAppInUseError, + reportError, + translateError +} from '../errors' + +describe('errorText', () => { + it('returns the message param of a PlatformError, as the GitLab service errors carry it', () => { + const err = new PlatformError(unknownError('Disconnect GitLab before changing the application (connected: alice)')) + expect(err.message).toContain('UnknownError') + expect(errorText(err)).toBe('Disconnect GitLab before changing the application (connected: alice)') + }) + + it('falls back to the message of a PlatformError without a message param', () => { + const err = new PlatformError(new Status(Severity.ERROR, 'platform:status:Forbidden' as any, {})) + expect(errorText(err)).toBe(err.message) + }) + + it('ignores a non-string message param', () => { + const err = new PlatformError(new Status(Severity.ERROR, 'platform:status:X' as any, { message: 42 })) + expect(errorText(err)).toBe(err.message) + }) + + it('returns the message of a plain Error', () => { + expect(errorText(new Error('boom'))).toBe('boom') + }) + + it.each([ + ['text', 'text'], + [42, '42'], + [undefined, 'undefined'] + ])('stringifies %p', (value, expected) => { + expect(errorText(value)).toBe(expected) + }) +}) + +describe('isAppInUseError', () => { + it('detects the pod refusal by its code', () => { + expect(isAppInUseError(new GitlabServiceError('Any wording (connected: alice)', APP_IN_USE_CODE))).toBe(true) + }) + + it('ignores the same text without the code, and other errors', () => { + expect(isAppInUseError(new GitlabServiceError('Disconnect GitLab before changing the application'))).toBe(false) + expect(isAppInUseError(new Error('Only workspace owners can change the GitLab application'))).toBe(false) + }) + + it('still shows the pod message', () => { + expect(errorText(new GitlabServiceError('Disconnect GitLab first', APP_IN_USE_CODE))).toBe( + 'Disconnect GitLab first' + ) + }) +}) + +describe('errorLabel', () => { + it('shows a GitlabError through its own translation', () => { + const label = 'gitlab:string:LinkFailed' as IntlString + expect(errorLabel(new GitlabError(label))).toBe(label) + }) + + it('shows any other error as its text', () => { + expect(errorLabel(new Error('boom'))).toBe(getEmbeddedLabel('boom')) + expect(errorLabel(new PlatformError(unknownError('from the pod')))).toBe(getEmbeddedLabel('from the pod')) + }) +}) + +describe('translateError', () => { + beforeAll(() => { + addStringsLoader('gitlab' as Plugin, async () => ({ string: { LinkFailed: 'Could not link the project' } })) + }) + + it('translates a GitlabError instead of returning its label id', async () => { + const err = new GitlabError('gitlab:string:LinkFailed' as IntlString) + expect(errorText(err)).toBe('gitlab:string:LinkFailed') + expect(await translateError(err, 'en')).toBe('Could not link the project') + }) + + it('returns the text of any other error', async () => { + expect(await translateError(new GitlabServiceError('Forbidden caller'), 'en')).toBe('Forbidden caller') + expect(await translateError(new Error('boom'), 'en')).toBe('boom') + }) +}) + +describe('reportError', () => { + afterEach(() => { + jest.restoreAllMocks() + }) + + it('reports an Error as it is', () => { + const handle = jest.spyOn(Analytics, 'handleError').mockImplementation(() => {}) + const err = new Error('boom') + reportError(err) + expect(handle).toHaveBeenCalledWith(err) + }) + + it('wraps a non-Error value with its text', () => { + const handle = jest.spyOn(Analytics, 'handleError').mockImplementation(() => {}) + reportError('refused') + expect(handle).toHaveBeenCalledTimes(1) + expect(handle.mock.calls[0][0].message).toBe('refused') + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/gitlab-host.test.ts b/services/gitlab/gitlab-resources/src/__tests__/gitlab-host.test.ts new file mode 100644 index 0000000000..9c015e13f0 --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/gitlab-host.test.ts @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: EPL-2.0 +import { applicationLinks, isValidHostInput, trimTrailingSlashes } from '../gitlab-host' + +describe('applicationLinks', () => { + it('builds user and admin application pages under a sub-path host', () => { + expect(applicationLinks('https://git.corp.local/gitlab/')).toEqual({ + user: 'https://git.corp.local/gitlab/-/user_settings/applications', + admin: 'https://git.corp.local/gitlab/admin/applications', + groupHint: 'https://git.corp.local/gitlab/groups//-/settings/applications' + }) + }) + + it('builds gitlab.com links', () => { + expect(applicationLinks('https://gitlab.com').user).toBe('https://gitlab.com/-/user_settings/applications') + }) +}) + +describe('isValidHostInput', () => { + it.each([ + ['https://gitlab.com', true], + ['http://localhost:8929', true], + ['http://gitlab.com', false], + ['gitlab.com', false], + ['', false], + ['https://git.corp.local/gitlab/', true], + ['http://git.corp.local', true], + ['http://127.0.0.1:8080', true], + ['https://gitlab.com/?x=1', false], + ['https://gitlab.com/#top', false], + ['https://user:pass@gitlab.com', false], + ['ftp://gitlab.com', false] + ])('%s -> %s', (raw, ok) => { + expect(isValidHostInput(raw)).toBe(ok) + }) +}) + +describe('trimTrailingSlashes', () => { + it('drops every trailing slash and nothing else', () => { + expect(trimTrailingSlashes('https://x.example//')).toBe('https://x.example') + expect(trimTrailingSlashes('https://x.example/a')).toBe('https://x.example/a') + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/hunk.test.ts b/services/gitlab/gitlab-resources/src/__tests__/hunk.test.ts new file mode 100644 index 0000000000..f73d6709f6 --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/hunk.test.ts @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: EPL-2.0 +import { parseDiff } from '@hcengineering/diffview-resources' +import { findHunk, parsePatch } from '../hunk' + +const PATCH = [ + 'diff --git a/src/a.ts b/src/a.ts', + 'index 1111111..2222222 100644', + '--- a/src/a.ts', + '+++ b/src/a.ts', + '@@ -10,4 +10,5 @@ export function f () {', + ' const a = 1', + '-const b = 2', + '+const b = 3', + '+const c = 4', + ' return a', + ' }', + 'diff --git a/src/b.ts b/src/b.ts', + '--- a/src/b.ts', + '+++ b/src/b.ts', + '@@ -1 +1 @@', + '-x', + '+y', + 'diff --git a/src/c.ts b/src/c.ts', + '--- a/src/c.ts', + '+++ b/src/c.ts', + '@@ -1,2 +1,2 @@', + ' k', + '-x', + '\\ No newline at end of file', + '+y', + '\\ No newline at end of file', + '' +].join('\n') + +const hunkAt = (path: string, line: number | null, oldLine: number | null): string => + findHunk(parsePatch(PATCH), path, line, oldLine) + +describe('findHunk over a large file', () => { + // More changed lines than the diff view parses by default + const CHANGES = 10010 + const LARGE = [ + 'diff --git a/big.ts b/big.ts', + '--- a/big.ts', + '+++ b/big.ts', + `@@ -0,0 +1,${CHANGES} @@`, + ...Array.from({ length: CHANGES }, (_, n) => `+line ${n + 1}`), + '' + ].join('\n') + + it('finds the hunk of a file too large for the diff view', () => { + expect(findHunk(parsePatch(LARGE), 'big.ts', 2, null)).toBe( + [`@@ -0,0 +1,${CHANGES} @@`, '+line 1', '+line 2'].join('\n') + ) + expect(findHunk(parseDiff(LARGE), 'big.ts', 2, null)).toBe('') + }) +}) + +describe('findHunk over a whole diff', () => { + it('cuts the hunk after the commented new line', () => { + expect(hunkAt('src/a.ts', 12, null)).toBe( + ['@@ -10,4 +10,5 @@ export function f () {', ' const a = 1', '-const b = 2', '+const b = 3', '+const c = 4'].join( + '\n' + ) + ) + }) + + it('finds a removed line by its old number', () => { + expect(hunkAt('src/a.ts', null, 11)).toBe( + ['@@ -10,4 +10,5 @@ export function f () {', ' const a = 1', '-const b = 2'].join('\n') + ) + }) + + it('looks in the right file', () => { + expect(hunkAt('src/b.ts', 1, null)).toBe(['@@ -1 +1 @@', '-x', '+y'].join('\n')) + }) + + it('leaves out the no-newline marker, which diff2html drops', () => { + expect(hunkAt('src/c.ts', null, 2)).toBe(['@@ -1,2 +1,2 @@', ' k', '-x'].join('\n')) + expect(hunkAt('src/c.ts', 2, null)).toBe(['@@ -1,2 +1,2 @@', ' k', '-x', '+y'].join('\n')) + }) + + it('returns nothing for an unknown file or a line outside every hunk', () => { + expect(hunkAt('src/d.ts', 1, null)).toBe('') + expect(hunkAt('src/a.ts', 40, null)).toBe('') + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/image-gallery.test.ts b/services/gitlab/gitlab-resources/src/__tests__/image-gallery.test.ts new file mode 100644 index 0000000000..0a2fdbb7af --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/image-gallery.test.ts @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: EPL-2.0 +import { stepIndex } from '../image-gallery' + +describe('stepIndex', () => { + it('moves within the list and stops at both ends', () => { + expect(stepIndex(0, 1, 3)).toBe(1) + expect(stepIndex(2, 1, 3)).toBe(2) + expect(stepIndex(0, -1, 3)).toBe(0) + expect(stepIndex(2, -1, 3)).toBe(1) + }) + + it('stays at 0 for an empty or one-image list', () => { + expect(stepIndex(0, 1, 0)).toBe(0) + expect(stepIndex(0, 1, 1)).toBe(0) + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/image-link.test.ts b/services/gitlab/gitlab-resources/src/__tests__/image-link.test.ts new file mode 100644 index 0000000000..1c57f4cbfc --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/image-link.test.ts @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: EPL-2.0 +import { imageNameOf, imageResultOf, loadGitlabImage } from '../image-link' + +const HREF = + 'https://gitlab.example.com/group/proj/uploads/0123456789abcdef0123456789abcdef/my%20shot.png#gitlab-image=width%3D3' + +function respond (status: number, body: BodyInit, type = 'application/json'): typeof fetch { + return async () => new Response(body, { status, headers: { 'Content-Type': type } }) +} + +describe('GitLab image link', () => { + it('names the image after its file, without the fragment', () => { + expect(imageNameOf(HREF)).toBe('my shot.png') + expect(imageNameOf('not a url')).toBe('not a url') + }) + + it('maps the pod answer to a placeholder', () => { + expect(imageResultOf(403, 'not-connected')).toEqual({ kind: 'not-connected' }) + expect(imageResultOf(403, 'no-access')).toEqual({ kind: 'no-access' }) + expect(imageResultOf(404, 'not-found')).toEqual({ kind: 'unavailable' }) + expect(imageResultOf(503, undefined)).toEqual({ kind: 'unavailable' }) + }) + + it('posts the link with the Huly token and returns the image', async () => { + const calls: Array<[string, RequestInit | undefined]> = [] + const fetchImage = (async (url: string, init?: RequestInit) => { + calls.push([url, init]) + return new Response('png', { status: 200, headers: { 'Content-Type': 'image/png' } }) + }) as unknown as typeof fetch + const result = await loadGitlabImage(HREF, { + base: 'https://pod.example.com', + token: 'tok', + accountId: 'p1', + fetch: fetchImage + }) + expect(result.kind).toBe('image') + expect(calls[0][0]).toBe('https://pod.example.com/api/v1/image') + expect(JSON.parse(String(calls[0][1]?.body))).toEqual({ token: 'tok', accountId: 'p1', url: HREF }) + }) + + it('accepts an image content type in any case', async () => { + const result = await loadGitlabImage(HREF, { + base: 'https://pod.example.com', + token: 'tok', + accountId: 'p1', + fetch: respond(200, 'png', 'IMAGE/PNG') + }) + expect(result.kind).toBe('image') + }) + + it('turns refusals and failures into placeholders, never an exception', async () => { + const request = { base: 'https://pod.example.com', token: 'tok', accountId: 'p1' } + expect( + await loadGitlabImage(HREF, { ...request, fetch: respond(403, JSON.stringify({ error: 'no-access' })) }) + ).toEqual({ kind: 'no-access' }) + expect( + await loadGitlabImage(HREF, { ...request, fetch: respond(403, JSON.stringify({ error: 'not-connected' })) }) + ).toEqual({ kind: 'not-connected' }) + expect(await loadGitlabImage(HREF, { ...request, fetch: respond(500, 'boom', 'text/plain') })).toEqual({ + kind: 'unavailable' + }) + const offline = (async () => { + throw new Error('offline') + }) as unknown as typeof fetch + expect(await loadGitlabImage(HREF, { ...request, fetch: offline })).toEqual({ kind: 'unavailable' }) + expect(await loadGitlabImage(HREF, { ...request, base: '', fetch: offline })).toEqual({ kind: 'unavailable' }) + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/image-mode.test.ts b/services/gitlab/gitlab-resources/src/__tests__/image-mode.test.ts new file mode 100644 index 0000000000..8d150f1688 --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/image-mode.test.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: EPL-2.0 +import { DEFAULT_IMAGE_MODE } from '@hcengineering/gitlab' +import gitlab from '../plugin' +import { IMAGE_MODES, imageModeHint, imageModeLabel } from '../image-mode' + +describe('image mode choices', () => { + it('offers the default mode first, then the others', () => { + expect(IMAGE_MODES[0]).toBe(DEFAULT_IMAGE_MODE) + expect([...IMAGE_MODES].sort()).toEqual(['copy', 'link']) + }) + + it('labels and explains each mode', () => { + expect(imageModeLabel('link')).toBe(gitlab.string.ImageModeLink) + expect(imageModeLabel('copy')).toBe(gitlab.string.ImageModeCopy) + expect(imageModeHint('link')).toBe(gitlab.string.ImageModeLinkHint) + expect(imageModeHint('copy')).toBe(gitlab.string.ImageModeCopyHint) + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/integration-card.test.ts b/services/gitlab/gitlab-resources/src/__tests__/integration-card.test.ts new file mode 100644 index 0000000000..ed5305e009 --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/integration-card.test.ts @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: EPL-2.0 +import { integrationCardStateOf } from '../integration-card' + +describe('integrationCardStateOf', () => { + it('is idle without an integration to show', () => { + expect(integrationCardStateOf({ hasUser: false, answered: false, loadFailed: false })).toEqual({ + isLoading: false, + hasError: false + }) + }) + + it('is loading until the query answers', () => { + expect(integrationCardStateOf({ hasUser: true, answered: false, loadFailed: false })).toEqual({ + isLoading: true, + hasError: false + }) + }) + + it('shows the error instead of loading when the probe lookup failed', () => { + expect(integrationCardStateOf({ hasUser: true, answered: false, loadFailed: true })).toEqual({ + isLoading: false, + hasError: true + }) + }) + + it('ignores a failed probe once the live query has answered', () => { + expect(integrationCardStateOf({ hasUser: true, answered: true, loadFailed: true })).toEqual({ + isLoading: false, + hasError: false + }) + }) + + it('is loaded and healthy once answered', () => { + expect(integrationCardStateOf({ hasUser: true, answered: true, loadFailed: false })).toEqual({ + isLoading: false, + hasError: false + }) + }) + + it('shows the error the integration itself carries', () => { + expect( + integrationCardStateOf({ hasUser: true, answered: true, loadFailed: false, integrationError: 'revoked' }) + ).toEqual({ isLoading: false, hasError: true }) + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/issue-header.test.ts b/services/gitlab/gitlab-resources/src/__tests__/issue-header.test.ts new file mode 100644 index 0000000000..8b3c5ea9b4 --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/issue-header.test.ts @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: EPL-2.0 +import { asMergeRequest, headerImages, issueHeaderState } from '../issue-header' +import gitlab from '../plugin' + +const base = { linkedRepositories: 1, readonly: false } + +describe('issueHeaderState', () => { + it('shows a merge request link with its error', () => { + expect(issueHeaderState({ ...base, mergeRequest: { syncError: 'boom' } })).toEqual({ + kind: 'mergeRequest', + error: 'boom' + }) + }) + + it('shows a linked issue, with a push error if any', () => { + expect(issueHeaderState({ ...base, link: { gitlabIid: 4, repository: 'r', syncError: null } })).toEqual({ + kind: 'linked', + error: null + }) + expect(issueHeaderState({ ...base, link: { gitlabIid: 4, repository: 'r', syncError: '403' } })).toEqual({ + kind: 'linked', + error: '403' + }) + }) + + it('shows "being created" until GitLab refuses, then the error instead', () => { + expect(issueHeaderState({ ...base, link: { gitlabIid: 0, repository: 'r' } })).toEqual({ + kind: 'creating', + error: null + }) + expect(issueHeaderState({ ...base, link: { gitlabIid: 0, repository: 'r', syncError: '403 Forbidden' } })).toEqual({ + kind: 'failed', + error: '403 Forbidden' + }) + }) + + it('offers the picker for an issue without a pick, with the error of a failed default creation', () => { + expect(issueHeaderState({ ...base })).toEqual({ kind: 'pick', error: null }) + expect(issueHeaderState({ ...base, link: { syncError: '422' } })).toEqual({ kind: 'pick', error: '422' }) + expect(issueHeaderState({ ...base, link: { repository: null, gitlabIid: 0 } })).toEqual({ + kind: 'pick', + error: null + }) + }) + + it('shows nothing without linked repositories or in a read-only view', () => { + expect(issueHeaderState({ linkedRepositories: 0, readonly: false })).toEqual({ kind: 'none', error: null }) + expect(issueHeaderState({ linkedRepositories: 1, readonly: true })).toEqual({ kind: 'none', error: null }) + }) +}) + +describe('headerImages', () => { + it("uses a merge request's images, else a linked issue's, else none", () => { + const merged = 'https://gitlab.com/m.png' + const linked = 'https://gitlab.com/i.png' + expect(headerImages({ mergeRequest: { images: [merged] }, link: { images: [linked] } })).toEqual([merged]) + expect(headerImages({ link: { images: [linked] } })).toEqual([linked]) + expect(headerImages({ mergeRequest: {} })).toEqual([]) + expect(headerImages({})).toEqual([]) + }) + + it('drops entries that are not http(s) URLs', () => { + const images = ['javascript:alert(1)', 'data:text/html,x', 'https://gitlab.com/a.png'] + expect(headerImages({ link: { images } })).toEqual(['https://gitlab.com/a.png']) + }) +}) + +describe('asMergeRequest', () => { + const hierarchy = { isDerived: (cls: string, of: string) => cls === of } + + it('returns a merge request as one, and nothing for a plain issue', () => { + const mergeRequest = { _class: gitlab.class.GitlabMergeRequest } + expect(asMergeRequest(hierarchy as never, mergeRequest as never)).toBe(mergeRequest) + const plainIssue = { _class: 'tracker:class:Issue' } + expect(asMergeRequest(hierarchy as never, plainIssue as never)).toBeUndefined() + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/merge-request-changes.test.ts b/services/gitlab/gitlab-resources/src/__tests__/merge-request-changes.test.ts new file mode 100644 index 0000000000..0b71f207c8 --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/merge-request-changes.test.ts @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: EPL-2.0 +import { changesUrl, isTooLargeForHuly, showDiffInHuly } from '../merge-request-changes' + +describe('changesUrl', () => { + it('points at the merge request changes page', () => { + expect(changesUrl('https://gitlab.example.com/group/repo/-/merge_requests/3')).toBe( + 'https://gitlab.example.com/group/repo/-/merge_requests/3/diffs' + ) + }) + + it('drops a trailing slash, query and fragment', () => { + expect(changesUrl('https://gitlab.example.com/g/r/-/merge_requests/3/?tab=overview#note_1')).toBe( + 'https://gitlab.example.com/g/r/-/merge_requests/3/diffs' + ) + }) + + it('refuses anything that is not http(s)', () => { + expect(changesUrl('javascript:alert(1)')).toBeUndefined() + expect(changesUrl('')).toBeUndefined() + expect(changesUrl('HTTP://gitlab.local/g/r/-/merge_requests/1')).toBe( + 'HTTP://gitlab.local/g/r/-/merge_requests/1/diffs' + ) + }) +}) + +describe('showDiffInHuly', () => { + it('needs a stored diff', () => { + expect(showDiffInHuly({ files: 1, additions: 1, deletions: 1 }, false)).toBe(false) + }) + + it('shows up to 50 files and 2,000 changed lines', () => { + expect(showDiffInHuly({ files: 50, additions: 1500, deletions: 500 }, true)).toBe(true) + expect(showDiffInHuly({ files: 51, additions: 1, deletions: 1 }, true)).toBe(false) + expect(showDiffInHuly({ files: 50, additions: 1500, deletions: 501 }, true)).toBe(false) + }) +}) + +describe('isTooLargeForHuly', () => { + it('is about the counts only, not whether a diff is stored', () => { + expect(isTooLargeForHuly({ files: 3, additions: 10, deletions: 2 })).toBe(false) + expect(isTooLargeForHuly({ files: 51, additions: 0, deletions: 0 })).toBe(true) + expect(isTooLargeForHuly({ files: 2, additions: 2001, deletions: 0 })).toBe(true) + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/merge-status.test.ts b/services/gitlab/gitlab-resources/src/__tests__/merge-status.test.ts new file mode 100644 index 0000000000..3bbf6fc5cd --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/merge-status.test.ts @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: EPL-2.0 +import gitlab from '../plugin' +import { mergeStatusLabel } from '../merge-status' + +describe('mergeStatusLabel', () => { + it('reports a conflict whatever the detailed status says', () => { + expect(mergeStatusLabel('mergeable', true)).toBe(gitlab.string.Conflict) + expect(mergeStatusLabel('conflict', false)).toBe(gitlab.string.Conflict) + expect(mergeStatusLabel('need_rebase', false)).toBe(gitlab.string.Conflict) + }) + + it('maps the GitLab detailed merge statuses', () => { + expect(mergeStatusLabel('mergeable', false)).toBe(gitlab.string.ReadyToMerge) + expect(mergeStatusLabel('checking', false)).toBe(gitlab.string.Checking) + expect(mergeStatusLabel('unchecked', false)).toBe(gitlab.string.Checking) + expect(mergeStatusLabel('ci_must_pass', false)).toBe(gitlab.string.PipelinePending) + expect(mergeStatusLabel('ci_still_running', false)).toBe(gitlab.string.PipelinePending) + expect(mergeStatusLabel('discussions_not_resolved', false)).toBe(gitlab.string.UnresolvedDiscussions) + expect(mergeStatusLabel('not_approved', false)).toBe(gitlab.string.NeedsApproval) + expect(mergeStatusLabel('draft_status', false)).toBe(gitlab.string.Draft) + }) + + it('shows nothing for statuses it does not know', () => { + expect(mergeStatusLabel('not_open', false)).toBeUndefined() + expect(mergeStatusLabel('', false)).toBeUndefined() + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/patch-text.test.ts b/services/gitlab/gitlab-resources/src/__tests__/patch-text.test.ts new file mode 100644 index 0000000000..c112fd116b --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/patch-text.test.ts @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: EPL-2.0 +import { createTextCache } from '../patch-text' + +describe('createTextCache', () => { + it('keeps only the most recently used diffs', async () => { + const load = jest.fn(async (file: string) => `text of ${file}`) + const get = createTextCache(load, 2) + await get('a', 'Patch.diff') + await get('b', 'Patch.diff') + await get('a', 'Patch.diff') + await get('c', 'Patch.diff') + // b was the least recently used one + await get('a', 'Patch.diff') + await get('b', 'Patch.diff') + expect(load.mock.calls.map(([file]) => file)).toEqual(['a', 'b', 'c', 'b']) + }) + + it('downloads each blob once', async () => { + const load = jest.fn(async (file: string) => `text of ${file}`) + const get = createTextCache(load) + expect(await get('a', 'Patch.diff')).toBe('text of a') + expect(await get('a', 'Patch.diff')).toBe('text of a') + expect(await get('b', 'Patch.diff')).toBe('text of b') + expect(load).toHaveBeenCalledTimes(2) + }) + + it('shares one download between concurrent calls', async () => { + const load = jest.fn(async (file: string) => `text of ${file}`) + const get = createTextCache(load) + expect(await Promise.all([get('a', 'Patch.diff'), get('a', 'Patch.diff')])).toEqual(['text of a', 'text of a']) + expect(load).toHaveBeenCalledTimes(1) + }) + + it('tries a failed download again', async () => { + const load = jest.fn().mockRejectedValueOnce(new Error('offline')).mockResolvedValue('ok') + const get = createTextCache(load) + await expect(get('a', 'Patch.diff')).rejects.toThrow('offline') + expect(await get('a', 'Patch.diff')).toBe('ok') + }) + + it('caches any parsed value per blob', async () => { + const load = jest.fn(async (file: string) => file.length) + const get = createTextCache(load) + expect(await get('abc', 'patch.diff')).toBe(3) + expect(await get('abc', 'patch.diff')).toBe(3) + expect(load).toHaveBeenCalledTimes(1) + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/project-link.test.ts b/services/gitlab/gitlab-resources/src/__tests__/project-link.test.ts new file mode 100644 index 0000000000..f98c5c1d26 --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/project-link.test.ts @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: EPL-2.0 +import { type GitlabIntegrationRepository } from '@hcengineering/gitlab' +import { type Project } from '@hcengineering/tracker' +import { + disableRepositoryHook, + isLinkableProject, + linkedProjectName, + linkRepository, + projectLinkChange, + unlinkRepository +} from '../project-link' + +describe('isLinkableProject', () => { + const existing = new Set(['current', 'other']) + + it('accepts a project without the GitLab mixin', () => { + expect(isLinkableProject(undefined, 'current', existing)).toBe(true) + }) + + it('accepts a project already owned by the same integration', () => { + expect(isLinkableProject({ integration: 'current', repositories: ['r1'] }, 'current', existing)).toBe(true) + }) + + it('accepts a project pointing at a deleted integration with no repositories (live case)', () => { + expect(isLinkableProject({ integration: 'gone', repositories: [] }, 'current', existing)).toBe(true) + }) + + it('accepts a project pointing at a deleted integration with dangling repositories', () => { + expect(isLinkableProject({ integration: 'gone', repositories: ['r1'] }, 'current', existing)).toBe(true) + }) + + it('accepts a project of another existing integration that links no repositories', () => { + expect(isLinkableProject({ integration: 'other', repositories: [] }, 'current', existing)).toBe(true) + expect(isLinkableProject({ integration: 'other' }, 'current', existing)).toBe(true) + }) + + it('rejects a project actively linked to another existing integration', () => { + expect(isLinkableProject({ integration: 'other', repositories: ['r1'] }, 'current', existing)).toBe(false) + }) +}) + +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- test double for TxOperations +type AnyClient = any + +function fakeClient (mixin: unknown, commitResult = true): { client: AnyClient, ops: Record } { + const ops = { + createMixin: jest.fn(async () => {}), + updateMixin: jest.fn(async () => {}), + update: jest.fn(async () => {}), + commit: jest.fn(async () => ({ result: commitResult })) + } + const hierarchy = { + asIf: () => mixin, + hasMixin: () => mixin !== undefined, + as: (doc: unknown) => ({ ...(doc as object), ...(mixin as object) }) + } + return { client: { apply: () => ops, getHierarchy: () => hierarchy }, ops } +} + +const project: AnyClient = { _id: 'p1', _class: 'tracker:class:Project', space: 'core:space:Space', identifier: 'P' } +const integration: AnyClient = { _id: 'i1' } +const repository: AnyClient = { _id: 'r1', pathWithNamespace: 'g/r' } + +function mixinOf (integration: string, repositories: string[]): AnyClient { + return { integration, repositories } +} + +describe('projectLinkChange', () => { + it('creates the mixin for a project that has none', () => { + expect(projectLinkChange(undefined, 'i1' as AnyClient, 'r1' as AnyClient)).toEqual({ + create: true, + data: { integration: 'i1', repositories: ['r1'] } + }) + }) + + it('takes over a project of another integration and drops its repository refs', () => { + expect(projectLinkChange(mixinOf('old', ['x']), 'i1' as AnyClient, 'r1' as AnyClient)).toEqual({ + create: false, + data: { integration: 'i1', repositories: ['r1'] } + }) + }) + + it('adds the repository to the same integration once', () => { + expect(projectLinkChange(mixinOf('i1', ['x']), 'i1' as AnyClient, 'r1' as AnyClient)).toEqual({ + create: false, + data: { integration: 'i1', $push: { repositories: 'r1' } } + }) + expect(projectLinkChange(mixinOf('i1', ['r1']), 'i1' as AnyClient, 'r1' as AnyClient)).toEqual({ + create: false, + data: { integration: 'i1' } + }) + }) +}) + +describe('linkRepository', () => { + it('installs the webhook, then links project and repository in one commit', async () => { + const { client, ops } = fakeClient(undefined) + const send = jest.fn(async () => ({})) + await linkRepository(client, send, project, integration, repository) + expect(send).toHaveBeenCalledWith('repository-enable', { repositoryId: 'r1' }) + expect(ops.createMixin).toHaveBeenCalledWith('p1', 'tracker:class:Project', 'core:space:Space', expect.anything(), { + integration: 'i1', + repositories: ['r1'] + }) + expect(ops.update).toHaveBeenCalledWith(repository, { gitlabProject: 'p1', enabled: true }) + expect(ops.commit).toHaveBeenCalledTimes(1) + }) + + it('removes the webhook again when the commit is refused, and rethrows', async () => { + const { client } = fakeClient(undefined, false) + const send = jest.fn(async () => ({})) + await expect(linkRepository(client, send, project, integration, repository)).rejects.toThrow() + expect(send.mock.calls.map((it: unknown[]) => it[0])).toEqual(['repository-enable', 'repository-disable']) + }) + + it('links nothing and removes no webhook when GitLab refuses the webhook', async () => { + const { client, ops } = fakeClient(undefined) + const send = jest.fn(async () => { + throw new Error('refused') + }) + await expect(linkRepository(client, send, project, integration, repository)).rejects.toThrow('refused') + expect(send).toHaveBeenCalledTimes(1) + expect(ops.commit).not.toHaveBeenCalled() + }) +}) + +describe('unlinkRepository', () => { + it('pulls the repository from the project in the same commit, then removes the webhook', async () => { + const { client, ops } = fakeClient({ integration: 'i1', repositories: ['r1'] }) + const send = jest.fn(async () => ({})) + await unlinkRepository(client, send, repository, project) + expect(ops.update).toHaveBeenNthCalledWith(1, repository, { enabled: false, gitlabProject: null }) + expect(ops.update).toHaveBeenNthCalledWith(2, expect.objectContaining({ _id: 'p1' }), { + $pull: { repositories: 'r1' } + }) + expect(send).toHaveBeenCalledWith('repository-disable', { repositoryId: 'r1' }) + }) + + it('keeps the webhook when the commit is refused', async () => { + const { client } = fakeClient(undefined, false) + const send = jest.fn(async () => ({})) + await expect(unlinkRepository(client, send, repository, undefined)).rejects.toThrow() + expect(send).not.toHaveBeenCalled() + }) +}) + +describe('disableRepositoryHook', () => { + it('reports a failure instead of throwing', async () => { + const send = jest.fn(async () => { + throw new Error('gone') + }) + await expect(disableRepositoryHook(send, 'r1' as AnyClient)).resolves.toBeUndefined() + }) +}) + +describe('linkedProjectName', () => { + const repository = { gitlabProject: 'p1' } as unknown as GitlabIntegrationRepository + const project = { _id: 'p1', name: 'Test project' } as unknown as Project + + it('names the linked project once it is loaded', () => { + expect(linkedProjectName(repository, new Map())).toBe('') + expect(linkedProjectName(repository, new Map([[project._id, project]]))).toBe('Test project') + }) + + it('is empty for an unlinked repository', () => { + const unlinked = { gitlabProject: null } as unknown as GitlabIntegrationRepository + expect(linkedProjectName(unlinked, new Map([[project._id, project]]))).toBe('') + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/repository-choice.test.ts b/services/gitlab/gitlab-resources/src/__tests__/repository-choice.test.ts new file mode 100644 index 0000000000..d1b3431999 --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/repository-choice.test.ts @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: EPL-2.0 +/* eslint-disable @typescript-eslint/no-explicit-any -- test data */ +import type { Ref, Space } from '@hcengineering/core' +import type { GitlabIntegrationRepository } from '@hcengineering/gitlab' +import { defaultRepository, issueLinkFor, linkedRepositories, shownRepository, validChoice } from '../repository-choice' + +function repo (id: string, extra: Partial = {}): GitlabIntegrationRepository { + return { + _id: id, + gitlabProject: 'prj-1', + enabled: true, + deleted: false, + pathWithNamespace: `group/${id}`, + ...extra + } as unknown as GitlabIntegrationRepository +} + +const r1 = repo('r1') +const r2 = repo('r2') +const project = 'prj-1' as Ref + +describe('repository choice', () => { + it('lists the enabled, present repositories of the project', () => { + const all = [ + r1, + repo('off', { enabled: false }), + repo('gone', { deleted: true }), + repo('other', { gitlabProject: 'prj-2' as any }) + ] + expect(linkedRepositories(all, project).map((it) => it._id)).toEqual(['r1']) + expect(linkedRepositories(all, undefined)).toEqual([]) + }) + + it('defaults to the only linked repository, and to Huly only with two', () => { + expect(defaultRepository([r1])).toBe(r1) + expect(defaultRepository([r1, r2])).toBeUndefined() + expect(shownRepository({}, [r1])).toBe(r1) + expect(shownRepository({}, [r1, r2])).toBeNull() + }) + + it('shows the pick, or Huly only for an explicit opt-out', () => { + expect(shownRepository({ repository: r2._id }, [r1, r2])).toBe(r2) + expect(shownRepository({ repository: null }, [r1])).toBeNull() + }) + + it('drops a pick that belongs to another project', () => { + expect(validChoice({ repository: r2._id }, [r1])).toEqual({}) + expect(validChoice({ repository: r1._id }, [r1])).toEqual({ repository: r1._id }) + expect(validChoice({ repository: null }, [r1])).toEqual({ repository: null }) + }) + + it('turns a pick into GitlabIssue mixin data, and no pick into nothing', () => { + expect(issueLinkFor({})).toBeUndefined() + expect(issueLinkFor({ repository: r1._id })).toEqual({ repository: r1._id, url: '', gitlabIid: 0 }) + expect(issueLinkFor({ repository: null })).toEqual({ repository: null, url: '', gitlabIid: 0 }) + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/review-look.test.ts b/services/gitlab/gitlab-resources/src/__tests__/review-look.test.ts new file mode 100644 index 0000000000..a0d6f706b1 --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/review-look.test.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: EPL-2.0 +import gitlab from '../plugin' +import { reviewLook } from '../review-look' + +describe('reviewLook', () => { + it('marks a review GitLab did not take as not sent', () => { + expect(reviewLook('approved', true)).toEqual({ label: gitlab.string.ReviewNotSent, color: 'Coin' }) + expect(reviewLook('approved', false)).toEqual({ label: gitlab.string.ReviewApproved, color: 'Grass' }) + }) + + it('labels and colors each review state', () => { + expect(reviewLook('approved')).toEqual({ label: gitlab.string.ReviewApproved, color: 'Grass' }) + expect(reviewLook('unapproved')).toEqual({ label: gitlab.string.ReviewUnapproved, color: 'Coin' }) + expect(reviewLook('requested_changes')).toEqual({ label: gitlab.string.ReviewRequestedChanges, color: 'Sunshine' }) + expect(reviewLook('reviewed')).toEqual({ label: gitlab.string.ReviewReviewed }) + expect(reviewLook(undefined)).toEqual({ label: gitlab.string.ReviewReviewed }) + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/review-thread.test.ts b/services/gitlab/gitlab-resources/src/__tests__/review-thread.test.ts new file mode 100644 index 0000000000..03f8b5addf --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/review-thread.test.ts @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: EPL-2.0 +import { type Blob, type PersonId, type Ref } from '@hcengineering/core' +import { type DiffFile } from '@hcengineering/diffview' +import { parseDiff } from '@hcengineering/diffview-resources' +import { createHunkLoader, resolveChange, threadLocation } from '../review-thread' + +const PATCH = ['diff --git a/a.ts b/a.ts', '--- a/a.ts', '+++ b/a.ts', '@@ -1 +1 @@', '-x', '+y'].join('\n') +const FILE = 'blob' as Ref +const request = { file: FILE, path: 'a.ts', line: 1, oldLine: null, outdated: false } + +function deferred (): { + promise: Promise + resolve: (files: DiffFile[]) => void + reject: (err: Error) => void +} { + let release: (files: DiffFile[]) => void = () => {} + let fail: (err: Error) => void = () => {} + const promise = new Promise((resolve, reject) => { + release = resolve + fail = reject + }) + return { promise, resolve: release, reject: fail } +} + +describe('threadLocation', () => { + it('names the new line, else the old line, else the file', () => { + expect(threadLocation({ path: 'a.ts', oldPath: 'o.ts', line: 3, oldLine: 2 })).toBe('a.ts:3') + expect(threadLocation({ path: 'a.ts', oldPath: 'o.ts', line: null, oldLine: 2 })).toBe('o.ts:2') + expect(threadLocation({ path: 'a.ts', oldPath: 'o.ts', line: null, oldLine: null })).toBe('a.ts') + }) +}) + +describe('resolveChange', () => { + it('reopens a resolved thread and resolves an open one by the given person', () => { + expect(resolveChange(true, 'me' as PersonId)).toEqual({ isResolved: false, resolvedBy: null }) + expect(resolveChange(false, 'me' as PersonId)).toEqual({ isResolved: true, resolvedBy: 'me' }) + }) +}) + +describe('createHunkLoader', () => { + it('cuts the hunk for the newest request', async () => { + const load = createHunkLoader(async () => parseDiff(PATCH)) + expect(await load(request)).toBe(['@@ -1 +1 @@', '-x', '+y'].join('\n')) + }) + + it('shows nothing for an outdated thread or a missing diff', async () => { + const load = createHunkLoader(async () => parseDiff(PATCH)) + expect(await load({ ...request, outdated: true })).toBe('') + expect(await load({ ...request, file: undefined })).toBe('') + }) + + it('drops a slower answer once the thread turned outdated (same diff)', async () => { + const files = deferred() + const load = createHunkLoader(async () => await files.promise) + const first = load(request) + expect(await load({ ...request, outdated: true })).toBe('') + files.resolve(parseDiff(PATCH)) + expect(await first).toBeUndefined() + }) + + it('drops a slower failure, so it cannot clear a newer hunk', async () => { + const failing = deferred() + let calls = 0 + const load = createHunkLoader(async () => (++calls === 1 ? await failing.promise : parseDiff(PATCH))) + const first = load(request) + expect(await load({ ...request, line: 1 })).not.toBe('') + failing.reject(new Error('HTTP 500')) + expect(await first).toBeUndefined() + }) + + it('shows nothing when the newest request fails', async () => { + const load = createHunkLoader(async () => { + throw new Error('HTTP 500') + }) + expect(await load(request)).toBe('') + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/safe-url.test.ts b/services/gitlab/gitlab-resources/src/__tests__/safe-url.test.ts new file mode 100644 index 0000000000..d67429a46c --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/safe-url.test.ts @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: EPL-2.0 +import { safeHttpUrl } from '../safe-url' + +describe('safeHttpUrl', () => { + it('keeps http and https URLs', () => { + expect(safeHttpUrl('https://gitlab.com/g/r/-/issues/1')).toBe('https://gitlab.com/g/r/-/issues/1') + expect(safeHttpUrl('HTTP://gitlab.local/g/r')).toBe('HTTP://gitlab.local/g/r') + expect(safeHttpUrl(' https://gitlab.com/g/r ')).toBe('https://gitlab.com/g/r') + }) + + it('refuses any other scheme and empty values', () => { + for (const url of [ + 'javascript:alert(1)', + ' javascript:alert(1)', + 'data:text/html,x', + '//evil.example', + 'vbscript:alert(1)', + 'JaVaScRiPt:alert(1)', + 'java\tscript:alert(1)', + '\u0001https://x', + 'https://', + '', + null, + undefined + ]) { + expect(safeHttpUrl(url)).toBeUndefined() + } + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/service.test.ts b/services/gitlab/gitlab-resources/src/__tests__/service.test.ts new file mode 100644 index 0000000000..955a2b6766 --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/service.test.ts @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: EPL-2.0 +import { GitlabServiceError, errorText } from '../errors' +import { parseServiceResponse, serviceUrl } from '../service' + +describe('serviceUrl', () => { + it('joins service url and path without double slashes', () => { + expect(serviceUrl('http://pod:3600/', 'auth')).toBe('http://pod:3600/api/v1/auth') + expect(serviceUrl('http://pod:3600', 'repository-enable')).toBe('http://pod:3600/api/v1/repository-enable') + }) +}) + +describe('parseServiceResponse', () => { + function thrownBy (fn: () => unknown): unknown { + try { + fn() + } catch (err) { + return err + } + throw new Error('expected a throw') + } + + it('returns the parsed body of an ok JSON response', () => { + expect(parseServiceResponse(200, true, '{"url":"https://x"}')).toEqual({ url: 'https://x' }) + }) + + it('throws the error field of an error JSON response', () => { + expect(errorText(thrownBy(() => parseServiceResponse(403, false, '{"error":"Forbidden caller"}')))).toBe( + 'Forbidden caller' + ) + }) + + it('throws HTTP for a non-JSON error response', () => { + expect(errorText(thrownBy(() => parseServiceResponse(502, false, 'Bad Gateway')))).toBe( + 'HTTP 502' + ) + }) + + it('handles an empty body', () => { + expect(errorText(thrownBy(() => parseServiceResponse(500, false, '')))).toBe('HTTP 500') + expect(parseServiceResponse(200, true, '')).toEqual({}) + }) + + it('rejects an ok response that is not a JSON object', () => { + expect(errorText(thrownBy(() => parseServiceResponse(200, true, '')))).toBe('HTTP 200') + }) + + it('throws a GitlabServiceError with the message and the machine-readable code', () => { + const err = thrownBy(() => + parseServiceResponse(409, false, '{"error":"Disconnect GitLab first","code":"app-in-use"}') + ) + expect(err).toBeInstanceOf(GitlabServiceError) + expect((err as GitlabServiceError).code).toBe('app-in-use') + expect(errorText(err)).toBe('Disconnect GitLab first') + }) + + it('has no code for an error without one', () => { + for (const [status, body] of [ + [409, '{"error":"x"}'], + [502, 'Bad Gateway'] + ] as const) { + const err = thrownBy(() => parseServiceResponse(status, false, body)) + expect(err).toBeInstanceOf(GitlabServiceError) + expect((err as GitlabServiceError).code).toBeUndefined() + } + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/state.test.ts b/services/gitlab/gitlab-resources/src/__tests__/state.test.ts new file mode 100644 index 0000000000..aaff24d2ca --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/state.test.ts @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: EPL-2.0 +import { + OAuthCallbackTracker, + isOAuthCallbackMessage, + isOAuthErrorMessage, + isOAuthResultMessage, + oauthCallbackMessage, + oauthErrorMessage, + oauthResultMessage, + stateFromAuthorizeUrl +} from '../state' + +const garbage: unknown[] = [ + undefined, + null, + 0, + 'gitlab-oauth-callback', + [], + {}, + { type: 'other', code: 'c', state: 's' }, + { type: 'gitlab-oauth-callback' }, + { type: 'gitlab-oauth-callback', code: 1, state: 's' }, + { type: 'gitlab-oauth-callback', code: 'c', state: null }, + { type: 'gitlab-oauth-result', state: 's' }, + { type: 'gitlab-oauth-result', state: 's', ok: 'yes' }, + { type: 'gitlab-oauth-result', state: 5, ok: true }, + { type: 'gitlab-oauth-result', state: 's', ok: false, error: 42 }, + { type: 'gitlab-oauth-error', error: 1 } +] + +describe('oauth broadcast messages', () => { + it('builds messages that pass their own guard only', () => { + const callback = oauthCallbackMessage('c', 's') + const result = oauthResultMessage('s', false, 'boom') + const error = oauthErrorMessage('access_denied', 'The user denied access') + expect(callback).toEqual({ type: 'gitlab-oauth-callback', code: 'c', state: 's' }) + expect(result).toEqual({ type: 'gitlab-oauth-result', state: 's', ok: false, error: 'boom' }) + expect(error).toEqual({ type: 'gitlab-oauth-error', error: 'access_denied', description: 'The user denied access' }) + expect(isOAuthCallbackMessage(callback)).toBe(true) + expect(isOAuthResultMessage(callback)).toBe(false) + expect(isOAuthResultMessage(result)).toBe(true) + expect(isOAuthCallbackMessage(result)).toBe(false) + expect(isOAuthErrorMessage(error)).toBe(true) + expect(isOAuthCallbackMessage(error)).toBe(false) + }) + + it('omits an undefined error from a successful result', () => { + expect(oauthResultMessage('s', true)).toEqual({ type: 'gitlab-oauth-result', state: 's', ok: true }) + expect(isOAuthResultMessage(oauthResultMessage('s', true))).toBe(true) + }) + + it('rejects garbage input', () => { + for (const value of garbage) { + const callbackOk = isOAuthCallbackMessage(value) + const resultOk = isOAuthResultMessage(value) + const errorOk = isOAuthErrorMessage(value) + expect({ value, callbackOk, resultOk, errorOk }).toEqual({ + value, + callbackOk: false, + resultOk: false, + errorOk: false + }) + } + }) +}) + +describe('OAuthCallbackTracker', () => { + it('accepts an expected state exactly once', () => { + const tracker = new OAuthCallbackTracker() + tracker.expect('s1') + expect(tracker.accept(oauthCallbackMessage('c', 's1'))).toBe(true) + expect(tracker.accept(oauthCallbackMessage('c', 's1'))).toBe(false) + }) + + it('rejects a state it did not request', () => { + const tracker = new OAuthCallbackTracker() + expect(tracker.accept(oauthCallbackMessage('c', 'unknown'))).toBe(false) + tracker.expect('s1') + expect(tracker.accept(oauthCallbackMessage('c', 'unknown'))).toBe(false) + expect(tracker.accept(oauthCallbackMessage('c', 's1'))).toBe(true) + }) + + it('keeps several pending states independent', () => { + const tracker = new OAuthCallbackTracker() + tracker.expect('a') + tracker.expect('b') + expect(tracker.accept(oauthCallbackMessage('c', 'b'))).toBe(true) + expect(tracker.accept(oauthCallbackMessage('c', 'a'))).toBe(true) + expect(tracker.accept(oauthCallbackMessage('c', 'a'))).toBe(false) + }) +}) + +describe('stateFromAuthorizeUrl', () => { + it('reads the state query parameter', () => { + expect(stateFromAuthorizeUrl('https://gitlab.com/oauth/authorize?client_id=x&state=abc.def')).toBe('abc.def') + }) + + it('returns undefined without a state or for an invalid url', () => { + expect(stateFromAuthorizeUrl('https://gitlab.com/oauth/authorize?client_id=x')).toBeUndefined() + expect(stateFromAuthorizeUrl('https://gitlab.com/oauth/authorize?state=')).toBeUndefined() + expect(stateFromAuthorizeUrl('not a url')).toBeUndefined() + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/thread-expansion.test.ts b/services/gitlab/gitlab-resources/src/__tests__/thread-expansion.test.ts new file mode 100644 index 0000000000..c7934212a3 --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/thread-expansion.test.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: EPL-2.0 +import { expansionAfter } from '../thread-expansion' + +describe('expansionAfter', () => { + it('collapses on resolve and expands on reopen, wherever it happened', () => { + expect(expansionAfter({ expanded: true, resolved: false }, true)).toEqual({ expanded: false, resolved: true }) + expect(expansionAfter({ expanded: false, resolved: true }, false)).toEqual({ expanded: true, resolved: false }) + }) + + it("keeps the user's choice while the state stays the same", () => { + const opened = { expanded: true, resolved: true } + expect(expansionAfter(opened, true)).toBe(opened) + }) +}) diff --git a/services/gitlab/gitlab-resources/src/__tests__/viewed-files.test.ts b/services/gitlab/gitlab-resources/src/__tests__/viewed-files.test.ts new file mode 100644 index 0000000000..b33a3ded14 --- /dev/null +++ b/services/gitlab/gitlab-resources/src/__tests__/viewed-files.test.ts @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: EPL-2.0 +import { createSerialQueue, saveViewedFile, toggleViewed } from '../viewed-files' + +describe('toggleViewed', () => { + it('ticks a file version once, and unticks only that version', () => { + const ticked = toggleViewed(toggleViewed([], 'a.ts', 's1', true), 'a.ts', 's1', true) + expect(ticked).toEqual([{ fileName: 'a.ts', sha: 's1' }]) + const both = toggleViewed(ticked, 'a.ts', 's2', true) + expect(toggleViewed(both, 'a.ts', 's1', false)).toEqual([{ fileName: 'a.ts', sha: 's2' }]) + }) +}) + +describe('createSerialQueue', () => { + it('starts a task only after the previous one finished', async () => { + const serial = createSerialQueue() + const events: string[] = [] + let releaseFirst: () => void = () => {} + let firstStarted: () => void = () => {} + const started = new Promise((resolve) => { + firstStarted = resolve + }) + const first = serial(async () => { + events.push('first start') + firstStarted() + await new Promise((resolve) => { + releaseFirst = resolve + }) + events.push('first end') + }) + const second = serial(async () => { + events.push('second start') + }) + await started + // A macrotask runs every pending microtask: a broken queue would have started the second task by now + await new Promise((resolve) => setTimeout(resolve, 0)) + expect(events).toEqual(['first start']) + releaseFirst() + await Promise.all([first, second]) + expect(events).toEqual(['first start', 'first end', 'second start']) + }) + + it('runs the next task after a failed one, and rejects only the failed call', async () => { + const serial = createSerialQueue() + const failed = serial(async () => { + throw new Error('refused') + }) + const next = jest.fn(async () => {}) + await expect(failed).rejects.toThrow('refused') + await serial(next) + expect(next).toHaveBeenCalledTimes(1) + }) +}) + +describe('saveViewedFile', () => { + const mergeRequestFields = { _id: 'mr1', _class: 'gitlab:class:GitlabMergeRequest', space: 's1' } + const mergeRequest = mergeRequestFields as never + const author = 'person1' as never + + it("creates this user's review doc on the first tick", async () => { + const client = { + findOne: jest.fn(async () => undefined), + update: jest.fn(), + addCollection: jest.fn(async () => 'id') + } + await saveViewedFile(client as never, mergeRequest, author, { fileName: 'a.ts', sha: 's1', viewed: true }) + expect(client.addCollection).toHaveBeenCalledWith( + expect.anything(), + 's1', + 'mr1', + 'gitlab:class:GitlabMergeRequest', + 'viewedFiles', + { author: 'person1', files: [{ fileName: 'a.ts', sha: 's1' }] } + ) + expect(client.update).not.toHaveBeenCalled() + }) + + it('updates the existing doc', async () => { + const current = { files: [{ fileName: 'a.ts', sha: 's1' }] } + const client = { findOne: jest.fn(async () => current), update: jest.fn(async () => {}), addCollection: jest.fn() } + await saveViewedFile(client as never, mergeRequest, author, { fileName: 'a.ts', sha: 's1', viewed: false }) + expect(client.update).toHaveBeenCalledWith(current, { files: [] }) + expect(client.addCollection).not.toHaveBeenCalled() + }) +}) diff --git a/services/gitlab/gitlab-resources/src/app-status.ts b/services/gitlab/gitlab-resources/src/app-status.ts new file mode 100644 index 0000000000..48d517042d --- /dev/null +++ b/services/gitlab/gitlab-resources/src/app-status.ts @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: EPL-2.0 + +import { GITLAB_COM } from './gitlab-host' + +/** The workspace GitLab application, as the GitLab service reports it (`app-status`). */ +export interface AppStatus { + configured: boolean + host?: string + clientId?: string + redirectUri: string + // Space-separated, as the pod sends it (e.g. 'api read_user') + scopes: string +} + +export function appStatusOf (body: Record): AppStatus { + return { + configured: body.configured === true, + host: typeof body.host === 'string' ? body.host : undefined, + clientId: typeof body.clientId === 'string' ? body.clientId : undefined, + redirectUri: typeof body.redirectUri === 'string' ? body.redirectUri : '', + scopes: typeof body.scopes === 'string' ? body.scopes : '' + } +} + +/** The setup form; unchecked "self-managed" means gitlab.com. */ +export interface SetupForm { + selfManaged: boolean + host: string + clientId: string + clientSecret: string +} + +/** The form for changing `status`; the secret is never shown, so it starts empty. */ +export function setupFormOf (status: AppStatus | undefined): SetupForm { + const host = status?.host ?? '' + const selfManaged = host !== '' && host !== GITLAB_COM + return { selfManaged, host: selfManaged ? host : '', clientId: status?.clientId ?? '', clientSecret: '' } +} + +/** The `app-config` request: no host for gitlab.com, no secret when left empty (the stored one is kept). */ +export function appConfigArgs (form: SetupForm): Record { + const args: Record = { clientId: form.clientId.trim() } + if (form.selfManaged) args.host = form.host.trim() + if (form.clientSecret.trim() !== '') args.clientSecret = form.clientSecret.trim() + return args +} diff --git a/services/gitlab/gitlab-resources/src/components/Configure.svelte b/services/gitlab/gitlab-resources/src/components/Configure.svelte new file mode 100644 index 0000000000..4c107ed19b --- /dev/null +++ b/services/gitlab/gitlab-resources/src/components/Configure.svelte @@ -0,0 +1,9 @@ + + + + diff --git a/services/gitlab/gitlab-resources/src/components/Connect.svelte b/services/gitlab/gitlab-resources/src/components/Connect.svelte new file mode 100644 index 0000000000..3d7b3f0e6c --- /dev/null +++ b/services/gitlab/gitlab-resources/src/components/Connect.svelte @@ -0,0 +1,174 @@ + + + + { + dispatch('close') + }} + canSave={true} + okLabel={presentation.string.Ok} + on:close={() => dispatch('close')} + on:changeContent +> +
+ { + appConfigured = ev.detail + }} + /> + {#if $gitlabAuthentication !== undefined && $gitlabAuthentication.login !== ''} +
+ + {#if integrations.length > 0 && isMaintainer} + + {:else if mergeRequest.files > 0} +