planning
All checks were successful
Publish To Prod / deploy_and_publish (push) Successful in 35s

This commit is contained in:
2024-10-14 09:15:30 +02:00
parent bcba00a730
commit 6e64e138e2
21059 changed files with 2317811 additions and 1 deletions

379
node_modules/decap-cms-backend-bitbucket/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,379 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.1.4](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-bitbucket@3.1.3...decap-cms-backend-bitbucket@3.1.4) (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.1.3](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-bitbucket@3.1.2...decap-cms-backend-bitbucket@3.1.3) (2024-08-13)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## [3.1.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-bitbucket@3.1.1...decap-cms-backend-bitbucket@3.1.2) (2024-04-03)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## [3.1.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-bitbucket@3.1.0-beta.1...decap-cms-backend-bitbucket@3.1.1) (2024-03-21)
**Note:** Version bump only for package decap-cms-backend-bitbucket
# [3.1.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-bitbucket@3.1.0-beta.1...decap-cms-backend-bitbucket@3.1.0) (2024-02-01)
**Note:** Version bump only for package decap-cms-backend-bitbucket
# [3.1.0-beta.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-bitbucket@3.1.0-beta.0...decap-cms-backend-bitbucket@3.1.0-beta.1) (2024-01-31)
**Note:** Version bump only for package decap-cms-backend-bitbucket
# [3.1.0-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-bitbucket@3.1.0...decap-cms-backend-bitbucket@3.1.0-beta.0) (2023-10-20)
### Reverts
- Revert "chore(release): publish" ([b89fc89](https://github.com/decaporg/decap-cms/commit/b89fc894dfbb5f4136b2e5427fd25a29378a58c6))
## [3.0.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-bitbucket@3.0.1...decap-cms-backend-bitbucket@3.0.2) (2023-10-13)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## [3.0.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-bitbucket@3.0.0...decap-cms-backend-bitbucket@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-bitbucket@2.15.0...decap-cms-backend-bitbucket@3.0.0) (2023-08-18)
**Note:** Version bump only for package decap-cms-backend-bitbucket
# [2.15.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-bitbucket@2.15.0-beta.0...decap-cms-backend-bitbucket@2.15.0) (2023-08-18)
**Note:** Version bump only for package decap-cms-backend-bitbucket
# 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.1-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-bitbucket@2.14.0...decap-cms-backend-bitbucket@2.14.1-beta.0) (2023-07-27)
**Note:** Version bump only for package decap-cms-backend-bitbucket
# [2.14.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-bitbucket@2.13.3...decap-cms-backend-bitbucket@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.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.13.2...decap-cms-backend-bitbucket@2.13.3) (2021-06-01)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## [2.13.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.13.1...decap-cms-backend-bitbucket@2.13.2) (2021-05-31)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## [2.13.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.13.0...decap-cms-backend-bitbucket@2.13.1) (2021-05-19)
**Note:** Version bump only for package decap-cms-backend-bitbucket
# [2.13.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.12.8...decap-cms-backend-bitbucket@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-bitbucket/issues/5316)) ([9e42380](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/9e423805707321396eec137f5b732a5b07a0dd3f))
## [2.12.8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.12.7...decap-cms-backend-bitbucket@2.12.8) (2021-02-23)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## [2.12.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.12.6...decap-cms-backend-bitbucket@2.12.7) (2021-02-15)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## [2.12.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.12.5...decap-cms-backend-bitbucket@2.12.6) (2021-02-10)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## [2.12.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.12.4...decap-cms-backend-bitbucket@2.12.5) (2020-09-20)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## [2.12.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.12.3...decap-cms-backend-bitbucket@2.12.4) (2020-09-15)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## 2.12.3 (2020-09-08)
### Reverts
- Revert "chore(release): publish" ([828bb16](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/828bb16415b8c22a34caa19c50c38b24ffe9ceae))
## 2.12.2 (2020-08-20)
### Reverts
- Revert "chore(release): publish" ([8262487](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/82624879ccbcb16610090041db28f00714d924c8))
## 2.12.1 (2020-07-27)
### Reverts
- Revert "chore(release): publish" ([118d50a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/118d50a7a70295f25073e564b5161aa2b9883056))
# [2.12.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.11.5...decap-cms-backend-bitbucket@2.12.0) (2020-06-18)
### Bug Fixes
- handle token expiry ([#3847](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/3847)) ([285c940](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/285c940562548d7bc88de244123ba87ff66fba65))
### Features
- add backend status down indicator ([#3889](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/3889)) ([a50edc7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/a50edc70553ad6afa1acee6a51996ad226443f8c))
- **backend-gitgateway:** improve deploy preview visibility ([#3882](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/3882)) ([afc9bf4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/afc9bf4f3fe14ccb60851fc24e68922a6e4a85a9))
## [2.11.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.11.4...decap-cms-backend-bitbucket@2.11.5) (2020-05-19)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## [2.11.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.11.3...decap-cms-backend-bitbucket@2.11.4) (2020-04-21)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## [2.11.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.11.2...decap-cms-backend-bitbucket@2.11.3) (2020-04-01)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## [2.11.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.11.1...decap-cms-backend-bitbucket@2.11.2) (2020-03-20)
### Bug Fixes
- missing workflow timestamp ([#3445](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/3445)) ([9616cdb](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/9616cdb8bb0a564771e5755bcd3718a07f2e2072))
## [2.11.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.11.0...decap-cms-backend-bitbucket@2.11.1) (2020-03-03)
### Bug Fixes
- **locale:** Remove hard coded string literals ([#3333](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/3333)) ([7c45a3c](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/7c45a3cda983be427864a56e58791565eb9232e2))
# [2.11.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.10.1...decap-cms-backend-bitbucket@2.11.0) (2020-02-25)
### Features
- **core:** align GitHub metadata handling with other backends ([#3316](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/3316)) ([7e0a8ad](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/7e0a8ad532012576dc5e40bd4e9d54522e307123)), closes [#3292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/3292)
## [2.10.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.10.0...decap-cms-backend-bitbucket@2.10.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-bitbucket/commit/5bdd3df9ccbb5149c22d79987ebdcd6cab4b261f)), closes [#3292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/3292)
# [2.10.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.9.0...decap-cms-backend-bitbucket@2.10.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-bitbucket/issues/3292)) ([8193b5a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/8193b5ace89d6f14a6c756235a50b186a763b6b1))
# [2.9.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.8.1...decap-cms-backend-bitbucket@2.9.0) (2020-02-10)
### Bug Fixes
- filter paginated results ([#3216](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/3216)) ([0a482b1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/0a482b10049bcfa022b81165cabf4512d77e0b88))
- workflow file collection ([#3207](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/3207)) ([d22f7e6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/d22f7e680e7064e8607cf8b420571fa40a6c314e))
### Features
- field based media/public folders ([#3208](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/3208)) ([97bc0c8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/97bc0c8dc489e736f89d748ba832d78400fe4332))
### Reverts
- Revert "chore(release): publish" ([a015d1d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/a015d1d92a4b1c0130c44fcef1c9ecdb157a0f07))
## [2.8.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.8.0...decap-cms-backend-bitbucket@2.8.1) (2020-02-06)
### Bug Fixes
- **locale:** remove hard coded strings ([#3193](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/3193)) ([fc91bf8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/fc91bf8781e65ce1dc946363dbb10419a145c66b))
# [2.8.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.7.2...decap-cms-backend-bitbucket@2.8.0) (2020-01-21)
### Features
- **backend-bitbucket:** Add Git-LFS support ([#3118](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/3118)) ([a48c02d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/a48c02d852ca5e11055da3a14cefae8d17a68498))
## [2.7.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.7.1...decap-cms-backend-bitbucket@2.7.2) (2020-01-14)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## [2.7.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.7.0...decap-cms-backend-bitbucket@2.7.1) (2020-01-09)
### Bug Fixes
- **backend-bitbucket:** fix media library not loaded on BitBucket ([#3059](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/3059)) ([8849c0e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/8849c0ea5777ee0eb85375d6e4a74d7f956c77ee))
# [2.7.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.7.0-beta.0...decap-cms-backend-bitbucket@2.7.0) (2020-01-07)
**Note:** Version bump only for package decap-cms-backend-bitbucket
# [2.7.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.6.0...decap-cms-backend-bitbucket@2.7.0-beta.0) (2019-12-18)
### Features
- bundle assets with content ([#2958](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/2958)) ([2b41d8a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/2b41d8a838a9c8a6b21cde2ddd16b9288334e298))
# [2.6.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.6.0-beta.1...decap-cms-backend-bitbucket@2.6.0) (2019-12-18)
### Bug Fixes
- **backend-bitbucket:** 404 for new entry ([#2976](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/2976)) ([20851fe](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/20851fe7eae0487484e775c9cb219d1aa973e878))
# [2.6.0-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.6.0-beta.0...decap-cms-backend-bitbucket@2.6.0-beta.1) (2019-12-16)
### Bug Fixes
- **bitbucket:** branchname containing slash ([#2963](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/2963)) ([afea448](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/afea44895b9ef7379f5a8726a60fb4d371c76ebf))
# [2.6.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.5.0...decap-cms-backend-bitbucket@2.6.0-beta.0) (2019-12-02)
### Features
- content in sub folders ([#2897](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/2897)) ([afcfe5b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/afcfe5b6d5f32669e9061ec596bd35ad545d61a3))
# [2.5.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.4.2...decap-cms-backend-bitbucket@2.5.0) (2019-11-07)
### Features
- add go back to site button ([#2538](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/2538)) ([f206e7e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/f206e7e5a13fb48ec6b27dce0dbb3a59b61de8f9))
- enable specifying custom open authoring commit message ([#2810](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/2810)) ([2841ff9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/2841ff9ffe58afcf4dba45514a84a262ad370f1d))
## [2.4.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.4.1...decap-cms-backend-bitbucket@2.4.2) (2019-09-26)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## [2.4.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.4.0...decap-cms-backend-bitbucket@2.4.1) (2019-07-24)
**Note:** Version bump only for package decap-cms-backend-bitbucket
# [2.4.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.4.0-beta.0...decap-cms-backend-bitbucket@2.4.0) (2019-04-10)
**Note:** Version bump only for package decap-cms-backend-bitbucket
# [2.4.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.3.1...decap-cms-backend-bitbucket@2.4.0-beta.0) (2019-04-05)
### Features
- **backend-bitbucket:** add implicit auth ([#2247](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/2247)) ([54fde06](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/54fde06))
## [2.3.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.3.1-beta.1...decap-cms-backend-bitbucket@2.3.1) (2019-03-29)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## [2.3.1-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.3.1-beta.0...decap-cms-backend-bitbucket@2.3.1-beta.1) (2019-03-26)
### Bug Fixes
- export on decap-cms and maps on esm ([#2244](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/2244)) ([6ffd13b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/6ffd13b))
## [2.3.1-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.3.0...decap-cms-backend-bitbucket@2.3.1-beta.0) (2019-03-25)
### Bug Fixes
- update peer dep versions ([#2234](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/2234)) ([7987091](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/7987091))
# [2.3.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.2.0...decap-cms-backend-bitbucket@2.3.0) (2019-03-22)
### Features
- add ES module builds ([#2215](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/2215)) ([d142b32](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/d142b32))
# [2.2.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.2.0-beta.0...decap-cms-backend-bitbucket@2.2.0) (2019-03-22)
**Note:** Version bump only for package decap-cms-backend-bitbucket
# [2.2.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.1.3-beta.0...decap-cms-backend-bitbucket@2.2.0-beta.0) (2019-03-21)
### Bug Fixes
- fix umd builds ([#2214](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/2214)) ([e04f6be](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/e04f6be))
### Features
- provide usable UMD builds for all packages ([#2141](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/2141)) ([82cc794](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/82cc794))
## [2.1.3-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.1.2...decap-cms-backend-bitbucket@2.1.3-beta.0) (2019-03-15)
### Features
- upgrade to Emotion 10 ([#2166](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/2166)) ([ccef446](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/ccef446))
## [2.1.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.1.1...decap-cms-backend-bitbucket@2.1.2) (2019-03-08)
**Note:** Version bump only for package decap-cms-backend-bitbucket
## [2.1.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.1.0...decap-cms-backend-bitbucket@2.1.1) (2019-02-26)
**Note:** Version bump only for package decap-cms-backend-bitbucket
# [2.1.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.0.7...decap-cms-backend-bitbucket@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-bitbucket/issues/1818)) ([c6ae1e8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/c6ae1e8))
<a name="2.0.7"></a>
## [2.0.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.0.6...decap-cms-backend-bitbucket@2.0.7) (2018-09-06)
### Bug Fixes
- **bitbucket:** setting site_id for BitBucket auth ([#1660](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/1660)) ([d139ac4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/d139ac4))
<a name="2.0.6"></a>
## [2.0.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.0.5...decap-cms-backend-bitbucket@2.0.6) (2018-08-27)
**Note:** Version bump only for package decap-cms-backend-bitbucket
<a name="2.0.5"></a>
## [2.0.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.0.4...decap-cms-backend-bitbucket@2.0.5) (2018-08-24)
### Bug Fixes
- **gitlab:** fetch media library images through API ([#1433](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/1433)) ([83d2adc](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/83d2adc))
<a name="2.0.4"></a>
## [2.0.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.0.3...decap-cms-backend-bitbucket@2.0.4) (2018-08-07)
### Bug Fixes
- **backends:** fix commit message handling ([#1568](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/issues/1568)) ([f7e7120](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/commit/f7e7120))
<a name="2.0.3"></a>
## [2.0.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.0.2...decap-cms-backend-bitbucket@2.0.3) (2018-08-01)
**Note:** Version bump only for package decap-cms-backend-bitbucket
<a name="2.0.2"></a>
## [2.0.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket/compare/decap-cms-backend-bitbucket@2.0.1...decap-cms-backend-bitbucket@2.0.2) (2018-07-28)
**Note:** Version bump only for package decap-cms-backend-bitbucket
<a name="2.0.1"></a>
## 2.0.1 (2018-07-26)
<a name="2.0.0"></a>
# 2.0.0 (2018-07-26)
### Bug Fixes
- **bitbucket:** fix rebasing mistakes in bitbucket backend and deps ([#1522](https://github.com/decaporg/decap-cms/issues/1522)) ([bdfd944](https://github.com/decaporg/decap-cms/commit/bdfd944))

22
node_modules/decap-cms-backend-bitbucket/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,22 @@
Copyright (c) 2016 Netlify <decap@p-m.si>
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

13
node_modules/decap-cms-backend-bitbucket/README.md generated vendored Normal file
View File

@@ -0,0 +1,13 @@
# Bitbucket backend
An abstraction layer between the CMS and [Bitbucket](https://docs.microsoft.com/en-us/rest/api/azure/devops/git/)
## 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` and `LargeMedia(LFS)`. With [Editorial Workflow](https://www.decapcms.org/docs/beta-features/#gitlab-and-bitbucket-editorial-workflow-support) uses pull requests comments to track unpublished entries statuses.
`Api` - A wrapper for Bitbucket REST 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.
Look at tests or types for more info.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,613 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.API_NAME = void 0;
var _get2 = _interopRequireDefault(require("lodash/get"));
var _flow2 = _interopRequireDefault(require("lodash/flow"));
var _decapCmsLibUtil = require("decap-cms-lib-util");
var _path = require("path");
var _commonTags = require("common-tags");
var _whatTheDiff = require("what-the-diff");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : String(i); }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
var BitBucketPullRequestState = /*#__PURE__*/function (BitBucketPullRequestState) {
BitBucketPullRequestState["MERGED"] = "MERGED";
BitBucketPullRequestState["SUPERSEDED"] = "SUPERSEDED";
BitBucketPullRequestState["OPEN"] = "OPEN";
BitBucketPullRequestState["DECLINED"] = "DECLINED";
return BitBucketPullRequestState;
}(BitBucketPullRequestState || {});
var BitBucketPullRequestStatusState = /*#__PURE__*/function (BitBucketPullRequestStatusState) {
BitBucketPullRequestStatusState["Successful"] = "SUCCESSFUL";
BitBucketPullRequestStatusState["Failed"] = "FAILED";
BitBucketPullRequestStatusState["InProgress"] = "INPROGRESS";
BitBucketPullRequestStatusState["Stopped"] = "STOPPED";
return BitBucketPullRequestStatusState;
}(BitBucketPullRequestStatusState || {});
const API_NAME = exports.API_NAME = 'Bitbucket';
const APPLICATION_JSON = 'application/json; charset=utf-8';
function replace404WithEmptyResponse(err) {
if (err && err.status === 404) {
console.log('This 404 was expected and handled appropriately.');
return {
size: 0,
values: []
};
} else {
return Promise.reject(err);
}
}
class API {
constructor(config) {
_defineProperty(this, "apiRoot", void 0);
_defineProperty(this, "branch", void 0);
_defineProperty(this, "repo", void 0);
_defineProperty(this, "requestFunction", void 0);
_defineProperty(this, "repoURL", void 0);
_defineProperty(this, "commitAuthor", void 0);
_defineProperty(this, "mergeStrategy", void 0);
_defineProperty(this, "initialWorkflowStatus", void 0);
_defineProperty(this, "cmsLabelPrefix", void 0);
_defineProperty(this, "buildRequest", req => {
const withRoot = _decapCmsLibUtil.unsentRequest.withRoot(this.apiRoot)(req);
if (withRoot.has('cache')) {
return withRoot;
} else {
const withNoCache = _decapCmsLibUtil.unsentRequest.withNoCache(withRoot);
return withNoCache;
}
});
_defineProperty(this, "request", req => {
try {
return (0, _decapCmsLibUtil.requestWithBackoff)(this, req);
} catch (err) {
throw new _decapCmsLibUtil.APIError(err.message, null, API_NAME);
}
});
_defineProperty(this, "responseToJSON", (0, _decapCmsLibUtil.responseParser)({
format: 'json',
apiName: API_NAME
}));
_defineProperty(this, "responseToBlob", (0, _decapCmsLibUtil.responseParser)({
format: 'blob',
apiName: API_NAME
}));
_defineProperty(this, "responseToText", (0, _decapCmsLibUtil.responseParser)({
format: 'text',
apiName: API_NAME
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_defineProperty(this, "requestJSON", req => this.request(req).then(this.responseToJSON));
_defineProperty(this, "requestText", req => this.request(req).then(this.responseToText));
_defineProperty(this, "user", () => this.requestJSON('/user'));
_defineProperty(this, "hasWriteAccess", async () => {
const response = await this.request(this.repoURL);
if (response.status === 404) {
throw Error('Repo not found');
}
return response.ok;
});
_defineProperty(this, "getBranch", async branchName => {
const branch = await this.requestJSON(`${this.repoURL}/refs/branches/${branchName}`);
return branch;
});
_defineProperty(this, "branchCommitSha", async branch => {
const {
target: {
hash: branchSha
}
} = await this.getBranch(branch);
return branchSha;
});
_defineProperty(this, "defaultBranchCommitSha", () => {
return this.branchCommitSha(this.branch);
});
_defineProperty(this, "isFile", ({
type
}) => type === 'commit_file');
_defineProperty(this, "getFileId", (commitHash, path) => {
return `${commitHash}/${path}`;
});
_defineProperty(this, "processFile", file => _objectSpread({
id: file.id,
type: file.type,
path: file.path,
name: (0, _decapCmsLibUtil.basename)(file.path)
}, file.commit && file.commit.hash ? {
id: this.getFileId(file.commit.hash, file.path)
} : {}));
_defineProperty(this, "processFiles", files => files.filter(this.isFile).map(this.processFile));
_defineProperty(this, "readFile", async (path, sha, {
parseText = true,
branch = this.branch,
head = ''
} = {}) => {
const fetchContent = async () => {
const node = head ? head : await this.branchCommitSha(branch);
const content = await this.request({
url: `${this.repoURL}/src/${node}/${path}`,
cache: 'no-store'
}).then(parseText ? this.responseToText : this.responseToBlob);
return content;
};
const content = await (0, _decapCmsLibUtil.readFile)(sha, fetchContent, _decapCmsLibUtil.localForage, parseText);
return content;
});
_defineProperty(this, "getEntriesAndCursor", jsonResponse => {
const {
size: count,
page,
pagelen: pageSize,
next,
previous: prev,
values: entries
} = jsonResponse;
const pageCount = pageSize && count ? Math.ceil(count / pageSize) : undefined;
return {
entries,
cursor: _decapCmsLibUtil.Cursor.create({
actions: [...(next ? ['next'] : []), ...(prev ? ['prev'] : [])],
meta: {
page,
count,
pageSize,
pageCount
},
data: {
links: {
next,
prev
}
}
})
};
});
_defineProperty(this, "listFiles", async (path, depth = 1, pagelen, branch) => {
const node = await this.branchCommitSha(branch);
const result = await this.requestJSON({
url: `${this.repoURL}/src/${node}/${path}`,
params: {
max_depth: depth,
pagelen
}
}).catch(replace404WithEmptyResponse);
const {
entries,
cursor
} = this.getEntriesAndCursor(result);
return {
entries: this.processFiles(entries),
cursor: cursor
};
});
_defineProperty(this, "traverseCursor", async (cursor, action) => (0, _flow2.default)([this.requestJSON, (0, _decapCmsLibUtil.then)(this.getEntriesAndCursor), (0, _decapCmsLibUtil.then)(({
cursor: newCursor,
entries
}) => ({
cursor: newCursor,
entries: this.processFiles(entries)
}))])(cursor.data.getIn(['links', action])));
_defineProperty(this, "listAllFiles", async (path, depth, branch) => {
const {
cursor: initialCursor,
entries: initialEntries
} = await this.listFiles(path, depth, 100, branch);
const entries = [...initialEntries];
let currentCursor = initialCursor;
while (currentCursor && currentCursor.actions.has('next')) {
const {
cursor: newCursor,
entries: newEntries
} = await this.traverseCursor(currentCursor, 'next');
entries.push(...newEntries);
currentCursor = newCursor;
}
return this.processFiles(entries);
});
_defineProperty(this, "deleteFiles", (paths, message) => {
const body = new FormData();
paths.forEach(path => {
body.append('files', path);
});
body.append('branch', this.branch);
if (message) {
body.append('message', message);
}
if (this.commitAuthor) {
const {
name,
email
} = this.commitAuthor;
body.append('author', `${name} <${email}>`);
}
return (0, _flow2.default)([_decapCmsLibUtil.unsentRequest.withMethod('POST'), _decapCmsLibUtil.unsentRequest.withBody(body), this.request])(`${this.repoURL}/src`);
});
this.apiRoot = config.apiRoot || 'https://api.bitbucket.org/2.0';
this.branch = config.branch || 'master';
this.repo = config.repo || '';
this.requestFunction = config.requestFunction || _decapCmsLibUtil.unsentRequest.performRequest;
// Allow overriding this.hasWriteAccess
this.hasWriteAccess = config.hasWriteAccess || this.hasWriteAccess;
this.repoURL = this.repo ? `/repositories/${this.repo}` : '';
this.mergeStrategy = config.squashMerges ? 'squash' : 'merge_commit';
this.initialWorkflowStatus = config.initialWorkflowStatus;
this.cmsLabelPrefix = config.cmsLabelPrefix;
}
async readFileMetadata(path, sha) {
const fetchFileMetadata = async () => {
try {
const {
values
} = await this.requestJSON({
url: `${this.repoURL}/commits`,
params: {
path,
include: this.branch
}
});
const commit = values[0];
return {
author: commit.author.user ? commit.author.user.display_name || commit.author.user.nickname : commit.author.raw,
updatedOn: commit.date
};
} catch (e) {
return {
author: '',
updatedOn: ''
};
}
};
const fileMetadata = await (0, _decapCmsLibUtil.readFileMetadata)(sha, fetchFileMetadata, _decapCmsLibUtil.localForage);
return fileMetadata;
}
async isShaExistsInBranch(branch, sha) {
const {
values
} = await this.requestJSON({
url: `${this.repoURL}/commits`,
params: {
include: branch,
pagelen: 100
}
}).catch(e => {
console.log(`Failed getting commits for branch '${branch}'`, e);
return [];
});
return values.some(v => v.hash === sha);
}
async uploadFiles(files, {
commitMessage,
branch,
parentSha
}) {
const formData = new FormData();
const toMove = [];
files.forEach(file => {
if (file.delete) {
// delete the file
formData.append('files', file.path);
} else if (file.newPath) {
const contentBlob = (0, _get2.default)(file, 'fileObj', new Blob([file.raw]));
toMove.push({
from: file.path,
to: file.newPath,
contentBlob
});
} else {
// add/modify the file
const contentBlob = (0, _get2.default)(file, 'fileObj', new Blob([file.raw]));
// Third param is filename header, in case path is `message`, `branch`, etc.
formData.append(file.path, contentBlob, (0, _decapCmsLibUtil.basename)(file.path));
}
});
for (const {
from,
to,
contentBlob
} of toMove) {
const sourceDir = (0, _path.dirname)(from);
const destDir = (0, _path.dirname)(to);
const filesBranch = parentSha ? this.branch : branch;
const files = await this.listAllFiles(sourceDir, 100, filesBranch);
for (const file of files) {
// to move a file in Bitbucket we need to delete the old path
// and upload the file content to the new path
// NOTE: this is very wasteful, and also the Bitbucket `diff` API
// reports these files as deleted+added instead of renamed
// delete current path
formData.append('files', file.path);
// create in new path
const content = file.path === from ? contentBlob : await this.readFile(file.path, null, {
branch: filesBranch,
parseText: false
});
formData.append(file.path.replace(sourceDir, destDir), content, (0, _decapCmsLibUtil.basename)(file.path));
}
}
if (commitMessage) {
formData.append('message', commitMessage);
}
if (this.commitAuthor) {
const {
name,
email
} = this.commitAuthor;
formData.append('author', `${name} <${email}>`);
}
formData.append('branch', branch);
if (parentSha) {
formData.append('parents', parentSha);
}
try {
await this.requestText({
url: `${this.repoURL}/src`,
method: 'POST',
body: formData
});
} catch (error) {
const message = error.message || '';
// very descriptive message from Bitbucket
if (parentSha && message.includes('Something went wrong')) {
await (0, _decapCmsLibUtil.throwOnConflictingBranches)(branch, name => this.getBranch(name), API_NAME);
}
throw error;
}
return files;
}
async persistFiles(dataFiles, mediaFiles, options) {
const files = [...dataFiles, ...mediaFiles];
if (options.useWorkflow) {
const slug = dataFiles[0].slug;
return this.editorialWorkflowGit(files, slug, options);
} else {
return this.uploadFiles(files, {
commitMessage: options.commitMessage,
branch: this.branch
});
}
}
async addPullRequestComment(pullRequest, comment) {
await this.requestJSON({
method: 'POST',
url: `${this.repoURL}/pullrequests/${pullRequest.id}/comments`,
headers: {
'Content-Type': APPLICATION_JSON
},
body: JSON.stringify({
content: {
raw: comment
}
})
});
}
async getPullRequestLabel(id) {
const comments = await this.requestJSON({
url: `${this.repoURL}/pullrequests/${id}/comments`,
params: {
pagelen: 100
}
});
return comments.values.map(c => c.content.raw)[comments.values.length - 1];
}
async createPullRequest(branch, commitMessage, status) {
const pullRequest = await this.requestJSON({
method: 'POST',
url: `${this.repoURL}/pullrequests`,
headers: {
'Content-Type': APPLICATION_JSON
},
body: JSON.stringify({
title: commitMessage,
source: {
branch: {
name: branch
}
},
destination: {
branch: {
name: this.branch
}
},
description: _decapCmsLibUtil.DEFAULT_PR_BODY,
close_source_branch: true
})
});
// use comments for status labels
await this.addPullRequestComment(pullRequest, (0, _decapCmsLibUtil.statusToLabel)(status, this.cmsLabelPrefix));
}
async getDifferences(source, destination = this.branch) {
if (source === destination) {
return [];
}
const rawDiff = await this.requestText({
url: `${this.repoURL}/diff/${source}..${destination}`,
params: {
binary: false
}
});
const diffs = (0, _whatTheDiff.parse)(rawDiff).map(d => {
var _d$oldPath, _d$newPath;
const oldPath = ((_d$oldPath = d.oldPath) === null || _d$oldPath === void 0 ? void 0 : _d$oldPath.replace(/b\//, '')) || '';
const newPath = ((_d$newPath = d.newPath) === null || _d$newPath === void 0 ? void 0 : _d$newPath.replace(/b\//, '')) || '';
const path = newPath || oldPath;
return {
oldPath,
newPath,
status: d.status,
newFile: d.status === 'added',
path,
binary: d.binary || /.svg$/.test(path)
};
});
return diffs;
}
async editorialWorkflowGit(files, slug, options) {
const contentKey = (0, _decapCmsLibUtil.generateContentKey)(options.collectionName, slug);
const branch = (0, _decapCmsLibUtil.branchFromContentKey)(contentKey);
const unpublished = options.unpublished || false;
if (!unpublished) {
const defaultBranchSha = await this.branchCommitSha(this.branch);
await this.uploadFiles(files, {
commitMessage: options.commitMessage,
branch,
parentSha: defaultBranchSha
});
await this.createPullRequest(branch, options.commitMessage, options.status || this.initialWorkflowStatus);
} else {
// mark files for deletion
const diffs = await this.getDifferences(branch);
const toDelete = [];
for (const diff of diffs.filter(d => d.binary && d.status !== 'deleted')) {
if (!files.some(file => file.path === diff.path)) {
toDelete.push({
path: diff.path,
delete: true
});
}
}
await this.uploadFiles([...files, ...toDelete], {
commitMessage: options.commitMessage,
branch
});
}
}
async getPullRequests(sourceBranch) {
const sourceQuery = sourceBranch ? `source.branch.name = "${sourceBranch}"` : `source.branch.name ~ "${_decapCmsLibUtil.CMS_BRANCH_PREFIX}/"`;
const pullRequests = await this.requestJSON({
url: `${this.repoURL}/pullrequests`,
params: {
pagelen: 50,
q: (0, _commonTags.oneLine)`
source.repository.full_name = "${this.repo}"
AND state = "${BitBucketPullRequestState.OPEN}"
AND destination.branch.name = "${this.branch}"
AND comment_count > 0
AND ${sourceQuery}
`
}
});
const labels = await Promise.all(pullRequests.values.map(pr => this.getPullRequestLabel(pr.id)));
return pullRequests.values.filter((_, index) => (0, _decapCmsLibUtil.isCMSLabel)(labels[index], this.cmsLabelPrefix));
}
async getBranchPullRequest(branch) {
const pullRequests = await this.getPullRequests(branch);
if (pullRequests.length <= 0) {
throw new _decapCmsLibUtil.EditorialWorkflowError('content is not under editorial workflow', true);
}
return pullRequests[0];
}
async listUnpublishedBranches() {
console.log('%c Checking for Unpublished entries', 'line-height: 30px;text-align: center;font-weight: bold');
const pullRequests = await this.getPullRequests();
const branches = pullRequests.map(mr => mr.source.branch.name);
return branches;
}
async retrieveUnpublishedEntryData(contentKey) {
const {
collection,
slug
} = (0, _decapCmsLibUtil.parseContentKey)(contentKey);
const branch = (0, _decapCmsLibUtil.branchFromContentKey)(contentKey);
const pullRequest = await this.getBranchPullRequest(branch);
const diffs = await this.getDifferences(branch);
const label = await this.getPullRequestLabel(pullRequest.id);
const status = (0, _decapCmsLibUtil.labelToStatus)(label, this.cmsLabelPrefix);
const updatedAt = pullRequest.updated_on;
const pullRequestAuthor = pullRequest.author.display_name;
return {
collection,
slug,
status,
// TODO: get real id
diffs: diffs.filter(d => d.status !== 'deleted').map(d => ({
path: d.path,
newFile: d.newFile,
id: ''
})),
updatedAt,
pullRequestAuthor
};
}
async updateUnpublishedEntryStatus(collection, slug, newStatus) {
const contentKey = (0, _decapCmsLibUtil.generateContentKey)(collection, slug);
const branch = (0, _decapCmsLibUtil.branchFromContentKey)(contentKey);
const pullRequest = await this.getBranchPullRequest(branch);
await this.addPullRequestComment(pullRequest, (0, _decapCmsLibUtil.statusToLabel)(newStatus, this.cmsLabelPrefix));
}
async mergePullRequest(pullRequest) {
await this.requestJSON({
method: 'POST',
url: `${this.repoURL}/pullrequests/${pullRequest.id}/merge`,
headers: {
'Content-Type': APPLICATION_JSON
},
body: JSON.stringify({
message: _decapCmsLibUtil.MERGE_COMMIT_MESSAGE,
close_source_branch: true,
merge_strategy: this.mergeStrategy
})
});
}
async publishUnpublishedEntry(collectionName, slug) {
const contentKey = (0, _decapCmsLibUtil.generateContentKey)(collectionName, slug);
const branch = (0, _decapCmsLibUtil.branchFromContentKey)(contentKey);
const pullRequest = await this.getBranchPullRequest(branch);
await this.mergePullRequest(pullRequest);
}
async declinePullRequest(pullRequest) {
await this.requestJSON({
method: 'POST',
url: `${this.repoURL}/pullrequests/${pullRequest.id}/decline`
});
}
async deleteBranch(branch) {
await this.request({
method: 'DELETE',
url: `${this.repoURL}/refs/branches/${branch}`
});
}
async deleteUnpublishedEntry(collectionName, slug) {
const contentKey = (0, _decapCmsLibUtil.generateContentKey)(collectionName, slug);
const branch = (0, _decapCmsLibUtil.branchFromContentKey)(contentKey);
const pullRequest = await this.getBranchPullRequest(branch);
await this.declinePullRequest(pullRequest);
await this.deleteBranch(branch);
}
async getPullRequestStatuses(pullRequest) {
const statuses = await this.requestJSON({
url: `${this.repoURL}/pullrequests/${pullRequest.id}/statuses`,
params: {
pagelen: 100
}
});
return statuses.values;
}
async getStatuses(collectionName, slug) {
const contentKey = (0, _decapCmsLibUtil.generateContentKey)(collectionName, slug);
const branch = (0, _decapCmsLibUtil.branchFromContentKey)(contentKey);
const pullRequest = await this.getBranchPullRequest(branch);
const statuses = await this.getPullRequestStatuses(pullRequest);
return statuses.map(({
key,
state,
url
}) => ({
context: key,
state: state === BitBucketPullRequestStatusState.Successful ? _decapCmsLibUtil.PreviewState.Success : _decapCmsLibUtil.PreviewState.Other,
target_url: url
}));
}
async getUnpublishedEntrySha(collection, slug) {
const contentKey = (0, _decapCmsLibUtil.generateContentKey)(collection, slug);
const branch = (0, _decapCmsLibUtil.branchFromContentKey)(contentKey);
const pullRequest = await this.getBranchPullRequest(branch);
return pullRequest.destination.commit.hash;
}
}
exports.default = API;

View File

@@ -0,0 +1,117 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _base = _interopRequireDefault(require("@emotion/styled/base"));
var _react = _interopRequireDefault(require("react"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _decapCmsLibAuth = require("decap-cms-lib-auth");
var _decapCmsUiDefault = require("decap-cms-ui-default");
var _react2 = require("@emotion/react");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : String(i); }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function _EMOTION_STRINGIFIED_CSS_ERROR__() { return "You have tried to stringify object returned from `css` function. It isn't supposed to be used directly (e.g. as value of the `className` prop), but rather handed to emotion so it can handle it (e.g. as value of `css` prop)."; }
const LoginButtonIcon = /*#__PURE__*/(0, _base.default)(_decapCmsUiDefault.Icon, {
target: "e15sc0jo0",
label: "LoginButtonIcon"
})(process.env.NODE_ENV === "production" ? {
name: "1gnqu05",
styles: "margin-right:18px"
} : {
name: "1gnqu05",
styles: "margin-right:18px",
map: "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9BdXRoZW50aWNhdGlvblBhZ2UuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBTW9DIiwiZmlsZSI6Ii4uLy4uL3NyYy9BdXRoZW50aWNhdGlvblBhZ2UuanMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnO1xuaW1wb3J0IFByb3BUeXBlcyBmcm9tICdwcm9wLXR5cGVzJztcbmltcG9ydCBzdHlsZWQgZnJvbSAnQGVtb3Rpb24vc3R5bGVkJztcbmltcG9ydCB7IE5ldGxpZnlBdXRoZW50aWNhdG9yLCBJbXBsaWNpdEF1dGhlbnRpY2F0b3IgfSBmcm9tICdkZWNhcC1jbXMtbGliLWF1dGgnO1xuaW1wb3J0IHsgQXV0aGVudGljYXRpb25QYWdlLCBJY29uIH0gZnJvbSAnZGVjYXAtY21zLXVpLWRlZmF1bHQnO1xuXG5jb25zdCBMb2dpbkJ1dHRvbkljb24gPSBzdHlsZWQoSWNvbilgXG4gIG1hcmdpbi1yaWdodDogMThweDtcbmA7XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIEJpdGJ1Y2tldEF1dGhlbnRpY2F0aW9uUGFnZSBleHRlbmRzIFJlYWN0LkNvbXBvbmVudCB7XG4gIHN0YXRpYyBwcm9wVHlwZXMgPSB7XG4gICAgb25Mb2dpbjogUHJvcFR5cGVzLmZ1bmMuaXNSZXF1aXJlZCxcbiAgICBpblByb2dyZXNzOiBQcm9wVHlwZXMuYm9vbCxcbiAgICBiYXNlX3VybDogUHJvcFR5cGVzLnN0cmluZyxcbiAgICBzaXRlSWQ6IFByb3BUeXBlcy5zdHJpbmcsXG4gICAgYXV0aEVuZHBvaW50OiBQcm9wVHlwZXMuc3RyaW5nLFxuICAgIGNvbmZpZzogUHJvcFR5cGVzLm9iamVjdC5pc1JlcXVpcmVkLFxuICAgIGNsZWFySGFzaDogUHJvcFR5cGVzLmZ1bmMsXG4gICAgdDogUHJvcFR5cGVzLmZ1bmMuaXNSZXF1aXJlZCxcbiAgfTtcblxuICBzdGF0ZSA9IHt9O1xuXG4gIGNvbXBvbmVudERpZE1vdW50KCkge1xuICAgIGNvbnN0IHsgYXV0aF90eXBlOiBhdXRoVHlwZSA9ICcnIH0gPSB0aGlzLnByb3BzLmNvbmZpZy5iYWNrZW5kO1xuXG4gICAgaWYgKGF1dGhUeXBlID09PSAnaW1wbGljaXQnKSB7XG4gICAgICBjb25zdCB7XG4gICAgICAgIGJhc2VfdXJsID0gJ2h0dHBzOi8vYml0YnVja2V0Lm9yZycsXG4gICAgICAgIGF1dGhfZW5kcG9pbnQgPSAnc2l0ZS9vYXV0aDIvYXV0aG9yaXplJyxcbiAgICAgICAgYXBwX2lkID0gJycsXG4gICAgICB9ID0gdGhpcy5wcm9wcy5jb25maWcuYmFja2VuZDtcblxuICAgICAgdGhpcy5hdXRoID0gbmV3IEltcGxpY2l0QXV0aGVudGljYXRvcih7XG4gICAgICAgIGJhc2VfdXJsLFxuICAgICAgICBhdXRoX2VuZHBvaW50LFxuICAgICAgICBhcHBfaWQsXG4gICAgICAgIGNsZWFySGFzaDogdGhpcy5wcm9wcy5jbGVhckhhc2gsXG4gICAgICB9KTtcbiAgICAgIC8vIENvbXBsZXRlIGltcGxpY2l0IGF1dGhlbnRpY2F0aW9uIGlmIHdlIHdlcmUgcmVkaXJlY3RlZCBiYWNrIHRvIGZyb20gdGhlIHByb3ZpZGVyLlxuICAgICAgdGhpcy5hdXRoLmNvbXBsZXRlQXV0aCgoZXJyLCBkYXRhKSA9PiB7XG4gICAgICAgIGlmIChlcnIpIHtcbiAgICAgICAgICB0aGlzLnNldFN0YXRlKHsgbG9naW5FcnJvcjogZXJyLnRvU3RyaW5nKCkgfSk7XG4gICAgICAgICAgcmV0dXJuO1xuICAgICAgICB9XG4gICAgICAgIHRoaXMucHJvcHMub25Mb2dpbihkYXRhKTtcbiAgICAgIH0pO1xuICAgICAgdGhpcy5hdXRoU2V0dGluZ3MgPSB7IHNjb3BlOiAncmVwb3NpdG9yeTp3cml0ZScgfTtcbiAgICB9IGVsc2Uge1xuICAgICAgdGhpcy5hdXRoID0gbmV3IE5ldGxpZnlBdXRoZW50aWNhdG9yKHtcbiAgICAgICAgYmFzZV91cmw6IHRoaXMucHJvcHMuYmFzZV91cmwsXG4gICAgICAgIHNpdGVfaWQ6XG4gICAgICAgICAgZG9jdW1lbnQubG9jYXRpb24uaG9zdC5zcGxpdCgnOicpWzBdID09PSAnbG9jYWxob3N0J1xuICAgICAgICAgICAgPyAnZGVtby5kZWNhcGNtcy5vcmcnXG4gICAgICAgICAgICA6IHRoaXMucHJvcHMuc2l0ZUlkLFxuICAgICAgICBhdXRoX2VuZHBvaW50OiB0aGlzLnByb3BzLmF1dGhFbmRwb2ludCxcbiAgICAgIH0pO1xuICAgICAgdGhpcy5hdXRoU2V0dGluZ3MgPSB7IHByb3ZpZGVyOiAnYml0YnVja2V0Jywgc2NvcGU6ICdyZXBvJyB9O1xuICAgIH1cbiAgfVxuXG4gIGhhbmRsZUxvZ2luID0gZSA9PiB7XG4gICAgZS5wcmV2ZW50RGVmYXVsdCgpO1xuICAgIHRoaXMuYXV0aC5hdXRoZW50aWNhdGUodGhpcy5hdXRoU2V0dGluZ3MsIChlcnIsIGRhdGEpID0+IHtcbiAgICAgIGlmIChlcnIpIHtcbiAgICAgICAgdGhpcy5zZXRTdGF0ZSh7IGxvZ2luRXJyb3I6IGVyci50b1N0cmluZygpIH0pO1xuICAgICAgICByZXR1cm47XG4gICAgICB9XG4gICAgICB0aGlzLnByb3BzLm9uTG9naW4oZGF0YSk7XG4gICAgfSk7XG4gIH07XG5cbiAgcmVuZGVyKCkge1xuICAgIGNvbnN0IHsgaW5Qcm9ncmVzcywgY29uZmlnLCB0IH0gPSB0aGlzLnByb3BzO1xuXG4gICAgcmV0dXJuIChcbiAgICAgIDxBdXRoZW50aWNhdGlvblBhZ2VcbiAgICAgICAgb25Mb2dpbj17dGhpcy5oYW5kbGVMb2dpbn1cbiAgICAgICAgbG9naW5EaXNhYmxlZD17aW5Qcm9ncmVzc31cbiAgICAgICAgbG9naW5FcnJvck1lc3NhZ2U9e3RoaXMuc3RhdGUubG9naW5FcnJvcn1cbiAgICAgICAgbG9nb1VybD17Y29uZmlnLmxvZ29fdXJsfVxuICAgICAgICBzaXRlVXJsPXtjb25maWcuc2l0ZV91cmx9XG4gICAgICAgIHJlbmRlckJ1dHRvbkNvbnRlbnQ9eygpID0+IChcbiAgICAgICAgICA8UmVhY3QuRnJhZ21lbnQ+XG4gICAgICAgICAgICA8TG9naW5CdXR0b25JY29uIHR5cGU9XCJiaXRidWNrZXRcIiAvPlxuICAgICAgICAgICAge2luUHJvZ3Jlc3MgPyB0KCdhdXRoLmxvZ2dpbmdJbicpIDogdCgnYXV0aC5sb2dpbldpdGhCaXRidWNrZXQnKX1cbiAgICAgICAgICA8L1JlYWN0LkZyYWdtZW50PlxuICAgICAgICApfVxuICAgICAgICB0PXt0fVxuICAgICAgLz5cbiAgICApO1xuICB9XG59XG4iXX0= */",
toString: _EMOTION_STRINGIFIED_CSS_ERROR__
});
class BitbucketAuthenticationPage extends _react.default.Component {
constructor(...args) {
super(...args);
_defineProperty(this, "state", {});
_defineProperty(this, "handleLogin", e => {
e.preventDefault();
this.auth.authenticate(this.authSettings, (err, data) => {
if (err) {
this.setState({
loginError: err.toString()
});
return;
}
this.props.onLogin(data);
});
});
}
componentDidMount() {
const {
auth_type: authType = ''
} = this.props.config.backend;
if (authType === 'implicit') {
const {
base_url = 'https://bitbucket.org',
auth_endpoint = 'site/oauth2/authorize',
app_id = ''
} = this.props.config.backend;
this.auth = new _decapCmsLibAuth.ImplicitAuthenticator({
base_url,
auth_endpoint,
app_id,
clearHash: this.props.clearHash
});
// Complete implicit authentication if we were redirected back to from the provider.
this.auth.completeAuth((err, data) => {
if (err) {
this.setState({
loginError: err.toString()
});
return;
}
this.props.onLogin(data);
});
this.authSettings = {
scope: 'repository:write'
};
} else {
this.auth = new _decapCmsLibAuth.NetlifyAuthenticator({
base_url: this.props.base_url,
site_id: document.location.host.split(':')[0] === 'localhost' ? 'demo.decapcms.org' : this.props.siteId,
auth_endpoint: this.props.authEndpoint
});
this.authSettings = {
provider: 'bitbucket',
scope: 'repo'
};
}
}
render() {
const {
inProgress,
config,
t
} = this.props;
return (0, _react2.jsx)(_decapCmsUiDefault.AuthenticationPage, {
onLogin: this.handleLogin,
loginDisabled: inProgress,
loginErrorMessage: this.state.loginError,
logoUrl: config.logo_url,
siteUrl: config.site_url,
renderButtonContent: () => (0, _react2.jsx)(_react.default.Fragment, null, (0, _react2.jsx)(LoginButtonIcon, {
type: "bitbucket"
}), inProgress ? t('auth.loggingIn') : t('auth.loginWithBitbucket')),
t: t
});
}
}
exports.default = BitbucketAuthenticationPage;
_defineProperty(BitbucketAuthenticationPage, "propTypes", {
onLogin: _propTypes.default.func.isRequired,
inProgress: _propTypes.default.bool,
base_url: _propTypes.default.string,
siteId: _propTypes.default.string,
authEndpoint: _propTypes.default.string,
config: _propTypes.default.object.isRequired,
clearHash: _propTypes.default.func,
t: _propTypes.default.func.isRequired
});

View File

@@ -0,0 +1,90 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.GitLfsClient = void 0;
var _minimatch = _interopRequireDefault(require("minimatch"));
var _decapCmsLibUtil = require("decap-cms-lib-util");
const _excluded = ["sha"];
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : String(i); }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
class GitLfsClient {
constructor(enabled, rootURL, patterns, makeAuthorizedRequest) {
this.enabled = enabled;
this.rootURL = rootURL;
this.patterns = patterns;
this.makeAuthorizedRequest = makeAuthorizedRequest;
}
matchPath(path) {
return this.patterns.some(pattern => (0, _minimatch.default)(path, pattern, {
matchBase: true
}));
}
async uploadResource(pointer, resource) {
const requests = await this.getResourceUploadRequests([pointer]);
for (const request of requests) {
await this.doUpload(request.actions.upload, resource);
if (request.actions.verify) {
await this.doVerify(request.actions.verify, request);
}
}
return pointer.sha;
}
async doUpload(upload, resource) {
await _decapCmsLibUtil.unsentRequest.fetchWithTimeout(decodeURI(upload.href), {
method: 'PUT',
body: resource,
headers: upload.header
});
}
async doVerify(verify, object) {
this.makeAuthorizedRequest({
url: decodeURI(verify.href),
method: 'POST',
headers: _objectSpread(_objectSpread({}, GitLfsClient.defaultContentHeaders), verify.header),
body: JSON.stringify({
oid: object.oid,
size: object.size
})
});
}
async getResourceUploadRequests(objects) {
const response = await this.makeAuthorizedRequest({
url: `${this.rootURL}/objects/batch`,
method: 'POST',
headers: GitLfsClient.defaultContentHeaders,
body: JSON.stringify({
operation: 'upload',
transfers: ['basic'],
objects: objects.map(_ref => {
let {
sha
} = _ref,
rest = _objectWithoutProperties(_ref, _excluded);
return _objectSpread(_objectSpread({}, rest), {}, {
oid: sha
});
})
})
});
return (await response.json()).objects.filter(object => {
if ('error' in object) {
console.error(object.error);
return false;
}
return object.actions;
});
}
}
exports.GitLfsClient = GitLfsClient;
_defineProperty(GitLfsClient, "defaultContentHeaders", {
Accept: 'application/vnd.git-lfs+json',
['Content-Type']: 'application/vnd.git-lfs+json'
});

View File

@@ -0,0 +1,542 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _trimStart2 = _interopRequireDefault(require("lodash/trimStart"));
var _semaphore = _interopRequireDefault(require("semaphore"));
var _commonTags = require("common-tags");
var _decapCmsLibUtil = require("decap-cms-lib-util");
var _decapCmsLibAuth = require("decap-cms-lib-auth");
var _AuthenticationPage = _interopRequireDefault(require("./AuthenticationPage"));
var _API = _interopRequireWildcard(require("./API"));
var _gitLfsClient = require("./git-lfs-client");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : String(i); }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
const MAX_CONCURRENT_DOWNLOADS = 10;
const STATUS_PAGE = 'https://bitbucket.status.atlassian.com';
const BITBUCKET_STATUS_ENDPOINT = `${STATUS_PAGE}/api/v2/components.json`;
const BITBUCKET_OPERATIONAL_UNITS = ['API', 'Authentication and user management', 'Git LFS'];
const {
fetchWithTimeout: fetch
} = _decapCmsLibUtil.unsentRequest;
// Implementation wrapper class
class BitbucketBackend {
constructor(config, options = {}) {
_defineProperty(this, "lock", void 0);
_defineProperty(this, "api", void 0);
_defineProperty(this, "updateUserCredentials", void 0);
_defineProperty(this, "options", void 0);
_defineProperty(this, "repo", void 0);
_defineProperty(this, "isBranchConfigured", void 0);
_defineProperty(this, "branch", void 0);
_defineProperty(this, "apiRoot", void 0);
_defineProperty(this, "baseUrl", void 0);
_defineProperty(this, "siteId", void 0);
_defineProperty(this, "token", void 0);
_defineProperty(this, "mediaFolder", void 0);
_defineProperty(this, "refreshToken", void 0);
_defineProperty(this, "refreshedTokenPromise", void 0);
_defineProperty(this, "authenticator", void 0);
_defineProperty(this, "_mediaDisplayURLSem", void 0);
_defineProperty(this, "squashMerges", void 0);
_defineProperty(this, "cmsLabelPrefix", void 0);
_defineProperty(this, "previewContext", void 0);
_defineProperty(this, "largeMediaURL", void 0);
_defineProperty(this, "_largeMediaClientPromise", void 0);
_defineProperty(this, "authType", void 0);
_defineProperty(this, "requestFunction", async req => {
const token = await this.getToken();
const authorizedRequest = _decapCmsLibUtil.unsentRequest.withHeaders({
Authorization: `Bearer ${token}`
}, req);
return _decapCmsLibUtil.unsentRequest.performRequest(authorizedRequest);
});
_defineProperty(this, "apiRequestFunction", async req => {
const token = this.refreshedTokenPromise ? await this.refreshedTokenPromise : this.token;
const authorizedRequest = _decapCmsLibUtil.unsentRequest.withHeaders({
Authorization: `Bearer ${token}`
}, req);
const response = await _decapCmsLibUtil.unsentRequest.performRequest(authorizedRequest);
if (response.status === 401) {
const json = await response.json().catch(() => null);
if (json && json.type === 'error' && /^access token expired/i.test(json.error.message)) {
const newToken = await this.getRefreshedAccessToken();
const reqWithNewToken = _decapCmsLibUtil.unsentRequest.withHeaders({
Authorization: `Bearer ${newToken}`
}, req);
return _decapCmsLibUtil.unsentRequest.performRequest(reqWithNewToken);
}
}
return response;
});
this.options = _objectSpread({
proxied: false,
API: null,
updateUserCredentials: async () => null,
initialWorkflowStatus: ''
}, options);
if (!this.options.proxied && (config.backend.repo === null || config.backend.repo === undefined)) {
throw new Error('The BitBucket backend needs a "repo" in the backend configuration.');
}
this.api = this.options.API || null;
this.updateUserCredentials = this.options.updateUserCredentials;
this.repo = config.backend.repo || '';
this.branch = config.backend.branch || 'master';
this.isBranchConfigured = config.backend.branch ? true : false;
this.apiRoot = config.backend.api_root || 'https://api.bitbucket.org/2.0';
this.baseUrl = config.base_url || '';
this.siteId = config.site_id || '';
this.largeMediaURL = config.backend.large_media_url || `https://bitbucket.org/${config.backend.repo}/info/lfs`;
this.token = '';
this.mediaFolder = config.media_folder;
this.squashMerges = config.backend.squash_merges || false;
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
this.previewContext = config.backend.preview_context || '';
this.lock = (0, _decapCmsLibUtil.asyncLock)();
this.authType = config.backend.auth_type || '';
}
isGitBackend() {
return true;
}
async status() {
const api = await fetch(BITBUCKET_STATUS_ENDPOINT).then(res => res.json()).then(res => {
return res['components'].filter(statusComponent => BITBUCKET_OPERATIONAL_UNITS.includes(statusComponent.name)).every(statusComponent => statusComponent.status === 'operational');
}).catch(e => {
console.warn('Failed getting BitBucket status', e);
return true;
});
let auth = false;
// no need to check auth if api is down
if (api) {
var _this$api;
auth = (await ((_this$api = this.api) === null || _this$api === void 0 ? void 0 : _this$api.user().then(user => !!user).catch(e => {
console.warn('Failed getting Bitbucket user', e);
return false;
}))) || false;
}
return {
auth: {
status: auth
},
api: {
status: api,
statusPage: STATUS_PAGE
}
};
}
authComponent() {
return _AuthenticationPage.default;
}
setUser(user) {
this.token = user.token;
this.api = new _API.default({
requestFunction: this.apiRequestFunction,
branch: this.branch,
repo: this.repo,
squashMerges: this.squashMerges,
cmsLabelPrefix: this.cmsLabelPrefix,
initialWorkflowStatus: this.options.initialWorkflowStatus
});
}
restoreUser(user) {
return this.authenticate(user);
}
async authenticate(state) {
this.token = state.token;
if (!this.isBranchConfigured) {
const repo = await fetch(`${this.apiRoot}/repositories/${this.repo}`, {
headers: {
Authorization: `token ${this.token}`
}
}).then(res => res.json()).catch(() => null);
if (repo) {
this.branch = repo.mainbranch.name;
}
}
this.refreshToken = state.refresh_token;
this.api = new _API.default({
requestFunction: this.apiRequestFunction,
branch: this.branch,
repo: this.repo,
apiRoot: this.apiRoot,
squashMerges: this.squashMerges,
cmsLabelPrefix: this.cmsLabelPrefix,
initialWorkflowStatus: this.options.initialWorkflowStatus
});
const isCollab = await this.api.hasWriteAccess().catch(error => {
error.message = (0, _commonTags.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 Bitbucket account with access.
`;
throw error;
});
// Unauthorized user
if (!isCollab) {
throw new Error('Your BitBucket user account does not have access to this repo.');
}
// if (!this.isBranchConfigured) {
// const defaultBranchName = await getDefaultBranchName({
// backend: 'bitbucket',
// repo: this.repo,
// token: this.token,
// });
// if (defaultBranchName) {
// this.branch = defaultBranchName;
// }
// }
const user = await this.api.user();
// Authorized user
return _objectSpread(_objectSpread({}, user), {}, {
name: user.display_name,
login: user.username,
token: state.token,
avatar_url: user.links.avatar.href,
refresh_token: state.refresh_token
});
}
getRefreshedAccessToken() {
if (this.authType === 'implicit') {
throw new _decapCmsLibUtil.AccessTokenError(`Can't refresh access token when using implicit auth`);
}
if (this.refreshedTokenPromise) {
return this.refreshedTokenPromise;
}
// instantiating a new Authenticator on each refresh isn't ideal,
if (!this.authenticator) {
const cfg = {
base_url: this.baseUrl,
site_id: this.siteId
};
this.authenticator = new _decapCmsLibAuth.NetlifyAuthenticator(cfg);
}
this.refreshedTokenPromise = this.authenticator.refresh({
provider: 'bitbucket',
refresh_token: this.refreshToken
}).then(({
token,
refresh_token
}) => {
this.token = token;
this.refreshToken = refresh_token;
this.refreshedTokenPromise = undefined;
this.updateUserCredentials({
token,
refresh_token
});
return token;
});
return this.refreshedTokenPromise;
}
logout() {
this.token = null;
return;
}
getToken() {
if (this.refreshedTokenPromise) {
return this.refreshedTokenPromise;
}
return Promise.resolve(this.token);
}
async entriesByFolder(folder, extension, depth) {
let cursor;
const listFiles = () => this.api.listFiles(folder, depth, 20, this.branch).then(({
entries,
cursor: c
}) => {
cursor = c.mergeMeta({
extension
});
return entries.filter(e => (0, _decapCmsLibUtil.filterByExtension)(e, extension));
});
const head = await this.api.defaultBranchCommitSha();
const readFile = (path, id) => {
return this.api.readFile(path, id, {
head
});
};
const files = await (0, _decapCmsLibUtil.entriesByFolder)(listFiles, readFile, this.api.readFileMetadata.bind(this.api), _API.API_NAME);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
files[_decapCmsLibUtil.CURSOR_COMPATIBILITY_SYMBOL] = cursor;
return files;
}
async listAllFiles(folder, extension, depth) {
const files = await this.api.listAllFiles(folder, depth, this.branch);
const filtered = files.filter(file => (0, _decapCmsLibUtil.filterByExtension)(file, extension));
return filtered;
}
async allEntriesByFolder(folder, extension, depth) {
const head = await this.api.defaultBranchCommitSha();
const readFile = (path, id) => {
return this.api.readFile(path, id, {
head
});
};
const files = await (0, _decapCmsLibUtil.allEntriesByFolder)({
listAllFiles: () => this.listAllFiles(folder, extension, depth),
readFile,
readFileMetadata: this.api.readFileMetadata.bind(this.api),
apiName: _API.API_NAME,
branch: this.branch,
localForage: _decapCmsLibUtil.localForage,
folder,
extension,
depth,
getDefaultBranch: () => Promise.resolve({
name: this.branch,
sha: head
}),
isShaExistsInBranch: this.api.isShaExistsInBranch.bind(this.api),
getDifferences: (source, destination) => this.api.getDifferences(source, destination),
getFileId: path => Promise.resolve(this.api.getFileId(head, path)),
filterFile: file => (0, _decapCmsLibUtil.filterByExtension)(file, extension)
});
return files;
}
async entriesByFiles(files) {
const head = await this.api.defaultBranchCommitSha();
const readFile = (path, id) => {
return this.api.readFile(path, id, {
head
});
};
return (0, _decapCmsLibUtil.entriesByFiles)(files, readFile, this.api.readFileMetadata.bind(this.api), _API.API_NAME);
}
getEntry(path) {
return this.api.readFile(path).then(data => ({
file: {
path,
id: null
},
data: data
}));
}
getMedia(mediaFolder = this.mediaFolder) {
return this.api.listAllFiles(mediaFolder, 1, this.branch).then(files => files.map(({
id,
name,
path
}) => ({
id,
name,
path,
displayURL: {
id,
path
}
})));
}
getLargeMediaClient() {
if (!this._largeMediaClientPromise) {
this._largeMediaClientPromise = (async () => {
const patterns = await this.api.readFile('.gitattributes').then(attributes => (0, _decapCmsLibUtil.getLargeMediaPatternsFromGitAttributesFile)(attributes)).catch(err => {
if (err.status === 404) {
console.log('This 404 was expected and handled appropriately.');
} else {
console.error(err);
}
return [];
});
return new _gitLfsClient.GitLfsClient(!!(this.largeMediaURL && patterns.length > 0), this.largeMediaURL, patterns, this.requestFunction);
})();
}
return this._largeMediaClientPromise;
}
getMediaDisplayURL(displayURL) {
this._mediaDisplayURLSem = this._mediaDisplayURLSem || (0, _semaphore.default)(MAX_CONCURRENT_DOWNLOADS);
return (0, _decapCmsLibUtil.getMediaDisplayURL)(displayURL, this.api.readFile.bind(this.api), this._mediaDisplayURLSem);
}
async getMediaFile(path) {
const name = (0, _decapCmsLibUtil.basename)(path);
const blob = await (0, _decapCmsLibUtil.getMediaAsBlob)(path, null, this.api.readFile.bind(this.api));
const fileObj = (0, _decapCmsLibUtil.blobToFileObj)(name, blob);
const url = URL.createObjectURL(fileObj);
const id = await (0, _decapCmsLibUtil.getBlobSHA)(fileObj);
return {
id,
displayURL: url,
path,
name,
size: fileObj.size,
file: fileObj,
url
};
}
async persistEntry(entry, options) {
const client = await this.getLargeMediaClient();
// persistEntry is a transactional operation
return (0, _decapCmsLibUtil.runWithLock)(this.lock, async () => this.api.persistFiles(entry.dataFiles, client.enabled ? await (0, _decapCmsLibUtil.getLargeMediaFilteredMediaFiles)(client, entry.assets) : entry.assets, options), 'Failed to acquire persist entry lock');
}
async persistMedia(mediaFile, options) {
const {
fileObj,
path
} = mediaFile;
const displayURL = fileObj ? URL.createObjectURL(fileObj) : '';
const client = await this.getLargeMediaClient();
const fixedPath = path.startsWith('/') ? path.slice(1) : path;
if (!client.enabled || !client.matchPath(fixedPath)) {
return this._persistMedia(mediaFile, options);
}
const persistMediaArgument = await (0, _decapCmsLibUtil.getPointerFileForMediaFileObj)(client, fileObj, path);
return _objectSpread(_objectSpread({}, await this._persistMedia(persistMediaArgument, options)), {}, {
displayURL
});
}
async _persistMedia(mediaFile, options) {
const fileObj = mediaFile.fileObj;
const [id] = await Promise.all([(0, _decapCmsLibUtil.getBlobSHA)(fileObj), this.api.persistFiles([], [mediaFile], options)]);
const url = URL.createObjectURL(fileObj);
return {
displayURL: url,
path: (0, _trimStart2.default)(mediaFile.path, '/k'),
name: fileObj.name,
size: fileObj.size,
id,
file: fileObj,
url
};
}
deleteFiles(paths, commitMessage) {
return this.api.deleteFiles(paths, commitMessage);
}
traverseCursor(cursor, action) {
return this.api.traverseCursor(cursor, action).then(async ({
entries,
cursor: newCursor
}) => {
var _cursor$meta;
const extension = (_cursor$meta = cursor.meta) === null || _cursor$meta === void 0 ? void 0 : _cursor$meta.get('extension');
if (extension) {
entries = entries.filter(e => (0, _decapCmsLibUtil.filterByExtension)(e, extension));
newCursor = newCursor.mergeMeta({
extension
});
}
const head = await this.api.defaultBranchCommitSha();
const readFile = (path, id) => {
return this.api.readFile(path, id, {
head
});
};
const entriesWithData = await (0, _decapCmsLibUtil.entriesByFiles)(entries, readFile, this.api.readFileMetadata.bind(this.api), _API.API_NAME);
return {
entries: entriesWithData,
cursor: newCursor
};
});
}
async loadMediaFile(path, id, {
branch
}) {
const readFile = async (path, id, {
parseText
}) => {
const content = await this.api.readFile(path, id, {
branch,
parseText
});
return content;
};
const blob = await (0, _decapCmsLibUtil.getMediaAsBlob)(path, id, readFile);
const name = (0, _decapCmsLibUtil.basename)(path);
const fileObj = (0, _decapCmsLibUtil.blobToFileObj)(name, blob);
return {
id: path,
displayURL: URL.createObjectURL(fileObj),
path,
name,
size: fileObj.size,
file: fileObj
};
}
async unpublishedEntries() {
const listEntriesKeys = () => this.api.listUnpublishedBranches().then(branches => branches.map(branch => (0, _decapCmsLibUtil.contentKeyFromBranch)(branch)));
const ids = await (0, _decapCmsLibUtil.unpublishedEntries)(listEntriesKeys);
return ids;
}
async unpublishedEntry({
id,
collection,
slug
}) {
if (id) {
const data = await this.api.retrieveUnpublishedEntryData(id);
return data;
} else if (collection && slug) {
const entryId = (0, _decapCmsLibUtil.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, slug) {
const contentKey = (0, _decapCmsLibUtil.generateContentKey)(collection, slug);
const branch = (0, _decapCmsLibUtil.branchFromContentKey)(contentKey);
return branch;
}
async unpublishedEntryDataFile(collection, slug, path, id) {
const branch = this.getBranch(collection, slug);
const data = await this.api.readFile(path, id, {
branch
});
return data;
}
async unpublishedEntryMediaFile(collection, slug, path, id) {
const branch = this.getBranch(collection, slug);
const mediaFile = await this.loadMediaFile(path, id, {
branch
});
return mediaFile;
}
async updateUnpublishedEntryStatus(collection, slug, newStatus) {
// updateUnpublishedEntryStatus is a transactional operation
return (0, _decapCmsLibUtil.runWithLock)(this.lock, () => this.api.updateUnpublishedEntryStatus(collection, slug, newStatus), 'Failed to acquire update entry status lock');
}
async deleteUnpublishedEntry(collection, slug) {
// deleteUnpublishedEntry is a transactional operation
return (0, _decapCmsLibUtil.runWithLock)(this.lock, () => this.api.deleteUnpublishedEntry(collection, slug), 'Failed to acquire delete entry lock');
}
async publishUnpublishedEntry(collection, slug) {
// publishUnpublishedEntry is a transactional operation
return (0, _decapCmsLibUtil.runWithLock)(this.lock, () => this.api.publishUnpublishedEntry(collection, slug), 'Failed to acquire publish entry lock');
}
async getDeployPreview(collection, slug) {
try {
const statuses = await this.api.getStatuses(collection, slug);
const deployStatus = (0, _decapCmsLibUtil.getPreviewStatus)(statuses, this.previewContext);
if (deployStatus) {
const {
target_url: url,
state
} = deployStatus;
return {
url,
status: state
};
} else {
return null;
}
} catch (e) {
return null;
}
}
}
exports.default = BitbucketBackend;

View File

@@ -0,0 +1,33 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "API", {
enumerable: true,
get: function () {
return _API.default;
}
});
Object.defineProperty(exports, "AuthenticationPage", {
enumerable: true,
get: function () {
return _AuthenticationPage.default;
}
});
Object.defineProperty(exports, "BitbucketBackend", {
enumerable: true,
get: function () {
return _implementation.default;
}
});
exports.DecapCmsBackendBitbucket = void 0;
var _implementation = _interopRequireDefault(require("./implementation"));
var _API = _interopRequireDefault(require("./API"));
var _AuthenticationPage = _interopRequireDefault(require("./AuthenticationPage"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const DecapCmsBackendBitbucket = exports.DecapCmsBackendBitbucket = {
BitbucketBackend: _implementation.default,
API: _API.default,
AuthenticationPage: _AuthenticationPage.default
};

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -0,0 +1 @@
"use strict";

39
node_modules/decap-cms-backend-bitbucket/package.json generated vendored Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "decap-cms-backend-bitbucket",
"description": "Bitbucket backend for Decap CMS",
"version": "3.1.4",
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbucket",
"bugs": "https://github.com/decaporg/decap-cms/issues",
"module": "dist/esm/index.js",
"main": "dist/decap-cms-backend-bitbucket.js",
"license": "MIT",
"keywords": [
"decap-cms",
"backend",
"bitbucket"
],
"sideEffects": false,
"scripts": {
"develop": "npm run build:esm -- --watch",
"build": "cross-env NODE_ENV=production webpack",
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward --extensions \".js,.jsx,.ts,.tsx\""
},
"dependencies": {
"common-tags": "^1.8.0",
"js-base64": "^3.0.0",
"semaphore": "^1.1.0",
"what-the-diff": "^0.6.0"
},
"peerDependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"decap-cms-lib-auth": "^3.0.0",
"decap-cms-lib-util": "^3.0.0",
"decap-cms-ui-default": "^3.0.0",
"immutable": "^3.7.6",
"lodash": "^4.17.11",
"prop-types": "^15.7.2",
"react": "^18.2.0"
},
"gitHead": "64d91b8bb3d0a93dd36c53800cdac4ba2e435000"
}

802
node_modules/decap-cms-backend-bitbucket/src/API.ts generated vendored Normal file
View File

@@ -0,0 +1,802 @@
import { flow, get } from 'lodash';
import {
localForage,
unsentRequest,
responseParser,
then,
basename,
Cursor,
APIError,
readFile,
CMS_BRANCH_PREFIX,
generateContentKey,
labelToStatus,
isCMSLabel,
EditorialWorkflowError,
statusToLabel,
DEFAULT_PR_BODY,
MERGE_COMMIT_MESSAGE,
PreviewState,
parseContentKey,
branchFromContentKey,
requestWithBackoff,
readFileMetadata,
throwOnConflictingBranches,
} from 'decap-cms-lib-util';
import { dirname } from 'path';
import { oneLine } from 'common-tags';
import { parse } from 'what-the-diff';
import type {
ApiRequest,
AssetProxy,
PersistOptions,
FetchError,
DataFile,
} from 'decap-cms-lib-util';
interface Config {
apiRoot?: string;
token?: string;
branch?: string;
repo?: string;
requestFunction?: (req: ApiRequest) => Promise<Response>;
hasWriteAccess?: () => Promise<boolean>;
squashMerges: boolean;
initialWorkflowStatus: string;
cmsLabelPrefix: string;
}
interface CommitAuthor {
name: string;
email: string;
}
enum BitBucketPullRequestState {
MERGED = 'MERGED',
SUPERSEDED = 'SUPERSEDED',
OPEN = 'OPEN',
DECLINED = 'DECLINED',
}
type BitBucketPullRequest = {
description: string;
id: number;
title: string;
state: BitBucketPullRequestState;
updated_on: string;
summary: {
raw: string;
};
source: {
commit: {
hash: string;
};
branch: {
name: string;
};
};
destination: {
commit: {
hash: string;
};
branch: {
name: string;
};
};
author: BitBucketUser;
};
type BitBucketPullRequests = {
size: number;
page: number;
pagelen: number;
next: string;
preview: string;
values: BitBucketPullRequest[];
};
type BitBucketPullComment = {
content: {
raw: string;
};
};
type BitBucketPullComments = {
size: number;
page: number;
pagelen: number;
next: string;
preview: string;
values: BitBucketPullComment[];
};
enum BitBucketPullRequestStatusState {
Successful = 'SUCCESSFUL',
Failed = 'FAILED',
InProgress = 'INPROGRESS',
Stopped = 'STOPPED',
}
type BitBucketPullRequestStatus = {
uuid: string;
name: string;
key: string;
refname: string;
url: string;
description: string;
state: BitBucketPullRequestStatusState;
};
type BitBucketPullRequestStatues = {
size: number;
page: number;
pagelen: number;
next: string;
preview: string;
values: BitBucketPullRequestStatus[];
};
type DeleteEntry = {
path: string;
delete: true;
};
type BitBucketFile = {
id: string;
type: string;
path: string;
commit?: { hash: string };
};
type BitBucketSrcResult = {
size: number;
page: number;
pagelen: number;
next: string;
previous: string;
values: BitBucketFile[];
};
type BitBucketUser = {
username: string;
display_name: string;
nickname: string;
links: {
avatar: {
href: string;
};
};
};
type BitBucketBranch = {
name: string;
target: { hash: string };
};
type BitBucketCommit = {
hash: string;
author: {
raw: string;
user: {
display_name: string;
nickname: string;
};
};
date: string;
};
export const API_NAME = 'Bitbucket';
const APPLICATION_JSON = 'application/json; charset=utf-8';
function replace404WithEmptyResponse(err: FetchError) {
if (err && err.status === 404) {
console.log('This 404 was expected and handled appropriately.');
return { size: 0, values: [] as BitBucketFile[] } as BitBucketSrcResult;
} else {
return Promise.reject(err);
}
}
export default class API {
apiRoot: string;
branch: string;
repo: string;
requestFunction: (req: ApiRequest) => Promise<Response>;
repoURL: string;
commitAuthor?: CommitAuthor;
mergeStrategy: string;
initialWorkflowStatus: string;
cmsLabelPrefix: string;
constructor(config: Config) {
this.apiRoot = config.apiRoot || 'https://api.bitbucket.org/2.0';
this.branch = config.branch || 'master';
this.repo = config.repo || '';
this.requestFunction = config.requestFunction || unsentRequest.performRequest;
// Allow overriding this.hasWriteAccess
this.hasWriteAccess = config.hasWriteAccess || this.hasWriteAccess;
this.repoURL = this.repo ? `/repositories/${this.repo}` : '';
this.mergeStrategy = config.squashMerges ? 'squash' : 'merge_commit';
this.initialWorkflowStatus = config.initialWorkflowStatus;
this.cmsLabelPrefix = config.cmsLabelPrefix;
}
buildRequest = (req: ApiRequest) => {
const withRoot = unsentRequest.withRoot(this.apiRoot)(req);
if (withRoot.has('cache')) {
return withRoot;
} else {
const withNoCache = unsentRequest.withNoCache(withRoot);
return withNoCache;
}
};
request = (req: ApiRequest): Promise<Response> => {
try {
return requestWithBackoff(this, req);
} catch (err) {
throw new APIError(err.message, null, API_NAME);
}
};
responseToJSON = responseParser({ format: 'json', apiName: API_NAME });
responseToBlob = responseParser({ format: 'blob', apiName: API_NAME });
responseToText = responseParser({ format: 'text', apiName: API_NAME });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
requestJSON = (req: ApiRequest) => this.request(req).then(this.responseToJSON) as Promise<any>;
requestText = (req: ApiRequest) => this.request(req).then(this.responseToText) as Promise<string>;
user = () => this.requestJSON('/user') as Promise<BitBucketUser>;
hasWriteAccess = async () => {
const response = await this.request(this.repoURL);
if (response.status === 404) {
throw Error('Repo not found');
}
return response.ok;
};
getBranch = async (branchName: string) => {
const branch: BitBucketBranch = await this.requestJSON(
`${this.repoURL}/refs/branches/${branchName}`,
);
return branch;
};
branchCommitSha = async (branch: string) => {
const {
target: { hash: branchSha },
}: BitBucketBranch = await this.getBranch(branch);
return branchSha;
};
defaultBranchCommitSha = () => {
return this.branchCommitSha(this.branch);
};
isFile = ({ type }: BitBucketFile) => type === 'commit_file';
getFileId = (commitHash: string, path: string) => {
return `${commitHash}/${path}`;
};
processFile = (file: BitBucketFile) => ({
id: file.id,
type: file.type,
path: file.path,
name: basename(file.path),
// BitBucket does not return file SHAs, but it does give us the
// commit SHA. Since the commit SHA will change if any files do,
// we can construct an ID using the commit SHA and the file path
// that will help with caching (though not as well as a normal
// SHA, since it will change even if the individual file itself
// doesn't.)
...(file.commit && file.commit.hash ? { id: this.getFileId(file.commit.hash, file.path) } : {}),
});
processFiles = (files: BitBucketFile[]) => files.filter(this.isFile).map(this.processFile);
readFile = async (
path: string,
sha?: string | null,
{ parseText = true, branch = this.branch, head = '' } = {},
): Promise<string | Blob> => {
const fetchContent = async () => {
const node = head ? head : await this.branchCommitSha(branch);
const content = await this.request({
url: `${this.repoURL}/src/${node}/${path}`,
cache: 'no-store',
}).then<string | Blob>(parseText ? this.responseToText : this.responseToBlob);
return content;
};
const content = await readFile(sha, fetchContent, localForage, parseText);
return content;
};
async readFileMetadata(path: string, sha: string | null | undefined) {
const fetchFileMetadata = async () => {
try {
const { values }: { values: BitBucketCommit[] } = await this.requestJSON({
url: `${this.repoURL}/commits`,
params: { path, include: this.branch },
});
const commit = values[0];
return {
author: commit.author.user
? commit.author.user.display_name || commit.author.user.nickname
: commit.author.raw,
updatedOn: commit.date,
};
} catch (e) {
return { author: '', updatedOn: '' };
}
};
const fileMetadata = await readFileMetadata(sha, fetchFileMetadata, localForage);
return fileMetadata;
}
async isShaExistsInBranch(branch: string, sha: string) {
const { values }: { values: BitBucketCommit[] } = await this.requestJSON({
url: `${this.repoURL}/commits`,
params: { include: branch, pagelen: 100 },
}).catch(e => {
console.log(`Failed getting commits for branch '${branch}'`, e);
return [];
});
return values.some(v => v.hash === sha);
}
getEntriesAndCursor = (jsonResponse: BitBucketSrcResult) => {
const {
size: count,
page,
pagelen: pageSize,
next,
previous: prev,
values: entries,
} = jsonResponse;
const pageCount = pageSize && count ? Math.ceil(count / pageSize) : undefined;
return {
entries,
cursor: Cursor.create({
actions: [...(next ? ['next'] : []), ...(prev ? ['prev'] : [])],
meta: { page, count, pageSize, pageCount },
data: { links: { next, prev } },
}),
};
};
listFiles = async (path: string, depth = 1, pagelen: number, branch: string) => {
const node = await this.branchCommitSha(branch);
const result: BitBucketSrcResult = await this.requestJSON({
url: `${this.repoURL}/src/${node}/${path}`,
params: {
max_depth: depth,
pagelen,
},
}).catch(replace404WithEmptyResponse);
const { entries, cursor } = this.getEntriesAndCursor(result);
return { entries: this.processFiles(entries), cursor: cursor as Cursor };
};
traverseCursor = async (
cursor: Cursor,
action: string,
): Promise<{
cursor: Cursor;
entries: { path: string; name: string; type: string; id: string }[];
}> =>
flow([
this.requestJSON,
then(this.getEntriesAndCursor),
then<
{ cursor: Cursor; entries: BitBucketFile[] },
{ cursor: Cursor; entries: BitBucketFile[] }
>(({ cursor: newCursor, entries }) => ({
cursor: newCursor,
entries: this.processFiles(entries),
})),
])(cursor.data!.getIn(['links', action]));
listAllFiles = async (path: string, depth: number, branch: string) => {
const { cursor: initialCursor, entries: initialEntries } = await this.listFiles(
path,
depth,
100,
branch,
);
const entries = [...initialEntries];
let currentCursor = initialCursor;
while (currentCursor && currentCursor.actions!.has('next')) {
const { cursor: newCursor, entries: newEntries } = await this.traverseCursor(
currentCursor,
'next',
);
entries.push(...newEntries);
currentCursor = newCursor;
}
return this.processFiles(entries);
};
async uploadFiles(
files: { path: string; newPath?: string; delete?: boolean }[],
{
commitMessage,
branch,
parentSha,
}: { commitMessage: string; branch: string; parentSha?: string },
) {
const formData = new FormData();
const toMove: { from: string; to: string; contentBlob: Blob }[] = [];
files.forEach(file => {
if (file.delete) {
// delete the file
formData.append('files', file.path);
} else if (file.newPath) {
const contentBlob = get(file, 'fileObj', new Blob([(file as DataFile).raw]));
toMove.push({ from: file.path, to: file.newPath, contentBlob });
} else {
// add/modify the file
const contentBlob = get(file, 'fileObj', new Blob([(file as DataFile).raw]));
// Third param is filename header, in case path is `message`, `branch`, etc.
formData.append(file.path, contentBlob, basename(file.path));
}
});
for (const { from, to, contentBlob } of toMove) {
const sourceDir = dirname(from);
const destDir = dirname(to);
const filesBranch = parentSha ? this.branch : branch;
const files = await this.listAllFiles(sourceDir, 100, filesBranch);
for (const file of files) {
// to move a file in Bitbucket we need to delete the old path
// and upload the file content to the new path
// NOTE: this is very wasteful, and also the Bitbucket `diff` API
// reports these files as deleted+added instead of renamed
// delete current path
formData.append('files', file.path);
// create in new path
const content =
file.path === from
? contentBlob
: await this.readFile(file.path, null, {
branch: filesBranch,
parseText: false,
});
formData.append(file.path.replace(sourceDir, destDir), content, basename(file.path));
}
}
if (commitMessage) {
formData.append('message', commitMessage);
}
if (this.commitAuthor) {
const { name, email } = this.commitAuthor;
formData.append('author', `${name} <${email}>`);
}
formData.append('branch', branch);
if (parentSha) {
formData.append('parents', parentSha);
}
try {
await this.requestText({
url: `${this.repoURL}/src`,
method: 'POST',
body: formData,
});
} catch (error) {
const message = error.message || '';
// very descriptive message from Bitbucket
if (parentSha && message.includes('Something went wrong')) {
await throwOnConflictingBranches(branch, name => this.getBranch(name), API_NAME);
}
throw error;
}
return files;
}
async persistFiles(dataFiles: DataFile[], mediaFiles: AssetProxy[], options: PersistOptions) {
const files = [...dataFiles, ...mediaFiles];
if (options.useWorkflow) {
const slug = dataFiles[0].slug;
return this.editorialWorkflowGit(files, slug, options);
} else {
return this.uploadFiles(files, { commitMessage: options.commitMessage, branch: this.branch });
}
}
async addPullRequestComment(pullRequest: BitBucketPullRequest, comment: string) {
await this.requestJSON({
method: 'POST',
url: `${this.repoURL}/pullrequests/${pullRequest.id}/comments`,
headers: { 'Content-Type': APPLICATION_JSON },
body: JSON.stringify({
content: {
raw: comment,
},
}),
});
}
async getPullRequestLabel(id: number) {
const comments: BitBucketPullComments = await this.requestJSON({
url: `${this.repoURL}/pullrequests/${id}/comments`,
params: {
pagelen: 100,
},
});
return comments.values.map(c => c.content.raw)[comments.values.length - 1];
}
async createPullRequest(branch: string, commitMessage: string, status: string) {
const pullRequest: BitBucketPullRequest = await this.requestJSON({
method: 'POST',
url: `${this.repoURL}/pullrequests`,
headers: { 'Content-Type': APPLICATION_JSON },
body: JSON.stringify({
title: commitMessage,
source: {
branch: {
name: branch,
},
},
destination: {
branch: {
name: this.branch,
},
},
description: DEFAULT_PR_BODY,
close_source_branch: true,
}),
});
// use comments for status labels
await this.addPullRequestComment(pullRequest, statusToLabel(status, this.cmsLabelPrefix));
}
async getDifferences(source: string, destination: string = this.branch) {
if (source === destination) {
return [];
}
const rawDiff = await this.requestText({
url: `${this.repoURL}/diff/${source}..${destination}`,
params: {
binary: false,
},
});
const diffs = parse(rawDiff).map(d => {
const oldPath = d.oldPath?.replace(/b\//, '') || '';
const newPath = d.newPath?.replace(/b\//, '') || '';
const path = newPath || (oldPath as string);
return {
oldPath,
newPath,
status: d.status,
newFile: d.status === 'added',
path,
binary: d.binary || /.svg$/.test(path),
};
});
return diffs;
}
async editorialWorkflowGit(
files: (DataFile | AssetProxy)[],
slug: string,
options: PersistOptions,
) {
const contentKey = generateContentKey(options.collectionName as string, slug);
const branch = branchFromContentKey(contentKey);
const unpublished = options.unpublished || false;
if (!unpublished) {
const defaultBranchSha = await this.branchCommitSha(this.branch);
await this.uploadFiles(files, {
commitMessage: options.commitMessage,
branch,
parentSha: defaultBranchSha,
});
await this.createPullRequest(
branch,
options.commitMessage,
options.status || this.initialWorkflowStatus,
);
} else {
// mark files for deletion
const diffs = await this.getDifferences(branch);
const toDelete: DeleteEntry[] = [];
for (const diff of diffs.filter(d => d.binary && d.status !== 'deleted')) {
if (!files.some(file => file.path === diff.path)) {
toDelete.push({ path: diff.path, delete: true });
}
}
await this.uploadFiles([...files, ...toDelete], {
commitMessage: options.commitMessage,
branch,
});
}
}
deleteFiles = (paths: string[], message: string) => {
const body = new FormData();
paths.forEach(path => {
body.append('files', path);
});
body.append('branch', this.branch);
if (message) {
body.append('message', message);
}
if (this.commitAuthor) {
const { name, email } = this.commitAuthor;
body.append('author', `${name} <${email}>`);
}
return flow([unsentRequest.withMethod('POST'), unsentRequest.withBody(body), this.request])(
`${this.repoURL}/src`,
);
};
async getPullRequests(sourceBranch?: string) {
const sourceQuery = sourceBranch
? `source.branch.name = "${sourceBranch}"`
: `source.branch.name ~ "${CMS_BRANCH_PREFIX}/"`;
const pullRequests: BitBucketPullRequests = await this.requestJSON({
url: `${this.repoURL}/pullrequests`,
params: {
pagelen: 50,
q: oneLine`
source.repository.full_name = "${this.repo}"
AND state = "${BitBucketPullRequestState.OPEN}"
AND destination.branch.name = "${this.branch}"
AND comment_count > 0
AND ${sourceQuery}
`,
},
});
const labels = await Promise.all(
pullRequests.values.map(pr => this.getPullRequestLabel(pr.id)),
);
return pullRequests.values.filter((_, index) => isCMSLabel(labels[index], this.cmsLabelPrefix));
}
async getBranchPullRequest(branch: string) {
const pullRequests = await this.getPullRequests(branch);
if (pullRequests.length <= 0) {
throw new EditorialWorkflowError('content is not under editorial workflow', true);
}
return pullRequests[0];
}
async listUnpublishedBranches() {
console.log(
'%c Checking for Unpublished entries',
'line-height: 30px;text-align: center;font-weight: bold',
);
const pullRequests = await this.getPullRequests();
const branches = pullRequests.map(mr => mr.source.branch.name);
return branches;
}
async retrieveUnpublishedEntryData(contentKey: string) {
const { collection, slug } = parseContentKey(contentKey);
const branch = branchFromContentKey(contentKey);
const pullRequest = await this.getBranchPullRequest(branch);
const diffs = await this.getDifferences(branch);
const label = await this.getPullRequestLabel(pullRequest.id);
const status = labelToStatus(label, this.cmsLabelPrefix);
const updatedAt = pullRequest.updated_on;
const pullRequestAuthor = pullRequest.author.display_name;
return {
collection,
slug,
status,
// TODO: get real id
diffs: diffs
.filter(d => d.status !== 'deleted')
.map(d => ({ path: d.path, newFile: d.newFile, id: '' })),
updatedAt,
pullRequestAuthor,
};
}
async updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
const contentKey = generateContentKey(collection, slug);
const branch = branchFromContentKey(contentKey);
const pullRequest = await this.getBranchPullRequest(branch);
await this.addPullRequestComment(pullRequest, statusToLabel(newStatus, this.cmsLabelPrefix));
}
async mergePullRequest(pullRequest: BitBucketPullRequest) {
await this.requestJSON({
method: 'POST',
url: `${this.repoURL}/pullrequests/${pullRequest.id}/merge`,
headers: { 'Content-Type': APPLICATION_JSON },
body: JSON.stringify({
message: MERGE_COMMIT_MESSAGE,
close_source_branch: true,
merge_strategy: this.mergeStrategy,
}),
});
}
async publishUnpublishedEntry(collectionName: string, slug: string) {
const contentKey = generateContentKey(collectionName, slug);
const branch = branchFromContentKey(contentKey);
const pullRequest = await this.getBranchPullRequest(branch);
await this.mergePullRequest(pullRequest);
}
async declinePullRequest(pullRequest: BitBucketPullRequest) {
await this.requestJSON({
method: 'POST',
url: `${this.repoURL}/pullrequests/${pullRequest.id}/decline`,
});
}
async deleteBranch(branch: string) {
await this.request({
method: 'DELETE',
url: `${this.repoURL}/refs/branches/${branch}`,
});
}
async deleteUnpublishedEntry(collectionName: string, slug: string) {
const contentKey = generateContentKey(collectionName, slug);
const branch = branchFromContentKey(contentKey);
const pullRequest = await this.getBranchPullRequest(branch);
await this.declinePullRequest(pullRequest);
await this.deleteBranch(branch);
}
async getPullRequestStatuses(pullRequest: BitBucketPullRequest) {
const statuses: BitBucketPullRequestStatues = await this.requestJSON({
url: `${this.repoURL}/pullrequests/${pullRequest.id}/statuses`,
params: {
pagelen: 100,
},
});
return statuses.values;
}
async getStatuses(collectionName: string, slug: string) {
const contentKey = generateContentKey(collectionName, slug);
const branch = branchFromContentKey(contentKey);
const pullRequest = await this.getBranchPullRequest(branch);
const statuses = await this.getPullRequestStatuses(pullRequest);
return statuses.map(({ key, state, url }) => ({
context: key,
state:
state === BitBucketPullRequestStatusState.Successful
? PreviewState.Success
: PreviewState.Other,
target_url: url,
}));
}
async getUnpublishedEntrySha(collection: string, slug: string) {
const contentKey = generateContentKey(collection, slug);
const branch = branchFromContentKey(contentKey);
const pullRequest = await this.getBranchPullRequest(branch);
return pullRequest.destination.commit.hash;
}
}

View File

@@ -0,0 +1,94 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from '@emotion/styled';
import { NetlifyAuthenticator, ImplicitAuthenticator } from 'decap-cms-lib-auth';
import { AuthenticationPage, Icon } from 'decap-cms-ui-default';
const LoginButtonIcon = styled(Icon)`
margin-right: 18px;
`;
export default class BitbucketAuthenticationPage 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() {
const { auth_type: authType = '' } = this.props.config.backend;
if (authType === 'implicit') {
const {
base_url = 'https://bitbucket.org',
auth_endpoint = 'site/oauth2/authorize',
app_id = '',
} = this.props.config.backend;
this.auth = new ImplicitAuthenticator({
base_url,
auth_endpoint,
app_id,
clearHash: this.props.clearHash,
});
// Complete implicit authentication if we were redirected back to from the provider.
this.auth.completeAuth((err, data) => {
if (err) {
this.setState({ loginError: err.toString() });
return;
}
this.props.onLogin(data);
});
this.authSettings = { scope: 'repository:write' };
} else {
this.auth = new NetlifyAuthenticator({
base_url: this.props.base_url,
site_id:
document.location.host.split(':')[0] === 'localhost'
? 'demo.decapcms.org'
: this.props.siteId,
auth_endpoint: this.props.authEndpoint,
});
this.authSettings = { provider: 'bitbucket', scope: 'repo' };
}
}
handleLogin = e => {
e.preventDefault();
this.auth.authenticate(this.authSettings, (err, data) => {
if (err) {
this.setState({ loginError: err.toString() });
return;
}
this.props.onLogin(data);
});
};
render() {
const { inProgress, config, t } = this.props;
return (
<AuthenticationPage
onLogin={this.handleLogin}
loginDisabled={inProgress}
loginErrorMessage={this.state.loginError}
logoUrl={config.logo_url}
siteUrl={config.site_url}
renderButtonContent={() => (
<React.Fragment>
<LoginButtonIcon type="bitbucket" />
{inProgress ? t('auth.loggingIn') : t('auth.loginWithBitbucket')}
</React.Fragment>
)}
t={t}
/>
);
}
}

View File

@@ -0,0 +1,35 @@
import API from '../API';
global.fetch = jest.fn().mockRejectedValue(new Error('should not call fetch inside tests'));
describe('bitbucket API', () => {
beforeEach(() => {
jest.resetAllMocks();
});
test('should get preview statuses', async () => {
const api = new API({});
const pr = { id: 1 };
const statuses = [
{ key: 'deploy', state: 'SUCCESSFUL', url: 'deploy-url' },
{ key: 'build', state: 'FAILED' },
];
api.getBranchPullRequest = jest.fn(() => Promise.resolve(pr));
api.getPullRequestStatuses = jest.fn(() => Promise.resolve(statuses));
const collectionName = 'posts';
const slug = 'title';
await expect(api.getStatuses(collectionName, slug)).resolves.toEqual([
{ context: 'deploy', state: 'success', target_url: 'deploy-url' },
{ context: 'build', state: 'other' },
]);
expect(api.getBranchPullRequest).toHaveBeenCalledTimes(1);
expect(api.getBranchPullRequest).toHaveBeenCalledWith(`cms/posts/title`);
expect(api.getPullRequestStatuses).toHaveBeenCalledTimes(1);
expect(api.getPullRequestStatuses).toHaveBeenCalledWith(pr);
});
});

View File

@@ -0,0 +1,102 @@
import minimatch from 'minimatch';
import { unsentRequest } from 'decap-cms-lib-util';
import type { ApiRequest, PointerFile } from 'decap-cms-lib-util';
type MakeAuthorizedRequest = (req: ApiRequest) => Promise<Response>;
interface LfsBatchAction {
href: string;
header?: { [key: string]: string };
expires_in?: number;
expires_at?: string;
}
interface LfsBatchObject {
oid: string;
size: number;
}
interface LfsBatchObjectUpload extends LfsBatchObject {
actions?: {
upload: LfsBatchAction;
verify?: LfsBatchAction;
};
}
interface LfsBatchObjectError extends LfsBatchObject {
error: {
code: number;
message: string;
};
}
interface LfsBatchUploadResponse {
transfer?: string;
objects: (LfsBatchObjectUpload | LfsBatchObjectError)[];
}
export class GitLfsClient {
private static defaultContentHeaders = {
Accept: 'application/vnd.git-lfs+json',
['Content-Type']: 'application/vnd.git-lfs+json',
};
constructor(
public enabled: boolean,
public rootURL: string,
public patterns: string[],
private makeAuthorizedRequest: MakeAuthorizedRequest,
) {}
matchPath(path: string) {
return this.patterns.some(pattern => minimatch(path, pattern, { matchBase: true }));
}
async uploadResource(pointer: PointerFile, resource: Blob): Promise<string> {
const requests = await this.getResourceUploadRequests([pointer]);
for (const request of requests) {
await this.doUpload(request.actions!.upload, resource);
if (request.actions!.verify) {
await this.doVerify(request.actions!.verify, request);
}
}
return pointer.sha;
}
private async doUpload(upload: LfsBatchAction, resource: Blob) {
await unsentRequest.fetchWithTimeout(decodeURI(upload.href), {
method: 'PUT',
body: resource,
headers: upload.header,
});
}
private async doVerify(verify: LfsBatchAction, object: LfsBatchObject) {
this.makeAuthorizedRequest({
url: decodeURI(verify.href),
method: 'POST',
headers: { ...GitLfsClient.defaultContentHeaders, ...verify.header },
body: JSON.stringify({ oid: object.oid, size: object.size }),
});
}
private async getResourceUploadRequests(objects: PointerFile[]): Promise<LfsBatchObjectUpload[]> {
const response = await this.makeAuthorizedRequest({
url: `${this.rootURL}/objects/batch`,
method: 'POST',
headers: GitLfsClient.defaultContentHeaders,
body: JSON.stringify({
operation: 'upload',
transfers: ['basic'],
objects: objects.map(({ sha, ...rest }) => ({ ...rest, oid: sha })),
}),
});
return ((await response.json()) as LfsBatchUploadResponse).objects.filter(object => {
if ('error' in object) {
console.error(object.error);
return false;
}
return object.actions;
});
}
}

View File

@@ -0,0 +1,655 @@
import semaphore from 'semaphore';
import { trimStart } from 'lodash';
import { stripIndent } from 'common-tags';
import {
CURSOR_COMPATIBILITY_SYMBOL,
filterByExtension,
unsentRequest,
basename,
getBlobSHA,
entriesByFolder,
entriesByFiles,
getMediaDisplayURL,
getMediaAsBlob,
unpublishedEntries,
runWithLock,
asyncLock,
getPreviewStatus,
getLargeMediaPatternsFromGitAttributesFile,
getPointerFileForMediaFileObj,
getLargeMediaFilteredMediaFiles,
blobToFileObj,
contentKeyFromBranch,
generateContentKey,
localForage,
allEntriesByFolder,
AccessTokenError,
branchFromContentKey,
} from 'decap-cms-lib-util';
import { NetlifyAuthenticator } from 'decap-cms-lib-auth';
import AuthenticationPage from './AuthenticationPage';
import API, { API_NAME } from './API';
import { GitLfsClient } from './git-lfs-client';
import type {
Entry,
ApiRequest,
Cursor,
AssetProxy,
PersistOptions,
DisplayURL,
Implementation,
User,
Credentials,
Config,
ImplementationFile,
AsyncLock,
FetchError,
} from 'decap-cms-lib-util';
import type { Semaphore } from 'semaphore';
const MAX_CONCURRENT_DOWNLOADS = 10;
const STATUS_PAGE = 'https://bitbucket.status.atlassian.com';
const BITBUCKET_STATUS_ENDPOINT = `${STATUS_PAGE}/api/v2/components.json`;
const BITBUCKET_OPERATIONAL_UNITS = ['API', 'Authentication and user management', 'Git LFS'];
type BitbucketStatusComponent = {
id: string;
name: string;
status: string;
};
const { fetchWithTimeout: fetch } = unsentRequest;
// Implementation wrapper class
export default class BitbucketBackend implements Implementation {
lock: AsyncLock;
api: API | null;
updateUserCredentials: (args: { token: string; refresh_token: string }) => Promise<null>;
options: {
proxied: boolean;
API: API | null;
updateUserCredentials: (args: { token: string; refresh_token: string }) => Promise<null>;
initialWorkflowStatus: string;
};
repo: string;
isBranchConfigured: boolean;
branch: string;
apiRoot: string;
baseUrl: string;
siteId: string;
token: string | null;
mediaFolder: string;
refreshToken?: string;
refreshedTokenPromise?: Promise<string>;
authenticator?: NetlifyAuthenticator;
_mediaDisplayURLSem?: Semaphore;
squashMerges: boolean;
cmsLabelPrefix: string;
previewContext: string;
largeMediaURL: string;
_largeMediaClientPromise?: Promise<GitLfsClient>;
authType: string;
constructor(config: Config, options = {}) {
this.options = {
proxied: false,
API: null,
updateUserCredentials: async () => null,
initialWorkflowStatus: '',
...options,
};
if (
!this.options.proxied &&
(config.backend.repo === null || config.backend.repo === undefined)
) {
throw new Error('The BitBucket backend needs a "repo" in the backend configuration.');
}
this.api = this.options.API || null;
this.updateUserCredentials = this.options.updateUserCredentials;
this.repo = config.backend.repo || '';
this.branch = config.backend.branch || 'master';
this.isBranchConfigured = config.backend.branch ? true : false;
this.apiRoot = config.backend.api_root || 'https://api.bitbucket.org/2.0';
this.baseUrl = config.base_url || '';
this.siteId = config.site_id || '';
this.largeMediaURL =
config.backend.large_media_url || `https://bitbucket.org/${config.backend.repo}/info/lfs`;
this.token = '';
this.mediaFolder = config.media_folder;
this.squashMerges = config.backend.squash_merges || false;
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
this.previewContext = config.backend.preview_context || '';
this.lock = asyncLock();
this.authType = config.backend.auth_type || '';
}
isGitBackend() {
return true;
}
async status() {
const api = await fetch(BITBUCKET_STATUS_ENDPOINT)
.then(res => res.json())
.then(res => {
return res['components']
.filter((statusComponent: BitbucketStatusComponent) =>
BITBUCKET_OPERATIONAL_UNITS.includes(statusComponent.name),
)
.every(
(statusComponent: BitbucketStatusComponent) => statusComponent.status === 'operational',
);
})
.catch(e => {
console.warn('Failed getting BitBucket status', e);
return true;
});
let auth = false;
// no need to check auth if api is down
if (api) {
auth =
(await this.api
?.user()
.then(user => !!user)
.catch(e => {
console.warn('Failed getting Bitbucket user', e);
return false;
})) || false;
}
return { auth: { status: auth }, api: { status: api, statusPage: STATUS_PAGE } };
}
authComponent() {
return AuthenticationPage;
}
setUser(user: { token: string }) {
this.token = user.token;
this.api = new API({
requestFunction: this.apiRequestFunction,
branch: this.branch,
repo: this.repo,
squashMerges: this.squashMerges,
cmsLabelPrefix: this.cmsLabelPrefix,
initialWorkflowStatus: this.options.initialWorkflowStatus,
});
}
requestFunction = async (req: ApiRequest) => {
const token = await this.getToken();
const authorizedRequest = unsentRequest.withHeaders({ Authorization: `Bearer ${token}` }, req);
return unsentRequest.performRequest(authorizedRequest);
};
restoreUser(user: User) {
return this.authenticate(user);
}
async authenticate(state: Credentials) {
this.token = state.token as string;
if (!this.isBranchConfigured) {
const repo = await fetch(`${this.apiRoot}/repositories/${this.repo}`, {
headers: {
Authorization: `token ${this.token}`,
},
})
.then(res => res.json())
.catch(() => null);
if (repo) {
this.branch = repo.mainbranch.name;
}
}
this.refreshToken = state.refresh_token;
this.api = new API({
requestFunction: this.apiRequestFunction,
branch: this.branch,
repo: this.repo,
apiRoot: this.apiRoot,
squashMerges: this.squashMerges,
cmsLabelPrefix: this.cmsLabelPrefix,
initialWorkflowStatus: this.options.initialWorkflowStatus,
});
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 Bitbucket account with access.
`;
throw error;
});
// Unauthorized user
if (!isCollab) {
throw new Error('Your BitBucket user account does not have access to this repo.');
}
// if (!this.isBranchConfigured) {
// const defaultBranchName = await getDefaultBranchName({
// backend: 'bitbucket',
// repo: this.repo,
// token: this.token,
// });
// if (defaultBranchName) {
// this.branch = defaultBranchName;
// }
// }
const user = await this.api.user();
// Authorized user
return {
...user,
name: user.display_name,
login: user.username,
token: state.token,
avatar_url: user.links.avatar.href,
refresh_token: state.refresh_token,
};
}
getRefreshedAccessToken() {
if (this.authType === 'implicit') {
throw new AccessTokenError(`Can't refresh access token when using implicit auth`);
}
if (this.refreshedTokenPromise) {
return this.refreshedTokenPromise;
}
// instantiating a new Authenticator on each refresh isn't ideal,
if (!this.authenticator) {
const cfg = {
base_url: this.baseUrl,
site_id: this.siteId,
};
this.authenticator = new NetlifyAuthenticator(cfg);
}
this.refreshedTokenPromise = this.authenticator!.refresh({
provider: 'bitbucket',
refresh_token: this.refreshToken as string,
}).then(({ token, refresh_token }) => {
this.token = token;
this.refreshToken = refresh_token;
this.refreshedTokenPromise = undefined;
this.updateUserCredentials({ token, refresh_token });
return token;
});
return this.refreshedTokenPromise;
}
logout() {
this.token = null;
return;
}
getToken() {
if (this.refreshedTokenPromise) {
return this.refreshedTokenPromise;
}
return Promise.resolve(this.token);
}
apiRequestFunction = async (req: ApiRequest) => {
const token = (
this.refreshedTokenPromise ? await this.refreshedTokenPromise : this.token
) as string;
const authorizedRequest = unsentRequest.withHeaders({ Authorization: `Bearer ${token}` }, req);
const response: Response = await unsentRequest.performRequest(authorizedRequest);
if (response.status === 401) {
const json = await response.json().catch(() => null);
if (json && json.type === 'error' && /^access token expired/i.test(json.error.message)) {
const newToken = await this.getRefreshedAccessToken();
const reqWithNewToken = unsentRequest.withHeaders(
{
Authorization: `Bearer ${newToken}`,
},
req,
) as ApiRequest;
return unsentRequest.performRequest(reqWithNewToken);
}
}
return response;
};
async entriesByFolder(folder: string, extension: string, depth: number) {
let cursor: Cursor;
const listFiles = () =>
this.api!.listFiles(folder, depth, 20, this.branch).then(({ entries, cursor: c }) => {
cursor = c.mergeMeta({ extension });
return entries.filter(e => filterByExtension(e, extension));
});
const head = await this.api!.defaultBranchCommitSha();
const readFile = (path: string, id: string | null | undefined) => {
return this.api!.readFile(path, id, { head }) 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 listAllFiles(folder: string, extension: string, depth: number) {
const files = await this.api!.listAllFiles(folder, depth, this.branch);
const filtered = files.filter(file => filterByExtension(file, extension));
return filtered;
}
async allEntriesByFolder(folder: string, extension: string, depth: number) {
const head = await this.api!.defaultBranchCommitSha();
const readFile = (path: string, id: string | null | undefined) => {
return this.api!.readFile(path, id, { head }) as Promise<string>;
};
const files = await allEntriesByFolder({
listAllFiles: () => this.listAllFiles(folder, extension, depth),
readFile,
readFileMetadata: this.api!.readFileMetadata.bind(this.api),
apiName: API_NAME,
branch: this.branch,
localForage,
folder,
extension,
depth,
getDefaultBranch: () => Promise.resolve({ name: this.branch, sha: head }),
isShaExistsInBranch: this.api!.isShaExistsInBranch.bind(this.api!),
getDifferences: (source, destination) => this.api!.getDifferences(source, destination),
getFileId: path => Promise.resolve(this.api!.getFileId(head, path)),
filterFile: file => filterByExtension(file, extension),
});
return files;
}
async entriesByFiles(files: ImplementationFile[]) {
const head = await this.api!.defaultBranchCommitSha();
const readFile = (path: string, id: string | null | undefined) => {
return this.api!.readFile(path, id, { head }) as Promise<string>;
};
return entriesByFiles(files, readFile, this.api!.readFileMetadata.bind(this.api), API_NAME);
}
getEntry(path: string) {
return this.api!.readFile(path).then(data => ({
file: { path, id: null },
data: data as string,
}));
}
getMedia(mediaFolder = this.mediaFolder) {
return this.api!.listAllFiles(mediaFolder, 1, this.branch).then(files =>
files.map(({ id, name, path }) => ({ id, name, path, displayURL: { id, path } })),
);
}
getLargeMediaClient() {
if (!this._largeMediaClientPromise) {
this._largeMediaClientPromise = (async (): Promise<GitLfsClient> => {
const patterns = await this.api!.readFile('.gitattributes')
.then(attributes => getLargeMediaPatternsFromGitAttributesFile(attributes as string))
.catch((err: FetchError) => {
if (err.status === 404) {
console.log('This 404 was expected and handled appropriately.');
} else {
console.error(err);
}
return [];
});
return new GitLfsClient(
!!(this.largeMediaURL && patterns.length > 0),
this.largeMediaURL,
patterns,
this.requestFunction,
);
})();
}
return this._largeMediaClientPromise;
}
getMediaDisplayURL(displayURL: DisplayURL) {
this._mediaDisplayURLSem = this._mediaDisplayURLSem || semaphore(MAX_CONCURRENT_DOWNLOADS);
return getMediaDisplayURL(
displayURL,
this.api!.readFile.bind(this.api!),
this._mediaDisplayURLSem,
);
}
async getMediaFile(path: string) {
const name = basename(path);
const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!));
const fileObj = blobToFileObj(name, blob);
const url = URL.createObjectURL(fileObj);
const id = await getBlobSHA(fileObj);
return {
id,
displayURL: url,
path,
name,
size: fileObj.size,
file: fileObj,
url,
};
}
async persistEntry(entry: Entry, options: PersistOptions) {
const client = await this.getLargeMediaClient();
// persistEntry is a transactional operation
return runWithLock(
this.lock,
async () =>
this.api!.persistFiles(
entry.dataFiles,
client.enabled
? await getLargeMediaFilteredMediaFiles(client, entry.assets)
: entry.assets,
options,
),
'Failed to acquire persist entry lock',
);
}
async persistMedia(mediaFile: AssetProxy, options: PersistOptions) {
const { fileObj, path } = mediaFile;
const displayURL = fileObj ? URL.createObjectURL(fileObj) : '';
const client = await this.getLargeMediaClient();
const fixedPath = path.startsWith('/') ? path.slice(1) : path;
if (!client.enabled || !client.matchPath(fixedPath)) {
return this._persistMedia(mediaFile, options);
}
const persistMediaArgument = await getPointerFileForMediaFileObj(client, fileObj as File, path);
return {
...(await this._persistMedia(persistMediaArgument, options)),
displayURL,
};
}
async _persistMedia(mediaFile: AssetProxy, options: PersistOptions) {
const fileObj = mediaFile.fileObj as File;
const [id] = await Promise.all([
getBlobSHA(fileObj),
this.api!.persistFiles([], [mediaFile], options),
]);
const url = URL.createObjectURL(fileObj);
return {
displayURL: url,
path: trimStart(mediaFile.path, '/k'),
name: fileObj!.name,
size: fileObj!.size,
id,
file: fileObj,
url,
};
}
deleteFiles(paths: string[], commitMessage: string) {
return this.api!.deleteFiles(paths, commitMessage);
}
traverseCursor(cursor: Cursor, action: string) {
return this.api!.traverseCursor(cursor, action).then(async ({ entries, cursor: newCursor }) => {
const extension = cursor.meta?.get('extension');
if (extension) {
entries = entries.filter(e => filterByExtension(e, extension));
newCursor = newCursor.mergeMeta({ extension });
}
const head = await this.api!.defaultBranchCommitSha();
const readFile = (path: string, id: string | null | undefined) => {
return this.api!.readFile(path, id, { head }) as Promise<string>;
};
const entriesWithData = await entriesByFiles(
entries,
readFile,
this.api!.readFileMetadata.bind(this.api)!,
API_NAME,
);
return {
entries: entriesWithData,
cursor: newCursor,
};
});
}
async loadMediaFile(path: string, id: string, { branch }: { branch: string }) {
const readFile = async (
path: string,
id: string | null | undefined,
{ parseText }: { parseText: boolean },
) => {
const content = await this.api!.readFile(path, id, { branch, parseText });
return content;
};
const blob = await getMediaAsBlob(path, id, readFile);
const name = basename(path);
const fileObj = blobToFileObj(name, blob);
return {
id: path,
displayURL: URL.createObjectURL(fileObj),
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 = generateContentKey(collection, slug);
const data = await this.api!.retrieveUnpublishedEntryData(entryId);
return data;
} else {
throw new Error('Missing unpublished entry id or collection and slug');
}
}
getBranch(collection: string, slug: string) {
const contentKey = generateContentKey(collection, slug);
const branch = branchFromContentKey(contentKey);
return branch;
}
async unpublishedEntryDataFile(collection: string, slug: string, path: string, id: string) {
const branch = this.getBranch(collection, slug);
const data = (await this.api!.readFile(path, id, { branch })) as string;
return data;
}
async unpublishedEntryMediaFile(collection: string, slug: string, path: string, id: string) {
const branch = this.getBranch(collection, slug);
const mediaFile = await this.loadMediaFile(path, id, { branch });
return mediaFile;
}
async updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
// updateUnpublishedEntryStatus is a transactional operation
return runWithLock(
this.lock,
() => this.api!.updateUnpublishedEntryStatus(collection, slug, newStatus),
'Failed to acquire update entry status lock',
);
}
async deleteUnpublishedEntry(collection: string, slug: string) {
// deleteUnpublishedEntry is a transactional operation
return runWithLock(
this.lock,
() => this.api!.deleteUnpublishedEntry(collection, slug),
'Failed to acquire delete entry lock',
);
}
async publishUnpublishedEntry(collection: string, slug: string) {
// publishUnpublishedEntry is a transactional operation
return runWithLock(
this.lock,
() => this.api!.publishUnpublishedEntry(collection, slug),
'Failed to acquire publish entry lock',
);
}
async getDeployPreview(collection: string, slug: string) {
try {
const statuses = await this.api!.getStatuses(collection, slug);
const deployStatus = getPreviewStatus(statuses, this.previewContext);
if (deployStatus) {
const { target_url: url, state } = deployStatus;
return { url, status: state };
} else {
return null;
}
} catch (e) {
return null;
}
}
}

10
node_modules/decap-cms-backend-bitbucket/src/index.ts generated vendored Normal file
View File

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

View 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;
}

View File

@@ -0,0 +1,5 @@
declare module 'what-the-diff' {
export const parse: (
rawDiff: string,
) => { oldPath?: string; newPath?: string; binary: boolean; status: string }[];
}

View File

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