This commit is contained in:
2025-08-25 20:24:23 +08:00
parent 30106e0129
commit 0ae8d7a709
1044 changed files with 321581 additions and 0 deletions

View File

@@ -0,0 +1,416 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.4.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.3.1...decap-cms-backend-gitlab@3.4.0) (2025-07-15)
### Features
- add logo to header ([#7487](https://github.com/decaporg/decap-cms/issues/7487)) ([b540ace](https://github.com/decaporg/decap-cms/commit/b540acec943eb231df6aac7b1d515d9b4b84fa5d))
## [3.3.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.3.0...decap-cms-backend-gitlab@3.3.1) (2025-07-10)
**Note:** Version bump only for package decap-cms-backend-gitlab
# [3.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.2.2...decap-cms-backend-gitlab@3.3.0) (2025-06-26)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [3.2.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.2.1...decap-cms-backend-gitlab@3.2.2) (2024-08-13)
### Reverts
- Revert "Update dependencies (#7264)" ([22d483a](https://github.com/decaporg/decap-cms/commit/22d483a5b0c654071ae05735ac4f49abdc13d38c)), closes [#7264](https://github.com/decaporg/decap-cms/issues/7264)
## [3.2.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.2.0...decap-cms-backend-gitlab@3.2.1) (2024-08-13)
**Note:** Version bump only for package decap-cms-backend-gitlab
# [3.2.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.1.3...decap-cms-backend-gitlab@3.2.0) (2024-08-07)
### Bug Fixes
- **backend:** allow a custom API root for backend ([#7214](https://github.com/decaporg/decap-cms/issues/7214)) ([fae3e05](https://github.com/decaporg/decap-cms/commit/fae3e057f898f60fdbe80091acc833d6ac92696e)), closes [#7168](https://github.com/decaporg/decap-cms/issues/7168)
## [3.1.3](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.1.2...decap-cms-backend-gitlab@3.1.3) (2024-04-03)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [3.1.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.1.1...decap-cms-backend-gitlab@3.1.2) (2024-03-21)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [3.1.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.1.0-beta.1...decap-cms-backend-gitlab@3.1.1) (2024-02-21)
### Bug Fixes
- gitlab pkce auth error ([#7110](https://github.com/decaporg/decap-cms/issues/7110)) ([bcd58d6](https://github.com/decaporg/decap-cms/commit/bcd58d6e117b4654b3e0dca173f7f8aaca8dabdf))
# [3.1.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.1.0-beta.1...decap-cms-backend-gitlab@3.1.0) (2024-02-01)
**Note:** Version bump only for package decap-cms-backend-gitlab
# [3.1.0-beta.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.1.0-beta.0...decap-cms-backend-gitlab@3.1.0-beta.1) (2024-01-31)
**Note:** Version bump only for package decap-cms-backend-gitlab
# [3.1.0-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.1.0...decap-cms-backend-gitlab@3.1.0-beta.0) (2023-10-20)
### Reverts
- Revert "chore(release): publish" ([b89fc89](https://github.com/decaporg/decap-cms/commit/b89fc894dfbb5f4136b2e5427fd25a29378a58c6))
## [3.0.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.0.1...decap-cms-backend-gitlab@3.0.2) (2023-10-13)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [3.0.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.0.0...decap-cms-backend-gitlab@3.0.1) (2023-08-25)
### Bug Fixes
- update peer dependencies ([#6886](https://github.com/decaporg/decap-cms/issues/6886)) ([e580ce5](https://github.com/decaporg/decap-cms/commit/e580ce52ce5f80fa040e8fbcab7fed0744f4f695))
# [3.0.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@2.14.0...decap-cms-backend-gitlab@3.0.0) (2023-08-18)
**Note:** Version bump only for package decap-cms-backend-gitlab
# [2.14.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@2.14.0-beta.0...decap-cms-backend-gitlab@2.14.0) (2023-08-18)
**Note:** Version bump only for package decap-cms-backend-gitlab
# 2.14.0-beta.0 (2023-08-18)
### Features
- rename packages ([#6863](https://github.com/decaporg/decap-cms/issues/6863)) ([d515e7b](https://github.com/decaporg/decap-cms/commit/d515e7bd33216a775d96887b08c4f7b1962941bb))
## [2.13.1-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@2.13.0...decap-cms-backend-gitlab@2.13.1-beta.0) (2023-07-27)
**Note:** Version bump only for package decap-cms-backend-gitlab
# [2.13.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@2.12.0...decap-cms-backend-gitlab@2.13.0) (2021-12-28)
### Features
- **backend-gitlab:** initial GraphQL support ([#6059](https://github.com/decaporg/decap-cms/issues/6059)) ([1523a41](https://github.com/decaporg/decap-cms/commit/1523a4140a3d2f4cc01a1548514ae17bc1ad504e))
# [2.12.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@2.11.4...decap-cms-backend-gitlab@2.12.0) (2021-10-18)
### Features
- display author of changes in workflow tab ([#5780](https://github.com/decaporg/decap-cms/issues/5780)) ([3f607e4](https://github.com/decaporg/decap-cms/commit/3f607e41d9c4d8fe5329a9ab6841cada7742825e))
## [2.11.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.11.3...decap-cms-backend-gitlab@2.11.4) (2021-06-01)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [2.11.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.11.2...decap-cms-backend-gitlab@2.11.3) (2021-05-31)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [2.11.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.11.1...decap-cms-backend-gitlab@2.11.2) (2021-05-19)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [2.11.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.11.0...decap-cms-backend-gitlab@2.11.1) (2021-05-04)
### Bug Fixes
- **backend-gitlab:** increase merge requests fetched to 100 ([#5320](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/5320)) ([0a1b00d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/0a1b00d8b29a065b7d72fbb1744eb787da3b916e))
# [2.11.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.10.0...decap-cms-backend-gitlab@2.11.0) (2021-05-04)
### Features
- added react 17 as peer dependency in packages ([#5316](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/5316)) ([9e42380](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/9e423805707321396eec137f5b732a5b07a0dd3f))
# [2.10.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.9.9...decap-cms-backend-gitlab@2.10.0) (2021-04-14)
### Features
- Adds PKCE authentication for GitLab closes [#5236](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/5236) ([#5239](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/5239)) ([829409e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/829409e0bc03b4591ee6b59d9895adc4e7190037))
## [2.9.9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.9.8...decap-cms-backend-gitlab@2.9.9) (2021-02-23)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [2.9.8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.9.7...decap-cms-backend-gitlab@2.9.8) (2021-02-10)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [2.9.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.9.6...decap-cms-backend-gitlab@2.9.7) (2021-02-01)
### Bug Fixes
- **backend-gitlab:** increase rebase timeout ([#4905](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/4905)) ([d9d6860](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/d9d686025fe848fe460917cdef1934dea3295c9d))
## [2.9.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.9.5...decap-cms-backend-gitlab@2.9.6) (2021-01-05)
### Bug Fixes
- **backend-gitlab:** skip creating a CI pipeline when rebasing merge request ([#4802](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/4802)) ([ce11dd2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/ce11dd23753fd7bb502c299058b83701f20058b2)), closes [#4786](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/4786)
## [2.9.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.9.4...decap-cms-backend-gitlab@2.9.5) (2020-09-20)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [2.9.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.9.3...decap-cms-backend-gitlab@2.9.4) (2020-09-15)
**Note:** Version bump only for package decap-cms-backend-gitlab
## 2.9.3 (2020-09-08)
### Reverts
- Revert "chore(release): publish" ([828bb16](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/828bb16415b8c22a34caa19c50c38b24ffe9ceae))
## 2.9.2 (2020-08-20)
### Reverts
- Revert "chore(release): publish" ([8262487](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/82624879ccbcb16610090041db28f00714d924c8))
## 2.9.1 (2020-07-27)
### Reverts
- Revert "chore(release): publish" ([118d50a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/118d50a7a70295f25073e564b5161aa2b9883056))
# [2.9.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.8.4...decap-cms-backend-gitlab@2.9.0) (2020-06-18)
### Bug Fixes
- handle token expiry ([#3847](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3847)) ([285c940](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/285c940562548d7bc88de244123ba87ff66fba65))
### Features
- add backend status down indicator ([#3889](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3889)) ([a50edc7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/a50edc70553ad6afa1acee6a51996ad226443f8c))
- **backend-gitgateway:** improve deploy preview visibility ([#3882](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3882)) ([afc9bf4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/afc9bf4f3fe14ccb60851fc24e68922a6e4a85a9))
## [2.8.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.8.3...decap-cms-backend-gitlab@2.8.4) (2020-04-21)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [2.8.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.8.2...decap-cms-backend-gitlab@2.8.3) (2020-04-01)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [2.8.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.8.1...decap-cms-backend-gitlab@2.8.2) (2020-03-20)
### Bug Fixes
- missing workflow timestamp ([#3445](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3445)) ([9616cdb](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/9616cdb8bb0a564771e5755bcd3718a07f2e2072))
## [2.8.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.8.0...decap-cms-backend-gitlab@2.8.1) (2020-03-03)
### Bug Fixes
- **locale:** Remove hard coded string literals ([#3333](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3333)) ([7c45a3c](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/7c45a3cda983be427864a56e58791565eb9232e2))
# [2.8.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.7.1...decap-cms-backend-gitlab@2.8.0) (2020-02-25)
### Features
- **core:** align GitHub metadata handling with other backends ([#3316](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3316)) ([7e0a8ad](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/7e0a8ad532012576dc5e40bd4e9d54522e307123)), closes [#3292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3292)
## [2.7.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.7.0...decap-cms-backend-gitlab@2.7.1) (2020-02-22)
### Reverts
- Revert "feat(core): Align GitHub metadata handling with other backends (#3292)" ([5bdd3df](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/5bdd3df9ccbb5149c22d79987ebdcd6cab4b261f)), closes [#3292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3292)
# [2.7.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.6.0...decap-cms-backend-gitlab@2.7.0) (2020-02-22)
### Features
- **core:** Align GitHub metadata handling with other backends ([#3292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3292)) ([8193b5a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/8193b5ace89d6f14a6c756235a50b186a763b6b1))
# [2.6.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.5.3...decap-cms-backend-gitlab@2.6.0) (2020-02-10)
### Bug Fixes
- filter paginated results ([#3216](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3216)) ([0a482b1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/0a482b10049bcfa022b81165cabf4512d77e0b88))
- workflow file collection ([#3207](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3207)) ([d22f7e6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/d22f7e680e7064e8607cf8b420571fa40a6c314e))
### Features
- field based media/public folders ([#3208](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3208)) ([97bc0c8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/97bc0c8dc489e736f89d748ba832d78400fe4332))
### Reverts
- Revert "chore(release): publish" ([a015d1d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/a015d1d92a4b1c0130c44fcef1c9ecdb157a0f07))
## [2.5.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.5.2...decap-cms-backend-gitlab@2.5.3) (2020-02-06)
### Bug Fixes
- **locale:** remove hard coded strings ([#3193](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3193)) ([fc91bf8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/fc91bf8781e65ce1dc946363dbb10419a145c66b))
## [2.5.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.5.1...decap-cms-backend-gitlab@2.5.2) (2020-01-21)
### Bug Fixes
- **backend-gitlab:** check for shared group permissions ([#3122](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3122)) ([f1739e9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/f1739e978f9dee1de42dd5479ec80a5d991a9bfe))
- **git-gateway-gitlab:** fix large media support for editorial workflow ([#3105](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3105)) ([038803c](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/038803c9f249de386812652372c35c4c53935295))
## [2.5.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.5.0...decap-cms-backend-gitlab@2.5.1) (2020-01-14)
**Note:** Version bump only for package decap-cms-backend-gitlab
# [2.5.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.5.0-beta.0...decap-cms-backend-gitlab@2.5.0) (2020-01-07)
**Note:** Version bump only for package decap-cms-backend-gitlab
# [2.5.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.4.0...decap-cms-backend-gitlab@2.5.0-beta.0) (2019-12-18)
### Features
- bundle assets with content ([#2958](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2958)) ([2b41d8a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/2b41d8a838a9c8a6b21cde2ddd16b9288334e298))
# [2.4.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.4.0-beta.1...decap-cms-backend-gitlab@2.4.0) (2019-12-18)
**Note:** Version bump only for package decap-cms-backend-gitlab
# [2.4.0-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.4.0-beta.0...decap-cms-backend-gitlab@2.4.0-beta.1) (2019-12-02)
### Features
- content in sub folders ([#2897](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2897)) ([afcfe5b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/afcfe5b6d5f32669e9061ec596bd35ad545d61a3))
# [2.4.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.3.3-beta.1...decap-cms-backend-gitlab@2.4.0-beta.0) (2019-11-07)
### Features
- add go back to site button ([#2538](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2538)) ([f206e7e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/f206e7e5a13fb48ec6b27dce0dbb3a59b61de8f9))
- enable specifying custom open authoring commit message ([#2810](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2810)) ([2841ff9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/2841ff9ffe58afcf4dba45514a84a262ad370f1d))
## [2.3.3-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.3.3-beta.0...decap-cms-backend-gitlab@2.3.3-beta.1) (2019-09-26)
### Bug Fixes
- **backend-gitlab:** exclude directories in paginated results ([#2668](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2668)) ([3903acb](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/3903acb))
## [2.3.3-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.3.2...decap-cms-backend-gitlab@2.3.3-beta.0) (2019-08-24)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [2.3.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.3.2-beta.0...decap-cms-backend-gitlab@2.3.2) (2019-04-10)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [2.3.2-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.3.1...decap-cms-backend-gitlab@2.3.2-beta.0) (2019-04-05)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [2.3.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.3.1-beta.1...decap-cms-backend-gitlab@2.3.1) (2019-03-29)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [2.3.1-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.3.1-beta.0...decap-cms-backend-gitlab@2.3.1-beta.1) (2019-03-26)
### Bug Fixes
- export on decap-cms and maps on esm ([#2244](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2244)) ([6ffd13b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/6ffd13b))
## [2.3.1-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.3.0...decap-cms-backend-gitlab@2.3.1-beta.0) (2019-03-25)
### Bug Fixes
- update peer dep versions ([#2234](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2234)) ([7987091](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/7987091))
# [2.3.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.2.0...decap-cms-backend-gitlab@2.3.0) (2019-03-22)
### Features
- add ES module builds ([#2215](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2215)) ([d142b32](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/d142b32))
# [2.2.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.2.0-beta.0...decap-cms-backend-gitlab@2.2.0) (2019-03-22)
**Note:** Version bump only for package decap-cms-backend-gitlab
# [2.2.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.1.4-beta.0...decap-cms-backend-gitlab@2.2.0-beta.0) (2019-03-21)
### Bug Fixes
- fix umd builds ([#2214](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2214)) ([e04f6be](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/e04f6be))
### Features
- provide usable UMD builds for all packages ([#2141](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2141)) ([82cc794](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/82cc794))
## [2.1.4-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.1.3...decap-cms-backend-gitlab@2.1.4-beta.0) (2019-03-15)
### Features
- upgrade to Emotion 10 ([#2166](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2166)) ([ccef446](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/ccef446))
## [2.1.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.1.2...decap-cms-backend-gitlab@2.1.3) (2019-03-08)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [2.1.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.1.1...decap-cms-backend-gitlab@2.1.2) (2019-02-26)
**Note:** Version bump only for package decap-cms-backend-gitlab
## [2.1.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.1.0...decap-cms-backend-gitlab@2.1.1) (2018-12-11)
### Bug Fixes
- **backend-gitlab:** show svg previews ([#1946](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/1946)) ([c3adebe](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/c3adebe))
# [2.1.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.0.6...decap-cms-backend-gitlab@2.1.0) (2018-11-12)
### Bug Fixes
- **backend-gitlab:** support folder names with whitespace ([#1799](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/1799)) ([a9f69f9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/a9f69f9))
### Features
- allow custom logo on auth page ([#1818](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/1818)) ([c6ae1e8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/c6ae1e8))
<a name="2.0.6"></a>
## [2.0.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.0.5...decap-cms-backend-gitlab@2.0.6) (2018-08-27)
**Note:** Version bump only for package decap-cms-backend-gitlab
<a name="2.0.5"></a>
## [2.0.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.0.4...decap-cms-backend-gitlab@2.0.5) (2018-08-24)
### Bug Fixes
- **gitlab:** fetch media library images through API ([#1433](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/1433)) ([83d2adc](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/83d2adc))
<a name="2.0.4"></a>
## [2.0.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.0.3...decap-cms-backend-gitlab@2.0.4) (2018-08-07)
### Bug Fixes
- **backends:** fix commit message handling ([#1568](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/1568)) ([f7e7120](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/f7e7120))
<a name="2.0.3"></a>
## [2.0.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.0.2...decap-cms-backend-gitlab@2.0.3) (2018-08-01)
### Bug Fixes
- **gitlab:** fix uploads ([#1566](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/1566)) ([d59c990](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/d59c990))
<a name="2.0.2"></a>
## [2.0.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.0.1...decap-cms-backend-gitlab@2.0.2) (2018-07-28)
**Note:** Version bump only for package decap-cms-backend-gitlab
<a name="2.0.1"></a>
## 2.0.1 (2018-07-26)
<a name="2.0.0"></a>
# 2.0.0 (2018-07-26)
**Note:** Version bump only for package decap-cms-backend-gitlab

View File

@@ -0,0 +1,13 @@
# GitLab backend
An abstraction layer between the CMS and [GitLab](https://docs.gitlab.com/ee/api/README.html)
## Code structure
`Implementation` for [File Management System API](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-lib-util/README.md) based on `Api`. With [Editorial Workflow](https://www.decapcms.org/docs/beta-features/#gitlab-and-bitbucket-editorial-workflow-support) uses merge requests labels to track unpublished entries statuses.
`Api` - A wrapper for GitLab REST API.
`AuthenticationPage` - A component uses [lib-auth](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-lib-auth/README.md) to facilitate OAuth, PKCE and implicit authentication.
Look at tests or types for more info.

View File

@@ -0,0 +1,44 @@
{
"name": "decap-cms-backend-gitlab",
"description": "GitLab backend for Decap CMS",
"version": "3.4.0",
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab",
"bugs": "https://github.com/decaporg/decap-cms/issues",
"license": "MIT",
"module": "dist/esm/index.js",
"main": "dist/decap-cms-backend-gitlab.js",
"keywords": [
"decap-cms",
"backend",
"gitlab"
],
"sideEffects": false,
"scripts": {
"develop": "npm run build:esm -- --watch",
"build": "cross-env NODE_ENV=production webpack",
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward --extensions \".js,.jsx,.ts,.tsx\""
},
"dependencies": {
"apollo-cache-inmemory": "^1.6.2",
"apollo-client": "^2.6.3",
"apollo-link-context": "^1.0.18",
"apollo-link-http": "^1.5.15",
"js-base64": "^3.0.0",
"path-browserify": "^1.0.1",
"semaphore": "^1.1.0"
},
"peerDependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"decap-cms-lib-auth": "^3.0.0",
"decap-cms-lib-util": "^3.0.0",
"decap-cms-ui-default": "^3.0.0",
"immutable": "^3.7.6",
"lodash": "^4.17.11",
"prop-types": "^15.7.2",
"react": "^19.1.0"
},
"browser": {
"path": "path-browserify"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,126 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from '@emotion/styled';
import {
NetlifyAuthenticator,
ImplicitAuthenticator,
PkceAuthenticator,
} from 'decap-cms-lib-auth';
import { AuthenticationPage, Icon } from 'decap-cms-ui-default';
const LoginButtonIcon = styled(Icon)`
margin-right: 18px;
`;
const clientSideAuthenticators = {
pkce: ({
base_url,
auth_endpoint,
app_id,
auth_token_endpoint}) =>
new PkceAuthenticator({
base_url,
auth_endpoint,
app_id,
auth_token_endpoint,
auth_token_endpoint_content_type: 'application/json; charset=utf-8',
}),
implicit: ({
base_url,
auth_endpoint,
app_id,
clearHash }) =>
new ImplicitAuthenticator({
base_url,
auth_endpoint,
app_id,
clearHash,
}),
};
export default class GitLabAuthenticationPage extends React.Component {
static propTypes = {
onLogin: PropTypes.func.isRequired,
inProgress: PropTypes.bool,
base_url: PropTypes.string,
siteId: PropTypes.string,
authEndpoint: PropTypes.string,
config: PropTypes.object.isRequired,
clearHash: PropTypes.func,
t: PropTypes.func.isRequired,
};
state = {};
componentDidMount() {
// Manually validate PropTypes - React 19 breaking change
PropTypes.checkPropTypes(GitLabAuthenticationPage.propTypes, this.props, 'prop', 'GitLabAuthenticationPage');
const {
auth_type: authType = '',
base_url = 'https://gitlab.com',
auth_endpoint = 'oauth/authorize',
app_id = '',
} = this.props.config.backend;
if (clientSideAuthenticators[authType]) {
this.auth = clientSideAuthenticators[authType]({
base_url,
auth_endpoint,
app_id,
auth_token_endpoint: 'oauth/token',
clearHash: this.props.clearHash,
});
// Complete implicit authentication if we were redirected back to from the provider.
this.auth.completeAuth((err, data) => {
if (err) {
this.setState({ loginError: err.toString() });
return;
}
this.props.onLogin(data);
});
} else {
this.auth = new NetlifyAuthenticator({
base_url: this.props.base_url,
site_id:
document.location.host.split(':')[0] === 'localhost'
? 'demo.decapcms.org'
: this.props.siteId,
auth_endpoint: this.props.authEndpoint,
});
}
}
handleLogin = e => {
e.preventDefault();
this.auth.authenticate({ provider: 'gitlab', scope: 'api' }, (err, data) => {
if (err) {
this.setState({ loginError: err.toString() });
return;
}
this.props.onLogin(data);
});
};
render() {
const { inProgress, config, t } = this.props;
return (
<AuthenticationPage
onLogin={this.handleLogin}
loginDisabled={inProgress}
loginErrorMessage={this.state.loginError}
logoUrl={config.logo_url} // Deprecated, replaced by `logo.src`
logo={config.logo}
siteUrl={config.site_url}
renderButtonContent={() => (
<React.Fragment>
<LoginButtonIcon type="gitlab" />{' '}
{inProgress ? t('auth.loggingIn') : t('auth.loginWithGitLab')}
</React.Fragment>
)}
t={t}
/>
);
}
}

View File

@@ -0,0 +1,187 @@
import API, { getMaxAccess } from '../API';
global.fetch = jest.fn().mockRejectedValue(new Error('should not call fetch inside tests'));
jest.spyOn(console, 'log').mockImplementation(() => undefined);
describe('GitLab API', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('hasWriteAccess', () => {
test('should return true on project access_level >= 30', async () => {
const api = new API({ repo: 'repo' });
api.requestJSON = jest
.fn()
.mockResolvedValueOnce({ permissions: { project_access: { access_level: 30 } } });
await expect(api.hasWriteAccess()).resolves.toBe(true);
});
test('should return false on project access_level < 30', async () => {
const api = new API({ repo: 'repo' });
api.requestJSON = jest
.fn()
.mockResolvedValueOnce({ permissions: { project_access: { access_level: 10 } } });
await expect(api.hasWriteAccess()).resolves.toBe(false);
});
test('should return true on group access_level >= 30', async () => {
const api = new API({ repo: 'repo' });
api.requestJSON = jest
.fn()
.mockResolvedValueOnce({ permissions: { group_access: { access_level: 30 } } });
await expect(api.hasWriteAccess()).resolves.toBe(true);
});
test('should return false on group access_level < 30', async () => {
const api = new API({ repo: 'repo' });
api.requestJSON = jest
.fn()
.mockResolvedValueOnce({ permissions: { group_access: { access_level: 10 } } });
await expect(api.hasWriteAccess()).resolves.toBe(false);
});
test('should return true on shared group access_level >= 40', async () => {
const api = new API({ repo: 'repo' });
api.requestJSON = jest.fn().mockResolvedValueOnce({
permissions: { project_access: null, group_access: null },
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 40 }],
});
await expect(api.hasWriteAccess()).resolves.toBe(true);
expect(api.requestJSON).toHaveBeenCalledTimes(1);
});
test('should return true on shared group access_level >= 30, developers can merge and push', async () => {
const api = new API({ repo: 'repo' });
api.requestJSON = jest.fn();
api.requestJSON.mockResolvedValueOnce({
permissions: { project_access: null, group_access: null },
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 30 }],
});
api.requestJSON.mockResolvedValueOnce({
developers_can_merge: true,
developers_can_push: true,
});
await expect(api.hasWriteAccess()).resolves.toBe(true);
});
test('should return false on shared group access_level < 30,', async () => {
const api = new API({ repo: 'repo' });
api.requestJSON = jest.fn();
api.requestJSON.mockResolvedValueOnce({
permissions: { project_access: null, group_access: null },
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 20 }],
});
api.requestJSON.mockResolvedValueOnce({
developers_can_merge: true,
developers_can_push: true,
});
await expect(api.hasWriteAccess()).resolves.toBe(false);
});
test("should return false on shared group access_level >= 30, developers can't merge", async () => {
const api = new API({ repo: 'repo' });
api.requestJSON = jest.fn();
api.requestJSON.mockResolvedValueOnce({
permissions: { project_access: null, group_access: null },
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 30 }],
});
api.requestJSON.mockResolvedValueOnce({
developers_can_merge: false,
developers_can_push: true,
});
await expect(api.hasWriteAccess()).resolves.toBe(false);
});
test("should return false on shared group access_level >= 30, developers can't push", async () => {
const api = new API({ repo: 'repo' });
api.requestJSON = jest.fn();
api.requestJSON.mockResolvedValueOnce({
permissions: { project_access: null, group_access: null },
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 30 }],
});
api.requestJSON.mockResolvedValueOnce({
developers_can_merge: true,
developers_can_push: false,
});
await expect(api.hasWriteAccess()).resolves.toBe(false);
});
test('should return false on shared group access_level >= 30, error getting branch', async () => {
const api = new API({ repo: 'repo' });
api.requestJSON = jest.fn();
api.requestJSON.mockResolvedValueOnce({
permissions: { project_access: null, group_access: null },
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 30 }],
});
const error = new Error('Not Found');
api.requestJSON.mockRejectedValue(error);
await expect(api.hasWriteAccess()).resolves.toBe(false);
expect(console.log).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith('Failed getting default branch', error);
});
});
describe('getStatuses', () => {
test('should get preview statuses', async () => {
const api = new API({ repo: 'repo' });
const mr = { sha: 'sha' };
const statuses = [
{ name: 'deploy', status: 'success', target_url: 'deploy-url' },
{ name: 'build', status: 'pending' },
];
api.getBranchMergeRequest = jest.fn(() => Promise.resolve(mr));
api.getMergeRequestStatues = jest.fn(() => Promise.resolve(statuses));
const collectionName = 'posts';
const slug = 'title';
await expect(api.getStatuses(collectionName, slug)).resolves.toEqual([
{ context: 'deploy', state: 'success', target_url: 'deploy-url' },
{ context: 'build', state: 'other' },
]);
expect(api.getBranchMergeRequest).toHaveBeenCalledTimes(1);
expect(api.getBranchMergeRequest).toHaveBeenCalledWith('cms/posts/title');
expect(api.getMergeRequestStatues).toHaveBeenCalledTimes(1);
expect(api.getMergeRequestStatues).toHaveBeenCalledWith(mr, 'cms/posts/title');
});
});
describe('getMaxAccess', () => {
it('should return group with max access level', () => {
const groups = [
{ group_access_level: 10 },
{ group_access_level: 5 },
{ group_access_level: 100 },
{ group_access_level: 1 },
];
expect(getMaxAccess(groups)).toBe(groups[2]);
});
});
});

View File

@@ -0,0 +1,552 @@
jest.mock('decap-cms-core/src/backend');
import { fromJS } from 'immutable';
import { oneLine, stripIndent } from 'common-tags';
import nock from 'nock';
import { Cursor } from 'decap-cms-lib-util';
import Gitlab from '../implementation';
import AuthenticationPage from '../AuthenticationPage';
const { Backend, LocalStorageAuthStore } = jest.requireActual('decap-cms-core/src/backend');
function generateEntries(path, length) {
const entries = Array.from({ length }, (val, idx) => {
const count = idx + 1;
const id = `00${count}`.slice(-3);
const fileName = `test${id}.md`;
return { id, fileName, filePath: `${path}/${fileName}` };
});
return {
tree: entries.map(({ id, fileName, filePath }) => ({
id: `d8345753a1d935fa47a26317a503e73e1192d${id}`,
name: fileName,
type: 'blob',
path: filePath,
mode: '100644',
})),
files: entries.reduce(
(acc, { id, filePath }) => ({
...acc,
[filePath]: stripIndent`
---
title: test ${id}
---
# test ${id}
`,
}),
{},
),
};
}
const manyEntries = generateEntries('many-entries', 500);
const mockRepo = {
tree: {
'/': [
{
id: '5d0620ebdbc92068a3e866866e928cc373f18429',
name: 'content',
type: 'tree',
path: 'content',
mode: '040000',
},
],
content: [
{
id: 'b1a200e48be54fde12b636f9563d659d44c206a5',
name: 'test1.md',
type: 'blob',
path: 'content/test1.md',
mode: '100644',
},
{
id: 'd8345753a1d935fa47a26317a503e73e1192d623',
name: 'test2.md',
type: 'blob',
path: 'content/test2.md',
mode: '100644',
},
],
'many-entries': manyEntries.tree,
},
files: {
'content/test1.md': stripIndent`
---
title: test
---
# test
`,
'content/test2.md': stripIndent`
---
title: test2
---
# test 2
`,
...manyEntries.files,
},
};
const resp = {
user: {
success: {
id: 1,
},
},
branch: {
success: {
name: 'master',
commit: {
id: 1,
},
},
},
project: {
success: {
permissions: {
project_access: {
access_level: 30,
},
},
default_branch: 'main',
},
readOnly: {
permissions: {
project_access: {
access_level: 10,
},
},
},
},
};
describe('gitlab backend', () => {
let authStore;
let backend;
const repo = 'foo/bar';
const defaultConfig = {
backend: {
name: 'gitlab',
repo,
},
};
const collectionContentConfig = {
name: 'foo',
folder: 'content',
fields: [{ name: 'title' }],
// TODO: folder_based_collection is an internal string, we should not
// be depending on it here
type: 'folder_based_collection',
};
const collectionManyEntriesConfig = {
name: 'foo',
folder: 'many-entries',
fields: [{ name: 'title' }],
// TODO: folder_based_collection is an internal string, we should not
// be depending on it here
type: 'folder_based_collection',
};
const collectionFilesConfig = {
name: 'foo',
files: [
{
label: 'foo',
name: 'foo',
file: 'content/test1.md',
fields: [{ name: 'title' }],
},
{
label: 'bar',
name: 'bar',
file: 'content/test2.md',
fields: [{ name: 'title' }],
},
],
type: 'file_based_collection',
};
const mockCredentials = { token: 'MOCK_TOKEN' };
const expectedRepo = encodeURIComponent(repo);
const expectedRepoUrl = `/projects/${expectedRepo}`;
function resolveBackend(config = {}) {
authStore = new LocalStorageAuthStore();
return new Backend(
{
init: (...args) => new Gitlab(...args),
},
{
backendName: 'gitlab',
config,
authStore,
},
);
}
function mockApi(backend) {
return nock(backend.implementation.apiRoot);
}
function interceptAuth(backend, { userResponse, projectResponse } = {}) {
const api = mockApi(backend);
api
.get('/user')
.query(true)
.reply(200, userResponse || resp.user.success);
api
// The `authenticate` method of the API class from netlify-cms-backend-gitlab
// calls the same endpoint twice for gettng a single project.
// First time through `this.api.hasWriteAccess()
// Second time through the method `getDefaultBranchName` from lib-util
// As a result, we need to repeat the same response twice.
// Otherwise, we'll get an error: "No match for request to
// https://gitlab.com/api/v4"
.get(expectedRepoUrl)
.times(2)
.query(true)
.reply(200, projectResponse || resp.project.success);
}
function interceptBranch(backend, { branch = 'master' } = {}) {
const api = mockApi(backend);
api
.get(`${expectedRepoUrl}/repository/branches/${encodeURIComponent(branch)}`)
.query(true)
.reply(200, resp.branch.success);
}
function parseQuery(uri) {
const query = uri.split('?')[1];
if (!query) {
return {};
}
return query.split('&').reduce((acc, q) => {
const [key, value] = q.split('=');
acc[key] = value;
return acc;
}, {});
}
function createHeaders(backend, { basePath, path, page, perPage, pageCount, totalCount }) {
const pageNum = parseInt(page, 10);
const pageCountNum = parseInt(pageCount, 10);
const url = `${backend.implementation.apiRoot}${basePath}`;
function link(linkPage) {
return `<${url}?id=${expectedRepo}&page=${linkPage}&path=${path}&per_page=${perPage}&recursive=false>`;
}
const linkHeader = oneLine`
${link(1)}; rel="first",
${link(pageCount)}; rel="last",
${pageNum === 1 ? '' : `${link(pageNum - 1)}; rel="prev",`}
${pageNum === pageCountNum ? '' : `${link(pageNum + 1)}; rel="next",`}
`.slice(0, -1);
return {
'X-Page': page,
'X-Total-Pages': pageCount,
'X-Per-Page': perPage,
'X-Total': totalCount,
Link: linkHeader,
};
}
function interceptCollection(
backend,
collection,
{ verb = 'get', repeat = 1, page: expectedPage } = {},
) {
const api = mockApi(backend);
const url = `${expectedRepoUrl}/repository/tree`;
const { folder } = collection;
const tree = mockRepo.tree[folder];
api[verb](url)
.query(({ path, page }) => {
if (path !== folder) {
return false;
}
if (expectedPage && page && parseInt(page, 10) !== parseInt(expectedPage, 10)) {
return false;
}
return true;
})
.times(repeat)
.reply(uri => {
const { page = 1, per_page = 20 } = parseQuery(uri);
const pageCount = tree.length <= per_page ? 1 : Math.round(tree.length / per_page);
const pageLastIndex = page * per_page;
const pageFirstIndex = pageLastIndex - per_page;
const resp = tree.slice(pageFirstIndex, pageLastIndex);
return [
200,
verb === 'head' ? null : resp,
createHeaders(backend, {
basePath: url,
path: folder,
page,
perPage: per_page,
pageCount,
totalCount: tree.length,
}),
];
});
}
function interceptFiles(backend, path) {
const api = mockApi(backend);
const url = `${expectedRepoUrl}/repository/files/${encodeURIComponent(path)}/raw`;
api.get(url).query(true).reply(200, mockRepo.files[path]);
api
.get(`${expectedRepoUrl}/repository/commits`)
.query(({ path }) => path === path)
.reply(200, [
{
author_name: 'author_name',
author_email: 'author_email',
authored_date: 'authored_date',
},
]);
}
function sharedSetup() {
beforeEach(async () => {
backend = resolveBackend(defaultConfig);
interceptAuth(backend);
await backend.authenticate(mockCredentials);
interceptCollection(backend, collectionManyEntriesConfig, { verb: 'head' });
interceptCollection(backend, collectionContentConfig, { verb: 'head' });
});
}
it('throws if configuration does not include repo', () => {
expect(() => resolveBackend({ backend: {} })).toThrowErrorMatchingInlineSnapshot(
`"The GitLab backend needs a \\"repo\\" in the backend configuration."`,
);
});
describe('authComponent', () => {
it('returns authentication page component', () => {
backend = resolveBackend(defaultConfig);
expect(backend.authComponent()).toEqual(AuthenticationPage);
});
});
describe('authenticate', () => {
it('throws if user does not have access to project', async () => {
backend = resolveBackend(defaultConfig);
interceptAuth(backend, { projectResponse: resp.project.readOnly });
await expect(
backend.authenticate(mockCredentials),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Your GitLab user account does not have access to this repo."`,
);
});
it('stores and returns user object on success', async () => {
const backendName = defaultConfig.backend.name;
backend = resolveBackend(defaultConfig);
interceptAuth(backend);
const user = await backend.authenticate(mockCredentials);
expect(authStore.retrieve()).toEqual(user);
expect(user).toEqual({ ...resp.user.success, ...mockCredentials, backendName });
});
});
describe('currentUser', () => {
it('returns null if no user', async () => {
backend = resolveBackend(defaultConfig);
const user = await backend.currentUser();
expect(user).toEqual(null);
});
it('returns the stored user if exists', async () => {
const backendName = defaultConfig.backend.name;
backend = resolveBackend(defaultConfig);
interceptAuth(backend);
await backend.authenticate(mockCredentials);
const user = await backend.currentUser();
expect(user).toEqual({ ...resp.user.success, ...mockCredentials, backendName });
});
});
describe('getToken', () => {
it('returns the token for the current user', async () => {
backend = resolveBackend(defaultConfig);
interceptAuth(backend);
await backend.authenticate(mockCredentials);
const token = await backend.getToken();
expect(token).toEqual(mockCredentials.token);
});
});
describe('logout', () => {
it('sets token to null', async () => {
backend = resolveBackend(defaultConfig);
interceptAuth(backend);
await backend.authenticate(mockCredentials);
await backend.logout();
const token = await backend.getToken();
expect(token).toEqual(null);
});
});
describe('getEntry', () => {
sharedSetup();
it('returns an entry from folder collection', async () => {
const entryTree = mockRepo.tree[collectionContentConfig.folder][0];
const slug = entryTree.path.split('/').pop().replace('.md', '');
interceptFiles(backend, entryTree.path);
interceptCollection(backend, collectionContentConfig);
const entry = await backend.getEntry(
{
config: {},
integrations: fromJS([]),
entryDraft: fromJS({}),
mediaLibrary: fromJS({}),
},
fromJS(collectionContentConfig),
slug,
);
expect(entry).toEqual(expect.objectContaining({ path: entryTree.path }));
});
});
describe('listEntries', () => {
sharedSetup();
it('returns entries from folder collection', async () => {
const tree = mockRepo.tree[collectionContentConfig.folder];
tree.forEach(file => interceptFiles(backend, file.path));
interceptCollection(backend, collectionContentConfig);
const entries = await backend.listEntries(fromJS(collectionContentConfig));
expect(entries).toEqual({
cursor: expect.any(Cursor),
pagination: 1,
entries: expect.arrayContaining(
tree.map(file => expect.objectContaining({ path: file.path })),
),
});
expect(entries.entries).toHaveLength(2);
});
it('returns all entries from folder collection', async () => {
const tree = mockRepo.tree[collectionManyEntriesConfig.folder];
interceptBranch(backend);
tree.forEach(file => interceptFiles(backend, file.path));
interceptCollection(backend, collectionManyEntriesConfig, { repeat: 5 });
const entries = await backend.listAllEntries(fromJS(collectionManyEntriesConfig));
expect(entries).toEqual(
expect.arrayContaining(tree.map(file => expect.objectContaining({ path: file.path }))),
);
expect(entries).toHaveLength(500);
}, 7000);
it('returns entries from file collection', async () => {
const { files } = collectionFilesConfig;
files.forEach(file => interceptFiles(backend, file.file));
const entries = await backend.listEntries(fromJS(collectionFilesConfig));
expect(entries).toEqual({
cursor: expect.any(Cursor),
entries: expect.arrayContaining(
files.map(file => expect.objectContaining({ path: file.file })),
),
});
expect(entries.entries).toHaveLength(2);
});
it('returns first page from paginated folder collection tree', async () => {
const tree = mockRepo.tree[collectionManyEntriesConfig.folder];
const pageTree = tree.slice(0, 20);
pageTree.forEach(file => interceptFiles(backend, file.path));
interceptCollection(backend, collectionManyEntriesConfig, { page: 1 });
const entries = await backend.listEntries(fromJS(collectionManyEntriesConfig));
expect(entries.entries).toEqual(
expect.arrayContaining(pageTree.map(file => expect.objectContaining({ path: file.path }))),
);
expect(entries.entries).toHaveLength(20);
});
});
describe('traverseCursor', () => {
sharedSetup();
it('returns complete last page of paginated tree', async () => {
const tree = mockRepo.tree[collectionManyEntriesConfig.folder];
tree.slice(0, 20).forEach(file => interceptFiles(backend, file.path));
interceptCollection(backend, collectionManyEntriesConfig, { page: 1 });
const entries = await backend.listEntries(fromJS(collectionManyEntriesConfig));
const nextPageTree = tree.slice(20, 40);
nextPageTree.forEach(file => interceptFiles(backend, file.path));
interceptCollection(backend, collectionManyEntriesConfig, { page: 2 });
const nextPage = await backend.traverseCursor(entries.cursor, 'next');
expect(nextPage.entries).toEqual(
expect.arrayContaining(
nextPageTree.map(file => expect.objectContaining({ path: file.path })),
),
);
expect(nextPage.entries).toHaveLength(20);
const lastPageTree = tree.slice(-20);
lastPageTree.forEach(file => interceptFiles(backend, file.path));
interceptCollection(backend, collectionManyEntriesConfig, { page: 25 });
const lastPage = await backend.traverseCursor(nextPage.cursor, 'last');
expect(lastPage.entries).toEqual(
expect.arrayContaining(
lastPageTree.map(file => expect.objectContaining({ path: file.path })),
),
);
expect(lastPage.entries).toHaveLength(20);
});
});
describe('filterFile', () => {
it('should return true for nested file with matching depth', () => {
backend = resolveBackend(defaultConfig);
expect(
backend.implementation.filterFile(
'content/posts',
{ name: 'index.md', path: 'content/posts/dir1/dir2/index.md' },
'md',
3,
),
).toBe(true);
});
it('should return false for nested file with non matching depth', () => {
backend = resolveBackend(defaultConfig);
expect(
backend.implementation.filterFile(
'content/posts',
{ name: 'index.md', path: 'content/posts/dir1/dir2/index.md' },
'md',
2,
),
).toBe(false);
});
});
afterEach(() => {
nock.cleanAll();
authStore.logout();
backend = null;
expect(authStore.retrieve()).toEqual(null);
});
});

View File

@@ -0,0 +1,470 @@
import trimStart from 'lodash/trimStart';
import semaphore from 'semaphore';
import trim from 'lodash/trim';
import { stripIndent } from 'common-tags';
import {
CURSOR_COMPATIBILITY_SYMBOL,
basename,
entriesByFolder,
entriesByFiles,
getMediaDisplayURL,
getMediaAsBlob,
unpublishedEntries,
getPreviewStatus,
asyncLock,
runWithLock,
getBlobSHA,
blobToFileObj,
contentKeyFromBranch,
generateContentKey,
localForage,
allEntriesByFolder,
filterByExtension,
branchFromContentKey,
getDefaultBranchName,
} from 'decap-cms-lib-util';
import AuthenticationPage from './AuthenticationPage';
import API, { API_NAME } from './API';
import type {
Entry,
AssetProxy,
PersistOptions,
Cursor,
Implementation,
DisplayURL,
User,
Credentials,
Config,
ImplementationFile,
UnpublishedEntryMediaFile,
AsyncLock,
} from 'decap-cms-lib-util';
import type { Semaphore } from 'semaphore';
const MAX_CONCURRENT_DOWNLOADS = 10;
export default class GitLab implements Implementation {
lock: AsyncLock;
api: API | null;
options: {
proxied: boolean;
API: API | null;
initialWorkflowStatus: string;
};
repo: string;
isBranchConfigured: boolean;
branch: string;
apiRoot: string;
token: string | null;
squashMerges: boolean;
cmsLabelPrefix: string;
mediaFolder: string;
previewContext: string;
useGraphQL: boolean;
graphQLAPIRoot: string;
_mediaDisplayURLSem?: Semaphore;
constructor(config: Config, options = {}) {
this.options = {
proxied: false,
API: null,
initialWorkflowStatus: '',
...options,
};
if (
!this.options.proxied &&
(config.backend.repo === null || config.backend.repo === undefined)
) {
throw new Error('The GitLab backend needs a "repo" in the backend configuration.');
}
this.api = this.options.API || null;
this.repo = config.backend.repo || '';
this.branch = config.backend.branch || 'master';
this.isBranchConfigured = config.backend.branch ? true : false;
this.apiRoot = config.backend.api_root || 'https://gitlab.com/api/v4';
this.token = '';
this.squashMerges = config.backend.squash_merges || false;
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
this.mediaFolder = config.media_folder;
this.previewContext = config.backend.preview_context || '';
this.useGraphQL = config.backend.use_graphql || false;
this.graphQLAPIRoot = config.backend.graphql_api_root || 'https://gitlab.com/api/graphql';
this.lock = asyncLock();
}
isGitBackend() {
return true;
}
async status() {
const auth =
(await this.api
?.user()
.then(user => !!user)
.catch(e => {
console.warn('Failed getting GitLab user', e);
return false;
})) || false;
return { auth: { status: auth }, api: { status: true, statusPage: '' } };
}
authComponent() {
return AuthenticationPage;
}
restoreUser(user: User) {
return this.authenticate(user);
}
async authenticate(state: Credentials) {
this.token = state.token as string;
this.api = new API({
token: this.token,
branch: this.branch,
repo: this.repo,
apiRoot: this.apiRoot,
squashMerges: this.squashMerges,
cmsLabelPrefix: this.cmsLabelPrefix,
initialWorkflowStatus: this.options.initialWorkflowStatus,
useGraphQL: this.useGraphQL,
graphQLAPIRoot: this.graphQLAPIRoot,
});
const user = await this.api.user();
const isCollab = await this.api.hasWriteAccess().catch((error: Error) => {
error.message = stripIndent`
Repo "${this.repo}" not found.
Please ensure the repo information is spelled correctly.
If the repo is private, make sure you're logged into a GitLab account with access.
`;
throw error;
});
// Unauthorized user
if (!isCollab) {
throw new Error('Your GitLab user account does not have access to this repo.');
}
if (!this.isBranchConfigured) {
const defaultBranchName = await getDefaultBranchName({
backend: 'gitlab',
repo: this.repo,
token: this.token,
apiRoot: this.apiRoot,
});
if (defaultBranchName) {
this.branch = defaultBranchName;
}
}
// Authorized user
return { ...user, login: user.username, token: state.token as string };
}
async logout() {
this.token = null;
return;
}
getToken() {
return Promise.resolve(this.token);
}
filterFile(
folder: string,
file: { path: string; name: string },
extension: string,
depth: number,
) {
// gitlab paths include the root folder
const fileFolder = trim(file.path.split(folder)[1] || '/', '/');
return filterByExtension(file, extension) && fileFolder.split('/').length <= depth;
}
async entriesByFolder(folder: string, extension: string, depth: number) {
let cursor: Cursor;
const listFiles = () =>
this.api!.listFiles(folder, depth > 1).then(({ files, cursor: c }) => {
cursor = c.mergeMeta({ folder, extension, depth });
return files.filter(file => this.filterFile(folder, file, extension, depth));
});
const files = await entriesByFolder(
listFiles,
this.api!.readFile.bind(this.api!),
this.api!.readFileMetadata.bind(this.api),
API_NAME,
);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
files[CURSOR_COMPATIBILITY_SYMBOL] = cursor;
return files;
}
async listAllFiles(folder: string, extension: string, depth: number) {
const files = await this.api!.listAllFiles(folder, depth > 1);
const filtered = files.filter(file => this.filterFile(folder, file, extension, depth));
return filtered;
}
async allEntriesByFolder(folder: string, extension: string, depth: number) {
const files = await allEntriesByFolder({
listAllFiles: () => this.listAllFiles(folder, extension, depth),
readFile: this.api!.readFile.bind(this.api!),
readFileMetadata: this.api!.readFileMetadata.bind(this.api),
apiName: API_NAME,
branch: this.branch,
localForage,
folder,
extension,
depth,
getDefaultBranch: () =>
this.api!.getDefaultBranch().then(b => ({ name: b.name, sha: b.commit.id })),
isShaExistsInBranch: this.api!.isShaExistsInBranch.bind(this.api!),
getDifferences: (to, from) => this.api!.getDifferences(to, from),
getFileId: path => this.api!.getFileId(path, this.branch),
filterFile: file => this.filterFile(folder, file, extension, depth),
customFetch: this.useGraphQL ? files => this.api!.readFilesGraphQL(files) : undefined,
});
return files;
}
entriesByFiles(files: ImplementationFile[]) {
return entriesByFiles(
files,
this.api!.readFile.bind(this.api!),
this.api!.readFileMetadata.bind(this.api),
API_NAME,
);
}
// Fetches a single entry.
getEntry(path: string) {
return this.api!.readFile(path).then(data => ({
file: { path, id: null },
data: data as string,
}));
}
getMedia(mediaFolder = this.mediaFolder) {
return this.api!.listAllFiles(mediaFolder).then(files =>
files.map(({ id, name, path }) => {
return { id, name, path, displayURL: { id, name, path } };
}),
);
}
getMediaDisplayURL(displayURL: DisplayURL) {
this._mediaDisplayURLSem = this._mediaDisplayURLSem || semaphore(MAX_CONCURRENT_DOWNLOADS);
return getMediaDisplayURL(
displayURL,
this.api!.readFile.bind(this.api!),
this._mediaDisplayURLSem,
);
}
async getMediaFile(path: string) {
const name = basename(path);
const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!));
const fileObj = blobToFileObj(name, blob);
const url = URL.createObjectURL(fileObj);
const id = await getBlobSHA(blob);
return {
id,
displayURL: url,
path,
name,
size: fileObj.size,
file: fileObj,
url,
};
}
async persistEntry(entry: Entry, options: PersistOptions) {
// persistEntry is a transactional operation
return runWithLock(
this.lock,
() => this.api!.persistFiles(entry.dataFiles, entry.assets, options),
'Failed to acquire persist entry lock',
);
}
async persistMedia(mediaFile: AssetProxy, options: PersistOptions) {
const fileObj = mediaFile.fileObj as File;
const [id] = await Promise.all([
getBlobSHA(fileObj),
this.api!.persistFiles([], [mediaFile], options),
]);
const { path } = mediaFile;
const url = URL.createObjectURL(fileObj);
return {
displayURL: url,
path: trimStart(path, '/'),
name: fileObj!.name,
size: fileObj!.size,
file: fileObj,
url,
id,
};
}
deleteFiles(paths: string[], commitMessage: string) {
return this.api!.deleteFiles(paths, commitMessage);
}
traverseCursor(cursor: Cursor, action: string) {
return this.api!.traverseCursor(cursor, action).then(async ({ entries, cursor: newCursor }) => {
const [folder, depth, extension] = [
cursor.meta?.get('folder') as string,
cursor.meta?.get('depth') as number,
cursor.meta?.get('extension') as string,
];
if (folder && depth && extension) {
entries = entries.filter(f => this.filterFile(folder, f, extension, depth));
newCursor = newCursor.mergeMeta({ folder, extension, depth });
}
const entriesWithData = await entriesByFiles(
entries,
this.api!.readFile.bind(this.api!),
this.api!.readFileMetadata.bind(this.api)!,
API_NAME,
);
return {
entries: entriesWithData,
cursor: newCursor,
};
});
}
loadMediaFile(branch: string, file: UnpublishedEntryMediaFile) {
const readFile = (
path: string,
id: string | null | undefined,
{ parseText }: { parseText: boolean },
) => this.api!.readFile(path, id, { branch, parseText });
return getMediaAsBlob(file.path, null, readFile).then(blob => {
const name = basename(file.path);
const fileObj = blobToFileObj(name, blob);
return {
id: file.path,
displayURL: URL.createObjectURL(fileObj),
path: file.path,
name,
size: fileObj.size,
file: fileObj,
};
});
}
async loadEntryMediaFiles(branch: string, files: UnpublishedEntryMediaFile[]) {
const mediaFiles = await Promise.all(files.map(file => this.loadMediaFile(branch, file)));
return mediaFiles;
}
async unpublishedEntries() {
const listEntriesKeys = () =>
this.api!.listUnpublishedBranches().then(branches =>
branches.map(branch => contentKeyFromBranch(branch)),
);
const ids = await unpublishedEntries(listEntriesKeys);
return ids;
}
async unpublishedEntry({
id,
collection,
slug,
}: {
id?: string;
collection?: string;
slug?: string;
}) {
if (id) {
const data = await this.api!.retrieveUnpublishedEntryData(id);
return data;
} else if (collection && slug) {
const entryId = generateContentKey(collection, slug);
const data = await this.api!.retrieveUnpublishedEntryData(entryId);
return data;
} else {
throw new Error('Missing unpublished entry id or collection and slug');
}
}
getBranch(collection: string, slug: string) {
const contentKey = generateContentKey(collection, slug);
const branch = branchFromContentKey(contentKey);
return branch;
}
async unpublishedEntryDataFile(collection: string, slug: string, path: string, id: string) {
const branch = this.getBranch(collection, slug);
const data = (await this.api!.readFile(path, id, { branch })) as string;
return data;
}
async unpublishedEntryMediaFile(collection: string, slug: string, path: string, id: string) {
const branch = this.getBranch(collection, slug);
const mediaFile = await this.loadMediaFile(branch, { path, id });
return mediaFile;
}
async updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
// updateUnpublishedEntryStatus is a transactional operation
return runWithLock(
this.lock,
() => this.api!.updateUnpublishedEntryStatus(collection, slug, newStatus),
'Failed to acquire update entry status lock',
);
}
async deleteUnpublishedEntry(collection: string, slug: string) {
// deleteUnpublishedEntry is a transactional operation
return runWithLock(
this.lock,
() => this.api!.deleteUnpublishedEntry(collection, slug),
'Failed to acquire delete entry lock',
);
}
async publishUnpublishedEntry(collection: string, slug: string) {
// publishUnpublishedEntry is a transactional operation
return runWithLock(
this.lock,
() => this.api!.publishUnpublishedEntry(collection, slug),
'Failed to acquire publish entry lock',
);
}
async getDeployPreview(collection: string, slug: string) {
try {
const statuses = await this.api!.getStatuses(collection, slug);
const deployStatus = getPreviewStatus(statuses, this.previewContext);
if (deployStatus) {
const { target_url: url, state } = deployStatus;
return { url, status: state };
} else {
return null;
}
} catch (e) {
return null;
}
}
}

View File

@@ -0,0 +1,10 @@
import GitLabBackend from './implementation';
import API from './API';
import AuthenticationPage from './AuthenticationPage';
export const DecapCmsBackendGitlab = {
GitLabBackend,
API,
AuthenticationPage,
};
export { GitLabBackend, API, AuthenticationPage };

View File

@@ -0,0 +1,73 @@
import { gql } from 'graphql-tag';
import { oneLine } from 'common-tags';
export const files = gql`
query files($repo: ID!, $branch: String!, $path: String!, $recursive: Boolean!, $cursor: String) {
project(fullPath: $repo) {
repository {
tree(ref: $branch, path: $path, recursive: $recursive) {
blobs(after: $cursor) {
nodes {
type
id: sha
path
name
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
}
}
`;
export const blobs = gql`
query blobs($repo: ID!, $branch: String!, $paths: [String!]!) {
project(fullPath: $repo) {
repository {
blobs(ref: $branch, paths: $paths) {
nodes {
id
data: rawBlob
}
}
}
}
}
`;
export function lastCommits(paths: string[]) {
const tree = paths
.map(
(path, index) => oneLine`
tree${index}: tree(ref: $branch, path: "${path}") {
lastCommit {
authorName
authoredDate
author {
id
username
name
publicEmail
}
}
}
`,
)
.join('\n');
const query = gql`
query lastCommits($repo: ID!, $branch: String!) {
project(fullPath: $repo) {
repository {
${tree}
}
}
}
`;
return query;
}

View File

@@ -0,0 +1,3 @@
const { getConfig } = require('../../scripts/webpack.js');
module.exports = getConfig();