add-cms
This commit is contained in:
555
source/admin/packages/decap-cms-backend-github/CHANGELOG.md
Normal file
555
source/admin/packages/decap-cms-backend-github/CHANGELOG.md
Normal file
@@ -0,0 +1,555 @@
|
||||
# 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-github@3.3.1...decap-cms-backend-github@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-github@3.3.0...decap-cms-backend-github@3.3.1) (2025-07-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [3.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.2.2...decap-cms-backend-github@3.3.0) (2025-06-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [3.2.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.2.1...decap-cms-backend-github@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-github@3.2.0...decap-cms-backend-github@3.2.1) (2024-08-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [3.2.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.1.2...decap-cms-backend-github@3.2.0) (2024-08-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fetch GitHub PR author name, fixes [#7232](https://github.com/decaporg/decap-cms/issues/7232) ([#7253](https://github.com/decaporg/decap-cms/issues/7253)) ([0e5335d](https://github.com/decaporg/decap-cms/commit/0e5335daba1b67816b4a0c24d1a2d9a185e3b54f))
|
||||
|
||||
## [3.1.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.1.1...decap-cms-backend-github@3.1.2) (2024-04-03)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [3.1.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.1.0-beta.2...decap-cms-backend-github@3.1.1) (2024-03-21)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [3.1.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.1.0-beta.2...decap-cms-backend-github@3.1.0) (2024-02-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [3.1.0-beta.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.1.0-beta.1...decap-cms-backend-github@3.1.0-beta.2) (2024-01-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [3.1.0-beta.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.1.0-beta.0...decap-cms-backend-github@3.1.0-beta.1) (2023-11-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- used merge-upstream to sync fork & upstream ([#6504](https://github.com/decaporg/decap-cms/issues/6504)) ([931399d](https://github.com/decaporg/decap-cms/commit/931399dd6eb675e06d59ac57ecfefc1b82467271))
|
||||
|
||||
# [3.1.0-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.1.0...decap-cms-backend-github@3.1.0-beta.0) (2023-10-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([b89fc89](https://github.com/decaporg/decap-cms/commit/b89fc894dfbb5f4136b2e5427fd25a29378a58c6))
|
||||
|
||||
## [3.0.3](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.0.2...decap-cms-backend-github@3.0.3) (2023-10-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [3.0.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.0.1...decap-cms-backend-github@3.0.2) (2023-09-06)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
- filter by path when loading collection from github backend ([#6898](https://github.com/decaporg/decap-cms/issues/6898)) ([18ef773](https://github.com/decaporg/decap-cms/commit/18ef773f35db1b7ef3ab5a0f25527d87745b9c73))
|
||||
|
||||
## [3.0.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.0.0...decap-cms-backend-github@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-github@2.15.0...decap-cms-backend-github@3.0.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [2.15.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@2.15.0-beta.0...decap-cms-backend-github@2.15.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# 2.15.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.14.2-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@2.14.1...decap-cms-backend-github@2.14.2-beta.0) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.14.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@2.14.0...decap-cms-backend-github@2.14.1) (2022-04-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [2.14.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@2.13.5...decap-cms-backend-github@2.14.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.13.5](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@2.13.4...decap-cms-backend-github@2.13.5) (2021-10-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- remove "Don't fork the repo"-Button - fixes [#5723](https://github.com/decaporg/decap-cms/issues/5723) ([#5872](https://github.com/decaporg/decap-cms/issues/5872)) ([05d8923](https://github.com/decaporg/decap-cms/commit/05d89230dca315ddcc734b1dc6223df1d8dc1ede))
|
||||
|
||||
## [2.13.4](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@2.13.3...decap-cms-backend-github@2.13.4) (2021-07-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add updated_at to graphql query ([#5611](https://github.com/decaporg/decap-cms/issues/5611)) ([8989550](https://github.com/decaporg/decap-cms/commit/89895508b2ccc8f07019abb6bc2d0162c0d86266))
|
||||
|
||||
## [2.13.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.13.2...decap-cms-backend-github@2.13.3) (2021-06-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.13.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.13.1...decap-cms-backend-github@2.13.2) (2021-05-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.13.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.13.0...decap-cms-backend-github@2.13.1) (2021-05-19)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [2.13.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.12.0...decap-cms-backend-github@2.13.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-github/issues/5316)) ([9e42380](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/9e423805707321396eec137f5b732a5b07a0dd3f))
|
||||
|
||||
# [2.12.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.11.9...decap-cms-backend-github@2.12.0) (2021-04-04)
|
||||
|
||||
### Features
|
||||
|
||||
- **open-authoring:** add alwaysFork option ([#5204](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/5204)) ([7b19e30](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/7b19e30dd2a310dbc20ccb6fcca45d5cbde1014b))
|
||||
|
||||
## [2.11.9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.11.8...decap-cms-backend-github@2.11.9) (2021-02-23)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.11.8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.11.7...decap-cms-backend-github@2.11.8) (2021-02-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.11.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.11.6...decap-cms-backend-github@2.11.7) (2020-12-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **large-media:** mark pointer files as binary ([#4678](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/4678)) ([7697b90](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/7697b907d7bae750f4ec041a184188aa46995320))
|
||||
|
||||
## [2.11.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.11.5...decap-cms-backend-github@2.11.6) (2020-09-20)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.11.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.11.4...decap-cms-backend-github@2.11.5) (2020-09-15)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## 2.11.4 (2020-09-08)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([828bb16](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/828bb16415b8c22a34caa19c50c38b24ffe9ceae))
|
||||
|
||||
## 2.11.3 (2020-08-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([8262487](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/82624879ccbcb16610090041db28f00714d924c8))
|
||||
|
||||
## 2.11.2 (2020-07-27)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([118d50a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/118d50a7a70295f25073e564b5161aa2b9883056))
|
||||
|
||||
## [2.11.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.11.0...decap-cms-backend-github@2.11.1) (2020-07-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** use workflow branch when listing files to move ([#4019](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/4019)) ([8720a42](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/8720a4233db16d91d6b86ee8653d05f8953cb430))
|
||||
|
||||
# [2.11.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.10.6...decap-cms-backend-github@2.11.0) (2020-06-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- handle token expiry ([#3847](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3847)) ([285c940](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/285c940562548d7bc88de244123ba87ff66fba65))
|
||||
|
||||
### Features
|
||||
|
||||
- add backend status down indicator ([#3889](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3889)) ([a50edc7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/a50edc70553ad6afa1acee6a51996ad226443f8c))
|
||||
- **backend-gitgateway:** improve deploy preview visibility ([#3882](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3882)) ([afc9bf4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/afc9bf4f3fe14ccb60851fc24e68922a6e4a85a9))
|
||||
|
||||
## [2.10.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.10.5...decap-cms-backend-github@2.10.6) (2020-05-19)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.10.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.10.4...decap-cms-backend-github@2.10.5) (2020-04-21)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.10.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.10.3...decap-cms-backend-github@2.10.4) (2020-04-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** add fallback for diff errors/warnings ([#3558](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3558)) ([1705c79](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/1705c79a9297d844d5421d685a7785e1e210e39e))
|
||||
|
||||
## [2.10.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.10.2...decap-cms-backend-github@2.10.3) (2020-04-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **open-authoring:** properly delete open authoring branches ([#3512](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3512)) ([cc89aa5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/cc89aa5c430a6bee51483cda91d0f92e7437f29e))
|
||||
|
||||
## [2.10.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.10.1...decap-cms-backend-github@2.10.2) (2020-04-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **open-authoring:** prevent workflow view from breaking on entry error ([#3508](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3508)) ([cbb3927](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/cbb39271012fc3beecfdf180e573e343ee48fe26))
|
||||
|
||||
## [2.10.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.10.0...decap-cms-backend-github@2.10.1) (2020-03-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- missing workflow timestamp ([#3445](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3445)) ([9616cdb](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/9616cdb8bb0a564771e5755bcd3718a07f2e2072))
|
||||
|
||||
# [2.10.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.9.3...decap-cms-backend-github@2.10.0) (2020-03-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** don't create new commits on empty diff when rebasing ([#3411](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3411)) ([70de9f6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/70de9f6b4b89dd8e23205929033745572562e8fc))
|
||||
- update repo owner from GitHub API to match casing ([#3410](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3410)) ([c2e7a24](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/c2e7a24dc20dfea5b1289c5705095d2cf8b04c54))
|
||||
|
||||
### Features
|
||||
|
||||
- **backend-github:** add pagination ([#3379](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3379)) ([39f1307](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/39f1307e3a36447da8c9b3ca79b1d7db52ea1a19))
|
||||
|
||||
## [2.9.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.9.2...decap-cms-backend-github@2.9.3) (2020-03-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **locale:** Remove hard coded string literals ([#3333](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3333)) ([7c45a3c](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/7c45a3cda983be427864a56e58791565eb9232e2))
|
||||
- **open-authoring:** use origin repo when calling compare API ([#3363](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3363)) ([e40b81a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/e40b81a5647d45487d6ddf17245beddd354e0f39))
|
||||
|
||||
## [2.9.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.9.1...decap-cms-backend-github@2.9.2) (2020-02-27)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.9.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.9.0...decap-cms-backend-github@2.9.1) (2020-02-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** fail workflow migrations gracefully ([#3325](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3325)) ([83e0383](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/83e0383b690fb452ea40cb165a56f65a695dc83c))
|
||||
|
||||
# [2.9.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.8.1...decap-cms-backend-github@2.9.0) (2020-02-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** improve workflow migration edge cases/messaging ([#3319](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3319)) ([684b79e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/684b79e43bebb63ce1e844eae5c8c0e76087687b))
|
||||
|
||||
### Features
|
||||
|
||||
- **core:** align GitHub metadata handling with other backends ([#3316](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3316)) ([7e0a8ad](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/7e0a8ad532012576dc5e40bd4e9d54522e307123)), closes [#3292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3292)
|
||||
|
||||
## [2.8.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.8.0...decap-cms-backend-github@2.8.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-github/commit/5bdd3df9ccbb5149c22d79987ebdcd6cab4b261f)), closes [#3292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3292)
|
||||
|
||||
# [2.8.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.7.1...decap-cms-backend-github@2.8.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-github/issues/3292)) ([8193b5a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/8193b5ace89d6f14a6c756235a50b186a763b6b1))
|
||||
|
||||
## [2.7.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.7.0...decap-cms-backend-github@2.7.1) (2020-02-17)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [2.7.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.6.6...decap-cms-backend-github@2.7.0) (2020-02-10)
|
||||
|
||||
### Features
|
||||
|
||||
- field based media/public folders ([#3208](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3208)) ([97bc0c8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/97bc0c8dc489e736f89d748ba832d78400fe4332))
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([a015d1d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/a015d1d92a4b1c0130c44fcef1c9ecdb157a0f07))
|
||||
|
||||
## [2.6.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.6.5...decap-cms-backend-github@2.6.6) (2020-02-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **locale:** remove hard coded strings ([#3193](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3193)) ([fc91bf8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/fc91bf8781e65ce1dc946363dbb10419a145c66b))
|
||||
|
||||
## [2.6.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.6.4...decap-cms-backend-github@2.6.5) (2020-01-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-git-gateway:** re-write GitHub pagination links ([#3135](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3135)) ([834f6b9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/834f6b9e457f3738ce0f240ddd4cc160aff9e2f5))
|
||||
|
||||
## [2.6.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.6.3...decap-cms-backend-github@2.6.4) (2020-01-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github-graphql:** handle trailing paths in collection folder ([#3099](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3099)) ([bc80804](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/bc808040661d345e65d49d64693cd6da3b6816fb))
|
||||
|
||||
## [2.6.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.6.2...decap-cms-backend-github@2.6.3) (2020-01-14)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.6.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.6.1...decap-cms-backend-github@2.6.2) (2020-01-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github-graphql:** return empty array on non existent folder ([#3079](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3079)) ([69b130a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/69b130a3f239590f828f0e4f6f6c0a872b17548b))
|
||||
|
||||
## [2.6.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.6.0...decap-cms-backend-github@2.6.1) (2020-01-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- trim '/' from folder ([#3052](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3052)) ([4b6c8de](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/4b6c8de6b2e3de28f0989b9a012cb302d4de4358))
|
||||
|
||||
# [2.6.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.6.0-beta.0...decap-cms-backend-github@2.6.0) (2020-01-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- rebase open authoring branches ([#2975](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2975)) ([8c175f6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/8c175f6132fa18a13763cc563f7d3201c1e3580e))
|
||||
|
||||
# [2.6.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0...decap-cms-backend-github@2.6.0-beta.0) (2019-12-18)
|
||||
|
||||
### Features
|
||||
|
||||
- bundle assets with content ([#2958](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2958)) ([2b41d8a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/2b41d8a838a9c8a6b21cde2ddd16b9288334e298))
|
||||
|
||||
# [2.5.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.8...decap-cms-backend-github@2.5.0) (2019-12-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [2.5.0-beta.8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.7...decap-cms-backend-github@2.5.0-beta.8) (2019-12-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- don't fail on deleting non existent branch ([1e77d4b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/1e77d4b7688de795ab1b01c6ce2483a0383bbfb6))
|
||||
|
||||
# [2.5.0-beta.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.6...decap-cms-backend-github@2.5.0-beta.7) (2019-12-02)
|
||||
|
||||
### Features
|
||||
|
||||
- content in sub folders ([#2897](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2897)) ([afcfe5b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/afcfe5b6d5f32669e9061ec596bd35ad545d61a3))
|
||||
|
||||
# [2.5.0-beta.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.5...decap-cms-backend-github@2.5.0-beta.6) (2019-11-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** prepend collection name ([#2878](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2878)) ([465f463](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/465f4639597f258d5aa2c1b65e9d2c16023ee7ae))
|
||||
|
||||
### Features
|
||||
|
||||
- workflow unpublished entry ([#2914](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2914)) ([41bb9aa](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/41bb9aac0dd6fd9f8ff157bb0b29c85aa87fe04d))
|
||||
|
||||
# [2.5.0-beta.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.4...decap-cms-backend-github@2.5.0-beta.5) (2019-11-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** editorial workflow commits ([#2867](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2867)) ([86adca3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/86adca3a18f25ab74d1c6702bafab250f005ceec))
|
||||
- make forkExists name matching case-insensitive ([#2869](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2869)) ([9978769](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/9978769ece9262265d3efa77357f9e8b46ad9a1e))
|
||||
- **backend-github:** loaded entries limit ([#2873](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2873)) ([68a8c8a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/68a8c8a693646ebd33fae791aaaec47b050e0186))
|
||||
- **git-gateway:** unpublished entries not loaded for git-gateway(GitHub) ([#2856](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2856)) ([4a2328b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/4a2328b2f10ea678184391e4caf235b41323cd3e))
|
||||
|
||||
### Features
|
||||
|
||||
- commit media with post ([#2851](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2851)) ([6515dee](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/6515dee8715d8571ea19484a7dfab7cfd0cc40be))
|
||||
|
||||
# [2.5.0-beta.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.3...decap-cms-backend-github@2.5.0-beta.4) (2019-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **github-backend:** load media URLs via API ([#2817](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2817)) ([eaeaf44](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/eaeaf4483287a1f724ee60ef321ff749f1c20acf))
|
||||
- change default open authoring scope, make it configurable ([#2821](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2821)) ([002cdd7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/002cdd77a856bde3672e75dde6d3a2b246e1035f))
|
||||
- display UI to fork a repo only when fork doesn't exist ([#2802](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2802)) ([7f90d0e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/7f90d0e065315b9073d21fd733f42f3838ecfe09))
|
||||
|
||||
### Features
|
||||
|
||||
- add go back to site button ([#2538](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2538)) ([f206e7e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/f206e7e5a13fb48ec6b27dce0dbb3a59b61de8f9))
|
||||
|
||||
# [2.5.0-beta.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.2...decap-cms-backend-github@2.5.0-beta.3) (2019-09-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** update Open Authoring branches with no PR ([#2618](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2618)) ([6817033](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/6817033))
|
||||
- **git-gateway:** pass api URL instead of constructing it from repo value ([#2631](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2631)) ([922c0f3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/922c0f3))
|
||||
- **github-backend:** handle race condition in editorial workflow ([#2658](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2658)) ([97f1f84](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/97f1f84))
|
||||
|
||||
# [2.5.0-beta.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.1...decap-cms-backend-github@2.5.0-beta.2) (2019-09-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **github-graphql:** use getMediaDisplayURL to load media with auth header ([#2652](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2652)) ([e674e43](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/e674e43))
|
||||
|
||||
### Features
|
||||
|
||||
- **backend-github:** GitHub GraphQL API support ([#2456](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2456)) ([ece136c](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/ece136c))
|
||||
|
||||
# [2.5.0-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.0...decap-cms-backend-github@2.5.0-beta.1) (2019-08-24)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [2.5.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.4.2...decap-cms-backend-github@2.5.0-beta.0) (2019-07-24)
|
||||
|
||||
### Features
|
||||
|
||||
- **backend-github:** Open Authoring ([#2430](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2430)) ([edf0a3a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/edf0a3a))
|
||||
|
||||
## [2.4.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.4.2-beta.0...decap-cms-backend-github@2.4.2) (2019-04-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.4.2-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.4.1...decap-cms-backend-github@2.4.2-beta.0) (2019-04-05)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.4.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.4.1-beta.1...decap-cms-backend-github@2.4.1) (2019-03-29)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.4.1-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.4.1-beta.0...decap-cms-backend-github@2.4.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-github/issues/2244)) ([6ffd13b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/6ffd13b))
|
||||
|
||||
## [2.4.1-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.4.0...decap-cms-backend-github@2.4.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-github/issues/2234)) ([7987091](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/7987091))
|
||||
|
||||
# [2.4.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.3.0...decap-cms-backend-github@2.4.0) (2019-03-22)
|
||||
|
||||
### Features
|
||||
|
||||
- add ES module builds ([#2215](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2215)) ([d142b32](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/d142b32))
|
||||
|
||||
# [2.3.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.3.0-beta.0...decap-cms-backend-github@2.3.0) (2019-03-22)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [2.3.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.2.3-beta.0...decap-cms-backend-github@2.3.0-beta.0) (2019-03-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix umd builds ([#2214](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2214)) ([e04f6be](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/e04f6be))
|
||||
|
||||
### Features
|
||||
|
||||
- provide usable UMD builds for all packages ([#2141](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2141)) ([82cc794](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/82cc794))
|
||||
|
||||
## [2.2.3-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.2.2...decap-cms-backend-github@2.2.3-beta.0) (2019-03-15)
|
||||
|
||||
### Features
|
||||
|
||||
- upgrade to Emotion 10 ([#2166](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2166)) ([ccef446](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/ccef446))
|
||||
|
||||
## [2.2.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.2.1...decap-cms-backend-github@2.2.2) (2019-03-08)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.2.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.2.0...decap-cms-backend-github@2.2.1) (2019-02-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [2.2.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.1.0...decap-cms-backend-github@2.2.0) (2019-02-08)
|
||||
|
||||
### Features
|
||||
|
||||
- **workflow:** add deploy preview links ([#2028](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2028)) ([15d221d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/15d221d))
|
||||
|
||||
# [2.1.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.9...decap-cms-backend-github@2.1.0) (2018-11-12)
|
||||
|
||||
### Features
|
||||
|
||||
- allow custom logo on auth page ([#1818](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/1818)) ([c6ae1e8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/c6ae1e8))
|
||||
|
||||
<a name="2.0.9"></a>
|
||||
|
||||
## [2.0.9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.8...decap-cms-backend-github@2.0.9) (2018-09-17)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
<a name="2.0.8"></a>
|
||||
|
||||
## [2.0.8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.7...decap-cms-backend-github@2.0.8) (2018-09-06)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
<a name="2.0.7"></a>
|
||||
|
||||
## [2.0.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.6...decap-cms-backend-github@2.0.7) (2018-08-27)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
<a name="2.0.6"></a>
|
||||
|
||||
## [2.0.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.5...decap-cms-backend-github@2.0.6) (2018-08-24)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
<a name="2.0.5"></a>
|
||||
|
||||
## [2.0.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.4...decap-cms-backend-github@2.0.5) (2018-08-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backends:** fix commit message handling ([#1568](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/1568)) ([f7e7120](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/f7e7120))
|
||||
|
||||
<a name="2.0.4"></a>
|
||||
|
||||
## [2.0.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.3...decap-cms-backend-github@2.0.4) (2018-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **workflow:** enable workflow per method ([#1569](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/1569)) ([90b8156](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/90b8156))
|
||||
|
||||
<a name="2.0.3"></a>
|
||||
|
||||
## [2.0.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.2...decap-cms-backend-github@2.0.3) (2018-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **github:** fix image uploading ([#1561](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/1561)) ([ddc8f04](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/ddc8f04))
|
||||
- **workflow:** fix status not set on new workflow entries ([#1558](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/1558)) ([0aa085f](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/0aa085f))
|
||||
|
||||
<a name="2.0.2"></a>
|
||||
|
||||
## [2.0.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.1...decap-cms-backend-github@2.0.2) (2018-07-28)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
<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-github
|
||||
17
source/admin/packages/decap-cms-backend-github/README.md
Normal file
17
source/admin/packages/decap-cms-backend-github/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# GitHub backend
|
||||
|
||||
An abstraction layer between the CMS and [GitHub](https://docs.github.com/en/rest)
|
||||
|
||||
## 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`.
|
||||
|
||||
`Api` - A wrapper for GitHub REST API.
|
||||
|
||||
`GraphQLApi` - `Api` with `ApolloClient`. [Api docs](https://docs.github.com/en/graphql) and [netlify docs](https://www.decapcms.org/docs/beta-features/#github-graphql-api).
|
||||
|
||||
`AuthenticationPage` - uses [lib-auth](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-lib-auth/README.md) to facilitate OAuth and implicit authentication.
|
||||
|
||||
`scripts` - use `createFragmentTypes.js` to create GitHub GraphQL API fragment types.
|
||||
|
||||
Look at tests or types for more info.
|
||||
47
source/admin/packages/decap-cms-backend-github/package.json
Normal file
47
source/admin/packages/decap-cms-backend-github/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "decap-cms-backend-github",
|
||||
"description": "GitHub backend for Decap CMS",
|
||||
"version": "3.4.0",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github",
|
||||
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
||||
"module": "dist/esm/index.js",
|
||||
"main": "dist/decap-cms-backend-github.js",
|
||||
"keywords": [
|
||||
"decap-cms",
|
||||
"backend",
|
||||
"github"
|
||||
],
|
||||
"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\"",
|
||||
"createFragmentTypes": "node scripts/createFragmentTypes.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"apollo-cache-inmemory": "^1.6.2",
|
||||
"apollo-client": "^2.6.3",
|
||||
"apollo-link-context": "^1.0.18",
|
||||
"apollo-link-http": "^1.5.15",
|
||||
"common-tags": "^1.8.0",
|
||||
"graphql": "^15.0.0",
|
||||
"graphql-tag": "^2.10.1",
|
||||
"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",
|
||||
"lodash": "^4.17.11",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^19.1.0"
|
||||
},
|
||||
"browser": {
|
||||
"path": "path-browserify"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
const fetch = require('node-fetch');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const API_HOST = process.env.GITHUB_HOST || 'https://api.github.com';
|
||||
const API_TOKEN = process.env.GITHUB_API_TOKEN;
|
||||
|
||||
if (!API_TOKEN) {
|
||||
throw new Error('Missing environment variable GITHUB_API_TOKEN');
|
||||
}
|
||||
|
||||
fetch(`${API_HOST}/graphql`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `bearer ${API_TOKEN}` },
|
||||
body: JSON.stringify({
|
||||
variables: {},
|
||||
query: `
|
||||
{
|
||||
__schema {
|
||||
types {
|
||||
kind
|
||||
name
|
||||
possibleTypes {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
})
|
||||
.then(result => result.json())
|
||||
.then(result => {
|
||||
// here we're filtering out any type information unrelated to unions or interfaces
|
||||
const filteredData = result.data.__schema.types.filter(type => type.possibleTypes !== null);
|
||||
result.data.__schema.types = filteredData;
|
||||
fs.writeFile(
|
||||
path.join(__dirname, '..', 'src', 'fragmentTypes.js'),
|
||||
`module.exports = ${JSON.stringify(result.data)}`,
|
||||
err => {
|
||||
if (err) {
|
||||
console.error('Error writing fragmentTypes file', err);
|
||||
} else {
|
||||
console.log('Fragment types successfully extracted!');
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
1477
source/admin/packages/decap-cms-backend-github/src/API.ts
Normal file
1477
source/admin/packages/decap-cms-backend-github/src/API.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,162 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from '@emotion/styled';
|
||||
import { NetlifyAuthenticator } from 'decap-cms-lib-auth';
|
||||
import { AuthenticationPage, Icon } from 'decap-cms-ui-default';
|
||||
|
||||
const LoginButtonIcon = styled(Icon)`
|
||||
margin-right: 18px;
|
||||
`;
|
||||
|
||||
const ForkApprovalContainer = styled.div`
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: space-around;
|
||||
flex-grow: 0.2;
|
||||
`;
|
||||
const ForkButtonsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export default class GitHubAuthenticationPage 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(
|
||||
GitHubAuthenticationPage.propTypes,
|
||||
this.props,
|
||||
'prop',
|
||||
'GitHubAuthenticationPage',
|
||||
);
|
||||
}
|
||||
|
||||
getPermissionToFork = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.setState({
|
||||
requestingFork: true,
|
||||
approveFork: () => {
|
||||
this.setState({ requestingFork: false });
|
||||
resolve();
|
||||
},
|
||||
refuseFork: () => {
|
||||
this.setState({ requestingFork: false });
|
||||
reject();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
loginWithOpenAuthoring(data) {
|
||||
const { backend } = this.props;
|
||||
|
||||
this.setState({ findingFork: true });
|
||||
return backend
|
||||
.authenticateWithFork({ userData: data, getPermissionToFork: this.getPermissionToFork })
|
||||
.catch(err => {
|
||||
this.setState({ findingFork: false });
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
handleLogin = e => {
|
||||
e.preventDefault();
|
||||
const cfg = {
|
||||
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,
|
||||
};
|
||||
const auth = new NetlifyAuthenticator(cfg);
|
||||
|
||||
const { open_authoring: openAuthoring = false, auth_scope: authScope = '' } =
|
||||
this.props.config.backend;
|
||||
|
||||
const scope = authScope || (openAuthoring ? 'public_repo' : 'repo');
|
||||
auth.authenticate({ provider: 'github', scope }, (err, data) => {
|
||||
if (err) {
|
||||
this.setState({ loginError: err.toString() });
|
||||
return;
|
||||
}
|
||||
if (openAuthoring) {
|
||||
return this.loginWithOpenAuthoring(data).then(() => this.props.onLogin(data));
|
||||
}
|
||||
this.props.onLogin(data);
|
||||
});
|
||||
};
|
||||
|
||||
renderLoginButton = () => {
|
||||
const { inProgress, t } = this.props;
|
||||
return inProgress || this.state.findingFork ? (
|
||||
t('auth.loggingIn')
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<LoginButtonIcon type="github" />
|
||||
{t('auth.loginWithGitHub')}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
getAuthenticationPageRenderArgs() {
|
||||
const { requestingFork } = this.state;
|
||||
|
||||
if (requestingFork) {
|
||||
const { approveFork, refuseFork } = this.state;
|
||||
return {
|
||||
renderPageContent: ({ LoginButton, TextButton, showAbortButton }) => (
|
||||
<ForkApprovalContainer>
|
||||
<p>
|
||||
Open Authoring is enabled: we need to use a fork on your github account. (If a fork
|
||||
already exists, we'll use that.)
|
||||
</p>
|
||||
<ForkButtonsContainer>
|
||||
<LoginButton onClick={approveFork}>Fork the repo</LoginButton>
|
||||
{showAbortButton && (
|
||||
<TextButton onClick={refuseFork}>Don't fork the repo</TextButton>
|
||||
)}
|
||||
</ForkButtonsContainer>
|
||||
</ForkApprovalContainer>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
renderButtonContent: this.renderLoginButton,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { inProgress, config, t } = this.props;
|
||||
const { loginError, requestingFork, findingFork } = this.state;
|
||||
|
||||
return (
|
||||
<AuthenticationPage
|
||||
onLogin={this.handleLogin}
|
||||
loginDisabled={inProgress || findingFork || requestingFork}
|
||||
loginErrorMessage={loginError}
|
||||
logoUrl={config.logo_url} // Deprecated, replaced by `logo.src`
|
||||
logo={config.logo}
|
||||
siteUrl={config.site_url}
|
||||
{...this.getAuthenticationPageRenderArgs()}
|
||||
t={t}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
710
source/admin/packages/decap-cms-backend-github/src/GraphQLAPI.ts
Normal file
710
source/admin/packages/decap-cms-backend-github/src/GraphQLAPI.ts
Normal file
@@ -0,0 +1,710 @@
|
||||
import { ApolloClient } from 'apollo-client';
|
||||
import {
|
||||
InMemoryCache,
|
||||
defaultDataIdFromObject,
|
||||
IntrospectionFragmentMatcher,
|
||||
} from 'apollo-cache-inmemory';
|
||||
import { createHttpLink } from 'apollo-link-http';
|
||||
import { setContext } from 'apollo-link-context';
|
||||
import {
|
||||
APIError,
|
||||
readFile,
|
||||
localForage,
|
||||
DEFAULT_PR_BODY,
|
||||
branchFromContentKey,
|
||||
CMS_BRANCH_PREFIX,
|
||||
throwOnConflictingBranches,
|
||||
} from 'decap-cms-lib-util';
|
||||
import trim from 'lodash/trim';
|
||||
import trimStart from 'lodash/trimStart';
|
||||
|
||||
import introspectionQueryResultData from './fragmentTypes';
|
||||
import API, { API_NAME, PullRequestState, MOCK_PULL_REQUEST } from './API';
|
||||
import * as queries from './queries';
|
||||
import * as mutations from './mutations';
|
||||
|
||||
import type { Config, BlobArgs } from './API';
|
||||
import type { NormalizedCacheObject } from 'apollo-cache-inmemory';
|
||||
import type { QueryOptions, MutationOptions, OperationVariables } from 'apollo-client';
|
||||
import type { GraphQLError } from 'graphql';
|
||||
import type { Octokit } from '@octokit/rest';
|
||||
|
||||
const NO_CACHE = 'no-cache';
|
||||
const CACHE_FIRST = 'cache-first';
|
||||
|
||||
const fragmentMatcher = new IntrospectionFragmentMatcher({
|
||||
introspectionQueryResultData,
|
||||
});
|
||||
|
||||
interface TreeEntry {
|
||||
object?: {
|
||||
entries: TreeEntry[];
|
||||
};
|
||||
type: 'blob' | 'tree';
|
||||
name: string;
|
||||
sha: string;
|
||||
blob?: {
|
||||
size: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface TreeFile {
|
||||
path: string;
|
||||
id: string;
|
||||
size: number;
|
||||
type: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
type GraphQLPullRequest = {
|
||||
id: string;
|
||||
baseRefName: string;
|
||||
baseRefOid: string;
|
||||
body: string;
|
||||
headRefName: string;
|
||||
headRefOid: string;
|
||||
number: number;
|
||||
state: string;
|
||||
title: string;
|
||||
mergedAt: string | null;
|
||||
updatedAt: string | null;
|
||||
labels: { nodes: { name: string }[] };
|
||||
repository: {
|
||||
id: string;
|
||||
isFork: boolean;
|
||||
};
|
||||
user: GraphQLPullsListResponseItemUser;
|
||||
};
|
||||
|
||||
type GraphQLPullsListResponseItemUser = {
|
||||
avatar_url: string;
|
||||
login: string;
|
||||
url: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
function transformPullRequest(pr: GraphQLPullRequest) {
|
||||
return {
|
||||
...pr,
|
||||
labels: pr.labels.nodes,
|
||||
head: { ref: pr.headRefName, sha: pr.headRefOid, repo: { fork: pr.repository.isFork } },
|
||||
base: { ref: pr.baseRefName, sha: pr.baseRefOid },
|
||||
};
|
||||
}
|
||||
|
||||
type Error = GraphQLError & { type: string };
|
||||
|
||||
export default class GraphQLAPI extends API {
|
||||
client: ApolloClient<NormalizedCacheObject>;
|
||||
|
||||
constructor(config: Config) {
|
||||
super(config);
|
||||
|
||||
this.client = this.getApolloClient();
|
||||
}
|
||||
|
||||
getApolloClient() {
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
return {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
...headers,
|
||||
authorization: this.token ? `${this.tokenKeyword} ${this.token}` : '',
|
||||
},
|
||||
};
|
||||
});
|
||||
const httpLink = createHttpLink({ uri: `${this.apiRoot}/graphql` });
|
||||
return new ApolloClient({
|
||||
link: authLink.concat(httpLink),
|
||||
cache: new InMemoryCache({ fragmentMatcher }),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: NO_CACHE,
|
||||
errorPolicy: 'ignore',
|
||||
},
|
||||
query: {
|
||||
fetchPolicy: NO_CACHE,
|
||||
errorPolicy: 'all',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
return this.client.resetStore();
|
||||
}
|
||||
|
||||
async getRepository(owner: string, name: string) {
|
||||
const { data } = await this.query({
|
||||
query: queries.repository,
|
||||
variables: { owner, name },
|
||||
fetchPolicy: CACHE_FIRST, // repository id doesn't change
|
||||
});
|
||||
return data.repository;
|
||||
}
|
||||
|
||||
query(options: QueryOptions<OperationVariables>) {
|
||||
return this.client.query(options).catch(error => {
|
||||
throw new APIError(error.message, 500, 'GitHub');
|
||||
});
|
||||
}
|
||||
|
||||
async mutate(options: MutationOptions<OperationVariables>) {
|
||||
try {
|
||||
const result = await this.client.mutate(options);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errors = error.graphQLErrors;
|
||||
if (Array.isArray(errors) && errors.some(e => e.message === 'Ref cannot be created.')) {
|
||||
const refName = options?.variables?.createRefInput?.name || '';
|
||||
const branchName = trimStart(refName, 'refs/heads/');
|
||||
if (branchName) {
|
||||
await throwOnConflictingBranches(branchName, name => this.getBranch(name), API_NAME);
|
||||
}
|
||||
} else if (
|
||||
Array.isArray(errors) &&
|
||||
errors.some(e =>
|
||||
new RegExp(
|
||||
`A ref named "refs/heads/${CMS_BRANCH_PREFIX}/.+?" already exists in the repository.`,
|
||||
).test(e.message),
|
||||
)
|
||||
) {
|
||||
const refName = options?.variables?.createRefInput?.name || '';
|
||||
const sha = options?.variables?.createRefInput?.oid || '';
|
||||
const branchName = trimStart(refName, 'refs/heads/');
|
||||
if (branchName && branchName.startsWith(`${CMS_BRANCH_PREFIX}/`) && sha) {
|
||||
try {
|
||||
// this can happen if the branch wasn't deleted when the PR was merged
|
||||
// we backup the existing branch just in case an re-run the mutation
|
||||
await this.backupBranch(branchName);
|
||||
await this.deleteBranch(branchName);
|
||||
const result = await this.client.mutate(options);
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new APIError(error.message, 500, 'GitHub');
|
||||
}
|
||||
}
|
||||
|
||||
async hasWriteAccess() {
|
||||
const { repoOwner: owner, repoName: name } = this;
|
||||
try {
|
||||
const { data } = await this.query({
|
||||
query: queries.repoPermission,
|
||||
variables: { owner, name },
|
||||
fetchPolicy: CACHE_FIRST, // we can assume permission doesn't change often
|
||||
});
|
||||
// https://developer.github.com/v4/enum/repositorypermission/
|
||||
const { viewerPermission } = data.repository;
|
||||
return ['ADMIN', 'MAINTAIN', 'WRITE'].includes(viewerPermission);
|
||||
} catch (error) {
|
||||
console.error('Problem fetching repo data from GitHub');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async user() {
|
||||
const { data } = await this.query({
|
||||
query: queries.user,
|
||||
fetchPolicy: CACHE_FIRST, // we can assume user details don't change often
|
||||
});
|
||||
return data.viewer;
|
||||
}
|
||||
|
||||
async retrieveBlobObject(owner: string, name: string, expression: string, options = {}) {
|
||||
const { data } = await this.query({
|
||||
query: queries.blob,
|
||||
variables: { owner, name, expression },
|
||||
...options,
|
||||
});
|
||||
// https://developer.github.com/v4/object/blob/
|
||||
if (data.repository.object) {
|
||||
const { is_binary: isBinary, text } = data.repository.object;
|
||||
return { isNull: false, isBinary, text };
|
||||
} else {
|
||||
return { isNull: true };
|
||||
}
|
||||
}
|
||||
|
||||
getOwnerAndNameFromRepoUrl(repoURL: string) {
|
||||
let { repoOwner: owner, repoName: name } = this;
|
||||
|
||||
if (repoURL === this.originRepoURL) {
|
||||
({ originRepoOwner: owner, originRepoName: name } = this);
|
||||
}
|
||||
|
||||
return { owner, name };
|
||||
}
|
||||
|
||||
async readFile(
|
||||
path: string,
|
||||
sha?: string | null,
|
||||
{
|
||||
branch = this.branch,
|
||||
repoURL = this.repoURL,
|
||||
parseText = true,
|
||||
}: {
|
||||
branch?: string;
|
||||
repoURL?: string;
|
||||
parseText?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
if (!sha) {
|
||||
sha = await this.getFileSha(path, { repoURL, branch });
|
||||
}
|
||||
const fetchContent = () => this.fetchBlobContent({ sha: sha as string, repoURL, parseText });
|
||||
const content = await readFile(sha, fetchContent, localForage, parseText);
|
||||
return content;
|
||||
}
|
||||
|
||||
async fetchBlobContent({ sha, repoURL, parseText }: BlobArgs) {
|
||||
if (!parseText) {
|
||||
return super.fetchBlobContent({ sha, repoURL, parseText });
|
||||
}
|
||||
const { owner, name } = this.getOwnerAndNameFromRepoUrl(repoURL);
|
||||
const { isNull, isBinary, text } = await this.retrieveBlobObject(
|
||||
owner,
|
||||
name,
|
||||
sha,
|
||||
{ fetchPolicy: CACHE_FIRST }, // blob sha is derived from file content
|
||||
);
|
||||
|
||||
if (isNull) {
|
||||
throw new APIError('Not Found', 404, 'GitHub');
|
||||
} else if (!isBinary) {
|
||||
return text;
|
||||
} else {
|
||||
return super.fetchBlobContent({ sha, repoURL, parseText });
|
||||
}
|
||||
}
|
||||
|
||||
async getPullRequestAuthor(pullRequest: Octokit.PullsListResponseItem) {
|
||||
const user = pullRequest.user as unknown as GraphQLPullsListResponseItemUser;
|
||||
return user?.name || user?.login;
|
||||
}
|
||||
|
||||
async getPullRequests(
|
||||
head: string | undefined,
|
||||
state: PullRequestState,
|
||||
predicate: (pr: Octokit.PullsListResponseItem) => boolean,
|
||||
) {
|
||||
const { originRepoOwner: owner, originRepoName: name } = this;
|
||||
let states;
|
||||
if (state === PullRequestState.Open) {
|
||||
states = ['OPEN'];
|
||||
} else if (state === PullRequestState.Closed) {
|
||||
states = ['CLOSED', 'MERGED'];
|
||||
} else {
|
||||
states = ['OPEN', 'CLOSED', 'MERGED'];
|
||||
}
|
||||
const { data } = await this.query({
|
||||
query: queries.pullRequests,
|
||||
variables: {
|
||||
owner,
|
||||
name,
|
||||
...(head ? { head } : {}),
|
||||
states,
|
||||
},
|
||||
});
|
||||
const {
|
||||
pullRequests,
|
||||
}: {
|
||||
pullRequests: {
|
||||
nodes: GraphQLPullRequest[];
|
||||
};
|
||||
} = data.repository;
|
||||
|
||||
const mapped = pullRequests.nodes.map(transformPullRequest);
|
||||
|
||||
return (mapped as unknown as Octokit.PullsListResponseItem[]).filter(
|
||||
pr => pr.head.ref.startsWith(`${CMS_BRANCH_PREFIX}/`) && predicate(pr),
|
||||
);
|
||||
}
|
||||
|
||||
async getOpenAuthoringBranches() {
|
||||
const { repoOwner: owner, repoName: name } = this;
|
||||
const { data } = await this.query({
|
||||
query: queries.openAuthoringBranches,
|
||||
variables: {
|
||||
owner,
|
||||
name,
|
||||
refPrefix: `refs/heads/cms/${this.repo}/`,
|
||||
},
|
||||
});
|
||||
|
||||
return data.repository.refs.nodes.map(({ name, prefix }: { name: string; prefix: string }) => ({
|
||||
ref: `${prefix}${name}`,
|
||||
}));
|
||||
}
|
||||
|
||||
async getStatuses(collectionName: string, slug: string) {
|
||||
const contentKey = this.generateContentKey(collectionName, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
const sha = pullRequest.head.sha;
|
||||
const { originRepoOwner: owner, originRepoName: name } = this;
|
||||
const { data } = await this.query({ query: queries.statues, variables: { owner, name, sha } });
|
||||
if (data.repository.object) {
|
||||
const { status } = data.repository.object;
|
||||
const { contexts } = status || { contexts: [] };
|
||||
return contexts;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
getAllFiles(entries: TreeEntry[], path: string) {
|
||||
const allFiles: TreeFile[] = entries.reduce((acc, item) => {
|
||||
if (item.type === 'tree') {
|
||||
const entries = item.object?.entries || [];
|
||||
return [...acc, ...this.getAllFiles(entries, `${path}/${item.name}`)];
|
||||
} else if (item.type === 'blob') {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
id: item.sha,
|
||||
path: `${path}/${item.name}`,
|
||||
size: item.blob ? item.blob.size : 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as TreeFile[]);
|
||||
return allFiles;
|
||||
}
|
||||
|
||||
async listFiles(path: string, { repoURL = this.repoURL, branch = this.branch, depth = 1 } = {}) {
|
||||
const { owner, name } = this.getOwnerAndNameFromRepoUrl(repoURL);
|
||||
const folder = trim(path, '/');
|
||||
const { data } = await this.query({
|
||||
query: queries.files(depth),
|
||||
variables: { owner, name, expression: `${branch}:${folder}` },
|
||||
});
|
||||
|
||||
if (data.repository.object) {
|
||||
const allFiles = this.getAllFiles(data.repository.object.entries, folder);
|
||||
return allFiles;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
getBranchQualifiedName(branch: string) {
|
||||
return `refs/heads/${branch}`;
|
||||
}
|
||||
|
||||
getBranchQuery(branch: string, owner: string, name: string) {
|
||||
return {
|
||||
query: queries.branch,
|
||||
variables: {
|
||||
owner,
|
||||
name,
|
||||
qualifiedName: this.getBranchQualifiedName(branch),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getDefaultBranch() {
|
||||
const { data } = await this.query({
|
||||
...this.getBranchQuery(this.branch, this.originRepoOwner, this.originRepoName),
|
||||
});
|
||||
return data.repository.branch;
|
||||
}
|
||||
|
||||
async getBranch(branch: string) {
|
||||
const { data } = await this.query({
|
||||
...this.getBranchQuery(branch, this.repoOwner, this.repoName),
|
||||
fetchPolicy: CACHE_FIRST,
|
||||
});
|
||||
if (!data.repository.branch) {
|
||||
throw new APIError('Branch not found', 404, API_NAME);
|
||||
}
|
||||
return data.repository.branch;
|
||||
}
|
||||
|
||||
async patchRef(type: string, name: string, sha: string, opts: { force?: boolean } = {}) {
|
||||
if (type !== 'heads') {
|
||||
return super.patchRef(type, name, sha, opts);
|
||||
}
|
||||
|
||||
const force = opts.force || false;
|
||||
|
||||
const branch = await this.getBranch(name);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.updateBranch,
|
||||
variables: {
|
||||
input: { oid: sha, refId: branch.id, force },
|
||||
},
|
||||
});
|
||||
return data!.updateRef.branch;
|
||||
}
|
||||
|
||||
async deleteBranch(branchName: string) {
|
||||
const branch = await this.getBranch(branchName);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.deleteBranch,
|
||||
variables: {
|
||||
deleteRefInput: { refId: branch.id },
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
update: (store: any) => store.data.delete(defaultDataIdFromObject(branch)),
|
||||
});
|
||||
|
||||
return data!.deleteRef;
|
||||
}
|
||||
|
||||
getPullRequestQuery(number: number) {
|
||||
const { originRepoOwner: owner, originRepoName: name } = this;
|
||||
|
||||
return {
|
||||
query: queries.pullRequest,
|
||||
variables: { owner, name, number },
|
||||
};
|
||||
}
|
||||
|
||||
async getPullRequest(number: number) {
|
||||
const { data } = await this.query({
|
||||
...this.getPullRequestQuery(number),
|
||||
fetchPolicy: CACHE_FIRST,
|
||||
});
|
||||
|
||||
// https://developer.github.com/v4/enum/pullrequeststate/
|
||||
// GraphQL state: [CLOSED, MERGED, OPEN]
|
||||
// REST API state: [closed, open]
|
||||
const state =
|
||||
data.repository.pullRequest.state === 'OPEN'
|
||||
? PullRequestState.Open
|
||||
: PullRequestState.Closed;
|
||||
return {
|
||||
...data.repository.pullRequest,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
getPullRequestAndBranchQuery(branch: string, number: number) {
|
||||
const { repoOwner: owner, repoName: name } = this;
|
||||
const { originRepoOwner, originRepoName } = this;
|
||||
return {
|
||||
query: queries.pullRequestAndBranch,
|
||||
variables: {
|
||||
owner,
|
||||
name,
|
||||
originRepoOwner,
|
||||
originRepoName,
|
||||
number,
|
||||
qualifiedName: this.getBranchQualifiedName(branch),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getPullRequestAndBranch(branch: string, number: number) {
|
||||
const { data } = await this.query({
|
||||
...this.getPullRequestAndBranchQuery(branch, number),
|
||||
fetchPolicy: CACHE_FIRST,
|
||||
});
|
||||
|
||||
const { repository, origin } = data;
|
||||
return { branch: repository.branch, pullRequest: origin.pullRequest };
|
||||
}
|
||||
|
||||
async openPR(number: number) {
|
||||
const pullRequest = await this.getPullRequest(number);
|
||||
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.reopenPullRequest,
|
||||
variables: {
|
||||
reopenPullRequestInput: { pullRequestId: pullRequest.id },
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { pullRequest } = mutationResult!.reopenPullRequest;
|
||||
const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestQuery(pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return data!.reopenPullRequest;
|
||||
}
|
||||
|
||||
async closePR(number: number) {
|
||||
const pullRequest = await this.getPullRequest(number);
|
||||
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.closePullRequest,
|
||||
variables: {
|
||||
closePullRequestInput: { pullRequestId: pullRequest.id },
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { pullRequest } = mutationResult!.closePullRequest;
|
||||
const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestQuery(pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return data!.closePullRequest;
|
||||
}
|
||||
|
||||
async deleteUnpublishedEntry(collectionName: string, slug: string) {
|
||||
try {
|
||||
const contentKey = this.generateContentKey(collectionName, slug);
|
||||
const branchName = branchFromContentKey(contentKey);
|
||||
const pr = await this.getBranchPullRequest(branchName);
|
||||
if (pr.number !== MOCK_PULL_REQUEST) {
|
||||
const { branch, pullRequest } = await this.getPullRequestAndBranch(branchName, pr.number);
|
||||
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.closePullRequestAndDeleteBranch,
|
||||
variables: {
|
||||
deleteRefInput: { refId: branch.id },
|
||||
closePullRequestInput: { pullRequestId: pullRequest.id },
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
update: (store: any) => {
|
||||
store.data.delete(defaultDataIdFromObject(branch));
|
||||
store.data.delete(defaultDataIdFromObject(pullRequest));
|
||||
},
|
||||
});
|
||||
|
||||
return data!.closePullRequest;
|
||||
} else {
|
||||
return await this.deleteBranch(branchName);
|
||||
}
|
||||
} catch (e) {
|
||||
const { graphQLErrors } = e;
|
||||
if (graphQLErrors && graphQLErrors.length > 0) {
|
||||
const branchNotFound = graphQLErrors.some((e: Error) => e.type === 'NOT_FOUND');
|
||||
if (branchNotFound) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async createPR(title: string, head: string) {
|
||||
const [repository, headReference] = await Promise.all([
|
||||
this.getRepository(this.originRepoOwner, this.originRepoName),
|
||||
this.useOpenAuthoring ? `${(await this.user()).login}:${head}` : head,
|
||||
]);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.createPullRequest,
|
||||
variables: {
|
||||
createPullRequestInput: {
|
||||
baseRefName: this.branch,
|
||||
body: DEFAULT_PR_BODY,
|
||||
title,
|
||||
headRefName: headReference,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { pullRequest } = mutationResult!.createPullRequest;
|
||||
const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestQuery(pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { pullRequest } = data!.createPullRequest;
|
||||
return { ...pullRequest, head: { sha: pullRequest.headRefOid } };
|
||||
}
|
||||
|
||||
async createBranch(branchName: string, sha: string) {
|
||||
const owner = this.repoOwner;
|
||||
const name = this.repoName;
|
||||
const repository = await this.getRepository(owner, name);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.createBranch,
|
||||
variables: {
|
||||
createRefInput: {
|
||||
name: this.getBranchQualifiedName(branchName),
|
||||
oid: sha,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { branch } = mutationResult!.createRef;
|
||||
const branchData = { repository: { ...branch.repository, branch } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getBranchQuery(branchName, owner, name),
|
||||
data: branchData,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { branch } = data!.createRef;
|
||||
return { ...branch, ref: `${branch.prefix}${branch.name}` };
|
||||
}
|
||||
|
||||
async createBranchAndPullRequest(branchName: string, sha: string, title: string) {
|
||||
const owner = this.originRepoOwner;
|
||||
const name = this.originRepoName;
|
||||
const repository = await this.getRepository(owner, name);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.createBranchAndPullRequest,
|
||||
variables: {
|
||||
createRefInput: {
|
||||
name: this.getBranchQualifiedName(branchName),
|
||||
oid: sha,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
createPullRequestInput: {
|
||||
baseRefName: this.branch,
|
||||
body: DEFAULT_PR_BODY,
|
||||
title,
|
||||
headRefName: branchName,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { branch } = mutationResult!.createRef;
|
||||
const { pullRequest } = mutationResult!.createPullRequest;
|
||||
const branchData = { repository: { ...branch.repository, branch } };
|
||||
const pullRequestData = {
|
||||
repository: { ...pullRequest.repository, branch },
|
||||
origin: { ...pullRequest.repository, pullRequest },
|
||||
};
|
||||
|
||||
store.writeQuery({
|
||||
...this.getBranchQuery(branchName, owner, name),
|
||||
data: branchData,
|
||||
});
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestAndBranchQuery(branchName, pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { pullRequest } = data!.createPullRequest;
|
||||
return transformPullRequest(pullRequest) as unknown as Octokit.PullsCreateResponse;
|
||||
}
|
||||
|
||||
async getFileSha(path: string, { repoURL = this.repoURL, branch = this.branch } = {}) {
|
||||
const { owner, name } = this.getOwnerAndNameFromRepoUrl(repoURL);
|
||||
const { data } = await this.query({
|
||||
query: queries.fileSha,
|
||||
variables: { owner, name, expression: `${branch}:${path}` },
|
||||
});
|
||||
|
||||
if (data.repository.file) {
|
||||
return data.repository.file.sha;
|
||||
}
|
||||
throw new APIError('Not Found', 404, API_NAME);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,833 @@
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
import API from '../API';
|
||||
|
||||
global.fetch = jest.fn().mockRejectedValue(new Error('should not call fetch inside tests'));
|
||||
|
||||
describe('github API', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
function mockAPI(api, responses) {
|
||||
api.request = jest.fn().mockImplementation((path, options = {}) => {
|
||||
const normalizedPath = path.indexOf('?') !== -1 ? path.slice(0, path.indexOf('?')) : path;
|
||||
const response = responses[normalizedPath];
|
||||
return typeof response === 'function'
|
||||
? Promise.resolve(response(options))
|
||||
: Promise.reject(new Error(`No response for path '${normalizedPath}'`));
|
||||
});
|
||||
}
|
||||
|
||||
describe('editorialWorkflowGit', () => {
|
||||
it('should create PR with correct base branch name when publishing with editorial workflow', () => {
|
||||
let prBaseBranch = null;
|
||||
let labels = null;
|
||||
const api = new API({
|
||||
branch: 'gh-pages',
|
||||
repo: 'owner/my-repo',
|
||||
initialWorkflowStatus: 'draft',
|
||||
});
|
||||
const responses = {
|
||||
'/repos/owner/my-repo/branches/gh-pages': () => ({ commit: { sha: 'def' } }),
|
||||
'/repos/owner/my-repo/git/trees/def': () => ({ tree: [] }),
|
||||
'/repos/owner/my-repo/git/trees': () => ({}),
|
||||
'/repos/owner/my-repo/git/commits': () => ({}),
|
||||
'/repos/owner/my-repo/git/refs': () => ({}),
|
||||
'/repos/owner/my-repo/pulls': req => {
|
||||
prBaseBranch = JSON.parse(req.body).base;
|
||||
return { head: { sha: 'cbd' }, labels: [], number: 1 };
|
||||
},
|
||||
'/repos/owner/my-repo/issues/1/labels': req => {
|
||||
labels = JSON.parse(req.body).labels;
|
||||
return {};
|
||||
},
|
||||
};
|
||||
mockAPI(api, responses);
|
||||
|
||||
return expect(
|
||||
api.editorialWorkflowGit([], { slug: 'entry', sha: 'abc' }, null, {}).then(() => ({
|
||||
prBaseBranch,
|
||||
labels,
|
||||
})),
|
||||
).resolves.toEqual({ prBaseBranch: 'gh-pages', labels: ['decap-cms/draft'] });
|
||||
});
|
||||
|
||||
it('should create PR with correct base branch name with custom prefix when publishing with editorial workflow', () => {
|
||||
let prBaseBranch = null;
|
||||
let labels = null;
|
||||
const api = new API({
|
||||
branch: 'gh-pages',
|
||||
repo: 'owner/my-repo',
|
||||
initialWorkflowStatus: 'draft',
|
||||
cmsLabelPrefix: 'other/',
|
||||
});
|
||||
const responses = {
|
||||
'/repos/owner/my-repo/branches/gh-pages': () => ({ commit: { sha: 'def' } }),
|
||||
'/repos/owner/my-repo/git/trees/def': () => ({ tree: [] }),
|
||||
'/repos/owner/my-repo/git/trees': () => ({}),
|
||||
'/repos/owner/my-repo/git/commits': () => ({}),
|
||||
'/repos/owner/my-repo/git/refs': () => ({}),
|
||||
'/repos/owner/my-repo/pulls': req => {
|
||||
prBaseBranch = JSON.parse(req.body).base;
|
||||
return { head: { sha: 'cbd' }, labels: [], number: 1 };
|
||||
},
|
||||
'/repos/owner/my-repo/issues/1/labels': req => {
|
||||
labels = JSON.parse(req.body).labels;
|
||||
return {};
|
||||
},
|
||||
};
|
||||
mockAPI(api, responses);
|
||||
|
||||
return expect(
|
||||
api.editorialWorkflowGit([], { slug: 'entry', sha: 'abc' }, null, {}).then(() => ({
|
||||
prBaseBranch,
|
||||
labels,
|
||||
})),
|
||||
).resolves.toEqual({ prBaseBranch: 'gh-pages', labels: ['other/draft'] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTree', () => {
|
||||
it('should create tree with nested paths', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
api.createTree = jest.fn().mockImplementation(() => Promise.resolve({ sha: 'newTreeSha' }));
|
||||
|
||||
const files = [
|
||||
{ path: '/static/media/new-image.jpeg', sha: null },
|
||||
{ path: 'content/posts/new-post.md', sha: 'new-post.md' },
|
||||
];
|
||||
|
||||
const baseTreeSha = 'baseTreeSha';
|
||||
|
||||
await expect(api.updateTree(baseTreeSha, files)).resolves.toEqual({
|
||||
sha: 'newTreeSha',
|
||||
parentSha: baseTreeSha,
|
||||
});
|
||||
|
||||
expect(api.createTree).toHaveBeenCalledTimes(1);
|
||||
expect(api.createTree).toHaveBeenCalledWith(baseTreeSha, [
|
||||
{
|
||||
path: 'static/media/new-image.jpeg',
|
||||
mode: '100644',
|
||||
type: 'blob',
|
||||
sha: null,
|
||||
},
|
||||
{
|
||||
path: 'content/posts/new-post.md',
|
||||
mode: '100644',
|
||||
type: 'blob',
|
||||
sha: 'new-post.md',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('request', () => {
|
||||
beforeEach(() => {
|
||||
const fetch = jest.fn();
|
||||
global.fetch = fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fetch url with authorization header', async () => {
|
||||
const api = new API({ branch: 'gh-pages', repo: 'my-repo', token: 'token' });
|
||||
|
||||
fetch.mockResolvedValue({
|
||||
text: jest.fn().mockResolvedValue('some response'),
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { get: () => '' },
|
||||
});
|
||||
const result = await api.request('/some-path');
|
||||
expect(result).toEqual('some response');
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(fetch).toHaveBeenCalledWith('https://api.github.com/some-path', {
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
Authorization: 'token token',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error on not ok response', async () => {
|
||||
const api = new API({ branch: 'gh-pages', repo: 'my-repo', token: 'token' });
|
||||
|
||||
fetch.mockResolvedValue({
|
||||
text: jest.fn().mockResolvedValue({ message: 'some error' }),
|
||||
ok: false,
|
||||
status: 404,
|
||||
headers: { get: () => '' },
|
||||
});
|
||||
|
||||
await expect(api.request('some-path')).rejects.toThrow(
|
||||
expect.objectContaining({
|
||||
message: 'some error',
|
||||
name: 'API_ERROR',
|
||||
status: 404,
|
||||
api: 'GitHub',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow overriding requestHeaders to return a promise ', async () => {
|
||||
const api = new API({ branch: 'gh-pages', repo: 'my-repo', token: 'token' });
|
||||
|
||||
api.requestHeaders = jest.fn().mockResolvedValue({
|
||||
Authorization: 'promise-token',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
});
|
||||
|
||||
fetch.mockResolvedValue({
|
||||
text: jest.fn().mockResolvedValue('some response'),
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { get: () => '' },
|
||||
});
|
||||
const result = await api.request('/some-path');
|
||||
expect(result).toEqual('some response');
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(fetch).toHaveBeenCalledWith('https://api.github.com/some-path', {
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
Authorization: 'promise-token',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('persistFiles', () => {
|
||||
it('should update tree, commit and patch branch when useWorkflow is false', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const responses = {
|
||||
// upload the file
|
||||
'/repos/owner/repo/git/blobs': () => ({ sha: 'new-file-sha' }),
|
||||
|
||||
// get the branch
|
||||
'/repos/owner/repo/branches/master': () => ({ commit: { sha: 'root' } }),
|
||||
|
||||
// create new tree
|
||||
'/repos/owner/repo/git/trees': options => {
|
||||
const data = JSON.parse(options.body);
|
||||
return { sha: data.base_tree };
|
||||
},
|
||||
|
||||
// update the commit with the tree
|
||||
'/repos/owner/repo/git/commits': () => ({ sha: 'commit-sha' }),
|
||||
|
||||
// patch the branch
|
||||
'/repos/owner/repo/git/refs/heads/master': () => ({}),
|
||||
};
|
||||
mockAPI(api, responses);
|
||||
|
||||
const entry = {
|
||||
dataFiles: [
|
||||
{
|
||||
slug: 'entry',
|
||||
sha: 'abc',
|
||||
path: 'content/posts/new-post.md',
|
||||
raw: 'content',
|
||||
},
|
||||
],
|
||||
assets: [],
|
||||
};
|
||||
await api.persistFiles(entry.dataFiles, entry.assets, { commitMessage: 'commitMessage' });
|
||||
|
||||
expect(api.request).toHaveBeenCalledTimes(5);
|
||||
|
||||
expect(api.request.mock.calls[0]).toEqual([
|
||||
'/repos/owner/repo/git/blobs',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
content: Base64.encode(entry.dataFiles[0].raw),
|
||||
encoding: 'base64',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
expect(api.request.mock.calls[1]).toEqual(['/repos/owner/repo/branches/master']);
|
||||
|
||||
expect(api.request.mock.calls[2]).toEqual([
|
||||
'/repos/owner/repo/git/trees',
|
||||
{
|
||||
body: JSON.stringify({
|
||||
base_tree: 'root',
|
||||
tree: [
|
||||
{
|
||||
path: 'content/posts/new-post.md',
|
||||
mode: '100644',
|
||||
type: 'blob',
|
||||
sha: 'new-file-sha',
|
||||
},
|
||||
],
|
||||
}),
|
||||
method: 'POST',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(api.request.mock.calls[3]).toEqual([
|
||||
'/repos/owner/repo/git/commits',
|
||||
{
|
||||
body: JSON.stringify({
|
||||
message: 'commitMessage',
|
||||
tree: 'root',
|
||||
parents: ['root'],
|
||||
}),
|
||||
method: 'POST',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(api.request.mock.calls[4]).toEqual([
|
||||
'/repos/owner/repo/git/refs/heads/master',
|
||||
{
|
||||
body: JSON.stringify({
|
||||
sha: 'commit-sha',
|
||||
force: false,
|
||||
}),
|
||||
method: 'PATCH',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call editorialWorkflowGit when useWorkflow is true', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
api.uploadBlob = jest.fn();
|
||||
api.editorialWorkflowGit = jest.fn();
|
||||
|
||||
const entry = {
|
||||
dataFiles: [
|
||||
{
|
||||
slug: 'entry',
|
||||
sha: 'abc',
|
||||
path: 'content/posts/new-post.md',
|
||||
raw: 'content',
|
||||
},
|
||||
],
|
||||
assets: [
|
||||
{
|
||||
path: '/static/media/image-1.png',
|
||||
sha: 'image-1.png',
|
||||
},
|
||||
{
|
||||
path: '/static/media/image-2.png',
|
||||
sha: 'image-2.png',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await api.persistFiles(entry.dataFiles, entry.assets, { useWorkflow: true });
|
||||
|
||||
expect(api.uploadBlob).toHaveBeenCalledTimes(3);
|
||||
expect(api.uploadBlob).toHaveBeenCalledWith(entry.dataFiles[0]);
|
||||
expect(api.uploadBlob).toHaveBeenCalledWith(entry.assets[0]);
|
||||
expect(api.uploadBlob).toHaveBeenCalledWith(entry.assets[1]);
|
||||
|
||||
expect(api.editorialWorkflowGit).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(api.editorialWorkflowGit).toHaveBeenCalledWith(
|
||||
entry.assets.concat(entry.dataFiles),
|
||||
entry.dataFiles[0].slug,
|
||||
[
|
||||
{ path: 'static/media/image-1.png', sha: 'image-1.png' },
|
||||
{ path: 'static/media/image-2.png', sha: 'image-2.png' },
|
||||
],
|
||||
{ useWorkflow: true },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('migratePullRequest', () => {
|
||||
it('should migrate to pull request labels when no version', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const pr = {
|
||||
head: { ref: 'cms/2019-11-11-post-title' },
|
||||
title: 'pr title',
|
||||
number: 1,
|
||||
labels: [],
|
||||
};
|
||||
const metadata = { type: 'PR' };
|
||||
api.retrieveMetadataOld = jest.fn().mockResolvedValue(metadata);
|
||||
const newBranch = 'cms/posts/2019-11-11-post-title';
|
||||
const migrateToVersion1Result = {
|
||||
metadata: { ...metadata, branch: newBranch, version: '1' },
|
||||
pullRequest: { ...pr, number: 2 },
|
||||
};
|
||||
api.migrateToVersion1 = jest.fn().mockResolvedValue(migrateToVersion1Result);
|
||||
api.migrateToPullRequestLabels = jest.fn();
|
||||
|
||||
await api.migratePullRequest(pr);
|
||||
|
||||
expect(api.migrateToVersion1).toHaveBeenCalledTimes(1);
|
||||
expect(api.migrateToVersion1).toHaveBeenCalledWith(pr, metadata);
|
||||
|
||||
expect(api.migrateToPullRequestLabels).toHaveBeenCalledTimes(1);
|
||||
expect(api.migrateToPullRequestLabels).toHaveBeenCalledWith(
|
||||
migrateToVersion1Result.pullRequest,
|
||||
migrateToVersion1Result.metadata,
|
||||
);
|
||||
|
||||
expect(api.retrieveMetadataOld).toHaveBeenCalledTimes(1);
|
||||
expect(api.retrieveMetadataOld).toHaveBeenCalledWith('2019-11-11-post-title');
|
||||
});
|
||||
|
||||
it('should migrate to pull request labels when version is 1', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
api.migrateToVersion1 = jest.fn();
|
||||
const pr = {
|
||||
head: { ref: 'cms/posts/2019-11-11-post-title' },
|
||||
title: 'pr title',
|
||||
number: 1,
|
||||
labels: [],
|
||||
};
|
||||
const metadata = { type: 'PR', version: '1' };
|
||||
api.retrieveMetadataOld = jest.fn().mockResolvedValue(metadata);
|
||||
api.migrateToPullRequestLabels = jest.fn().mockResolvedValue(pr, metadata);
|
||||
|
||||
await api.migratePullRequest(pr);
|
||||
|
||||
expect(api.migrateToVersion1).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(api.migrateToPullRequestLabels).toHaveBeenCalledTimes(1);
|
||||
expect(api.migrateToPullRequestLabels).toHaveBeenCalledWith(pr, metadata);
|
||||
|
||||
expect(api.retrieveMetadataOld).toHaveBeenCalledTimes(1);
|
||||
expect(api.retrieveMetadataOld).toHaveBeenCalledWith('posts/2019-11-11-post-title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateToVersion1', () => {
|
||||
it('should migrate to version 1', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const pr = {
|
||||
head: { ref: 'cms/2019-11-11-post-title', sha: 'pr_head' },
|
||||
title: 'pr title',
|
||||
number: 1,
|
||||
labels: [],
|
||||
};
|
||||
|
||||
const newBranch = { ref: 'refs/heads/cms/posts/2019-11-11-post-title' };
|
||||
api.createBranch = jest.fn().mockResolvedValue(newBranch);
|
||||
api.getBranch = jest.fn().mockRejectedValue(new Error('Branch not found'));
|
||||
|
||||
const newPr = { ...pr, number: 2 };
|
||||
api.createPR = jest.fn().mockResolvedValue(newPr);
|
||||
api.getPullRequests = jest.fn().mockResolvedValue([]);
|
||||
|
||||
api.storeMetadata = jest.fn();
|
||||
api.closePR = jest.fn();
|
||||
api.deleteBranch = jest.fn();
|
||||
api.deleteMetadata = jest.fn();
|
||||
|
||||
const branch = 'cms/2019-11-11-post-title';
|
||||
const metadata = {
|
||||
branch,
|
||||
type: 'PR',
|
||||
pr: { head: pr.head.sha },
|
||||
commitMessage: 'commitMessage',
|
||||
collection: 'posts',
|
||||
};
|
||||
|
||||
const expectedMetadata = {
|
||||
type: 'PR',
|
||||
pr: { head: newPr.head.sha, number: 2 },
|
||||
commitMessage: 'commitMessage',
|
||||
collection: 'posts',
|
||||
branch: 'cms/posts/2019-11-11-post-title',
|
||||
version: '1',
|
||||
};
|
||||
await expect(api.migrateToVersion1(pr, metadata)).resolves.toEqual({
|
||||
metadata: expectedMetadata,
|
||||
pullRequest: newPr,
|
||||
});
|
||||
|
||||
expect(api.getBranch).toHaveBeenCalledTimes(1);
|
||||
expect(api.getBranch).toHaveBeenCalledWith('cms/posts/2019-11-11-post-title');
|
||||
expect(api.createBranch).toHaveBeenCalledTimes(1);
|
||||
expect(api.createBranch).toHaveBeenCalledWith('cms/posts/2019-11-11-post-title', 'pr_head');
|
||||
|
||||
expect(api.getPullRequests).toHaveBeenCalledTimes(1);
|
||||
expect(api.getPullRequests).toHaveBeenCalledWith(
|
||||
'cms/posts/2019-11-11-post-title',
|
||||
'all',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(api.createPR).toHaveBeenCalledTimes(1);
|
||||
expect(api.createPR).toHaveBeenCalledWith('pr title', 'cms/posts/2019-11-11-post-title');
|
||||
|
||||
expect(api.storeMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(api.storeMetadata).toHaveBeenCalledWith(
|
||||
'posts/2019-11-11-post-title',
|
||||
expectedMetadata,
|
||||
);
|
||||
|
||||
expect(api.closePR).toHaveBeenCalledTimes(1);
|
||||
expect(api.closePR).toHaveBeenCalledWith(pr.number);
|
||||
|
||||
expect(api.deleteBranch).toHaveBeenCalledTimes(1);
|
||||
expect(api.deleteBranch).toHaveBeenCalledWith('cms/2019-11-11-post-title');
|
||||
|
||||
expect(api.deleteMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(api.deleteMetadata).toHaveBeenCalledWith('2019-11-11-post-title');
|
||||
});
|
||||
|
||||
it('should not create new branch if exists', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const pr = {
|
||||
head: { ref: 'cms/2019-11-11-post-title', sha: 'pr_head' },
|
||||
title: 'pr title',
|
||||
number: 1,
|
||||
labels: [],
|
||||
};
|
||||
|
||||
const newBranch = { ref: 'refs/heads/cms/posts/2019-11-11-post-title' };
|
||||
api.createBranch = jest.fn();
|
||||
api.getBranch = jest.fn().mockResolvedValue(newBranch);
|
||||
|
||||
const newPr = { ...pr, number: 2 };
|
||||
api.createPR = jest.fn().mockResolvedValue(newPr);
|
||||
api.getPullRequests = jest.fn().mockResolvedValue([]);
|
||||
|
||||
api.storeMetadata = jest.fn();
|
||||
api.closePR = jest.fn();
|
||||
api.deleteBranch = jest.fn();
|
||||
api.deleteMetadata = jest.fn();
|
||||
|
||||
const branch = 'cms/2019-11-11-post-title';
|
||||
const metadata = {
|
||||
branch,
|
||||
type: 'PR',
|
||||
pr: { head: pr.head.sha },
|
||||
commitMessage: 'commitMessage',
|
||||
collection: 'posts',
|
||||
};
|
||||
|
||||
const expectedMetadata = {
|
||||
type: 'PR',
|
||||
pr: { head: newPr.head.sha, number: 2 },
|
||||
commitMessage: 'commitMessage',
|
||||
collection: 'posts',
|
||||
branch: 'cms/posts/2019-11-11-post-title',
|
||||
version: '1',
|
||||
};
|
||||
await expect(api.migrateToVersion1(pr, metadata)).resolves.toEqual({
|
||||
metadata: expectedMetadata,
|
||||
pullRequest: newPr,
|
||||
});
|
||||
|
||||
expect(api.getBranch).toHaveBeenCalledTimes(1);
|
||||
expect(api.getBranch).toHaveBeenCalledWith('cms/posts/2019-11-11-post-title');
|
||||
expect(api.createBranch).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(api.getPullRequests).toHaveBeenCalledTimes(1);
|
||||
expect(api.getPullRequests).toHaveBeenCalledWith(
|
||||
'cms/posts/2019-11-11-post-title',
|
||||
'all',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(api.createPR).toHaveBeenCalledTimes(1);
|
||||
expect(api.createPR).toHaveBeenCalledWith('pr title', 'cms/posts/2019-11-11-post-title');
|
||||
|
||||
expect(api.storeMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(api.storeMetadata).toHaveBeenCalledWith(
|
||||
'posts/2019-11-11-post-title',
|
||||
expectedMetadata,
|
||||
);
|
||||
|
||||
expect(api.closePR).toHaveBeenCalledTimes(1);
|
||||
expect(api.closePR).toHaveBeenCalledWith(pr.number);
|
||||
|
||||
expect(api.deleteBranch).toHaveBeenCalledTimes(1);
|
||||
expect(api.deleteBranch).toHaveBeenCalledWith('cms/2019-11-11-post-title');
|
||||
|
||||
expect(api.deleteMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(api.deleteMetadata).toHaveBeenCalledWith('2019-11-11-post-title');
|
||||
});
|
||||
|
||||
it('should not create new pr if exists', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const pr = {
|
||||
head: { ref: 'cms/2019-11-11-post-title', sha: 'pr_head' },
|
||||
title: 'pr title',
|
||||
number: 1,
|
||||
labels: [],
|
||||
};
|
||||
|
||||
const newBranch = { ref: 'refs/heads/cms/posts/2019-11-11-post-title' };
|
||||
api.createBranch = jest.fn();
|
||||
api.getBranch = jest.fn().mockResolvedValue(newBranch);
|
||||
|
||||
const newPr = { ...pr, number: 2 };
|
||||
api.createPR = jest.fn();
|
||||
api.getPullRequests = jest.fn().mockResolvedValue([newPr]);
|
||||
|
||||
api.storeMetadata = jest.fn();
|
||||
api.closePR = jest.fn();
|
||||
api.deleteBranch = jest.fn();
|
||||
api.deleteMetadata = jest.fn();
|
||||
|
||||
const branch = 'cms/2019-11-11-post-title';
|
||||
const metadata = {
|
||||
branch,
|
||||
type: 'PR',
|
||||
pr: { head: pr.head.sha },
|
||||
commitMessage: 'commitMessage',
|
||||
collection: 'posts',
|
||||
};
|
||||
|
||||
const expectedMetadata = {
|
||||
type: 'PR',
|
||||
pr: { head: newPr.head.sha, number: 2 },
|
||||
commitMessage: 'commitMessage',
|
||||
collection: 'posts',
|
||||
branch: 'cms/posts/2019-11-11-post-title',
|
||||
version: '1',
|
||||
};
|
||||
await expect(api.migrateToVersion1(pr, metadata)).resolves.toEqual({
|
||||
metadata: expectedMetadata,
|
||||
pullRequest: newPr,
|
||||
});
|
||||
|
||||
expect(api.getBranch).toHaveBeenCalledTimes(1);
|
||||
expect(api.getBranch).toHaveBeenCalledWith('cms/posts/2019-11-11-post-title');
|
||||
expect(api.createBranch).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(api.getPullRequests).toHaveBeenCalledTimes(1);
|
||||
expect(api.getPullRequests).toHaveBeenCalledWith(
|
||||
'cms/posts/2019-11-11-post-title',
|
||||
'all',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(api.createPR).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(api.storeMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(api.storeMetadata).toHaveBeenCalledWith(
|
||||
'posts/2019-11-11-post-title',
|
||||
expectedMetadata,
|
||||
);
|
||||
|
||||
expect(api.closePR).toHaveBeenCalledTimes(1);
|
||||
expect(api.closePR).toHaveBeenCalledWith(pr.number);
|
||||
|
||||
expect(api.deleteBranch).toHaveBeenCalledTimes(1);
|
||||
expect(api.deleteBranch).toHaveBeenCalledWith('cms/2019-11-11-post-title');
|
||||
|
||||
expect(api.deleteMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(api.deleteMetadata).toHaveBeenCalledWith('2019-11-11-post-title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateToPullRequestLabels', () => {
|
||||
it('should migrate to pull request labels', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const pr = {
|
||||
head: { ref: 'cms/posts/2019-11-11-post-title', sha: 'pr_head' },
|
||||
title: 'pr title',
|
||||
number: 1,
|
||||
labels: [],
|
||||
};
|
||||
|
||||
api.setPullRequestStatus = jest.fn();
|
||||
api.deleteMetadata = jest.fn();
|
||||
|
||||
const metadata = {
|
||||
branch: pr.head.ref,
|
||||
type: 'PR',
|
||||
pr: { head: pr.head.sha },
|
||||
commitMessage: 'commitMessage',
|
||||
collection: 'posts',
|
||||
status: 'pending_review',
|
||||
};
|
||||
|
||||
await api.migrateToPullRequestLabels(pr, metadata);
|
||||
|
||||
expect(api.setPullRequestStatus).toHaveBeenCalledTimes(1);
|
||||
expect(api.setPullRequestStatus).toHaveBeenCalledWith(pr, 'pending_review');
|
||||
|
||||
expect(api.deleteMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(api.deleteMetadata).toHaveBeenCalledWith('posts/2019-11-11-post-title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('rebaseSingleCommit', () => {
|
||||
it('should create updated tree and commit', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
api.getDifferences = jest.fn().mockResolvedValueOnce({
|
||||
files: [
|
||||
{ filename: 'removed.md', status: 'removed', sha: 'removed_sha' },
|
||||
{
|
||||
filename: 'renamed.md',
|
||||
status: 'renamed',
|
||||
previous_filename: 'previous_filename.md',
|
||||
sha: 'renamed_sha',
|
||||
},
|
||||
{ filename: 'added.md', status: 'added', sha: 'added_sha' },
|
||||
],
|
||||
});
|
||||
|
||||
const newTree = { sha: 'new_tree_sha' };
|
||||
api.updateTree = jest.fn().mockResolvedValueOnce(newTree);
|
||||
|
||||
const newCommit = { sha: 'newCommit' };
|
||||
api.createCommit = jest.fn().mockResolvedValueOnce(newCommit);
|
||||
|
||||
const baseCommit = { sha: 'base_commit_sha' };
|
||||
const commit = {
|
||||
sha: 'sha',
|
||||
parents: [{ sha: 'parent_sha' }],
|
||||
commit: {
|
||||
message: 'message',
|
||||
author: { name: 'author' },
|
||||
committer: { name: 'committer' },
|
||||
},
|
||||
};
|
||||
|
||||
await expect(api.rebaseSingleCommit(baseCommit, commit)).resolves.toBe(newCommit);
|
||||
|
||||
expect(api.getDifferences).toHaveBeenCalledTimes(1);
|
||||
expect(api.getDifferences).toHaveBeenCalledWith('parent_sha', 'sha');
|
||||
|
||||
expect(api.updateTree).toHaveBeenCalledTimes(1);
|
||||
expect(api.updateTree).toHaveBeenCalledWith('base_commit_sha', [
|
||||
{ path: 'removed.md', sha: null },
|
||||
{ path: 'previous_filename.md', sha: null },
|
||||
{ path: 'renamed.md', sha: 'renamed_sha' },
|
||||
{ path: 'added.md', sha: 'added_sha' },
|
||||
]);
|
||||
|
||||
expect(api.createCommit).toHaveBeenCalledTimes(1);
|
||||
expect(api.createCommit).toHaveBeenCalledWith(
|
||||
'message',
|
||||
newTree.sha,
|
||||
[baseCommit.sha],
|
||||
{ name: 'author' },
|
||||
{ name: 'committer' },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listFiles', () => {
|
||||
it('should get files by depth', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const tree = [
|
||||
{
|
||||
path: 'post.md',
|
||||
type: 'blob',
|
||||
},
|
||||
{
|
||||
path: 'dir1',
|
||||
type: 'tree',
|
||||
},
|
||||
{
|
||||
path: 'dir1/nested-post.md',
|
||||
type: 'blob',
|
||||
},
|
||||
{
|
||||
path: 'dir1/dir2',
|
||||
type: 'tree',
|
||||
},
|
||||
{
|
||||
path: 'dir1/dir2/nested-post.md',
|
||||
type: 'blob',
|
||||
},
|
||||
];
|
||||
api.request = jest.fn().mockResolvedValue({ tree });
|
||||
|
||||
await expect(api.listFiles('posts', { depth: 1 })).resolves.toEqual([
|
||||
{
|
||||
path: 'posts/post.md',
|
||||
type: 'blob',
|
||||
name: 'post.md',
|
||||
},
|
||||
]);
|
||||
expect(api.request).toHaveBeenCalledTimes(1);
|
||||
expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:posts', {
|
||||
params: {},
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
await expect(api.listFiles('posts', { depth: 2 })).resolves.toEqual([
|
||||
{
|
||||
path: 'posts/post.md',
|
||||
type: 'blob',
|
||||
name: 'post.md',
|
||||
},
|
||||
{
|
||||
path: 'posts/dir1/nested-post.md',
|
||||
type: 'blob',
|
||||
name: 'nested-post.md',
|
||||
},
|
||||
]);
|
||||
expect(api.request).toHaveBeenCalledTimes(1);
|
||||
expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:posts', {
|
||||
params: { recursive: 1 },
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
await expect(api.listFiles('posts', { depth: 3 })).resolves.toEqual([
|
||||
{
|
||||
path: 'posts/post.md',
|
||||
type: 'blob',
|
||||
name: 'post.md',
|
||||
},
|
||||
{
|
||||
path: 'posts/dir1/nested-post.md',
|
||||
type: 'blob',
|
||||
name: 'nested-post.md',
|
||||
},
|
||||
{
|
||||
path: 'posts/dir1/dir2/nested-post.md',
|
||||
type: 'blob',
|
||||
name: 'nested-post.md',
|
||||
},
|
||||
]);
|
||||
expect(api.request).toHaveBeenCalledTimes(1);
|
||||
expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:posts', {
|
||||
params: { recursive: 1 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should get preview statuses', async () => {
|
||||
const api = new API({ repo: 'repo' });
|
||||
|
||||
const statuses = [
|
||||
{ context: 'deploy', state: 'success', target_url: 'deploy-url' },
|
||||
{ context: 'build', state: 'error' },
|
||||
];
|
||||
|
||||
api.request = jest.fn(() => Promise.resolve({ statuses }));
|
||||
const sha = 'sha';
|
||||
api.getBranchPullRequest = jest.fn(() => Promise.resolve({ head: { sha } }));
|
||||
|
||||
const collection = 'collection';
|
||||
const slug = 'slug';
|
||||
await expect(api.getStatuses(collection, slug)).resolves.toEqual([
|
||||
{ context: 'deploy', state: 'success', target_url: 'deploy-url' },
|
||||
{ context: 'build', state: 'other' },
|
||||
]);
|
||||
|
||||
expect(api.getBranchPullRequest).toHaveBeenCalledTimes(1);
|
||||
expect(api.getBranchPullRequest).toHaveBeenCalledWith('cms/collection/slug');
|
||||
expect(api.request).toHaveBeenCalledTimes(1);
|
||||
expect(api.request).toHaveBeenCalledWith(`/repos/repo/commits/${sha}/status`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import GraphQLAPI from '../GraphQLAPI';
|
||||
|
||||
global.fetch = jest.fn().mockRejectedValue(new Error('should not call fetch inside tests'));
|
||||
|
||||
describe('github GraphQL API', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('editorialWorkflowGit', () => {
|
||||
it('should should flatten nested tree into a list of files', () => {
|
||||
const api = new GraphQLAPI({ branch: 'gh-pages', repo: 'owner/my-repo' });
|
||||
const entries = [
|
||||
{
|
||||
name: 'post-1.md',
|
||||
sha: 'sha-1',
|
||||
type: 'blob',
|
||||
blob: { size: 1 },
|
||||
},
|
||||
{
|
||||
name: 'post-2.md',
|
||||
sha: 'sha-2',
|
||||
type: 'blob',
|
||||
blob: { size: 2 },
|
||||
},
|
||||
{
|
||||
name: '2019',
|
||||
sha: 'dir-sha',
|
||||
type: 'tree',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
name: 'nested-post.md',
|
||||
sha: 'nested-post-sha',
|
||||
type: 'blob',
|
||||
blob: { size: 3 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
const path = 'posts';
|
||||
|
||||
expect(api.getAllFiles(entries, path)).toEqual([
|
||||
{
|
||||
name: 'post-1.md',
|
||||
id: 'sha-1',
|
||||
type: 'blob',
|
||||
size: 1,
|
||||
path: 'posts/post-1.md',
|
||||
},
|
||||
{
|
||||
name: 'post-2.md',
|
||||
id: 'sha-2',
|
||||
type: 'blob',
|
||||
size: 2,
|
||||
path: 'posts/post-2.md',
|
||||
},
|
||||
{
|
||||
name: 'nested-post.md',
|
||||
id: 'nested-post-sha',
|
||||
type: 'blob',
|
||||
size: 3,
|
||||
path: 'posts/2019/nested-post.md',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,361 @@
|
||||
import { Cursor, CURSOR_COMPATIBILITY_SYMBOL } from 'decap-cms-lib-util';
|
||||
|
||||
import GitHubImplementation from '../implementation';
|
||||
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
describe('github backend implementation', () => {
|
||||
const config = {
|
||||
backend: {
|
||||
repo: 'owner/repo',
|
||||
open_authoring: false,
|
||||
api_root: 'https://api.github.com',
|
||||
},
|
||||
};
|
||||
|
||||
const createObjectURL = jest.fn();
|
||||
global.URL = {
|
||||
createObjectURL,
|
||||
};
|
||||
|
||||
createObjectURL.mockReturnValue('displayURL');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('forkExists', () => {
|
||||
it('should return true when repo is fork and parent matches originRepo', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.currentUser = jest.fn().mockResolvedValue({ login: 'login' });
|
||||
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
// matching should be case-insensitive
|
||||
json: () => ({ fork: true, parent: { full_name: 'OWNER/REPO' } }),
|
||||
});
|
||||
|
||||
await expect(gitHubImplementation.forkExists({ token: 'token' })).resolves.toBe(true);
|
||||
|
||||
expect(gitHubImplementation.currentUser).toHaveBeenCalledTimes(1);
|
||||
expect(gitHubImplementation.currentUser).toHaveBeenCalledWith({ token: 'token' });
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://api.github.com/repos/login/repo', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'token token',
|
||||
},
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false when repo is not a fork', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.currentUser = jest.fn().mockResolvedValue({ login: 'login' });
|
||||
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
// matching should be case-insensitive
|
||||
json: () => ({ fork: false }),
|
||||
});
|
||||
|
||||
expect.assertions(1);
|
||||
await expect(gitHubImplementation.forkExists({ token: 'token' })).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when parent doesn't match originRepo", async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.currentUser = jest.fn().mockResolvedValue({ login: 'login' });
|
||||
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
json: () => ({ fork: true, parent: { full_name: 'owner/other_repo' } }),
|
||||
});
|
||||
|
||||
expect.assertions(1);
|
||||
await expect(gitHubImplementation.forkExists({ token: 'token' })).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('persistMedia', () => {
|
||||
const persistFiles = jest.fn();
|
||||
const mockAPI = {
|
||||
persistFiles,
|
||||
};
|
||||
|
||||
persistFiles.mockImplementation((_, files) => {
|
||||
files.forEach((file, index) => {
|
||||
file.sha = index;
|
||||
});
|
||||
});
|
||||
|
||||
it('should persist media file', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.api = mockAPI;
|
||||
|
||||
const mediaFile = {
|
||||
fileObj: { size: 100, name: 'image.png' },
|
||||
path: '/media/image.png',
|
||||
};
|
||||
|
||||
expect.assertions(5);
|
||||
await expect(gitHubImplementation.persistMedia(mediaFile, {})).resolves.toEqual({
|
||||
id: 0,
|
||||
name: 'image.png',
|
||||
size: 100,
|
||||
displayURL: 'displayURL',
|
||||
path: 'media/image.png',
|
||||
});
|
||||
|
||||
expect(persistFiles).toHaveBeenCalledTimes(1);
|
||||
expect(persistFiles).toHaveBeenCalledWith([], [mediaFile], {});
|
||||
expect(createObjectURL).toHaveBeenCalledTimes(1);
|
||||
expect(createObjectURL).toHaveBeenCalledWith(mediaFile.fileObj);
|
||||
});
|
||||
|
||||
it('should log and throw error on "persistFiles" error', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.api = mockAPI;
|
||||
|
||||
const error = new Error('failed to persist files');
|
||||
persistFiles.mockRejectedValue(error);
|
||||
|
||||
const mediaFile = {
|
||||
value: 'image.png',
|
||||
fileObj: { size: 100 },
|
||||
path: '/media/image.png',
|
||||
};
|
||||
|
||||
expect.assertions(5);
|
||||
await expect(gitHubImplementation.persistMedia(mediaFile)).rejects.toThrowError(error);
|
||||
|
||||
expect(persistFiles).toHaveBeenCalledTimes(1);
|
||||
expect(createObjectURL).toHaveBeenCalledTimes(0);
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
expect(console.error).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unpublishedEntry', () => {
|
||||
const generateContentKey = jest.fn();
|
||||
const retrieveUnpublishedEntryData = jest.fn();
|
||||
|
||||
const mockAPI = {
|
||||
generateContentKey,
|
||||
retrieveUnpublishedEntryData,
|
||||
};
|
||||
|
||||
it('should return unpublished entry data', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.api = mockAPI;
|
||||
gitHubImplementation.loadEntryMediaFiles = jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ path: 'image.png', id: 'sha' }]);
|
||||
|
||||
generateContentKey.mockReturnValue('contentKey');
|
||||
|
||||
const data = {
|
||||
collection: 'collection',
|
||||
slug: 'slug',
|
||||
status: 'draft',
|
||||
diffs: [],
|
||||
updatedAt: 'updatedAt',
|
||||
};
|
||||
retrieveUnpublishedEntryData.mockResolvedValue(data);
|
||||
|
||||
const collection = 'posts';
|
||||
const slug = 'slug';
|
||||
await expect(gitHubImplementation.unpublishedEntry({ collection, slug })).resolves.toEqual(
|
||||
data,
|
||||
);
|
||||
|
||||
expect(generateContentKey).toHaveBeenCalledTimes(1);
|
||||
expect(generateContentKey).toHaveBeenCalledWith('posts', 'slug');
|
||||
|
||||
expect(retrieveUnpublishedEntryData).toHaveBeenCalledTimes(1);
|
||||
expect(retrieveUnpublishedEntryData).toHaveBeenCalledWith('contentKey');
|
||||
});
|
||||
});
|
||||
|
||||
describe('entriesByFolder', () => {
|
||||
const listFiles = jest.fn();
|
||||
const readFile = jest.fn();
|
||||
const readFileMetadata = jest.fn(() => Promise.resolve({ author: '', updatedOn: '' }));
|
||||
|
||||
const mockAPI = {
|
||||
listFiles,
|
||||
readFile,
|
||||
readFileMetadata,
|
||||
originRepoURL: 'originRepoURL',
|
||||
};
|
||||
|
||||
it('should return entries and cursor', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.api = mockAPI;
|
||||
|
||||
const files = [];
|
||||
const count = 1501;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const id = `${i}`.padStart(`${count}`.length, '0');
|
||||
files.push({
|
||||
id,
|
||||
path: `posts/post-${id}.md`,
|
||||
});
|
||||
}
|
||||
|
||||
listFiles.mockResolvedValue(files);
|
||||
readFile.mockImplementation((path, id) => Promise.resolve(`${id}`));
|
||||
|
||||
const expectedEntries = files
|
||||
.slice(0, 20)
|
||||
.map(({ id, path }) => ({ data: id, file: { path, id, author: '', updatedOn: '' } }));
|
||||
|
||||
const expectedCursor = Cursor.create({
|
||||
actions: ['next', 'last'],
|
||||
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
expectedEntries[CURSOR_COMPATIBILITY_SYMBOL] = expectedCursor;
|
||||
|
||||
const result = await gitHubImplementation.entriesByFolder('posts', 'md', 1);
|
||||
|
||||
expect(result).toEqual(expectedEntries);
|
||||
expect(listFiles).toHaveBeenCalledTimes(1);
|
||||
expect(listFiles).toHaveBeenCalledWith('posts', { depth: 1, repoURL: 'originRepoURL' });
|
||||
expect(readFile).toHaveBeenCalledTimes(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('traverseCursor', () => {
|
||||
const listFiles = jest.fn();
|
||||
const readFile = jest.fn((path, id) => Promise.resolve(`${id}`));
|
||||
const readFileMetadata = jest.fn(() => Promise.resolve({}));
|
||||
|
||||
const mockAPI = {
|
||||
listFiles,
|
||||
readFile,
|
||||
originRepoURL: 'originRepoURL',
|
||||
readFileMetadata,
|
||||
};
|
||||
|
||||
const files = [];
|
||||
const count = 1501;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const id = `${i}`.padStart(`${count}`.length, '0');
|
||||
files.push({
|
||||
id,
|
||||
path: `posts/post-${id}.md`,
|
||||
});
|
||||
}
|
||||
|
||||
it('should handle next action', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.api = mockAPI;
|
||||
|
||||
const cursor = Cursor.create({
|
||||
actions: ['next', 'last'],
|
||||
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const expectedEntries = files
|
||||
.slice(20, 40)
|
||||
.map(({ id, path }) => ({ data: id, file: { path, id } }));
|
||||
|
||||
const expectedCursor = Cursor.create({
|
||||
actions: ['prev', 'first', 'next', 'last'],
|
||||
meta: { page: 2, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const result = await gitHubImplementation.traverseCursor(cursor, 'next');
|
||||
|
||||
expect(result).toEqual({
|
||||
entries: expectedEntries,
|
||||
cursor: expectedCursor,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle prev action', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.api = mockAPI;
|
||||
|
||||
const cursor = Cursor.create({
|
||||
actions: ['prev', 'first', 'next', 'last'],
|
||||
meta: { page: 2, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const expectedEntries = files
|
||||
.slice(0, 20)
|
||||
.map(({ id, path }) => ({ data: id, file: { path, id } }));
|
||||
|
||||
const expectedCursor = Cursor.create({
|
||||
actions: ['next', 'last'],
|
||||
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const result = await gitHubImplementation.traverseCursor(cursor, 'prev');
|
||||
|
||||
expect(result).toEqual({
|
||||
entries: expectedEntries,
|
||||
cursor: expectedCursor,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle last action', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.api = mockAPI;
|
||||
|
||||
const cursor = Cursor.create({
|
||||
actions: ['next', 'last'],
|
||||
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const expectedEntries = files
|
||||
.slice(1500)
|
||||
.map(({ id, path }) => ({ data: id, file: { path, id } }));
|
||||
|
||||
const expectedCursor = Cursor.create({
|
||||
actions: ['prev', 'first'],
|
||||
meta: { page: 76, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const result = await gitHubImplementation.traverseCursor(cursor, 'last');
|
||||
|
||||
expect(result).toEqual({
|
||||
entries: expectedEntries,
|
||||
cursor: expectedCursor,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle first action', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.api = mockAPI;
|
||||
|
||||
const cursor = Cursor.create({
|
||||
actions: ['prev', 'first'],
|
||||
meta: { page: 76, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const expectedEntries = files
|
||||
.slice(0, 20)
|
||||
.map(({ id, path }) => ({ data: id, file: { path, id } }));
|
||||
|
||||
const expectedCursor = Cursor.create({
|
||||
actions: ['next', 'last'],
|
||||
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const result = await gitHubImplementation.traverseCursor(cursor, 'first');
|
||||
|
||||
expect(result).toEqual({
|
||||
entries: expectedEntries,
|
||||
cursor: expectedCursor,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,92 @@
|
||||
import { gql } from 'graphql-tag';
|
||||
|
||||
export const repository = gql`
|
||||
fragment RepositoryParts on Repository {
|
||||
id
|
||||
isFork
|
||||
}
|
||||
`;
|
||||
|
||||
export const blobWithText = gql`
|
||||
fragment BlobWithTextParts on Blob {
|
||||
id
|
||||
text
|
||||
is_binary: isBinary
|
||||
}
|
||||
`;
|
||||
|
||||
export const object = gql`
|
||||
fragment ObjectParts on GitObject {
|
||||
id
|
||||
sha: oid
|
||||
}
|
||||
`;
|
||||
|
||||
export const branch = gql`
|
||||
fragment BranchParts on Ref {
|
||||
commit: target {
|
||||
...ObjectParts
|
||||
}
|
||||
id
|
||||
name
|
||||
prefix
|
||||
repository {
|
||||
...RepositoryParts
|
||||
}
|
||||
}
|
||||
${object}
|
||||
${repository}
|
||||
`;
|
||||
|
||||
export const pullRequest = gql`
|
||||
fragment PullRequestParts on PullRequest {
|
||||
id
|
||||
baseRefName
|
||||
baseRefOid
|
||||
body
|
||||
headRefName
|
||||
headRefOid
|
||||
number
|
||||
state
|
||||
title
|
||||
merged_at: mergedAt
|
||||
updated_at: updatedAt
|
||||
user: author {
|
||||
login
|
||||
... on User {
|
||||
name
|
||||
}
|
||||
}
|
||||
repository {
|
||||
...RepositoryParts
|
||||
}
|
||||
labels(last: 100) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
${repository}
|
||||
`;
|
||||
|
||||
export const treeEntry = gql`
|
||||
fragment TreeEntryParts on TreeEntry {
|
||||
path: name
|
||||
sha: oid
|
||||
type
|
||||
mode
|
||||
}
|
||||
`;
|
||||
|
||||
export const fileEntry = gql`
|
||||
fragment FileEntryParts on TreeEntry {
|
||||
name
|
||||
sha: oid
|
||||
type
|
||||
blob: object {
|
||||
... on Blob {
|
||||
size: byteSize
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,719 @@
|
||||
import * as React from 'react';
|
||||
import semaphore from 'semaphore';
|
||||
import trimStart from 'lodash/trimStart';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import {
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
Cursor,
|
||||
asyncLock,
|
||||
basename,
|
||||
getBlobSHA,
|
||||
entriesByFolder,
|
||||
entriesByFiles,
|
||||
unpublishedEntries,
|
||||
getMediaDisplayURL,
|
||||
getMediaAsBlob,
|
||||
filterByExtension,
|
||||
getPreviewStatus,
|
||||
runWithLock,
|
||||
blobToFileObj,
|
||||
contentKeyFromBranch,
|
||||
unsentRequest,
|
||||
branchFromContentKey,
|
||||
} from 'decap-cms-lib-util';
|
||||
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import API, { API_NAME } from './API';
|
||||
import GraphQLAPI from './GraphQLAPI';
|
||||
|
||||
import type { Octokit } from '@octokit/rest';
|
||||
import type {
|
||||
AsyncLock,
|
||||
Implementation,
|
||||
AssetProxy,
|
||||
PersistOptions,
|
||||
DisplayURL,
|
||||
User,
|
||||
Credentials,
|
||||
Config,
|
||||
ImplementationFile,
|
||||
UnpublishedEntryMediaFile,
|
||||
Entry,
|
||||
} from 'decap-cms-lib-util';
|
||||
import type { Semaphore } from 'semaphore';
|
||||
|
||||
export type GitHubUser = Octokit.UsersGetAuthenticatedResponse;
|
||||
|
||||
const MAX_CONCURRENT_DOWNLOADS = 10;
|
||||
|
||||
type ApiFile = { id: string; type: string; name: string; path: string; size: number };
|
||||
|
||||
const { fetchWithTimeout: fetch } = unsentRequest;
|
||||
|
||||
const STATUS_PAGE = 'https://www.githubstatus.com';
|
||||
const GITHUB_STATUS_ENDPOINT = `${STATUS_PAGE}/api/v2/components.json`;
|
||||
const GITHUB_OPERATIONAL_UNITS = ['API Requests', 'Issues, Pull Requests, Projects'];
|
||||
type GitHubStatusComponent = {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
export default class GitHub implements Implementation {
|
||||
lock: AsyncLock;
|
||||
api: API | null;
|
||||
options: {
|
||||
proxied: boolean;
|
||||
API: API | null;
|
||||
useWorkflow?: boolean;
|
||||
initialWorkflowStatus: string;
|
||||
};
|
||||
originRepo: string;
|
||||
isBranchConfigured: boolean;
|
||||
repo?: string;
|
||||
openAuthoringEnabled: boolean;
|
||||
useOpenAuthoring?: boolean;
|
||||
alwaysForkEnabled: boolean;
|
||||
branch: string;
|
||||
apiRoot: string;
|
||||
mediaFolder: string;
|
||||
previewContext: string;
|
||||
token: string | null;
|
||||
tokenKeyword: string;
|
||||
squashMerges: boolean;
|
||||
cmsLabelPrefix: string;
|
||||
useGraphql: boolean;
|
||||
baseUrl?: string;
|
||||
bypassWriteAccessCheckForAppTokens = false;
|
||||
_currentUserPromise?: Promise<GitHubUser>;
|
||||
_userIsOriginMaintainerPromises?: {
|
||||
[key: string]: Promise<boolean>;
|
||||
};
|
||||
_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 GitHub backend needs a "repo" in the backend configuration.');
|
||||
}
|
||||
|
||||
this.api = this.options.API || null;
|
||||
this.isBranchConfigured = config.backend.branch ? true : false;
|
||||
this.openAuthoringEnabled = config.backend.open_authoring || false;
|
||||
if (this.openAuthoringEnabled) {
|
||||
if (!this.options.useWorkflow) {
|
||||
throw new Error(
|
||||
'backend.open_authoring is true but publish_mode is not set to editorial_workflow.',
|
||||
);
|
||||
}
|
||||
this.originRepo = config.backend.repo || '';
|
||||
} else {
|
||||
this.repo = this.originRepo = config.backend.repo || '';
|
||||
}
|
||||
this.alwaysForkEnabled = config.backend.always_fork || false;
|
||||
this.branch = config.backend.branch?.trim() || 'master';
|
||||
this.apiRoot = config.backend.api_root || 'https://api.github.com';
|
||||
this.token = '';
|
||||
this.tokenKeyword = 'token';
|
||||
this.baseUrl = config.backend.base_url;
|
||||
this.squashMerges = config.backend.squash_merges || false;
|
||||
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
|
||||
this.useGraphql = config.backend.use_graphql || false;
|
||||
this.mediaFolder = config.media_folder;
|
||||
this.previewContext = config.backend.preview_context || '';
|
||||
this.lock = asyncLock();
|
||||
}
|
||||
|
||||
isGitBackend() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async status() {
|
||||
const api = await fetch(GITHUB_STATUS_ENDPOINT)
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
return res['components']
|
||||
.filter((statusComponent: GitHubStatusComponent) =>
|
||||
GITHUB_OPERATIONAL_UNITS.includes(statusComponent.name),
|
||||
)
|
||||
.every(
|
||||
(statusComponent: GitHubStatusComponent) => statusComponent.status === 'operational',
|
||||
);
|
||||
})
|
||||
.catch(e => {
|
||||
console.warn('Failed getting GitHub status', e);
|
||||
return true;
|
||||
});
|
||||
|
||||
let auth = false;
|
||||
// no need to check auth if api is down
|
||||
if (api) {
|
||||
auth =
|
||||
(await this.api
|
||||
?.getUser({ token: this.token ?? '' })
|
||||
.then(user => !!user)
|
||||
.catch(e => {
|
||||
console.warn('Failed getting GitHub user', e);
|
||||
return false;
|
||||
})) || false;
|
||||
}
|
||||
|
||||
return { auth: { status: auth }, api: { status: api, statusPage: STATUS_PAGE } };
|
||||
}
|
||||
|
||||
authComponent() {
|
||||
const wrappedAuthenticationPage = (props: Record<string, unknown>) => (
|
||||
<AuthenticationPage {...props} backend={this} />
|
||||
);
|
||||
wrappedAuthenticationPage.displayName = 'AuthenticationPage';
|
||||
return wrappedAuthenticationPage;
|
||||
}
|
||||
|
||||
restoreUser(user: User) {
|
||||
return this.openAuthoringEnabled
|
||||
? this.authenticateWithFork({ userData: user, getPermissionToFork: () => true }).then(() =>
|
||||
this.authenticate(user),
|
||||
)
|
||||
: this.authenticate(user);
|
||||
}
|
||||
|
||||
async pollUntilForkExists({ repo, token }: { repo: string; token: string }) {
|
||||
const pollDelay = 250; // milliseconds
|
||||
let repoExists = false;
|
||||
while (!repoExists) {
|
||||
repoExists = await fetch(`${this.apiRoot}/repos/${repo}`, {
|
||||
headers: { Authorization: `${this.tokenKeyword} ${token}` },
|
||||
})
|
||||
.then(() => true)
|
||||
.catch(err => {
|
||||
if (err && err.status === 404) {
|
||||
console.log('This 404 was expected and handled appropriately.');
|
||||
return false;
|
||||
} else {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
});
|
||||
// wait between polls
|
||||
if (!repoExists) {
|
||||
await new Promise(resolve => setTimeout(resolve, pollDelay));
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async currentUser({ token }: { token: string }) {
|
||||
if (!this._currentUserPromise) {
|
||||
this._currentUserPromise = fetch(`${this.apiRoot}/user`, {
|
||||
headers: {
|
||||
Authorization: `${this.tokenKeyword} ${token}`,
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
return this._currentUserPromise;
|
||||
}
|
||||
|
||||
async userIsOriginMaintainer({
|
||||
username: usernameArg,
|
||||
token,
|
||||
}: {
|
||||
username?: string;
|
||||
token: string;
|
||||
}) {
|
||||
const username = usernameArg || (await this.currentUser({ token })).login;
|
||||
this._userIsOriginMaintainerPromises = this._userIsOriginMaintainerPromises || {};
|
||||
if (!this._userIsOriginMaintainerPromises[username]) {
|
||||
this._userIsOriginMaintainerPromises[username] = fetch(
|
||||
`${this.apiRoot}/repos/${this.originRepo}/collaborators/${username}/permission`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `${this.tokenKeyword} ${token}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
.then(res => res.json())
|
||||
.then(({ permission }) => permission === 'admin' || permission === 'write');
|
||||
}
|
||||
return this._userIsOriginMaintainerPromises[username];
|
||||
}
|
||||
|
||||
async forkExists({ token }: { token: string }) {
|
||||
try {
|
||||
const currentUser = await this.currentUser({ token });
|
||||
const repoName = this.originRepo.split('/')[1];
|
||||
const repo = await fetch(`${this.apiRoot}/repos/${currentUser.login}/${repoName}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `${this.tokenKeyword} ${token}`,
|
||||
},
|
||||
}).then(res => res.json());
|
||||
|
||||
// https://developer.github.com/v3/repos/#get
|
||||
// The parent and source objects are present when the repository is a fork.
|
||||
// parent is the repository this repository was forked from, source is the ultimate source for the network.
|
||||
const forkExists =
|
||||
repo.fork === true &&
|
||||
repo.parent &&
|
||||
repo.parent.full_name.toLowerCase() === this.originRepo.toLowerCase();
|
||||
return forkExists;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async authenticateWithFork({
|
||||
userData,
|
||||
getPermissionToFork,
|
||||
}: {
|
||||
userData: User;
|
||||
getPermissionToFork: () => Promise<boolean> | boolean;
|
||||
}) {
|
||||
if (!this.openAuthoringEnabled) {
|
||||
throw new Error('Cannot authenticate with fork; Open Authoring is turned off.');
|
||||
}
|
||||
const token = userData.token as string;
|
||||
|
||||
// Origin maintainers should be able to use the CMS normally. If alwaysFork
|
||||
// is enabled we always fork (and avoid the origin maintainer check)
|
||||
if (!this.alwaysForkEnabled && (await this.userIsOriginMaintainer({ token }))) {
|
||||
this.repo = this.originRepo;
|
||||
this.useOpenAuthoring = false;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// If a fork exists merge it with upstream
|
||||
// otherwise create a new fork.
|
||||
const currentUser = await this.currentUser({ token });
|
||||
const repoName = this.originRepo.split('/')[1];
|
||||
this.repo = `${currentUser.login}/${repoName}`;
|
||||
this.useOpenAuthoring = true;
|
||||
|
||||
if (await this.forkExists({ token })) {
|
||||
return fetch(`${this.apiRoot}/repos/${this.repo}/merge-upstream`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `${this.tokenKeyword} ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
branch: this.branch,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
await getPermissionToFork();
|
||||
|
||||
const fork = await fetch(`${this.apiRoot}/repos/${this.originRepo}/forks`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `${this.tokenKeyword} ${token}`,
|
||||
},
|
||||
}).then(res => res.json());
|
||||
return this.pollUntilForkExists({ repo: fork.full_name, token });
|
||||
}
|
||||
}
|
||||
|
||||
async authenticate(state: Credentials) {
|
||||
this.token = state.token as string;
|
||||
// Query the default branch name when the `branch` property is missing
|
||||
// in the config file
|
||||
if (!this.isBranchConfigured) {
|
||||
const repoInfo = await fetch(`${this.apiRoot}/repos/${this.originRepo}`, {
|
||||
headers: { Authorization: `token ${this.token}` },
|
||||
})
|
||||
.then(res => res.json())
|
||||
.catch(() => null);
|
||||
if (repoInfo && repoInfo.default_branch) {
|
||||
this.branch = repoInfo.default_branch;
|
||||
}
|
||||
}
|
||||
const apiCtor = this.useGraphql ? GraphQLAPI : API;
|
||||
this.api = new apiCtor({
|
||||
token: this.token,
|
||||
tokenKeyword: this.tokenKeyword,
|
||||
branch: this.branch,
|
||||
repo: this.repo,
|
||||
originRepo: this.originRepo,
|
||||
apiRoot: this.apiRoot,
|
||||
squashMerges: this.squashMerges,
|
||||
cmsLabelPrefix: this.cmsLabelPrefix,
|
||||
useOpenAuthoring: this.useOpenAuthoring,
|
||||
initialWorkflowStatus: this.options.initialWorkflowStatus,
|
||||
baseUrl: this.baseUrl,
|
||||
getUser: this.currentUser,
|
||||
});
|
||||
const user = await this.api!.user();
|
||||
const isCollab = await this.api!.hasWriteAccess().catch(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 GitHub account with access.
|
||||
|
||||
If your repo is under an organization, ensure the organization has granted access to Decap CMS.
|
||||
`;
|
||||
throw error;
|
||||
});
|
||||
|
||||
// Unauthorized user
|
||||
if (!isCollab && !this.bypassWriteAccessCheckForAppTokens) {
|
||||
throw new Error('Your GitHub user account does not have access to this repo.');
|
||||
}
|
||||
|
||||
// if (!this.isBranchConfigured) {
|
||||
// const defaultBranchName = await this.api.getDefaultBranchName()
|
||||
// if (defaultBranchName) {
|
||||
// this.branch = defaultBranchName;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Authorized user
|
||||
return { ...user, token: state.token as string, useOpenAuthoring: this.useOpenAuthoring };
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.token = null;
|
||||
if (this.api && this.api.reset && typeof this.api.reset === 'function') {
|
||||
return this.api.reset();
|
||||
}
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return Promise.resolve(this.token);
|
||||
}
|
||||
|
||||
getCursorAndFiles = (files: ApiFile[], page: number) => {
|
||||
const pageSize = 20;
|
||||
const count = files.length;
|
||||
const pageCount = Math.ceil(files.length / pageSize);
|
||||
|
||||
const actions = [] as string[];
|
||||
if (page > 1) {
|
||||
actions.push('prev');
|
||||
actions.push('first');
|
||||
}
|
||||
if (page < pageCount) {
|
||||
actions.push('next');
|
||||
actions.push('last');
|
||||
}
|
||||
|
||||
const cursor = Cursor.create({
|
||||
actions,
|
||||
meta: { page, count, pageSize, pageCount },
|
||||
data: { files },
|
||||
});
|
||||
const pageFiles = files.slice((page - 1) * pageSize, page * pageSize);
|
||||
return { cursor, files: pageFiles };
|
||||
};
|
||||
|
||||
async entriesByFolder(folder: string, extension: string, depth: number) {
|
||||
const repoURL = this.api!.originRepoURL;
|
||||
|
||||
let cursor: Cursor;
|
||||
|
||||
const listFiles = () =>
|
||||
this.api!.listFiles(folder, {
|
||||
repoURL,
|
||||
depth,
|
||||
}).then(files => {
|
||||
const filtered = files.filter(file => filterByExtension(file, extension));
|
||||
const result = this.getCursorAndFiles(filtered, 1);
|
||||
cursor = result.cursor;
|
||||
return result.files;
|
||||
});
|
||||
|
||||
const readFile = (path: string, id: string | null | undefined) =>
|
||||
this.api!.readFile(path, id, { repoURL }) as Promise<string>;
|
||||
|
||||
const files = await entriesByFolder(
|
||||
listFiles,
|
||||
readFile,
|
||||
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 allEntriesByFolder(folder: string, extension: string, depth: number, pathRegex?: RegExp) {
|
||||
const repoURL = this.api!.originRepoURL;
|
||||
|
||||
const listFiles = () =>
|
||||
this.api!.listFiles(folder, {
|
||||
repoURL,
|
||||
depth,
|
||||
}).then(files =>
|
||||
files.filter(
|
||||
file => (!pathRegex || pathRegex.test(file.path)) && filterByExtension(file, extension),
|
||||
),
|
||||
);
|
||||
|
||||
const readFile = (path: string, id: string | null | undefined) => {
|
||||
return this.api!.readFile(path, id, { repoURL }) as Promise<string>;
|
||||
};
|
||||
|
||||
const files = await entriesByFolder(
|
||||
listFiles,
|
||||
readFile,
|
||||
this.api!.readFileMetadata.bind(this.api),
|
||||
API_NAME,
|
||||
);
|
||||
return files;
|
||||
}
|
||||
|
||||
entriesByFiles(files: ImplementationFile[]) {
|
||||
const repoURL = this.useOpenAuthoring ? this.api!.originRepoURL : this.api!.repoURL;
|
||||
|
||||
const readFile = (path: string, id: string | null | undefined) =>
|
||||
this.api!.readFile(path, id, { repoURL }).catch(() => '') as Promise<string>;
|
||||
|
||||
return entriesByFiles(files, readFile, this.api!.readFileMetadata.bind(this.api), API_NAME);
|
||||
}
|
||||
|
||||
// Fetches a single entry.
|
||||
getEntry(path: string) {
|
||||
const repoURL = this.api!.originRepoURL;
|
||||
return this.api!.readFile(path, null, { repoURL })
|
||||
.then(data => ({
|
||||
file: { path, id: null },
|
||||
data: data as string,
|
||||
}))
|
||||
.catch(() => ({ file: { path, id: null }, data: '' }));
|
||||
}
|
||||
|
||||
getMedia(mediaFolder = this.mediaFolder) {
|
||||
return this.api!.listFiles(mediaFolder).then(files =>
|
||||
files.map(({ id, name, size, path }) => {
|
||||
// load media using getMediaDisplayURL to avoid token expiration with GitHub raw content urls
|
||||
// for private repositories
|
||||
return { id, name, size, displayURL: { id, path }, path };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getMediaFile(path: string) {
|
||||
const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!));
|
||||
|
||||
const name = basename(path);
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
getMediaDisplayURL(displayURL: DisplayURL) {
|
||||
this._mediaDisplayURLSem = this._mediaDisplayURLSem || semaphore(MAX_CONCURRENT_DOWNLOADS);
|
||||
return getMediaDisplayURL(
|
||||
displayURL,
|
||||
this.api!.readFile.bind(this.api!),
|
||||
this._mediaDisplayURLSem,
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
await this.api!.persistFiles([], [mediaFile], options);
|
||||
const { sha, path, fileObj } = mediaFile as AssetProxy & { sha: string };
|
||||
const displayURL = fileObj ? URL.createObjectURL(fileObj) : '';
|
||||
return {
|
||||
id: sha,
|
||||
name: fileObj!.name,
|
||||
size: fileObj!.size,
|
||||
displayURL,
|
||||
path: trimStart(path, '/'),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
deleteFiles(paths: string[], commitMessage: string) {
|
||||
return this.api!.deleteFiles(paths, commitMessage);
|
||||
}
|
||||
|
||||
async traverseCursor(cursor: Cursor, action: string) {
|
||||
const meta = cursor.meta!;
|
||||
const files = cursor.data!.get('files')!.toJS() as ApiFile[];
|
||||
|
||||
let result: { cursor: Cursor; files: ApiFile[] };
|
||||
switch (action) {
|
||||
case 'first': {
|
||||
result = this.getCursorAndFiles(files, 1);
|
||||
break;
|
||||
}
|
||||
case 'last': {
|
||||
result = this.getCursorAndFiles(files, meta.get('pageCount'));
|
||||
break;
|
||||
}
|
||||
case 'next': {
|
||||
result = this.getCursorAndFiles(files, meta.get('page') + 1);
|
||||
break;
|
||||
}
|
||||
case 'prev': {
|
||||
result = this.getCursorAndFiles(files, meta.get('page') - 1);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
result = this.getCursorAndFiles(files, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const readFile = (path: string, id: string | null | undefined) =>
|
||||
this.api!.readFile(path, id, { repoURL: this.api!.originRepoURL }).catch(
|
||||
() => '',
|
||||
) as Promise<string>;
|
||||
|
||||
const entries = await entriesByFiles(
|
||||
result.files,
|
||||
readFile,
|
||||
this.api!.readFileMetadata.bind(this.api),
|
||||
API_NAME,
|
||||
);
|
||||
|
||||
return {
|
||||
entries,
|
||||
cursor: result.cursor,
|
||||
};
|
||||
}
|
||||
|
||||
async loadMediaFile(branch: string, file: UnpublishedEntryMediaFile) {
|
||||
const readFile = (
|
||||
path: string,
|
||||
id: string | null | undefined,
|
||||
{ parseText }: { parseText: boolean },
|
||||
) => this.api!.readFile(path, id, { branch, parseText });
|
||||
|
||||
const blob = await getMediaAsBlob(file.path, file.id, readFile);
|
||||
const name = basename(file.path);
|
||||
const fileObj = blobToFileObj(name, blob);
|
||||
return {
|
||||
id: file.id,
|
||||
displayURL: URL.createObjectURL(fileObj),
|
||||
path: file.path,
|
||||
name,
|
||||
size: fileObj.size,
|
||||
file: fileObj,
|
||||
};
|
||||
}
|
||||
|
||||
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 = this.api!.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 = this.api!.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 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;
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
);
|
||||
}
|
||||
}
|
||||
10
source/admin/packages/decap-cms-backend-github/src/index.ts
Normal file
10
source/admin/packages/decap-cms-backend-github/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import GitHubBackend from './implementation';
|
||||
import API from './API';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
|
||||
export const DecapCmsBackendGithub = {
|
||||
GitHubBackend,
|
||||
API,
|
||||
AuthenticationPage,
|
||||
};
|
||||
export { GitHubBackend, API, AuthenticationPage };
|
||||
110
source/admin/packages/decap-cms-backend-github/src/mutations.ts
Normal file
110
source/admin/packages/decap-cms-backend-github/src/mutations.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { gql } from 'graphql-tag';
|
||||
|
||||
import * as fragments from './fragments';
|
||||
|
||||
// updateRef only works for branches at the moment
|
||||
export const updateBranch = gql`
|
||||
mutation updateRef($input: UpdateRefInput!) {
|
||||
updateRef(input: $input) {
|
||||
branch: ref {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
// deleteRef only works for branches at the moment
|
||||
const deleteRefMutationPart = `
|
||||
deleteRef(input: $deleteRefInput) {
|
||||
clientMutationId
|
||||
}
|
||||
`;
|
||||
export const deleteBranch = gql`
|
||||
mutation deleteRef($deleteRefInput: DeleteRefInput!) {
|
||||
${deleteRefMutationPart}
|
||||
}
|
||||
`;
|
||||
|
||||
const closePullRequestMutationPart = `
|
||||
closePullRequest(input: $closePullRequestInput) {
|
||||
clientMutationId
|
||||
pullRequest {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const closePullRequest = gql`
|
||||
mutation closePullRequestAndDeleteBranch($closePullRequestInput: ClosePullRequestInput!) {
|
||||
${closePullRequestMutationPart}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const closePullRequestAndDeleteBranch = gql`
|
||||
mutation closePullRequestAndDeleteBranch(
|
||||
$closePullRequestInput: ClosePullRequestInput!
|
||||
$deleteRefInput: DeleteRefInput!
|
||||
) {
|
||||
${closePullRequestMutationPart}
|
||||
${deleteRefMutationPart}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
const createPullRequestMutationPart = `
|
||||
createPullRequest(input: $createPullRequestInput) {
|
||||
clientMutationId
|
||||
pullRequest {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const createPullRequest = gql`
|
||||
mutation createPullRequest($createPullRequestInput: CreatePullRequestInput!) {
|
||||
${createPullRequestMutationPart}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const createBranch = gql`
|
||||
mutation createBranch($createRefInput: CreateRefInput!) {
|
||||
createRef(input: $createRefInput) {
|
||||
branch: ref {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
// createRef only works for branches at the moment
|
||||
export const createBranchAndPullRequest = gql`
|
||||
mutation createBranchAndPullRequest(
|
||||
$createRefInput: CreateRefInput!
|
||||
$createPullRequestInput: CreatePullRequestInput!
|
||||
) {
|
||||
createRef(input: $createRefInput) {
|
||||
branch: ref {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
${createPullRequestMutationPart}
|
||||
}
|
||||
${fragments.branch}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const reopenPullRequest = gql`
|
||||
mutation reopenPullRequest($reopenPullRequestInput: ReopenPullRequestInput!) {
|
||||
reopenPullRequest(input: $reopenPullRequestInput) {
|
||||
clientMutationId
|
||||
pullRequest {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
213
source/admin/packages/decap-cms-backend-github/src/queries.ts
Normal file
213
source/admin/packages/decap-cms-backend-github/src/queries.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { gql } from 'graphql-tag';
|
||||
import { oneLine } from 'common-tags';
|
||||
|
||||
import * as fragments from './fragments';
|
||||
|
||||
export const repoPermission = gql`
|
||||
query repoPermission($owner: String!, $name: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
viewerPermission
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
`;
|
||||
|
||||
export const user = gql`
|
||||
query {
|
||||
viewer {
|
||||
id
|
||||
avatar_url: avatarUrl
|
||||
name
|
||||
login
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const blob = gql`
|
||||
query blob($owner: String!, $name: String!, $expression: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
object(expression: $expression) {
|
||||
... on Blob {
|
||||
...BlobWithTextParts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.blobWithText}
|
||||
`;
|
||||
|
||||
export const statues = gql`
|
||||
query statues($owner: String!, $name: String!, $sha: GitObjectID!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
object(oid: $sha) {
|
||||
...ObjectParts
|
||||
... on Commit {
|
||||
status {
|
||||
id
|
||||
contexts {
|
||||
id
|
||||
context
|
||||
state
|
||||
target_url: targetUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.object}
|
||||
`;
|
||||
|
||||
function buildFilesQuery(depth = 1) {
|
||||
const PLACE_HOLDER = 'PLACE_HOLDER';
|
||||
let query = oneLine`
|
||||
...ObjectParts
|
||||
... on Tree {
|
||||
entries {
|
||||
...FileEntryParts
|
||||
${PLACE_HOLDER}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
for (let i = 0; i < depth - 1; i++) {
|
||||
query = query.replace(
|
||||
PLACE_HOLDER,
|
||||
oneLine`
|
||||
object {
|
||||
... on Tree {
|
||||
entries {
|
||||
...FileEntryParts
|
||||
${PLACE_HOLDER}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
query = query.replace(PLACE_HOLDER, '');
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
export function files(depth: number) {
|
||||
return gql`
|
||||
query files($owner: String!, $name: String!, $expression: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
object(expression: $expression) {
|
||||
${buildFilesQuery(depth)}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.object}
|
||||
${fragments.fileEntry}
|
||||
`;
|
||||
}
|
||||
|
||||
const branchQueryPart = `
|
||||
branch: ref(qualifiedName: $qualifiedName) {
|
||||
...BranchParts
|
||||
}
|
||||
`;
|
||||
|
||||
export const branch = gql`
|
||||
query branch($owner: String!, $name: String!, $qualifiedName: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
${branchQueryPart}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
export const openAuthoringBranches = gql`
|
||||
query openAuthoringBranches($owner: String!, $name: String!, $refPrefix: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
refs(refPrefix: $refPrefix, last: 100) {
|
||||
nodes {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
export const repository = gql`
|
||||
query repository($owner: String!, $name: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
`;
|
||||
|
||||
const pullRequestQueryPart = `
|
||||
pullRequest(number: $number) {
|
||||
...PullRequestParts
|
||||
}
|
||||
`;
|
||||
|
||||
export const pullRequest = gql`
|
||||
query pullRequest($owner: String!, $name: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
id
|
||||
${pullRequestQueryPart}
|
||||
}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const pullRequests = gql`
|
||||
query pullRequests($owner: String!, $name: String!, $head: String, $states: [PullRequestState!]) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
id
|
||||
pullRequests(last: 100, headRefName: $head, states: $states) {
|
||||
nodes {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const pullRequestAndBranch = gql`
|
||||
query pullRequestAndBranch($owner: String!, $name: String!, $originRepoOwner: String!, $originRepoName: String!, $qualifiedName: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
${branchQueryPart}
|
||||
}
|
||||
origin: repository(owner: $originRepoOwner, name: $originRepoName) {
|
||||
...RepositoryParts
|
||||
${pullRequestQueryPart}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.branch}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const fileSha = gql`
|
||||
query fileSha($owner: String!, $name: String!, $expression: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
file: object(expression: $expression) {
|
||||
...ObjectParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.object}
|
||||
`;
|
||||
5
source/admin/packages/decap-cms-backend-github/src/types/semaphore.d.ts
vendored
Normal file
5
source/admin/packages/decap-cms-backend-github/src/types/semaphore.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module 'semaphore' {
|
||||
export type Semaphore = { take: (f: Function) => void; leave: () => void };
|
||||
const semaphore: (count: number) => Semaphore;
|
||||
export default semaphore;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
const { getConfig } = require('../../scripts/webpack.js');
|
||||
|
||||
module.exports = getConfig();
|
||||
Reference in New Issue
Block a user