add-cms
This commit is contained in:
393
source/admin/packages/decap-cms-backend-bitbucket/CHANGELOG.md
Normal file
393
source/admin/packages/decap-cms-backend-bitbucket/CHANGELOG.md
Normal file
@@ -0,0 +1,393 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-bitbucket@3.2.1...decap-cms-backend-bitbucket@3.3.0) (2025-07-15)
|
||||
|
||||
### Features
|
||||
|
||||
- add logo to header ([#7487](https://github.com/decaporg/decap-cms/issues/7487)) ([b540ace](https://github.com/decaporg/decap-cms/commit/b540acec943eb231df6aac7b1d515d9b4b84fa5d))
|
||||
|
||||
## [3.2.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-bitbucket@3.2.0...decap-cms-backend-bitbucket@3.2.1) (2025-07-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-bitbucket
|
||||
|
||||
# [3.2.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-bitbucket@3.1.4...decap-cms-backend-bitbucket@3.2.0) (2025-06-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-bitbucket
|
||||
|
||||
## [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))
|
||||
13
source/admin/packages/decap-cms-backend-bitbucket/README.md
Normal file
13
source/admin/packages/decap-cms-backend-bitbucket/README.md
Normal 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.
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "decap-cms-backend-bitbucket",
|
||||
"description": "Bitbucket backend for Decap CMS",
|
||||
"version": "3.3.0",
|
||||
"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",
|
||||
"minimatch": "^7.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"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": "^19.1.0"
|
||||
},
|
||||
"browser": {
|
||||
"path": "path-browserify"
|
||||
}
|
||||
}
|
||||
803
source/admin/packages/decap-cms-backend-bitbucket/src/API.ts
Normal file
803
source/admin/packages/decap-cms-backend-bitbucket/src/API.ts
Normal file
@@ -0,0 +1,803 @@
|
||||
import flow from 'lodash/flow';
|
||||
import get from 'lodash/get';
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
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() {
|
||||
// Manually validate PropTypes - React 19 breaking change
|
||||
PropTypes.checkPropTypes(
|
||||
BitbucketAuthenticationPage.propTypes,
|
||||
this.props,
|
||||
'prop',
|
||||
'BitbucketAuthenticationPage',
|
||||
);
|
||||
|
||||
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} // Deprecated, replaced by `logo.src`
|
||||
logo={config.logo}
|
||||
siteUrl={config.site_url}
|
||||
renderButtonContent={() => (
|
||||
<React.Fragment>
|
||||
<LoginButtonIcon type="bitbucket" />
|
||||
{inProgress ? t('auth.loggingIn') : t('auth.loginWithBitbucket')}
|
||||
</React.Fragment>
|
||||
)}
|
||||
t={t}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
import semaphore from 'semaphore';
|
||||
import trimStart from 'lodash/trimStart';
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
5
source/admin/packages/decap-cms-backend-bitbucket/src/types/semaphore.d.ts
vendored
Normal file
5
source/admin/packages/decap-cms-backend-bitbucket/src/types/semaphore.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module 'semaphore' {
|
||||
export type Semaphore = { take: (f: Function) => void; leave: () => void };
|
||||
const semaphore: (count: number) => Semaphore;
|
||||
export default semaphore;
|
||||
}
|
||||
5
source/admin/packages/decap-cms-backend-bitbucket/src/types/what-the-diff.d.ts
vendored
Normal file
5
source/admin/packages/decap-cms-backend-bitbucket/src/types/what-the-diff.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module 'what-the-diff' {
|
||||
export const parse: (
|
||||
rawDiff: string,
|
||||
) => { oldPath?: string; newPath?: string; binary: boolean; status: string }[];
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
const { getConfig } = require('../../scripts/webpack.js');
|
||||
|
||||
module.exports = getConfig();
|
||||
Reference in New Issue
Block a user