add-cms
This commit is contained in:
1162
source/admin/packages/decap-cms-app/CHANGELOG.md
Normal file
1162
source/admin/packages/decap-cms-app/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
41
source/admin/packages/decap-cms-app/README.md
Normal file
41
source/admin/packages/decap-cms-app/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Decap CMS App
|
||||
_For a Decap CMS overview, see the general [Decap CMS project README](https://github.com/decaporg/decap-cms)._
|
||||
|
||||
## Purpose
|
||||
This package is similar to the [`decap-cms`](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms/) package, but is designed for use with extensions. It serves that purpose in the following ways.
|
||||
|
||||
- It does not automatically initialize - you must run the CMS `init` method.
|
||||
- It does not include `react` or `react-dom` - they are required as peer dependencies.
|
||||
- It does not include the following extensions:
|
||||
- [`decap-cms-media-library-cloudinary`]
|
||||
- [`decap-cms-media-library-uploadcare`]
|
||||
|
||||
## Usage
|
||||
Install via script tag:
|
||||
|
||||
```html
|
||||
<!-- Excluding `doctype` and `head` but you should add them -->
|
||||
<body>
|
||||
<!-- Add these scripts to the bottom of the body -->
|
||||
<script src="https://unpkg.com/react@^18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@^18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/decap-cms-app/dist/decap-cms-app.js"></script>
|
||||
|
||||
<!-- Initialize the CMS -->
|
||||
<script>
|
||||
DecapCmsApp.init();
|
||||
</script>
|
||||
</body>
|
||||
```
|
||||
|
||||
Install via npm:
|
||||
|
||||
```
|
||||
npm i react react-dom decap-cms-app
|
||||
```
|
||||
|
||||
```js
|
||||
import CMS from 'decap-cms-app';
|
||||
|
||||
CMS.init();
|
||||
```
|
||||
7
source/admin/packages/decap-cms-app/index.d.ts
vendored
Normal file
7
source/admin/packages/decap-cms-app/index.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
declare module 'decap-cms-app' {
|
||||
import type { CMS } from 'decap-cms-core';
|
||||
|
||||
export const DecapCmsApp: CMS;
|
||||
|
||||
export default DecapCmsApp;
|
||||
}
|
||||
80
source/admin/packages/decap-cms-app/package.json
Normal file
80
source/admin/packages/decap-cms-app/package.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"name": "decap-cms-app",
|
||||
"description": "An extensible, open source, Git-based, React CMS for static sites. Reusable congiuration with React as peer.",
|
||||
"version": "3.8.3",
|
||||
"homepage": "https://www.decapcms.org",
|
||||
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-app",
|
||||
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
||||
"module": "dist/esm/index.js",
|
||||
"main": "dist/decap-cms-app.js",
|
||||
"files": [
|
||||
"src/",
|
||||
"dist/",
|
||||
"index.d.ts"
|
||||
],
|
||||
"types": "index.d.ts",
|
||||
"scripts": {
|
||||
"develop": "npm run build:esm -- --watch",
|
||||
"webpack": "node --max_old_space_size=4096 ../../node_modules/webpack/bin/webpack.js",
|
||||
"build": "cross-env NODE_ENV=production run-s webpack",
|
||||
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward"
|
||||
},
|
||||
"keywords": [
|
||||
"cms",
|
||||
"content editing",
|
||||
"static site generators",
|
||||
"jamstack"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-errors": "^3.0.0",
|
||||
"ajv-keywords": "^5.1.0",
|
||||
"codemirror": "^5.46.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"decap-cms-backend-aws-cognito-github-proxy": "^3.4.1",
|
||||
"decap-cms-backend-azure": "^3.3.0",
|
||||
"decap-cms-backend-bitbucket": "^3.3.0",
|
||||
"decap-cms-backend-git-gateway": "^3.4.1",
|
||||
"decap-cms-backend-gitea": "^3.3.0",
|
||||
"decap-cms-backend-github": "^3.4.0",
|
||||
"decap-cms-backend-gitlab": "^3.4.0",
|
||||
"decap-cms-backend-proxy": "^3.3.0",
|
||||
"decap-cms-backend-test": "^3.2.1",
|
||||
"decap-cms-core": "^3.8.1",
|
||||
"decap-cms-editor-component-image": "^3.2.0",
|
||||
"decap-cms-lib-auth": "^3.0.6",
|
||||
"decap-cms-lib-util": "^3.3.1",
|
||||
"decap-cms-lib-widgets": "^3.2.1",
|
||||
"decap-cms-locales": "^3.4.0",
|
||||
"decap-cms-ui-auth": "^3.2.3",
|
||||
"decap-cms-ui-default": "^3.3.0",
|
||||
"decap-cms-widget-boolean": "^3.2.0",
|
||||
"decap-cms-widget-code": "^3.2.0",
|
||||
"decap-cms-widget-colorstring": "^3.2.0",
|
||||
"decap-cms-widget-datetime": "^3.4.0",
|
||||
"decap-cms-widget-file": "^3.2.0",
|
||||
"decap-cms-widget-image": "^3.2.0",
|
||||
"decap-cms-widget-list": "^3.4.0",
|
||||
"decap-cms-widget-map": "^3.2.0",
|
||||
"decap-cms-widget-markdown": "^3.5.0",
|
||||
"decap-cms-widget-number": "^3.2.0",
|
||||
"decap-cms-widget-object": "^3.4.0",
|
||||
"decap-cms-widget-relation": "^3.5.2",
|
||||
"decap-cms-widget-select": "^3.3.0",
|
||||
"decap-cms-widget-string": "^3.2.0",
|
||||
"decap-cms-widget-text": "^3.2.0",
|
||||
"immutable": "^3.7.6",
|
||||
"lodash": "^4.17.11",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-immutable-proptypes": "^2.1.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"incrementToForceBump": 2
|
||||
}
|
||||
70
source/admin/packages/decap-cms-app/src/extensions.js
Normal file
70
source/admin/packages/decap-cms-app/src/extensions.js
Normal file
@@ -0,0 +1,70 @@
|
||||
// Core
|
||||
import { DecapCmsCore as CMS } from 'decap-cms-core';
|
||||
// Backends
|
||||
import { AzureBackend } from 'decap-cms-backend-azure';
|
||||
import { AwsCognitoGitHubProxyBackend } from 'decap-cms-backend-aws-cognito-github-proxy';
|
||||
import { GitHubBackend } from 'decap-cms-backend-github';
|
||||
import { GitLabBackend } from 'decap-cms-backend-gitlab';
|
||||
import { GiteaBackend } from 'decap-cms-backend-gitea';
|
||||
import { GitGatewayBackend } from 'decap-cms-backend-git-gateway';
|
||||
import { BitbucketBackend } from 'decap-cms-backend-bitbucket';
|
||||
import { TestBackend } from 'decap-cms-backend-test';
|
||||
import { ProxyBackend } from 'decap-cms-backend-proxy';
|
||||
// Widgets
|
||||
import DecapCmsWidgetString from 'decap-cms-widget-string';
|
||||
import DecapCmsWidgetNumber from 'decap-cms-widget-number';
|
||||
import DecapCmsWidgetText from 'decap-cms-widget-text';
|
||||
import DecapCmsWidgetImage from 'decap-cms-widget-image';
|
||||
import DecapCmsWidgetFile from 'decap-cms-widget-file';
|
||||
import DecapCmsWidgetSelect from 'decap-cms-widget-select';
|
||||
import DecapCmsWidgetMarkdown from 'decap-cms-widget-markdown';
|
||||
import DecapCmsWidgetList from 'decap-cms-widget-list';
|
||||
import DecapCmsWidgetObject from 'decap-cms-widget-object';
|
||||
import DecapCmsWidgetRelation from 'decap-cms-widget-relation';
|
||||
import DecapCmsWidgetBoolean from 'decap-cms-widget-boolean';
|
||||
import DecapCmsWidgetMap from 'decap-cms-widget-map';
|
||||
import DecapCmsWidgetDatetime from 'decap-cms-widget-datetime';
|
||||
import DecapCmsWidgetCode from 'decap-cms-widget-code';
|
||||
import DecapCmsWidgetColorString from 'decap-cms-widget-colorstring';
|
||||
// Editor Components
|
||||
import image from 'decap-cms-editor-component-image';
|
||||
// Locales
|
||||
import * as locales from 'decap-cms-locales';
|
||||
|
||||
// Register all the things
|
||||
CMS.registerBackend('git-gateway', GitGatewayBackend);
|
||||
CMS.registerBackend('azure', AzureBackend);
|
||||
CMS.registerBackend('aws-cognito-github-proxy', AwsCognitoGitHubProxyBackend);
|
||||
CMS.registerBackend('github', GitHubBackend);
|
||||
CMS.registerBackend('gitlab', GitLabBackend);
|
||||
CMS.registerBackend('gitea', GiteaBackend);
|
||||
CMS.registerBackend('bitbucket', BitbucketBackend);
|
||||
CMS.registerBackend('test-repo', TestBackend);
|
||||
CMS.registerBackend('proxy', ProxyBackend);
|
||||
CMS.registerWidget([
|
||||
DecapCmsWidgetString.Widget(),
|
||||
DecapCmsWidgetNumber.Widget(),
|
||||
DecapCmsWidgetText.Widget(),
|
||||
DecapCmsWidgetImage.Widget(),
|
||||
DecapCmsWidgetFile.Widget(),
|
||||
DecapCmsWidgetSelect.Widget(),
|
||||
DecapCmsWidgetMarkdown.Widget(),
|
||||
DecapCmsWidgetList.Widget(),
|
||||
DecapCmsWidgetObject.Widget(),
|
||||
DecapCmsWidgetRelation.Widget(),
|
||||
DecapCmsWidgetBoolean.Widget(),
|
||||
DecapCmsWidgetMap.Widget(),
|
||||
DecapCmsWidgetDatetime.Widget(),
|
||||
DecapCmsWidgetCode.Widget(),
|
||||
DecapCmsWidgetColorString.Widget(),
|
||||
]);
|
||||
CMS.registerEditorComponent(image);
|
||||
CMS.registerEditorComponent({
|
||||
id: 'code-block',
|
||||
label: 'Code Block',
|
||||
widget: 'code',
|
||||
type: 'code-block',
|
||||
});
|
||||
Object.keys(locales).forEach(locale => {
|
||||
CMS.registerLocale(locale, locales[locale]);
|
||||
});
|
||||
14
source/admin/packages/decap-cms-app/src/index.js
Normal file
14
source/admin/packages/decap-cms-app/src/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { DecapCmsCore as CMS } from 'decap-cms-core';
|
||||
import './extensions.js';
|
||||
|
||||
// Log version
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof DECAP_CMS_APP_VERSION === 'string') {
|
||||
console.log(`decap-cms-app ${DECAP_CMS_APP_VERSION}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const DecapCmsApp = {
|
||||
...CMS,
|
||||
};
|
||||
export default CMS;
|
||||
4
source/admin/packages/decap-cms-app/src/locales.js
Normal file
4
source/admin/packages/decap-cms-app/src/locales.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { DecapCmsCore as CMS } from 'decap-cms-core';
|
||||
import { en } from 'decap-cms-locales';
|
||||
|
||||
CMS.registerLocale('en', en);
|
||||
22
source/admin/packages/decap-cms-app/webpack.config.js
Normal file
22
source/admin/packages/decap-cms-app/webpack.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const webpack = require('webpack');
|
||||
|
||||
const pkg = require('./package.json');
|
||||
const { getConfig, plugins } = require('../../scripts/webpack');
|
||||
const baseWebpackConfig = getConfig({ baseOnly: true });
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
console.log(`${pkg.version}${isProduction ? '' : '-dev'}`);
|
||||
|
||||
const baseConfig = {
|
||||
...baseWebpackConfig,
|
||||
plugins: [
|
||||
...Object.entries(plugins)
|
||||
.filter(([key]) => key !== 'friendlyErrors')
|
||||
.map(([, plugin]) => plugin()),
|
||||
new webpack.DefinePlugin({
|
||||
DECAP_CMS_APP_VERSION: JSON.stringify(`${pkg.version}${isProduction ? '' : '-dev'}`),
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = baseConfig;
|
||||
@@ -0,0 +1,52 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.4.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-aws-cognito-github-proxy@3.4.0...decap-cms-backend-aws-cognito-github-proxy@3.4.1) (2025-07-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-aws-cognito-github-proxy
|
||||
|
||||
# [3.4.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-aws-cognito-github-proxy@3.2.2...decap-cms-backend-aws-cognito-github-proxy@3.4.0) (2025-07-15)
|
||||
|
||||
### Features
|
||||
|
||||
- add logo to header ([#7487](https://github.com/decaporg/decap-cms/issues/7487)) ([b540ace](https://github.com/decaporg/decap-cms/commit/b540acec943eb231df6aac7b1d515d9b4b84fa5d))
|
||||
|
||||
# [3.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-aws-cognito-github-proxy@3.2.2...decap-cms-backend-aws-cognito-github-proxy@3.3.0) (2025-06-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-aws-cognito-github-proxy
|
||||
|
||||
## [3.2.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-aws-cognito-github-proxy@3.2.1...decap-cms-backend-aws-cognito-github-proxy@3.2.2) (2024-08-13)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "Update dependencies (#7264)" ([22d483a](https://github.com/decaporg/decap-cms/commit/22d483a5b0c654071ae05735ac4f49abdc13d38c)), closes [#7264](https://github.com/decaporg/decap-cms/issues/7264)
|
||||
|
||||
## [3.2.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-aws-cognito-github-proxy@3.2.0...decap-cms-backend-aws-cognito-github-proxy@3.2.1) (2024-08-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-aws-cognito-github-proxy
|
||||
|
||||
# [3.2.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-aws-cognito-github-proxy@3.1.2...decap-cms-backend-aws-cognito-github-proxy@3.2.0) (2024-08-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fetch GitHub PR author name, fixes [#7232](https://github.com/decaporg/decap-cms/issues/7232) ([#7253](https://github.com/decaporg/decap-cms/issues/7253)) ([0e5335d](https://github.com/decaporg/decap-cms/commit/0e5335daba1b67816b4a0c24d1a2d9a185e3b54f))
|
||||
|
||||
## [3.1.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-aws-cognito-github-proxy@3.1.1...decap-cms-backend-aws-cognito-github-proxy@3.1.2) (2024-03-21)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-aws-cognito-github-proxy
|
||||
|
||||
## [3.1.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-aws-cognito-github-proxy@3.1.0...decap-cms-backend-aws-cognito-github-proxy@3.1.1) (2024-02-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- gitlab pkce auth error ([#7110](https://github.com/decaporg/decap-cms/issues/7110)) ([bcd58d6](https://github.com/decaporg/decap-cms/commit/bcd58d6e117b4654b3e0dca173f7f8aaca8dabdf))
|
||||
|
||||
# [3.1.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-aws-cognito-github-proxy@3.1.0-beta.2...decap-cms-backend-aws-cognito-github-proxy@3.1.0) (2024-02-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-aws-cognito-github-proxy
|
||||
|
||||
# 3.1.0-beta.2 (2024-01-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-aws-cognito-github-proxy
|
||||
@@ -0,0 +1,9 @@
|
||||
# GitHub backend
|
||||
|
||||
An abstraction layer between the CMS and a proxied version of [Github](https://docs.github.com/en/rest).
|
||||
|
||||
## Code structure
|
||||
|
||||
`Implementation` - wraps [Github Backend](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-lib-auth/README.md) for proxied version of Github.
|
||||
|
||||
`AuthenticationPage` - uses [lib-auth](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-lib-auth/README.md) to create an AWS Cognito compatible generic Authentication page supporting PKCE.
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "decap-cms-backend-aws-cognito-github-proxy",
|
||||
"description": "GitHub backend for Decap CMS proxied through AWS Cognito",
|
||||
"version": "3.4.1",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-aws-cognito-github-proxy",
|
||||
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
||||
"module": "dist/esm/index.js",
|
||||
"main": "dist/decap-cms-backend-aws-cognito-github-proxy.js",
|
||||
"keywords": [
|
||||
"decap-cms",
|
||||
"backend",
|
||||
"github",
|
||||
"aws-cognito"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"develop": "npm run build:esm -- --watch",
|
||||
"build": "cross-env NODE_ENV=production webpack",
|
||||
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore **/__tests__ --root-mode upward --extensions \".js,.jsx,.ts,.tsx\""
|
||||
},
|
||||
"dependencies": {
|
||||
"apollo-cache-inmemory": "^1.6.2",
|
||||
"apollo-client": "^2.6.3",
|
||||
"apollo-link-context": "^1.0.18",
|
||||
"apollo-link-http": "^1.5.15",
|
||||
"common-tags": "^1.8.0",
|
||||
"graphql": "^15.0.0",
|
||||
"graphql-tag": "^2.10.1",
|
||||
"js-base64": "^3.0.0",
|
||||
"semaphore": "^1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"decap-cms-backend-github": "^3.0.0",
|
||||
"decap-cms-lib-auth": "^3.0.0",
|
||||
"decap-cms-lib-util": "^3.0.0",
|
||||
"decap-cms-ui-auth": "^3.0.0",
|
||||
"decap-cms-ui-default": "^3.0.0",
|
||||
"lodash": "^4.17.11",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from '@emotion/styled';
|
||||
import { PkceAuthenticator } from 'decap-cms-lib-auth';
|
||||
import { AuthenticationPage, Icon } from 'decap-cms-ui-default';
|
||||
|
||||
const LoginButtonIcon = styled(Icon)`
|
||||
margin-right: 18px;
|
||||
`;
|
||||
|
||||
export default class GenericPKCEAuthenticationPage extends React.Component {
|
||||
static propTypes = {
|
||||
inProgress: PropTypes.bool,
|
||||
config: PropTypes.object.isRequired,
|
||||
onLogin: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
componentDidMount() {
|
||||
// Manually validate PropTypes - React 19 breaking change
|
||||
PropTypes.checkPropTypes(
|
||||
GenericPKCEAuthenticationPage.propTypes,
|
||||
this.props,
|
||||
'prop',
|
||||
'GenericPKCEAuthenticationPage',
|
||||
);
|
||||
|
||||
const {
|
||||
base_url = '',
|
||||
app_id = '',
|
||||
auth_endpoint = 'oauth2/authorize',
|
||||
auth_token_endpoint = 'oauth2/token',
|
||||
} = this.props.config.backend;
|
||||
this.auth = new PkceAuthenticator({
|
||||
base_url,
|
||||
auth_endpoint,
|
||||
app_id,
|
||||
auth_token_endpoint,
|
||||
auth_token_endpoint_content_type: 'application/x-www-form-urlencoded; charset=utf-8',
|
||||
});
|
||||
// Complete 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);
|
||||
});
|
||||
}
|
||||
|
||||
handleLogin = e => {
|
||||
e.preventDefault();
|
||||
this.auth.authenticate({ scope: 'https://api.github.com/repo openid email' }, (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="link" /> {inProgress ? t('auth.loggingIn') : t('auth.login')}
|
||||
</React.Fragment>
|
||||
)}
|
||||
t={t}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import * as React from 'react';
|
||||
import { GitHubBackend } from 'decap-cms-backend-github';
|
||||
import { PKCEAuthenticationPage } from 'decap-cms-ui-auth';
|
||||
|
||||
import type { GitHubUser } from 'decap-cms-backend-github/src/implementation';
|
||||
import type { Config } from 'decap-cms-lib-util/src';
|
||||
import type { Octokit } from '@octokit/rest';
|
||||
|
||||
export default class AwsCognitoGitHubProxyBackend extends GitHubBackend {
|
||||
constructor(config: Config, options = {}) {
|
||||
super(config, options);
|
||||
|
||||
this.bypassWriteAccessCheckForAppTokens = true;
|
||||
this.tokenKeyword = 'Bearer';
|
||||
}
|
||||
|
||||
authComponent() {
|
||||
const wrappedAuthenticationPage = (props: Record<string, unknown>) => {
|
||||
const allProps = { ...props, backend: this };
|
||||
return <PKCEAuthenticationPage {...allProps} />;
|
||||
};
|
||||
wrappedAuthenticationPage.displayName = 'AuthenticationPage';
|
||||
return wrappedAuthenticationPage;
|
||||
}
|
||||
|
||||
async currentUser({ token }: { token: string }): Promise<GitHubUser> {
|
||||
if (!this._currentUserPromise) {
|
||||
this._currentUserPromise = fetch(this.baseUrl + '/oauth2/userInfo', {
|
||||
headers: {
|
||||
Authorization: `${this.tokenKeyword} ${token}`,
|
||||
},
|
||||
}).then(async (res: Response): Promise<GitHubUser> => {
|
||||
if (res.status == 401) {
|
||||
this.logout();
|
||||
return Promise.reject('Token expired');
|
||||
}
|
||||
const userInfo = await res.json();
|
||||
const owner = this.originRepo.split('/')[1];
|
||||
return {
|
||||
name: userInfo.email,
|
||||
login: owner,
|
||||
avatar_url: `https://github.com/${owner}.png`,
|
||||
} as GitHubUser;
|
||||
});
|
||||
}
|
||||
return this._currentUserPromise;
|
||||
}
|
||||
|
||||
async getPullRequestAuthor(pullRequest: Octokit.PullsListResponseItem) {
|
||||
return pullRequest.user?.login;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { API } from 'decap-cms-backend-github';
|
||||
|
||||
import AwsCognitoGitHubProxyBackend from './implementation';
|
||||
|
||||
export const DecapCmsBackendAwsCognitoGithubProxy = {
|
||||
AwsCognitoGitHubProxyBackend,
|
||||
API,
|
||||
};
|
||||
|
||||
export { AwsCognitoGitHubProxyBackend, API };
|
||||
@@ -0,0 +1,3 @@
|
||||
const { getConfig } = require('../../scripts/webpack.js');
|
||||
|
||||
module.exports = getConfig();
|
||||
112
source/admin/packages/decap-cms-backend-azure/CHANGELOG.md
Normal file
112
source/admin/packages/decap-cms-backend-azure/CHANGELOG.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# 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-azure@3.2.1...decap-cms-backend-azure@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-azure@3.2.0...decap-cms-backend-azure@3.2.1) (2025-07-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-azure
|
||||
|
||||
# [3.2.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-azure@3.1.3...decap-cms-backend-azure@3.2.0) (2025-06-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-azure
|
||||
|
||||
## [3.1.3](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-azure@3.1.2...decap-cms-backend-azure@3.1.3) (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.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-azure@3.1.1...decap-cms-backend-azure@3.1.2) (2024-08-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-azure
|
||||
|
||||
## [3.1.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-azure@3.1.0-beta.1...decap-cms-backend-azure@3.1.1) (2024-03-21)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-azure
|
||||
|
||||
# [3.1.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-azure@3.1.0-beta.1...decap-cms-backend-azure@3.1.0) (2024-02-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-azure
|
||||
|
||||
# [3.1.0-beta.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-azure@3.1.0-beta.0...decap-cms-backend-azure@3.1.0-beta.1) (2024-01-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-azure
|
||||
|
||||
# [3.1.0-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-azure@3.1.0...decap-cms-backend-azure@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-azure@3.0.1...decap-cms-backend-azure@3.0.2) (2023-10-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-azure
|
||||
|
||||
## [3.0.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-azure@3.0.0...decap-cms-backend-azure@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-azure@1.4.0...decap-cms-backend-azure@3.0.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-azure
|
||||
|
||||
# [1.4.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-azure@1.4.0-beta.0...decap-cms-backend-azure@1.4.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-azure
|
||||
|
||||
# 1.4.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))
|
||||
|
||||
## [1.3.2-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-azure@1.3.1...decap-cms-backend-azure@1.3.2-beta.0) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-azure
|
||||
|
||||
## [1.3.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-azure@1.3.0...decap-cms-backend-azure@1.3.1) (2022-04-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-azure
|
||||
|
||||
# [1.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-azure@1.2.2...decap-cms-backend-azure@1.3.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))
|
||||
|
||||
## [1.2.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-azure/compare/decap-cms-backend-azure@1.2.1...decap-cms-backend-azure@1.2.2) (2021-06-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-azure
|
||||
|
||||
## [1.2.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-azure/compare/decap-cms-backend-azure@1.2.0...decap-cms-backend-azure@1.2.1) (2021-05-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-azure
|
||||
|
||||
# [1.2.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-azure/compare/decap-cms-backend-azure@1.1.2...decap-cms-backend-azure@1.2.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-azure/issues/5316)) ([9e42380](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-azure/commit/9e423805707321396eec137f5b732a5b07a0dd3f))
|
||||
|
||||
## [1.1.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-azure/compare/decap-cms-backend-azure@1.1.1...decap-cms-backend-azure@1.1.2) (2021-02-23)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-azure
|
||||
|
||||
## [1.1.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-azure/compare/decap-cms-backend-azure@1.1.0...decap-cms-backend-azure@1.1.1) (2021-02-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-azure
|
||||
|
||||
# 1.1.0 (2020-11-26)
|
||||
|
||||
### Features
|
||||
|
||||
- add azure devops backend ([#4427](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-azure/issues/4427)) ([4e6dc88](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-azure/commit/4e6dc88efb1dae4cf6137730c3b4fb6d0f75a8cc))
|
||||
13
source/admin/packages/decap-cms-backend-azure/README.md
Normal file
13
source/admin/packages/decap-cms-backend-azure/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Azure backend
|
||||
|
||||
An abstraction layer between the CMS and [Azure DevOps](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`.
|
||||
|
||||
`Api` - A wrapper for Azure DevOps REST API.
|
||||
|
||||
`AuthenticationPage` - facilitates implicit authentication flow. Uses [lib-auth](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-lib-auth/README.md).
|
||||
|
||||
Look at tests or types for more info.
|
||||
41
source/admin/packages/decap-cms-backend-azure/package.json
Normal file
41
source/admin/packages/decap-cms-backend-azure/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "decap-cms-backend-azure",
|
||||
"description": "Azure DevOps backend for Decap CMS",
|
||||
"version": "3.3.0",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-azure",
|
||||
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
||||
"module": "dist/esm/index.js",
|
||||
"main": "dist/decap-cms-backend-azure.js",
|
||||
"keywords": [
|
||||
"decap-cms",
|
||||
"backend",
|
||||
"azure",
|
||||
"devops"
|
||||
],
|
||||
"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": {
|
||||
"js-base64": "^3.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"semaphore": "^1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"decap-cms-lib-auth": "^3.0.0",
|
||||
"decap-cms-lib-util": "^3.0.0",
|
||||
"decap-cms-ui-default": "^3.0.0",
|
||||
"immutable": "^3.7.6",
|
||||
"lodash": "^4.17.11",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^19.1.0"
|
||||
},
|
||||
"browser": {
|
||||
"path": "path-browserify"
|
||||
}
|
||||
}
|
||||
796
source/admin/packages/decap-cms-backend-azure/src/API.ts
Normal file
796
source/admin/packages/decap-cms-backend-azure/src/API.ts
Normal file
@@ -0,0 +1,796 @@
|
||||
import { Base64 } from 'js-base64';
|
||||
import partial from 'lodash/partial';
|
||||
import result from 'lodash/result';
|
||||
import trim from 'lodash/trim';
|
||||
import trimStart from 'lodash/trimStart';
|
||||
import {
|
||||
localForage,
|
||||
APIError,
|
||||
unsentRequest,
|
||||
requestWithBackoff,
|
||||
responseParser,
|
||||
readFile,
|
||||
DEFAULT_PR_BODY,
|
||||
MERGE_COMMIT_MESSAGE,
|
||||
generateContentKey,
|
||||
parseContentKey,
|
||||
labelToStatus,
|
||||
isCMSLabel,
|
||||
EditorialWorkflowError,
|
||||
statusToLabel,
|
||||
PreviewState,
|
||||
readFileMetadata,
|
||||
branchFromContentKey,
|
||||
} from 'decap-cms-lib-util';
|
||||
import { dirname, basename } from 'path';
|
||||
|
||||
import type { ApiRequest, AssetProxy, PersistOptions, DataFile } from 'decap-cms-lib-util';
|
||||
import type { Map } from 'immutable';
|
||||
|
||||
export const API_NAME = 'Azure DevOps';
|
||||
|
||||
const API_VERSION = 'api-version';
|
||||
|
||||
type AzureUser = {
|
||||
coreAttributes?: {
|
||||
Avatar?: { value?: { value?: string } };
|
||||
DisplayName?: { value?: string };
|
||||
EmailAddress?: { value?: string };
|
||||
};
|
||||
};
|
||||
|
||||
type AzureGitItem = {
|
||||
objectId: string;
|
||||
gitObjectType: AzureObjectType;
|
||||
path: string;
|
||||
};
|
||||
|
||||
// https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20requests/get%20pull%20request?view=azure-devops-rest-6.1#gitpullrequest
|
||||
type AzureWebApiTagDefinition = {
|
||||
active: boolean;
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type AzurePullRequest = {
|
||||
title: string;
|
||||
artifactId: string;
|
||||
closedDate: string;
|
||||
creationDate: string;
|
||||
isDraft: string;
|
||||
status: AzurePullRequestStatus;
|
||||
lastMergeSourceCommit: AzureGitChangeItem;
|
||||
mergeStatus: AzureAsyncPullRequestStatus;
|
||||
pullRequestId: number;
|
||||
labels: AzureWebApiTagDefinition[];
|
||||
sourceRefName: string;
|
||||
createdBy?: {
|
||||
displayName?: string;
|
||||
uniqueName: string;
|
||||
};
|
||||
};
|
||||
|
||||
type AzurePullRequestCommit = { commitId: string };
|
||||
|
||||
enum AzureCommitStatusState {
|
||||
ERROR = 'error',
|
||||
FAILED = 'failed',
|
||||
NOT_APPLICABLE = 'notApplicable',
|
||||
NOT_SET = 'notSet',
|
||||
PENDING = 'pending',
|
||||
SUCCEEDED = 'succeeded',
|
||||
}
|
||||
|
||||
type AzureCommitStatus = {
|
||||
context: { genre?: string | null; name: string };
|
||||
state: AzureCommitStatusState;
|
||||
targetUrl: string;
|
||||
};
|
||||
|
||||
// This does not match Azure documentation, but it is what comes back from some calls
|
||||
// PullRequest as an example is documented as returning PullRequest[], but it actually
|
||||
// returns that inside of this value prop in the json
|
||||
interface AzureArray<T> {
|
||||
value: T[];
|
||||
}
|
||||
|
||||
enum AzureCommitChangeType {
|
||||
ADD = 'add',
|
||||
DELETE = 'delete',
|
||||
RENAME = 'rename',
|
||||
EDIT = 'edit',
|
||||
}
|
||||
|
||||
enum AzureItemContentType {
|
||||
BASE64 = 'base64encoded',
|
||||
}
|
||||
|
||||
enum AzurePullRequestStatus {
|
||||
ACTIVE = 'active',
|
||||
COMPLETED = 'completed',
|
||||
ABANDONED = 'abandoned',
|
||||
}
|
||||
|
||||
enum AzureAsyncPullRequestStatus {
|
||||
CONFLICTS = 'conflicts',
|
||||
FAILURE = 'failure',
|
||||
QUEUED = 'queued',
|
||||
REJECTED = 'rejectedByPolicy',
|
||||
SUCCEEDED = 'succeeded',
|
||||
}
|
||||
|
||||
enum AzureObjectType {
|
||||
BLOB = 'blob',
|
||||
TREE = 'tree',
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/rest/api/azure/devops/git/diffs/get?view=azure-devops-rest-6.1#gitcommitdiffs
|
||||
interface AzureGitCommitDiffs {
|
||||
changes: AzureGitChange[];
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/rest/api/azure/devops/git/diffs/get?view=azure-devops-rest-6.1#gitchange
|
||||
interface AzureGitChange {
|
||||
changeId: number;
|
||||
item: AzureGitChangeItem;
|
||||
changeType: AzureCommitChangeType;
|
||||
originalPath: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface AzureGitChangeItem {
|
||||
objectId: string;
|
||||
originalObjectId: string;
|
||||
gitObjectType: string;
|
||||
commitId: string;
|
||||
path: string;
|
||||
isFolder: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
type AzureRef = {
|
||||
name: string;
|
||||
objectId: string;
|
||||
};
|
||||
|
||||
type AzureCommit = {
|
||||
author: {
|
||||
date: string;
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
function delay(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function getChangeItem(item: AzureCommitItem) {
|
||||
switch (item.action) {
|
||||
case AzureCommitChangeType.ADD:
|
||||
return {
|
||||
changeType: AzureCommitChangeType.ADD,
|
||||
item: { path: item.path },
|
||||
newContent: {
|
||||
content: item.base64Content,
|
||||
contentType: AzureItemContentType.BASE64,
|
||||
},
|
||||
};
|
||||
case AzureCommitChangeType.EDIT:
|
||||
return {
|
||||
changeType: AzureCommitChangeType.EDIT,
|
||||
item: { path: item.path },
|
||||
newContent: {
|
||||
content: item.base64Content,
|
||||
contentType: AzureItemContentType.BASE64,
|
||||
},
|
||||
};
|
||||
case AzureCommitChangeType.DELETE:
|
||||
return {
|
||||
changeType: AzureCommitChangeType.DELETE,
|
||||
item: { path: item.path },
|
||||
};
|
||||
case AzureCommitChangeType.RENAME:
|
||||
return {
|
||||
changeType: AzureCommitChangeType.RENAME,
|
||||
item: { path: item.path },
|
||||
sourceServerItem: item.oldPath,
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
type AzureCommitItem = {
|
||||
action: AzureCommitChangeType;
|
||||
base64Content?: string;
|
||||
text?: string;
|
||||
path: string;
|
||||
oldPath?: string;
|
||||
};
|
||||
|
||||
interface AzureApiConfig {
|
||||
apiRoot: string;
|
||||
repo: { org: string; project: string; repoName: string };
|
||||
branch: string;
|
||||
squashMerges: boolean;
|
||||
initialWorkflowStatus: string;
|
||||
cmsLabelPrefix: string;
|
||||
apiVersion: string;
|
||||
}
|
||||
|
||||
export default class API {
|
||||
apiVersion: string;
|
||||
token: string;
|
||||
branch: string;
|
||||
mergeStrategy: string;
|
||||
endpointUrl: string;
|
||||
initialWorkflowStatus: string;
|
||||
cmsLabelPrefix: string;
|
||||
|
||||
constructor(config: AzureApiConfig, token: string) {
|
||||
const { repo } = config;
|
||||
const apiRoot = trim(config.apiRoot, '/');
|
||||
this.endpointUrl = `${apiRoot}/${repo.org}/${repo.project}/_apis/git/repositories/${repo.repoName}`;
|
||||
this.token = token;
|
||||
this.branch = config.branch;
|
||||
this.mergeStrategy = config.squashMerges ? 'squash' : 'noFastForward';
|
||||
this.initialWorkflowStatus = config.initialWorkflowStatus;
|
||||
this.apiVersion = config.apiVersion;
|
||||
this.cmsLabelPrefix = config.cmsLabelPrefix;
|
||||
}
|
||||
|
||||
withHeaders = (req: ApiRequest) => {
|
||||
const withHeaders = unsentRequest.withHeaders(
|
||||
{
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
req,
|
||||
);
|
||||
return withHeaders;
|
||||
};
|
||||
|
||||
withAzureFeatures = (req: Map<string, Map<string, string>>) => {
|
||||
if (req.hasIn(['params', API_VERSION])) {
|
||||
return req;
|
||||
}
|
||||
const withParams = unsentRequest.withParams(
|
||||
{
|
||||
[API_VERSION]: `${this.apiVersion}`,
|
||||
},
|
||||
req,
|
||||
);
|
||||
|
||||
return withParams;
|
||||
};
|
||||
|
||||
buildRequest = (req: ApiRequest) => {
|
||||
const withHeaders = this.withHeaders(req);
|
||||
const withAzureFeatures = this.withAzureFeatures(withHeaders);
|
||||
if (withAzureFeatures.has('cache')) {
|
||||
return withAzureFeatures;
|
||||
} else {
|
||||
const withNoCache = unsentRequest.withNoCache(withAzureFeatures);
|
||||
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 });
|
||||
|
||||
requestJSON = <T>(req: ApiRequest) => this.request(req).then(this.responseToJSON) as Promise<T>;
|
||||
requestText = (req: ApiRequest) => this.request(req).then(this.responseToText) as Promise<string>;
|
||||
|
||||
toBase64 = (str: string) => Promise.resolve(Base64.encode(str));
|
||||
fromBase64 = (str: string) => Base64.decode(str);
|
||||
|
||||
branchToRef = (branch: string): string => `refs/heads/${branch}`;
|
||||
refToBranch = (ref: string): string => ref.slice('refs/heads/'.length);
|
||||
|
||||
user = async () => {
|
||||
const result = await this.requestJSON<AzureUser>({
|
||||
url: 'https://app.vssps.visualstudio.com/_apis/profile/profiles/me',
|
||||
params: { [API_VERSION]: '6.1-preview.2' },
|
||||
});
|
||||
|
||||
const name = result.coreAttributes?.DisplayName?.value;
|
||||
const email = result.coreAttributes?.EmailAddress?.value;
|
||||
const url = result.coreAttributes?.Avatar?.value?.value;
|
||||
const user = {
|
||||
name: name || email || '',
|
||||
avatar_url: `data:image/png;base64,${url}`,
|
||||
email,
|
||||
};
|
||||
return user;
|
||||
};
|
||||
|
||||
async readFileMetadata(
|
||||
path: string,
|
||||
sha: string | null | undefined,
|
||||
{ branch = this.branch } = {},
|
||||
) {
|
||||
const fetchFileMetadata = async () => {
|
||||
try {
|
||||
const { value } = await this.requestJSON<AzureArray<AzureCommit>>({
|
||||
url: `${this.endpointUrl}/commits/`,
|
||||
params: {
|
||||
'searchCriteria.itemPath': path,
|
||||
'searchCriteria.itemVersion.version': branch,
|
||||
'searchCriteria.$top': 1,
|
||||
},
|
||||
});
|
||||
const [commit] = value;
|
||||
|
||||
return {
|
||||
author: commit.author.name || commit.author.email,
|
||||
updatedOn: commit.author.date,
|
||||
};
|
||||
} catch (error) {
|
||||
return { author: '', updatedOn: '' };
|
||||
}
|
||||
};
|
||||
|
||||
const fileMetadata = await readFileMetadata(sha, fetchFileMetadata, localForage);
|
||||
return fileMetadata;
|
||||
}
|
||||
|
||||
readFile = (
|
||||
path: string,
|
||||
sha?: string | null,
|
||||
{ parseText = true, branch = this.branch } = {},
|
||||
) => {
|
||||
const fetchContent = () => {
|
||||
return this.request({
|
||||
url: `${this.endpointUrl}/items/`,
|
||||
params: { version: branch, path },
|
||||
cache: 'no-store',
|
||||
}).then<Blob | string>(parseText ? this.responseToText : this.responseToBlob);
|
||||
};
|
||||
|
||||
return readFile(sha, fetchContent, localForage, parseText);
|
||||
};
|
||||
|
||||
listFiles = async (path: string, recursive: boolean, branch = this.branch) => {
|
||||
try {
|
||||
const { value: items } = await this.requestJSON<AzureArray<AzureGitItem>>({
|
||||
url: `${this.endpointUrl}/items/`,
|
||||
params: {
|
||||
version: branch,
|
||||
scopePath: path,
|
||||
recursionLevel: recursive ? 'full' : 'oneLevel',
|
||||
},
|
||||
});
|
||||
|
||||
const files = items
|
||||
.filter(item => item.gitObjectType === AzureObjectType.BLOB)
|
||||
.map(file => ({
|
||||
id: file.objectId,
|
||||
path: trimStart(file.path, '/'),
|
||||
name: basename(file.path),
|
||||
}));
|
||||
return files;
|
||||
} catch (err) {
|
||||
if (err && err.status === 404) {
|
||||
console.log('This 404 was expected and handled appropriately.');
|
||||
return [];
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async getRef(branch: string = this.branch) {
|
||||
const { value: refs } = await this.requestJSON<AzureArray<AzureRef>>({
|
||||
url: `${this.endpointUrl}/refs`,
|
||||
params: {
|
||||
$top: '1', // There's only one ref, so keep the payload small
|
||||
filter: 'heads/' + branch,
|
||||
},
|
||||
});
|
||||
|
||||
return refs.find(b => b.name == this.branchToRef(branch))!;
|
||||
}
|
||||
|
||||
async deleteRef(ref: AzureRef): Promise<void> {
|
||||
const deleteBranchPayload = [
|
||||
{
|
||||
name: ref.name,
|
||||
oldObjectId: ref.objectId,
|
||||
newObjectId: '0000000000000000000000000000000000000000',
|
||||
},
|
||||
];
|
||||
|
||||
await this.requestJSON({
|
||||
method: 'POST',
|
||||
url: `${this.endpointUrl}/refs`,
|
||||
body: JSON.stringify(deleteBranchPayload),
|
||||
});
|
||||
}
|
||||
|
||||
async uploadAndCommit(
|
||||
items: AzureCommitItem[],
|
||||
comment: string,
|
||||
branch: string,
|
||||
newBranch: boolean,
|
||||
) {
|
||||
const ref = await this.getRef(newBranch ? this.branch : branch);
|
||||
|
||||
const refUpdate = [
|
||||
{
|
||||
name: this.branchToRef(branch),
|
||||
oldObjectId: ref.objectId,
|
||||
},
|
||||
];
|
||||
|
||||
const changes = items.map(item => getChangeItem(item));
|
||||
const commits = [{ comment, changes }];
|
||||
const push = {
|
||||
refUpdates: refUpdate,
|
||||
commits,
|
||||
};
|
||||
|
||||
return this.requestJSON({
|
||||
url: `${this.endpointUrl}/pushes`,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(push),
|
||||
});
|
||||
}
|
||||
|
||||
async retrieveUnpublishedEntryData(contentKey: string) {
|
||||
const { collection, slug } = parseContentKey(contentKey);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
const diffs = await this.getDifferences(pullRequest.sourceRefName);
|
||||
const diffsWithIds = await Promise.all(
|
||||
diffs.map(async d => {
|
||||
const path = trimStart(d.item.path, '/');
|
||||
const newFile = d.changeType === AzureCommitChangeType.ADD;
|
||||
const id = d.item.objectId;
|
||||
return { id, path, newFile };
|
||||
}),
|
||||
);
|
||||
const label = pullRequest.labels.find(l => isCMSLabel(l.name, this.cmsLabelPrefix));
|
||||
const labelName = label && label.name ? label.name : this.cmsLabelPrefix;
|
||||
const status = labelToStatus(labelName, this.cmsLabelPrefix);
|
||||
// Uses creationDate, as we do not have direct access to the updated date
|
||||
const updatedAt = pullRequest.closedDate ? pullRequest.closedDate : pullRequest.creationDate;
|
||||
const pullRequestAuthor =
|
||||
pullRequest.createdBy?.displayName || pullRequest.createdBy?.uniqueName;
|
||||
return {
|
||||
collection,
|
||||
slug,
|
||||
status,
|
||||
diffs: diffsWithIds,
|
||||
updatedAt,
|
||||
pullRequestAuthor,
|
||||
};
|
||||
}
|
||||
|
||||
async getPullRequestStatues(pullRequest: AzurePullRequest) {
|
||||
const { value: commits } = await this.requestJSON<AzureArray<AzurePullRequestCommit>>({
|
||||
url: `${this.endpointUrl}/pullrequests/${pullRequest.pullRequestId}/commits`,
|
||||
params: {
|
||||
$top: 1,
|
||||
},
|
||||
});
|
||||
const { value: statuses } = await this.requestJSON<AzureArray<AzureCommitStatus>>({
|
||||
url: `${this.endpointUrl}/commits/${commits[0].commitId}/statuses`,
|
||||
params: { latestOnly: true },
|
||||
});
|
||||
return statuses;
|
||||
}
|
||||
|
||||
async getStatuses(collection: string, slug: string) {
|
||||
const contentKey = generateContentKey(collection, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
const statuses = await this.getPullRequestStatues(pullRequest);
|
||||
return statuses.map(({ context, state, targetUrl }) => ({
|
||||
context: context.name,
|
||||
state: state === AzureCommitStatusState.SUCCEEDED ? PreviewState.Success : PreviewState.Other,
|
||||
target_url: targetUrl,
|
||||
}));
|
||||
}
|
||||
|
||||
async getCommitItems(files: { path: string; newPath?: string }[], branch: string) {
|
||||
const items = await Promise.all(
|
||||
files.map(async file => {
|
||||
const [base64Content, fileExists] = await Promise.all([
|
||||
result(file, 'toBase64', partial(this.toBase64, (file as DataFile).raw)),
|
||||
this.isFileExists(file.path, branch),
|
||||
]);
|
||||
|
||||
const path = file.newPath || file.path;
|
||||
const oldPath = file.path;
|
||||
const renameOrEdit =
|
||||
path !== oldPath ? AzureCommitChangeType.RENAME : AzureCommitChangeType.EDIT;
|
||||
|
||||
const action = fileExists ? renameOrEdit : AzureCommitChangeType.ADD;
|
||||
return {
|
||||
action,
|
||||
base64Content,
|
||||
path,
|
||||
oldPath,
|
||||
} as AzureCommitItem;
|
||||
}),
|
||||
);
|
||||
|
||||
// move children
|
||||
for (const item of items.filter(i => i.oldPath && i.action === AzureCommitChangeType.RENAME)) {
|
||||
const sourceDir = dirname(item.oldPath as string);
|
||||
const destDir = dirname(item.path);
|
||||
const children = await this.listFiles(sourceDir, true, branch);
|
||||
children
|
||||
.filter(file => file.path !== item.oldPath)
|
||||
.forEach(file => {
|
||||
items.push({
|
||||
action: AzureCommitChangeType.RENAME,
|
||||
path: file.path.replace(sourceDir, destDir),
|
||||
oldPath: file.path,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
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 {
|
||||
const items = await this.getCommitItems(files, this.branch);
|
||||
|
||||
return this.uploadAndCommit(items, options.commitMessage, this.branch, true);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFiles(paths: string[], comment: string) {
|
||||
const ref = await this.getRef(this.branch);
|
||||
const refUpdate = {
|
||||
name: ref.name,
|
||||
oldObjectId: ref.objectId,
|
||||
};
|
||||
|
||||
const changes = paths.map(path =>
|
||||
getChangeItem({ action: AzureCommitChangeType.DELETE, path }),
|
||||
);
|
||||
const commits = [{ comment, changes }];
|
||||
const push = {
|
||||
refUpdates: [refUpdate],
|
||||
commits,
|
||||
};
|
||||
|
||||
return this.requestJSON({
|
||||
url: `${this.endpointUrl}/pushes`,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(push),
|
||||
});
|
||||
}
|
||||
|
||||
async getPullRequests(sourceBranch?: string) {
|
||||
const { value: pullRequests } = await this.requestJSON<AzureArray<AzurePullRequest>>({
|
||||
url: `${this.endpointUrl}/pullrequests`,
|
||||
params: {
|
||||
'searchCriteria.status': 'active',
|
||||
'searchCriteria.targetRefName': this.branchToRef(this.branch),
|
||||
'searchCriteria.includeLinks': false,
|
||||
...(sourceBranch ? { 'searchCriteria.sourceRefName': this.branchToRef(sourceBranch) } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
const filtered = pullRequests.filter(pr => {
|
||||
return pr.labels.some(label => isCMSLabel(label.name, this.cmsLabelPrefix));
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
|
||||
async listUnpublishedBranches(): Promise<string[]> {
|
||||
const pullRequests = await this.getPullRequests();
|
||||
const branches = pullRequests.map(pr => this.refToBranch(pr.sourceRefName));
|
||||
return branches;
|
||||
}
|
||||
|
||||
async isFileExists(path: string, branch: string) {
|
||||
try {
|
||||
await this.requestText({
|
||||
url: `${this.endpointUrl}/items/`,
|
||||
params: { version: branch, path },
|
||||
cache: 'no-store',
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof APIError && error.status === 404) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createPullRequest(branch: string, commitMessage: string, status: string) {
|
||||
const pr = {
|
||||
sourceRefName: this.branchToRef(branch),
|
||||
targetRefName: this.branchToRef(this.branch),
|
||||
title: commitMessage,
|
||||
description: DEFAULT_PR_BODY,
|
||||
labels: [
|
||||
{
|
||||
name: statusToLabel(status, this.cmsLabelPrefix),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await this.requestJSON({
|
||||
method: 'POST',
|
||||
url: `${this.endpointUrl}/pullrequests`,
|
||||
params: {
|
||||
supportsIterations: false,
|
||||
},
|
||||
body: JSON.stringify(pr),
|
||||
});
|
||||
}
|
||||
|
||||
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 getDifferences(to: string) {
|
||||
const result = await this.requestJSON<AzureGitCommitDiffs>({
|
||||
url: `${this.endpointUrl}/diffs/commits`,
|
||||
params: {
|
||||
baseVersion: this.branch,
|
||||
targetVersion: this.refToBranch(to),
|
||||
},
|
||||
});
|
||||
|
||||
return result.changes.filter(
|
||||
d =>
|
||||
d.item.gitObjectType === AzureObjectType.BLOB &&
|
||||
Object.values(AzureCommitChangeType).includes(d.changeType),
|
||||
);
|
||||
}
|
||||
|
||||
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 items = await this.getCommitItems(files, this.branch);
|
||||
|
||||
await this.uploadAndCommit(items, options.commitMessage, branch, true);
|
||||
await this.createPullRequest(
|
||||
branch,
|
||||
options.commitMessage,
|
||||
options.status || this.initialWorkflowStatus,
|
||||
);
|
||||
} else {
|
||||
const items = await this.getCommitItems(files, branch);
|
||||
await this.uploadAndCommit(items, options.commitMessage, branch, false);
|
||||
}
|
||||
}
|
||||
|
||||
async updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
const contentKey = generateContentKey(collection, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
|
||||
const nonCmsLabels = pullRequest.labels
|
||||
.filter(label => !isCMSLabel(label.name, this.cmsLabelPrefix))
|
||||
.map(label => label.name);
|
||||
|
||||
const labels = [...nonCmsLabels, statusToLabel(newStatus, this.cmsLabelPrefix)];
|
||||
await this.updatePullRequestLabels(pullRequest, labels);
|
||||
}
|
||||
|
||||
async deleteUnpublishedEntry(collectionName: string, slug: string) {
|
||||
const contentKey = generateContentKey(collectionName, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
await this.abandonPullRequest(pullRequest);
|
||||
}
|
||||
|
||||
async publishUnpublishedEntry(collectionName: string, slug: string) {
|
||||
const contentKey = generateContentKey(collectionName, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
await this.completePullRequest(pullRequest);
|
||||
}
|
||||
|
||||
async updatePullRequestLabels(pullRequest: AzurePullRequest, labels: string[]) {
|
||||
const cmsLabels = pullRequest.labels.filter(l => isCMSLabel(l.name, this.cmsLabelPrefix));
|
||||
await Promise.all(
|
||||
cmsLabels.map(l => {
|
||||
return this.requestText({
|
||||
method: 'DELETE',
|
||||
url: `${this.endpointUrl}/pullrequests/${encodeURIComponent(
|
||||
pullRequest.pullRequestId,
|
||||
)}/labels/${encodeURIComponent(l.id)}`,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
labels.map(l => {
|
||||
return this.requestText({
|
||||
method: 'POST',
|
||||
url: `${this.endpointUrl}/pullrequests/${encodeURIComponent(
|
||||
pullRequest.pullRequestId,
|
||||
)}/labels`,
|
||||
body: JSON.stringify({ name: l }),
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async completePullRequest(pullRequest: AzurePullRequest) {
|
||||
const pullRequestCompletion = {
|
||||
status: AzurePullRequestStatus.COMPLETED,
|
||||
lastMergeSourceCommit: pullRequest.lastMergeSourceCommit,
|
||||
completionOptions: {
|
||||
deleteSourceBranch: true,
|
||||
mergeCommitMessage: MERGE_COMMIT_MESSAGE,
|
||||
mergeStrategy: this.mergeStrategy,
|
||||
},
|
||||
};
|
||||
|
||||
let response = await this.requestJSON<AzurePullRequest>({
|
||||
method: 'PATCH',
|
||||
url: `${this.endpointUrl}/pullrequests/${encodeURIComponent(pullRequest.pullRequestId)}`,
|
||||
body: JSON.stringify(pullRequestCompletion),
|
||||
});
|
||||
|
||||
// We need to wait for Azure to complete the pull request to actually complete
|
||||
// Sometimes this is instant, but frequently it is 1-3 seconds
|
||||
const DELAY_MILLISECONDS = 500;
|
||||
const MAX_ATTEMPTS = 10;
|
||||
let attempt = 1;
|
||||
while (response.mergeStatus === AzureAsyncPullRequestStatus.QUEUED && attempt <= MAX_ATTEMPTS) {
|
||||
await delay(DELAY_MILLISECONDS);
|
||||
response = await this.requestJSON({
|
||||
url: `${this.endpointUrl}/pullrequests/${encodeURIComponent(pullRequest.pullRequestId)}`,
|
||||
});
|
||||
attempt = attempt + 1;
|
||||
}
|
||||
}
|
||||
|
||||
async abandonPullRequest(pullRequest: AzurePullRequest) {
|
||||
const pullRequestAbandon = {
|
||||
status: AzurePullRequestStatus.ABANDONED,
|
||||
};
|
||||
|
||||
await this.requestJSON({
|
||||
method: 'PATCH',
|
||||
url: `${this.endpointUrl}/pullrequests/${encodeURIComponent(pullRequest.pullRequestId)}`,
|
||||
body: JSON.stringify(pullRequestAbandon),
|
||||
});
|
||||
|
||||
await this.deleteRef({
|
||||
name: pullRequest.sourceRefName,
|
||||
objectId: pullRequest.lastMergeSourceCommit.commitId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from '@emotion/styled';
|
||||
import { ImplicitAuthenticator } from 'decap-cms-lib-auth';
|
||||
import { AuthenticationPage, Icon } from 'decap-cms-ui-default';
|
||||
|
||||
const LoginButtonIcon = styled(Icon)`
|
||||
margin-right: 18px;
|
||||
`;
|
||||
|
||||
export default class AzureAuthenticationPage 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(
|
||||
AzureAuthenticationPage.propTypes,
|
||||
this.props,
|
||||
'prop',
|
||||
'AzureAuthenticationPage',
|
||||
);
|
||||
|
||||
this.auth = new ImplicitAuthenticator({
|
||||
base_url: `https://login.microsoftonline.com/${this.props.config.backend.tenant_id}`,
|
||||
auth_endpoint: 'oauth2/authorize',
|
||||
app_id: this.props.config.backend.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) {
|
||||
alert(err);
|
||||
return;
|
||||
}
|
||||
this.props.onLogin(data);
|
||||
});
|
||||
}
|
||||
|
||||
handleLogin = e => {
|
||||
e.preventDefault();
|
||||
this.auth.authenticate(
|
||||
{
|
||||
scope: 'vso.code_full,user.read',
|
||||
resource: '499b84ac-1321-427f-aa17-267ca6975798',
|
||||
prompt: 'select_account',
|
||||
},
|
||||
(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}
|
||||
renderButtonContent={() => (
|
||||
<React.Fragment>
|
||||
<LoginButtonIcon type="azure" />
|
||||
{inProgress ? t('auth.loggingIn') : t('auth.loginWithAzure')}
|
||||
</React.Fragment>
|
||||
)}
|
||||
t={t}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
import trimStart from 'lodash/trimStart';
|
||||
import trim from 'lodash/trim';
|
||||
import semaphore from 'semaphore';
|
||||
import {
|
||||
basename,
|
||||
getMediaDisplayURL,
|
||||
generateContentKey,
|
||||
getMediaAsBlob,
|
||||
getPreviewStatus,
|
||||
asyncLock,
|
||||
runWithLock,
|
||||
unpublishedEntries,
|
||||
entriesByFiles,
|
||||
filterByExtension,
|
||||
branchFromContentKey,
|
||||
entriesByFolder,
|
||||
contentKeyFromBranch,
|
||||
getBlobSHA,
|
||||
} from 'decap-cms-lib-util';
|
||||
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import API, { API_NAME } from './API';
|
||||
|
||||
import type { Semaphore } from 'semaphore';
|
||||
import type {
|
||||
Credentials,
|
||||
Implementation,
|
||||
ImplementationFile,
|
||||
ImplementationMediaFile,
|
||||
DisplayURL,
|
||||
Entry,
|
||||
AssetProxy,
|
||||
PersistOptions,
|
||||
Config,
|
||||
AsyncLock,
|
||||
User,
|
||||
UnpublishedEntryMediaFile,
|
||||
} from 'decap-cms-lib-util';
|
||||
|
||||
const MAX_CONCURRENT_DOWNLOADS = 10;
|
||||
|
||||
function parseAzureRepo(config: Config) {
|
||||
const { repo } = config.backend;
|
||||
|
||||
if (typeof repo !== 'string') {
|
||||
throw new Error('The Azure backend needs a "repo" in the backend configuration.');
|
||||
}
|
||||
|
||||
const parts = repo.split('/');
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('The Azure backend must be in a the format of {org}/{project}/{repo}');
|
||||
}
|
||||
|
||||
const [org, project, repoName] = parts;
|
||||
return {
|
||||
org,
|
||||
project,
|
||||
repoName,
|
||||
};
|
||||
}
|
||||
|
||||
export default class Azure implements Implementation {
|
||||
lock: AsyncLock;
|
||||
api?: API;
|
||||
options: {
|
||||
initialWorkflowStatus: string;
|
||||
};
|
||||
repo: {
|
||||
org: string;
|
||||
project: string;
|
||||
repoName: string;
|
||||
};
|
||||
branch: string;
|
||||
apiRoot: string;
|
||||
apiVersion: string;
|
||||
token: string | null;
|
||||
squashMerges: boolean;
|
||||
cmsLabelPrefix: string;
|
||||
mediaFolder: string;
|
||||
previewContext: string;
|
||||
|
||||
_mediaDisplayURLSem?: Semaphore;
|
||||
|
||||
constructor(config: Config, options = {}) {
|
||||
this.options = {
|
||||
initialWorkflowStatus: '',
|
||||
...options,
|
||||
};
|
||||
|
||||
this.repo = parseAzureRepo(config);
|
||||
this.branch = config.backend.branch || 'master';
|
||||
this.apiRoot = config.backend.api_root || 'https://dev.azure.com';
|
||||
this.apiVersion = config.backend.api_version || '6.1-preview';
|
||||
this.token = '';
|
||||
this.squashMerges = config.backend.squash_merges || false;
|
||||
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
|
||||
this.mediaFolder = trim(config.media_folder, '/');
|
||||
this.previewContext = config.backend.preview_context || '';
|
||||
this.lock = asyncLock();
|
||||
}
|
||||
|
||||
isGitBackend() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async status() {
|
||||
const auth =
|
||||
(await this.api!.user()
|
||||
.then(user => !!user)
|
||||
.catch(e => {
|
||||
console.warn('Failed getting Azure user', e);
|
||||
return false;
|
||||
})) || false;
|
||||
|
||||
return { auth: { status: auth }, api: { status: true, statusPage: '' } };
|
||||
}
|
||||
|
||||
authComponent() {
|
||||
return AuthenticationPage;
|
||||
}
|
||||
|
||||
restoreUser(user: User) {
|
||||
return this.authenticate(user);
|
||||
}
|
||||
|
||||
async authenticate(state: Credentials) {
|
||||
this.token = state.token as string;
|
||||
this.api = new API(
|
||||
{
|
||||
apiRoot: this.apiRoot,
|
||||
apiVersion: this.apiVersion,
|
||||
repo: this.repo,
|
||||
branch: this.branch,
|
||||
squashMerges: this.squashMerges,
|
||||
cmsLabelPrefix: this.cmsLabelPrefix,
|
||||
initialWorkflowStatus: this.options.initialWorkflowStatus,
|
||||
},
|
||||
this.token,
|
||||
);
|
||||
|
||||
const user = await this.api.user();
|
||||
return { token: state.token as string, ...user };
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the user out by forgetting their access token.
|
||||
* TODO: *Actual* logout by redirecting to:
|
||||
* https://login.microsoftonline.com/{tenantId}/oauth2/logout?client_id={clientId}&post_logout_redirect_uri={baseUrl}
|
||||
*/
|
||||
logout() {
|
||||
this.token = null;
|
||||
return;
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return Promise.resolve(this.token);
|
||||
}
|
||||
|
||||
async entriesByFolder(folder: string, extension: string, depth: number) {
|
||||
const listFiles = async () => {
|
||||
const files = await this.api!.listFiles(folder, depth > 1);
|
||||
const filtered = files.filter(file => filterByExtension({ path: file.path }, extension));
|
||||
return filtered.map(file => ({
|
||||
id: file.id,
|
||||
path: file.path,
|
||||
}));
|
||||
};
|
||||
|
||||
const entries = await entriesByFolder(
|
||||
listFiles,
|
||||
this.api!.readFile.bind(this.api!),
|
||||
this.api!.readFileMetadata.bind(this.api),
|
||||
API_NAME,
|
||||
);
|
||||
return entries;
|
||||
}
|
||||
|
||||
entriesByFiles(files: ImplementationFile[]) {
|
||||
return entriesByFiles(
|
||||
files,
|
||||
this.api!.readFile.bind(this.api!),
|
||||
this.api!.readFileMetadata.bind(this.api),
|
||||
API_NAME,
|
||||
);
|
||||
}
|
||||
|
||||
async getEntry(path: string) {
|
||||
const data = (await this.api!.readFile(path)) as string;
|
||||
return {
|
||||
file: { path },
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
async getMedia() {
|
||||
const files = await this.api!.listFiles(this.mediaFolder, false);
|
||||
const mediaFiles = await Promise.all(
|
||||
files.map(async ({ id, path, name }) => {
|
||||
const blobUrl = await this.getMediaDisplayURL({ id, path });
|
||||
return { id, name, displayURL: blobUrl, path };
|
||||
}),
|
||||
);
|
||||
return mediaFiles;
|
||||
}
|
||||
|
||||
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 = new File([blob], name);
|
||||
const url = URL.createObjectURL(fileObj);
|
||||
const id = await getBlobSHA(blob);
|
||||
|
||||
return {
|
||||
id,
|
||||
displayURL: url,
|
||||
path,
|
||||
name,
|
||||
size: fileObj.size,
|
||||
file: fileObj,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
async persistEntry(entry: Entry, options: PersistOptions): Promise<void> {
|
||||
const mediaFiles: AssetProxy[] = entry.assets;
|
||||
await this.api!.persistFiles(entry.dataFiles, mediaFiles, options);
|
||||
}
|
||||
|
||||
async persistMedia(
|
||||
mediaFile: AssetProxy,
|
||||
options: PersistOptions,
|
||||
): Promise<ImplementationMediaFile> {
|
||||
const fileObj = mediaFile.fileObj as File;
|
||||
|
||||
const [id] = await Promise.all([
|
||||
getBlobSHA(fileObj),
|
||||
this.api!.persistFiles([], [mediaFile], options),
|
||||
]);
|
||||
|
||||
const { path } = mediaFile;
|
||||
const url = URL.createObjectURL(fileObj);
|
||||
|
||||
return {
|
||||
displayURL: url,
|
||||
path: trimStart(path, '/'),
|
||||
name: fileObj!.name,
|
||||
size: fileObj!.size,
|
||||
file: fileObj,
|
||||
url,
|
||||
id: id as string,
|
||||
};
|
||||
}
|
||||
|
||||
async deleteFiles(paths: string[], commitMessage: string) {
|
||||
await this.api!.deleteFiles(paths, commitMessage);
|
||||
}
|
||||
|
||||
async loadMediaFile(branch: string, file: UnpublishedEntryMediaFile) {
|
||||
const readFile = (
|
||||
path: string,
|
||||
id: string | null | undefined,
|
||||
{ parseText }: { parseText: boolean },
|
||||
) => this.api!.readFile(path, id, { branch, parseText });
|
||||
|
||||
const blob = await getMediaAsBlob(file.path, null, readFile);
|
||||
const name = basename(file.path);
|
||||
const fileObj = new File([blob], name);
|
||||
return {
|
||||
id: file.path,
|
||||
displayURL: URL.createObjectURL(fileObj),
|
||||
path: file.path,
|
||||
name,
|
||||
size: fileObj.size,
|
||||
file: fileObj,
|
||||
};
|
||||
}
|
||||
|
||||
async loadEntryMediaFiles(branch: string, files: UnpublishedEntryMediaFile[]) {
|
||||
const mediaFiles = await Promise.all(files.map(file => this.loadMediaFile(branch, file)));
|
||||
|
||||
return mediaFiles;
|
||||
}
|
||||
|
||||
async unpublishedEntries() {
|
||||
const listEntriesKeys = () =>
|
||||
this.api!.listUnpublishedBranches().then(branches =>
|
||||
branches.map(branch => contentKeyFromBranch(branch)),
|
||||
);
|
||||
|
||||
const ids = await unpublishedEntries(listEntriesKeys);
|
||||
return ids;
|
||||
}
|
||||
|
||||
async unpublishedEntry({
|
||||
id,
|
||||
collection,
|
||||
slug,
|
||||
}: {
|
||||
id?: string;
|
||||
collection?: string;
|
||||
slug?: string;
|
||||
}) {
|
||||
if (id) {
|
||||
const data = await this.api!.retrieveUnpublishedEntryData(id);
|
||||
return data;
|
||||
} else if (collection && slug) {
|
||||
const contentKey = generateContentKey(collection, slug);
|
||||
const data = await this.api!.retrieveUnpublishedEntryData(contentKey);
|
||||
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 unpublishedEntryMediaFile(collection: string, slug: string, path: string, id: string) {
|
||||
const branch = this.getBranch(collection, slug);
|
||||
const mediaFile = await this.loadMediaFile(branch, { path, id });
|
||||
return mediaFile;
|
||||
}
|
||||
|
||||
async 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;
|
||||
}
|
||||
|
||||
updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
// updateUnpublishedEntryStatus is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.updateUnpublishedEntryStatus(collection, slug, newStatus),
|
||||
'Failed to acquire update entry status lock',
|
||||
);
|
||||
}
|
||||
|
||||
deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
// deleteUnpublishedEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.deleteUnpublishedEntry(collection, slug),
|
||||
'Failed to acquire delete entry lock',
|
||||
);
|
||||
}
|
||||
|
||||
publishUnpublishedEntry(collection: string, slug: string) {
|
||||
// publishUnpublishedEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.publishUnpublishedEntry(collection, slug),
|
||||
'Failed to acquire publish entry lock',
|
||||
);
|
||||
}
|
||||
|
||||
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
source/admin/packages/decap-cms-backend-azure/src/index.ts
Normal file
10
source/admin/packages/decap-cms-backend-azure/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import AzureBackend from './implementation';
|
||||
import API from './API';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
|
||||
export const DecapCmsBackendAzure = {
|
||||
AzureBackend,
|
||||
API,
|
||||
AuthenticationPage,
|
||||
};
|
||||
export { AzureBackend, API, AuthenticationPage };
|
||||
@@ -0,0 +1,3 @@
|
||||
const { getConfig } = require('../../scripts/webpack.js');
|
||||
|
||||
module.exports = getConfig();
|
||||
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();
|
||||
490
source/admin/packages/decap-cms-backend-git-gateway/CHANGELOG.md
Normal file
490
source/admin/packages/decap-cms-backend-git-gateway/CHANGELOG.md
Normal file
@@ -0,0 +1,490 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.4.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@3.4.0...decap-cms-backend-git-gateway@3.4.1) (2025-07-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
# [3.4.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@3.3.1...decap-cms-backend-git-gateway@3.4.0) (2025-07-15)
|
||||
|
||||
### Features
|
||||
|
||||
- add logo to header ([#7487](https://github.com/decaporg/decap-cms/issues/7487)) ([b540ace](https://github.com/decaporg/decap-cms/commit/b540acec943eb231df6aac7b1d515d9b4b84fa5d))
|
||||
|
||||
## [3.3.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@3.3.0...decap-cms-backend-git-gateway@3.3.1) (2025-07-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
# [3.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@3.2.2...decap-cms-backend-git-gateway@3.3.0) (2025-06-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## [3.2.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@3.2.1...decap-cms-backend-git-gateway@3.2.2) (2024-08-13)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "Update dependencies (#7264)" ([22d483a](https://github.com/decaporg/decap-cms/commit/22d483a5b0c654071ae05735ac4f49abdc13d38c)), closes [#7264](https://github.com/decaporg/decap-cms/issues/7264)
|
||||
|
||||
## [3.2.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@3.2.0...decap-cms-backend-git-gateway@3.2.1) (2024-08-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
# [3.2.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@3.1.1...decap-cms-backend-git-gateway@3.2.0) (2024-08-07)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## [3.1.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@3.1.0-beta.1...decap-cms-backend-git-gateway@3.1.1) (2024-03-21)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
# [3.1.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@3.1.0-beta.1...decap-cms-backend-git-gateway@3.1.0) (2024-02-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
# [3.1.0-beta.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@3.1.0-beta.0...decap-cms-backend-git-gateway@3.1.0-beta.1) (2024-01-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
# [3.1.0-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@3.1.0...decap-cms-backend-git-gateway@3.1.0-beta.0) (2023-10-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([b89fc89](https://github.com/decaporg/decap-cms/commit/b89fc894dfbb5f4136b2e5427fd25a29378a58c6))
|
||||
|
||||
## [3.0.3](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@3.0.2...decap-cms-backend-git-gateway@3.0.3) (2023-10-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## [3.0.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@3.0.1...decap-cms-backend-git-gateway@3.0.2) (2023-09-06)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
- filter by path when loading collection from github backend ([#6898](https://github.com/decaporg/decap-cms/issues/6898)) ([18ef773](https://github.com/decaporg/decap-cms/commit/18ef773f35db1b7ef3ab5a0f25527d87745b9c73))
|
||||
|
||||
## [3.0.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@3.0.0...decap-cms-backend-git-gateway@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-git-gateway@2.14.0...decap-cms-backend-git-gateway@3.0.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
# [2.14.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@2.14.0-beta.0...decap-cms-backend-git-gateway@2.14.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
# 2.14.0-beta.0 (2023-08-18)
|
||||
|
||||
### Features
|
||||
|
||||
- rename packages ([#6863](https://github.com/decaporg/decap-cms/issues/6863)) ([d515e7b](https://github.com/decaporg/decap-cms/commit/d515e7bd33216a775d96887b08c4f7b1962941bb))
|
||||
|
||||
## [2.13.2-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@2.13.1...decap-cms-backend-git-gateway@2.13.2-beta.0) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## [2.13.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@2.13.0...decap-cms-backend-git-gateway@2.13.1) (2022-03-08)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
# [2.13.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@2.12.2...decap-cms-backend-git-gateway@2.13.0) (2021-12-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **decap-cms-ui-default:** use grayDark for button ([#6069](https://github.com/decaporg/decap-cms/issues/6069)) ([ad85514](https://github.com/decaporg/decap-cms/commit/ad85514cba607f066ab7071bee5932b2192466ee)), closes [/github.com/decaporg/decap-cms/issues/1333#issuecomment-998115794](https://github.com//github.com/decaporg/decap-cms/issues/1333/issues/issuecomment-998115794)
|
||||
|
||||
### Features
|
||||
|
||||
- **backend-gitlab:** initial GraphQL support ([#6059](https://github.com/decaporg/decap-cms/issues/6059)) ([1523a41](https://github.com/decaporg/decap-cms/commit/1523a4140a3d2f4cc01a1548514ae17bc1ad504e))
|
||||
- disable 'Save' button when there are no changes ([#5595](https://github.com/decaporg/decap-cms/issues/5595)) ([4b566a7](https://github.com/decaporg/decap-cms/commit/4b566a78f4282a6f04caf3deafaaac4d74acfd63))
|
||||
|
||||
## [2.12.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.12.1...decap-cms-backend-git-gateway@2.12.2) (2021-06-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## [2.12.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.12.0...decap-cms-backend-git-gateway@2.12.1) (2021-05-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
# [2.12.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.11.11...decap-cms-backend-git-gateway@2.12.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-git-gateway/issues/5316)) ([9e42380](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/9e423805707321396eec137f5b732a5b07a0dd3f))
|
||||
|
||||
## [2.11.11](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.11.10...decap-cms-backend-git-gateway@2.11.11) (2021-02-25)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## [2.11.10](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.11.9...decap-cms-backend-git-gateway@2.11.10) (2021-02-23)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## [2.11.9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.11.8...decap-cms-backend-git-gateway@2.11.9) (2021-02-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## [2.11.8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.11.7...decap-cms-backend-git-gateway@2.11.8) (2020-12-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** update dependency ini to v2 ([#4722](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/4722)) ([e14ace3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/e14ace373b11510159a9b4d3f977d27ed886b288))
|
||||
|
||||
## [2.11.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.11.6...decap-cms-backend-git-gateway@2.11.7) (2020-12-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **large-media:** mark pointer files as binary ([#4678](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/4678)) ([7697b90](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/7697b907d7bae750f4ec041a184188aa46995320))
|
||||
|
||||
## [2.11.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.11.5...decap-cms-backend-git-gateway@2.11.6) (2020-10-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** update dependency jwt-decode to v3 ([#4408](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/4408)) ([03492e4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/03492e4e684ffce3a541ef15edb591d1fd5b5854))
|
||||
|
||||
## [2.11.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.11.4...decap-cms-backend-git-gateway@2.11.5) (2020-09-20)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## [2.11.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.11.3...decap-cms-backend-git-gateway@2.11.4) (2020-09-15)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## 2.11.3 (2020-09-08)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([828bb16](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/828bb16415b8c22a34caa19c50c38b24ffe9ceae))
|
||||
|
||||
## 2.11.2 (2020-08-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([8262487](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/82624879ccbcb16610090041db28f00714d924c8))
|
||||
|
||||
## 2.11.1 (2020-07-27)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([118d50a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/118d50a7a70295f25073e564b5161aa2b9883056))
|
||||
|
||||
# [2.11.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.10.5...decap-cms-backend-git-gateway@2.11.0) (2020-06-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- handle token expiry ([#3847](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/3847)) ([285c940](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/285c940562548d7bc88de244123ba87ff66fba65))
|
||||
|
||||
### Features
|
||||
|
||||
- add backend status down indicator ([#3889](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/3889)) ([a50edc7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/a50edc70553ad6afa1acee6a51996ad226443f8c))
|
||||
- **backend-gitgateway:** improve deploy preview visibility ([#3882](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/3882)) ([afc9bf4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/afc9bf4f3fe14ccb60851fc24e68922a6e4a85a9))
|
||||
|
||||
## [2.10.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.10.4...decap-cms-backend-git-gateway@2.10.5) (2020-05-19)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## [2.10.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.10.3...decap-cms-backend-git-gateway@2.10.4) (2020-05-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **git-gateway:** wait for identity widget to initialize ([#3660](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/3660)) ([6c229c5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/6c229c5149e3beff05bcfb42ca286d3e9170e54e))
|
||||
|
||||
## [2.10.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.10.2...decap-cms-backend-git-gateway@2.10.3) (2020-04-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **large-media:** match netlify.app as lfs host ([#3642](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/3642)) ([9b79623](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/9b79623bc8b8fe212fb2d15dec8a75328cde9c64))
|
||||
|
||||
## [2.10.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.10.1...decap-cms-backend-git-gateway@2.10.2) (2020-04-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## [2.10.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.10.0...decap-cms-backend-git-gateway@2.10.1) (2020-03-30)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
# [2.10.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.9.1...decap-cms-backend-git-gateway@2.10.0) (2020-03-12)
|
||||
|
||||
### Features
|
||||
|
||||
- add media lib virtualization ([#3381](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/3381)) ([92e7601](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/92e76011e7a9e8b5370088b0a2c065df66b5f7fb))
|
||||
- **backend-github:** add pagination ([#3379](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/3379)) ([39f1307](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/39f1307e3a36447da8c9b3ca79b1d7db52ea1a19))
|
||||
|
||||
## [2.9.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.9.0...decap-cms-backend-git-gateway@2.9.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-git-gateway/issues/3333)) ([7c45a3c](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/7c45a3cda983be427864a56e58791565eb9232e2))
|
||||
|
||||
# [2.9.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.8.1...decap-cms-backend-git-gateway@2.9.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-git-gateway/issues/3316)) ([7e0a8ad](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/7e0a8ad532012576dc5e40bd4e9d54522e307123)), closes [#3292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/3292)
|
||||
|
||||
## [2.8.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.8.0...decap-cms-backend-git-gateway@2.8.1) (2020-02-22)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "feat(core): Align GitHub metadata handling with other backends (#3292)" ([5bdd3df](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/5bdd3df9ccbb5149c22d79987ebdcd6cab4b261f)), closes [#3292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/3292)
|
||||
|
||||
# [2.8.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.7.2...decap-cms-backend-git-gateway@2.8.0) (2020-02-22)
|
||||
|
||||
### Features
|
||||
|
||||
- **core:** Align GitHub metadata handling with other backends ([#3292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/3292)) ([8193b5a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/8193b5ace89d6f14a6c756235a50b186a763b6b1))
|
||||
|
||||
## [2.7.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.7.1...decap-cms-backend-git-gateway@2.7.2) (2020-02-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- stringify error message ([#3233](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/3233)) ([249bd7e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/249bd7ec1ed2197106cbb01f8c05e1b8830aa5bc))
|
||||
|
||||
## [2.7.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.7.0...decap-cms-backend-git-gateway@2.7.1) (2020-01-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-git-gateway:** re-write GitHub pagination links ([#3135](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/3135)) ([834f6b9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/834f6b9e457f3738ce0f240ddd4cc160aff9e2f5))
|
||||
|
||||
# [2.7.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.6.2...decap-cms-backend-git-gateway@2.7.0) (2020-01-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **git-gateway-gitlab:** fix large media support for editorial workflow ([#3105](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/3105)) ([038803c](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/038803c9f249de386812652372c35c4c53935295))
|
||||
|
||||
### Features
|
||||
|
||||
- **backend-bitbucket:** Add Git-LFS support ([#3118](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/3118)) ([a48c02d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/a48c02d852ca5e11055da3a14cefae8d17a68498))
|
||||
|
||||
## [2.6.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.6.1...decap-cms-backend-git-gateway@2.6.2) (2020-01-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- don't fail on malformed pointer files ([#3095](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/3095)) ([9210843](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/92108431f0c3df3e99b5aa7f462006ec3fa7777e))
|
||||
|
||||
## [2.6.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.6.0...decap-cms-backend-git-gateway@2.6.1) (2020-01-14)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
# [2.6.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.6.0-beta.0...decap-cms-backend-git-gateway@2.6.0) (2020-01-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- rebase open authoring branches ([#2975](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2975)) ([8c175f6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/8c175f6132fa18a13763cc563f7d3201c1e3580e))
|
||||
|
||||
### Features
|
||||
|
||||
- **backend-git-gateway:** handle identity disabled error message ([#3002](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/3002)) ([b5ffccd](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/b5ffccdac506db416c09aaebb38611783487c52a))
|
||||
|
||||
# [2.6.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.5.1...decap-cms-backend-git-gateway@2.6.0-beta.0) (2019-12-18)
|
||||
|
||||
### Features
|
||||
|
||||
- bundle assets with content ([#2958](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2958)) ([2b41d8a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/2b41d8a838a9c8a6b21cde2ddd16b9288334e298))
|
||||
|
||||
## [2.5.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.5.0...decap-cms-backend-git-gateway@2.5.1) (2019-11-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **git-gateway:** unpublished entries not loaded for git-gateway(GitHub) ([#2856](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2856)) ([4a2328b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/4a2328b2f10ea678184391e4caf235b41323cd3e))
|
||||
|
||||
# [2.5.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.4.6...decap-cms-backend-git-gateway@2.5.0) (2019-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-git-gateway:** omit /repos/ when no repo ([#2846](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2846)) ([da2dab3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/da2dab305ab7f0655791ef0fb5376e3d5e72897c))
|
||||
|
||||
### Features
|
||||
|
||||
- add go back to site button ([#2538](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2538)) ([f206e7e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/f206e7e5a13fb48ec6b27dce0dbb3a59b61de8f9))
|
||||
- enable specifying custom open authoring commit message ([#2810](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2810)) ([2841ff9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/2841ff9ffe58afcf4dba45514a84a262ad370f1d))
|
||||
|
||||
## [2.4.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.4.5...decap-cms-backend-git-gateway@2.4.6) (2019-09-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **git-gateway:** pass api URL instead of constructing it from repo value ([#2631](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2631)) ([922c0f3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/922c0f3))
|
||||
|
||||
## [2.4.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.4.4...decap-cms-backend-git-gateway@2.4.5) (2019-07-24)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## [2.4.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.4.3...decap-cms-backend-git-gateway@2.4.4) (2019-06-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## [2.4.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.4.2...decap-cms-backend-git-gateway@2.4.3) (2019-06-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **core:** address new entries error for non-github backends ([#2390](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2390)) ([a5bd6b3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/a5bd6b3))
|
||||
|
||||
## [2.4.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.4.2-beta.0...decap-cms-backend-git-gateway@2.4.2) (2019-04-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## [2.4.2-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.4.1...decap-cms-backend-git-gateway@2.4.2-beta.0) (2019-04-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-git-gateway:** fix image display w/o large media ([#2271](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2271)) ([6c3506b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/6c3506b))
|
||||
|
||||
## [2.4.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.4.1-beta.1...decap-cms-backend-git-gateway@2.4.1) (2019-03-29)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## [2.4.1-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.4.1-beta.0...decap-cms-backend-git-gateway@2.4.1-beta.1) (2019-03-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- export on decap-cms and maps on esm ([#2244](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2244)) ([6ffd13b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/6ffd13b))
|
||||
|
||||
## [2.4.1-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.4.0...decap-cms-backend-git-gateway@2.4.1-beta.0) (2019-03-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update peer dep versions ([#2234](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2234)) ([7987091](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/7987091))
|
||||
|
||||
# [2.4.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.3.1-beta.0...decap-cms-backend-git-gateway@2.4.0) (2019-03-22)
|
||||
|
||||
### Features
|
||||
|
||||
- add ES module builds ([#2215](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2215)) ([d142b32](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/d142b32))
|
||||
|
||||
## [2.3.1-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.3.0...decap-cms-backend-git-gateway@2.3.1-beta.0) (2019-03-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **editorial-workflow:** fix LM pointers changing to binary files ([#2228](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2228)) ([d39a361](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/d39a361))
|
||||
|
||||
# [2.3.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.3.0-beta.0...decap-cms-backend-git-gateway@2.3.0) (2019-03-22)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
# [2.3.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.2.5-beta.0...decap-cms-backend-git-gateway@2.3.0-beta.0) (2019-03-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix umd builds ([#2214](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2214)) ([e04f6be](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/e04f6be))
|
||||
|
||||
### Features
|
||||
|
||||
- provide usable UMD builds for all packages ([#2141](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2141)) ([82cc794](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/82cc794))
|
||||
|
||||
## [2.2.5-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.2.4...decap-cms-backend-git-gateway@2.2.5-beta.0) (2019-03-15)
|
||||
|
||||
### Features
|
||||
|
||||
- upgrade to Emotion 10 ([#2166](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2166)) ([ccef446](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/ccef446))
|
||||
|
||||
## [2.2.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.2.3...decap-cms-backend-git-gateway@2.2.4) (2019-03-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** make non-Large Media previews work with Git Gateway+GitHub ([#2151](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2151)) ([63582dc](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/63582dc))
|
||||
|
||||
## [2.2.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.2.2...decap-cms-backend-git-gateway@2.2.3) (2019-03-08)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
## [2.2.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.2.1...decap-cms-backend-git-gateway@2.2.2) (2019-02-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **git-gateway:** fix previews for GitHub images not in Large Media ([#2125](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2125)) ([d17f896](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/d17f896))
|
||||
|
||||
## [2.2.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.2.0...decap-cms-backend-git-gateway@2.2.1) (2019-02-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
# [2.2.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.1.2...decap-cms-backend-git-gateway@2.2.0) (2019-02-08)
|
||||
|
||||
### Features
|
||||
|
||||
- **workflow:** add deploy preview links ([#2028](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/2028)) ([15d221d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/15d221d))
|
||||
|
||||
## [2.1.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.1.1...decap-cms-backend-git-gateway@2.1.2) (2018-12-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **decap-cms-backend-git-gateway:** content-type may have charset ([#1951](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/1951)) ([c74dbae](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/c74dbae))
|
||||
|
||||
## [2.1.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.1.0...decap-cms-backend-git-gateway@2.1.1) (2018-11-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-git-gateway:** double slashes when gateway_url contained a backend ([#1712](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/1712)) ([6de47cd](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/6de47cd))
|
||||
|
||||
# [2.1.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.0.8...decap-cms-backend-git-gateway@2.1.0) (2018-11-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **identity:** switch user name reference to full_name ([#1809](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/1809)) ([55d45a8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/55d45a8))
|
||||
|
||||
### Features
|
||||
|
||||
- allow custom logo on auth page ([#1818](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/1818)) ([c6ae1e8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/c6ae1e8))
|
||||
|
||||
<a name="2.0.8"></a>
|
||||
|
||||
## [2.0.8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.0.7...decap-cms-backend-git-gateway@2.0.8) (2018-09-06)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
<a name="2.0.7"></a>
|
||||
|
||||
## [2.0.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.0.6...decap-cms-backend-git-gateway@2.0.7) (2018-08-27)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
<a name="2.0.6"></a>
|
||||
|
||||
## [2.0.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.0.5...decap-cms-backend-git-gateway@2.0.6) (2018-08-24)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
<a name="2.0.5"></a>
|
||||
|
||||
## [2.0.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.0.4...decap-cms-backend-git-gateway@2.0.5) (2018-08-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **workflow:** fix workflow entries not appearing ([#1581](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/1581)) ([95c8de0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/95c8de0))
|
||||
|
||||
<a name="2.0.4"></a>
|
||||
|
||||
## [2.0.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.0.3...decap-cms-backend-git-gateway@2.0.4) (2018-08-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-git-gateway
|
||||
|
||||
<a name="2.0.3"></a>
|
||||
|
||||
## [2.0.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/compare/decap-cms-backend-git-gateway@2.0.2...decap-cms-backend-git-gateway@2.0.3) (2018-07-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **git-gateway:** correct `proxied` value for proxied backends ([#1540](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/issues/1540)) ([f7dba87](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway/commit/f7dba87))
|
||||
|
||||
<a name="2.0.2"></a>
|
||||
|
||||
## 2.0.2 (2018-07-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **git-gateway:** pass options through git-gateway backend ([#1532](https://github.com/decaporg/decap-cms/issues/1532)) ([4c5436a](https://github.com/decaporg/decap-cms/commit/4c5436a))
|
||||
|
||||
<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))
|
||||
@@ -0,0 +1,53 @@
|
||||
# Git Gateway
|
||||
|
||||
Netlify's [gateway](https://github.com/netlify/git-gateway) to hosted git APIs.
|
||||
|
||||
## Code structure
|
||||
|
||||
`Implementation` for [File Management System API](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-lib-util/README.md) based on `Api`.
|
||||
|
||||
`Api` and `Implementation` from backend-[github](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/README.md)/[gitlab](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/README.md)/[bitbacket](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-bitbacket/README.md) extended with Netlify-specific `LargeMedia(LFS)` and `JWT` auth.
|
||||
|
||||
`AuthenticationPage` - uses [lib-auth](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-lib-auth/README.md) and implements Netlify Identity authentication flow.
|
||||
|
||||
`PKCEAuthenticationPage` = uses [lib-auth](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-lib-auth/README.md) and implements OAuth2 PKCE authentication flow. Enabled if the config.auth_type is set to `pkce`.
|
||||
|
||||
Look at tests or types for more info.
|
||||
|
||||
## Debugging
|
||||
|
||||
When debugging the CMS with Git Gateway you must:
|
||||
|
||||
1. Have a Netlify site with [Git Gateway](https://docs.netlify.com/visitor-access/git-gateway/) and [Netlify Identity](https://docs.netlify.com/visitor-access/identity/) enabled. An easy way to create such a site is to use a [template](https://www.decapcms.org/docs/start-with-a-template/), for example the [Gatsby template](https://app.netlify.com/start/deploy?repository=https://github.com/decaporg/gatsby-starter-decap-cms&stack=cms)
|
||||
2. Tell the CMS the URL of your Netlify site using a local storage item. To do so:
|
||||
|
||||
1. Open `http://localhost:8080/` in the browser
|
||||
2. Write the below command and press enter: `localStorage.setItem('netlifySiteURL', 'https://yourwebsiteurl.netlify.app/')`
|
||||
3. To be sure, you can run this command as well: `localStorage.getItem('netlifySiteURL')`
|
||||
4. Refresh the page
|
||||
5. You should be able to log in via your Netlify Identity email/password
|
||||
|
||||
## PKCE with custom Git-Gateway
|
||||
|
||||
To use a custom Git-Gateway implementation with PKCE authentication, use a configuration similar to the following:
|
||||
|
||||
backend:
|
||||
name: git-gateway
|
||||
# Enables PKCE authentication with the git-gateway backend. After auth,
|
||||
# sends the access_token for all requests to the git-gateway host.
|
||||
auth_type: pkce
|
||||
# The base OAuth2 URL. Here is an obfuscated AWS Cognito example.
|
||||
base_url: https://your-cognito-instance.auth.us-east-1.amazoncognito.com
|
||||
# If you need to customize the authorize or token endpoints for PKCE, do that here
|
||||
#auth_endpoint: oauth2/authorize
|
||||
#auth_token_endpoint: oauth2/token
|
||||
# The OAuth2 client ID
|
||||
app_id: your-oauth2-client-id
|
||||
# The base URL of your custom git-gateway. Note that the last part of the path
|
||||
# should be "bitbucket", "gitlab", or "github", so the implementation can automatically
|
||||
# determine which backend API to use when making requests.
|
||||
gateway_url: https://your.gitgateway.host/git-gateway/bitbucket/
|
||||
# Override the Netlify git-gateway status check
|
||||
status_endpoint: https://your.gitgateway.host/api/v2/components.json
|
||||
# Optional: defaults to "master"
|
||||
branch: main
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "decap-cms-backend-git-gateway",
|
||||
"description": "Git Gateway backend for Decap CMS",
|
||||
"version": "3.4.1",
|
||||
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway",
|
||||
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
||||
"module": "dist/esm/index.js",
|
||||
"main": "dist/decap-cms-backend-git-gateway.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"decap-cms",
|
||||
"backend",
|
||||
"git-gateway",
|
||||
"gateway"
|
||||
],
|
||||
"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": {
|
||||
"gotrue-js": "^0.9.24",
|
||||
"ini": "^2.0.0",
|
||||
"jwt-decode": "^3.0.0",
|
||||
"minimatch": "^7.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"decap-cms-backend-bitbucket": "^3.0.0",
|
||||
"decap-cms-backend-github": "^3.0.0",
|
||||
"decap-cms-backend-gitlab": "^3.0.0",
|
||||
"decap-cms-lib-auth": "^3.0.0",
|
||||
"decap-cms-lib-util": "^3.0.0",
|
||||
"decap-cms-ui-auth": "^3.0.0",
|
||||
"decap-cms-ui-default": "^3.0.0",
|
||||
"lodash": "^4.17.11",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { API as GithubAPI } from 'decap-cms-backend-github';
|
||||
import { APIError } from 'decap-cms-lib-util';
|
||||
|
||||
import type { Config as GitHubConfig, Diff } from 'decap-cms-backend-github/src/API';
|
||||
import type { FetchError } from 'decap-cms-lib-util';
|
||||
import type { Octokit } from '@octokit/rest';
|
||||
|
||||
type Config = Omit<GitHubConfig, 'getUser'> & {
|
||||
apiRoot: string;
|
||||
tokenPromise: () => Promise<string>;
|
||||
commitAuthor: { name: string };
|
||||
isLargeMedia: (filename: string) => Promise<boolean>;
|
||||
};
|
||||
|
||||
export default class API extends GithubAPI {
|
||||
tokenPromise: () => Promise<string>;
|
||||
commitAuthor: { name: string };
|
||||
isLargeMedia: (filename: string) => Promise<boolean>;
|
||||
|
||||
constructor(config: Config) {
|
||||
super({
|
||||
getUser: () => Promise.reject('Never used'),
|
||||
...config,
|
||||
});
|
||||
this.apiRoot = config.apiRoot;
|
||||
this.tokenPromise = config.tokenPromise;
|
||||
this.commitAuthor = config.commitAuthor;
|
||||
this.isLargeMedia = config.isLargeMedia;
|
||||
this.repoURL = '';
|
||||
this.originRepoURL = '';
|
||||
}
|
||||
|
||||
hasWriteAccess() {
|
||||
return this.getDefaultBranch()
|
||||
.then(() => true)
|
||||
.catch((error: FetchError) => {
|
||||
if (error.status === 401) {
|
||||
if (error.message === 'Bad credentials') {
|
||||
throw new APIError(
|
||||
'Git Gateway Error: Please ask your site administrator to reissue the Git Gateway token.',
|
||||
error.status,
|
||||
'Git Gateway',
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if (
|
||||
error.status === 404 &&
|
||||
(error.message === undefined || error.message === 'Unable to locate site configuration')
|
||||
) {
|
||||
throw new APIError(
|
||||
`Git Gateway Error: Please make sure Git Gateway is enabled on your site.`,
|
||||
error.status,
|
||||
'Git Gateway',
|
||||
);
|
||||
} else {
|
||||
console.error('Problem fetching repo data from Git Gateway');
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
requestHeaders(headers = {}) {
|
||||
return this.tokenPromise().then(jwtToken => {
|
||||
const baseHeader = {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
...headers,
|
||||
};
|
||||
|
||||
return baseHeader;
|
||||
});
|
||||
}
|
||||
|
||||
handleRequestError(error: FetchError & { msg: string }, responseStatus: number) {
|
||||
throw new APIError(error.message || error.msg, responseStatus, 'Git Gateway');
|
||||
}
|
||||
|
||||
user() {
|
||||
return Promise.resolve({ login: '', ...this.commitAuthor });
|
||||
}
|
||||
|
||||
async getHeadReference(head: string) {
|
||||
if (!this.repoOwner) {
|
||||
// get the repo owner from the branch url
|
||||
// this is required for returning the full head reference, e.g. owner:head
|
||||
// when filtering pull requests based on the head
|
||||
const branch = await this.getDefaultBranch();
|
||||
const self = branch._links.self;
|
||||
const regex = new RegExp('https?://.+?/repos/(.+?)/');
|
||||
const owner = self.match(regex);
|
||||
this.repoOwner = owner ? owner[1] : '';
|
||||
}
|
||||
return super.getHeadReference(head);
|
||||
}
|
||||
|
||||
commit(message: string, changeTree: { parentSha?: string; sha: string }) {
|
||||
const commitParams: {
|
||||
message: string;
|
||||
tree: string;
|
||||
parents: string[];
|
||||
author?: { name: string; date: string };
|
||||
} = {
|
||||
message,
|
||||
tree: changeTree.sha,
|
||||
parents: changeTree.parentSha ? [changeTree.parentSha] : [],
|
||||
};
|
||||
|
||||
if (this.commitAuthor) {
|
||||
commitParams.author = {
|
||||
...this.commitAuthor,
|
||||
date: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
return this.request('/git/commits', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(commitParams),
|
||||
});
|
||||
}
|
||||
|
||||
nextUrlProcessor() {
|
||||
return (url: string) => url.replace(/^(?:[a-z]+:\/\/.+?\/.+?\/.+?\/)/, `${this.apiRoot}/`);
|
||||
}
|
||||
|
||||
async diffFromFile(file: Octokit.ReposCompareCommitsResponseFilesItem): Promise<Diff> {
|
||||
const diff = await super.diffFromFile(file);
|
||||
return {
|
||||
...diff,
|
||||
binary: diff.binary || (await this.isLargeMedia(file.filename)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { API as GitlabAPI } from 'decap-cms-backend-gitlab';
|
||||
import { unsentRequest } from 'decap-cms-lib-util';
|
||||
|
||||
import type { Config as GitLabConfig, CommitAuthor } from 'decap-cms-backend-gitlab/src/API';
|
||||
import type { ApiRequest } from 'decap-cms-lib-util';
|
||||
|
||||
type Config = GitLabConfig & { tokenPromise: () => Promise<string>; commitAuthor: CommitAuthor };
|
||||
|
||||
export default class API extends GitlabAPI {
|
||||
tokenPromise: () => Promise<string>;
|
||||
|
||||
constructor(config: Config) {
|
||||
super(config);
|
||||
this.tokenPromise = config.tokenPromise;
|
||||
this.commitAuthor = config.commitAuthor;
|
||||
this.repoURL = '';
|
||||
}
|
||||
|
||||
withAuthorizationHeaders = async (req: ApiRequest) => {
|
||||
const token = await this.tokenPromise();
|
||||
return unsentRequest.withHeaders(
|
||||
{
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
req,
|
||||
);
|
||||
};
|
||||
|
||||
hasWriteAccess = () => Promise.resolve(true);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import API from '../GitHubAPI';
|
||||
|
||||
describe('github API', () => {
|
||||
describe('request', () => {
|
||||
beforeEach(() => {
|
||||
const fetch = jest.fn();
|
||||
global.fetch = fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fetch url with authorization header', async () => {
|
||||
const api = new API({
|
||||
apiRoot: 'https://site.netlify.com/.netlify/git/github',
|
||||
tokenPromise: () => Promise.resolve('token'),
|
||||
});
|
||||
|
||||
fetch.mockResolvedValue({
|
||||
text: jest.fn().mockResolvedValue('some response'),
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { get: () => '' },
|
||||
});
|
||||
const result = await api.request('/some-path');
|
||||
expect(result).toEqual('some response');
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(fetch).toHaveBeenCalledWith('https://site.netlify.com/.netlify/git/github/some-path', {
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
Authorization: 'Bearer token',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error on not ok response with message property', async () => {
|
||||
const api = new API({
|
||||
apiRoot: 'https://site.netlify.com/.netlify/git/github',
|
||||
tokenPromise: () => Promise.resolve('token'),
|
||||
});
|
||||
|
||||
fetch.mockResolvedValue({
|
||||
text: jest.fn().mockResolvedValue({ message: 'some error' }),
|
||||
ok: false,
|
||||
status: 404,
|
||||
headers: { get: () => '' },
|
||||
});
|
||||
|
||||
await expect(api.request('some-path')).rejects.toThrow(
|
||||
expect.objectContaining({
|
||||
message: 'some error',
|
||||
name: 'API_ERROR',
|
||||
status: 404,
|
||||
api: 'Git Gateway',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error on not ok response with msg property', async () => {
|
||||
const api = new API({
|
||||
apiRoot: 'https://site.netlify.com/.netlify/git/github',
|
||||
tokenPromise: () => Promise.resolve('token'),
|
||||
});
|
||||
|
||||
fetch.mockResolvedValue({
|
||||
text: jest.fn().mockResolvedValue({ msg: 'some error' }),
|
||||
ok: false,
|
||||
status: 404,
|
||||
headers: { get: () => '' },
|
||||
});
|
||||
|
||||
await expect(api.request('some-path')).rejects.toThrow(
|
||||
expect.objectContaining({
|
||||
message: 'some error',
|
||||
name: 'API_ERROR',
|
||||
status: 404,
|
||||
api: 'Git Gateway',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nextUrlProcessor', () => {
|
||||
it('should re-write github url', () => {
|
||||
const api = new API({
|
||||
apiRoot: 'https://site.netlify.com/.netlify/git/github',
|
||||
});
|
||||
|
||||
expect(api.nextUrlProcessor()('https://api.github.com/repositories/10000/pulls')).toEqual(
|
||||
'https://site.netlify.com/.netlify/git/github/pulls',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,645 @@
|
||||
import GoTrue from 'gotrue-js';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import get from 'lodash/get';
|
||||
import pick from 'lodash/pick';
|
||||
import intersection from 'lodash/intersection';
|
||||
import ini from 'ini';
|
||||
import {
|
||||
APIError,
|
||||
unsentRequest,
|
||||
basename,
|
||||
entriesByFiles,
|
||||
parsePointerFile,
|
||||
getLargeMediaPatternsFromGitAttributesFile,
|
||||
getPointerFileForMediaFileObj,
|
||||
getLargeMediaFilteredMediaFiles,
|
||||
AccessTokenError,
|
||||
PreviewState,
|
||||
} from 'decap-cms-lib-util';
|
||||
import { GitHubBackend } from 'decap-cms-backend-github';
|
||||
import { GitLabBackend } from 'decap-cms-backend-gitlab';
|
||||
import { BitbucketBackend, API as BitBucketAPI } from 'decap-cms-backend-bitbucket';
|
||||
import { NetlifyAuthenticationPage, PKCEAuthenticationPage } from 'decap-cms-ui-auth';
|
||||
|
||||
import GitHubAPI from './GitHubAPI';
|
||||
import GitLabAPI from './GitLabAPI';
|
||||
import { getClient } from './netlify-lfs-client';
|
||||
|
||||
import type { Client } from './netlify-lfs-client';
|
||||
import type {
|
||||
ApiRequest,
|
||||
AssetProxy,
|
||||
PersistOptions,
|
||||
Entry,
|
||||
Cursor,
|
||||
Implementation,
|
||||
DisplayURL,
|
||||
User,
|
||||
Credentials,
|
||||
Config,
|
||||
ImplementationFile,
|
||||
DisplayURLObject,
|
||||
} from 'decap-cms-lib-util';
|
||||
|
||||
const STATUS_PAGE = 'https://www.netlifystatus.com';
|
||||
const GIT_GATEWAY_STATUS_ENDPOINT = `${STATUS_PAGE}/api/v2/components.json`;
|
||||
const GIT_GATEWAY_OPERATIONAL_UNITS = ['Git Gateway'];
|
||||
type GitGatewayStatus = {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
type NetlifyIdentity = {
|
||||
logout: () => void;
|
||||
currentUser: () => User;
|
||||
on: (event: string, args: unknown) => void;
|
||||
init: () => void;
|
||||
store: { user: unknown; modal: { page: string }; saving: boolean };
|
||||
};
|
||||
|
||||
type AuthClient = {
|
||||
logout: () => void;
|
||||
currentUser: () => unknown;
|
||||
login?(email: string, password: string, remember?: boolean): Promise<unknown>;
|
||||
clearStore: () => void;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
netlifyIdentity?: NetlifyIdentity;
|
||||
}
|
||||
}
|
||||
|
||||
const localHosts: Record<string, boolean> = {
|
||||
localhost: true,
|
||||
'127.0.0.1': true,
|
||||
'0.0.0.0': true,
|
||||
};
|
||||
const defaults = {
|
||||
identity: '/.netlify/identity',
|
||||
gateway: '/.netlify/git',
|
||||
largeMedia: '/.netlify/large-media',
|
||||
};
|
||||
|
||||
function getEndpoint(endpoint: string, netlifySiteURL: string | null) {
|
||||
if (
|
||||
localHosts[document.location.host.split(':').shift() as string] &&
|
||||
netlifySiteURL &&
|
||||
endpoint.match(/^\/\.netlify\//)
|
||||
) {
|
||||
const parts = [];
|
||||
if (netlifySiteURL) {
|
||||
parts.push(netlifySiteURL);
|
||||
if (!netlifySiteURL.match(/\/$/)) {
|
||||
parts.push('/');
|
||||
}
|
||||
}
|
||||
parts.push(endpoint.replace(/^\//, ''));
|
||||
return parts.join('');
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
// wait for identity widget to initialize
|
||||
// force init on timeout
|
||||
let initPromise = Promise.resolve() as Promise<unknown>;
|
||||
if (window.netlifyIdentity) {
|
||||
let initialized = false;
|
||||
initPromise = Promise.race([
|
||||
new Promise<void>(resolve => {
|
||||
window.netlifyIdentity?.on('init', () => {
|
||||
initialized = true;
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
new Promise(resolve => setTimeout(resolve, 2500)).then(() => {
|
||||
if (!initialized) {
|
||||
console.log('Manually initializing identity widget');
|
||||
window.netlifyIdentity?.init();
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
interface GitGatewayUser extends Credentials {
|
||||
jwt: () => Promise<string>;
|
||||
email: string;
|
||||
user_metadata: { full_name: string; avatar_url: string };
|
||||
}
|
||||
|
||||
async function apiGet(path: string) {
|
||||
const apiRoot = 'https://api.netlify.com/api/v1/sites';
|
||||
const response = await fetch(`${apiRoot}/${path}`).then(res => res.json());
|
||||
return response;
|
||||
}
|
||||
|
||||
export default class GitGateway implements Implementation {
|
||||
config: Config;
|
||||
api?: GitHubAPI | GitLabAPI | BitBucketAPI;
|
||||
branch: string;
|
||||
squashMerges: boolean;
|
||||
cmsLabelPrefix: string;
|
||||
mediaFolder: string;
|
||||
transformImages: boolean;
|
||||
gatewayUrl: string;
|
||||
gitGatewayStatusEndpoint: string;
|
||||
netlifyLargeMediaURL: string;
|
||||
backendType: string | null;
|
||||
apiUrl: string;
|
||||
authType: 'pkce' | 'netlify';
|
||||
authClient?: AuthClient;
|
||||
backend: GitHubBackend | GitLabBackend | BitbucketBackend | null;
|
||||
acceptRoles?: string[];
|
||||
tokenPromise?: () => Promise<string>;
|
||||
_largeMediaClientPromise?: Promise<Client>;
|
||||
|
||||
options: {
|
||||
proxied: boolean;
|
||||
API: GitHubAPI | GitLabAPI | BitBucketAPI | null;
|
||||
initialWorkflowStatus: string;
|
||||
};
|
||||
constructor(config: Config, options = {}) {
|
||||
this.options = {
|
||||
proxied: true,
|
||||
API: null,
|
||||
initialWorkflowStatus: '',
|
||||
...options,
|
||||
};
|
||||
this.config = config;
|
||||
this.branch = config.backend.branch?.trim() || 'master';
|
||||
this.squashMerges = config.backend.squash_merges || false;
|
||||
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
|
||||
this.mediaFolder = config.media_folder;
|
||||
this.gitGatewayStatusEndpoint = config.backend.status_endpoint || GIT_GATEWAY_STATUS_ENDPOINT;
|
||||
const { use_large_media_transforms_in_media_library: transformImages = true } = config.backend;
|
||||
this.transformImages = transformImages;
|
||||
|
||||
const netlifySiteURL = localStorage.getItem('netlifySiteURL');
|
||||
this.apiUrl = getEndpoint(config.backend.identity_url || defaults.identity, netlifySiteURL);
|
||||
this.gatewayUrl = getEndpoint(config.backend.gateway_url || defaults.gateway, netlifySiteURL);
|
||||
this.netlifyLargeMediaURL = getEndpoint(
|
||||
config.backend.large_media_url || defaults.largeMedia,
|
||||
netlifySiteURL,
|
||||
);
|
||||
const backendTypeRegex = /\/(github|gitlab|bitbucket)\/?$/;
|
||||
const backendTypeMatches = this.gatewayUrl.match(backendTypeRegex);
|
||||
if (backendTypeMatches) {
|
||||
this.backendType = backendTypeMatches[1];
|
||||
this.gatewayUrl = this.gatewayUrl.replace(backendTypeRegex, '');
|
||||
} else {
|
||||
this.backendType = null;
|
||||
}
|
||||
|
||||
this.backend = null;
|
||||
if (config.backend.auth_type === 'pkce') {
|
||||
this.authType = 'pkce';
|
||||
} else {
|
||||
this.authType = 'netlify';
|
||||
NetlifyAuthenticationPage.authClient = () => this.getAuthClient();
|
||||
}
|
||||
}
|
||||
|
||||
isGitBackend() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async status() {
|
||||
const api = await fetch(this.gitGatewayStatusEndpoint)
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
return res['components']
|
||||
.filter((statusComponent: GitGatewayStatus) =>
|
||||
GIT_GATEWAY_OPERATIONAL_UNITS.includes(statusComponent.name),
|
||||
)
|
||||
.every((statusComponent: GitGatewayStatus) => statusComponent.status === 'operational');
|
||||
})
|
||||
.catch(e => {
|
||||
console.warn('Failed getting Git Gateway status', e);
|
||||
return true;
|
||||
});
|
||||
|
||||
let auth = false;
|
||||
// no need to check auth if api is down
|
||||
if (api) {
|
||||
auth =
|
||||
(await this.tokenPromise?.()
|
||||
.then(token => !!token)
|
||||
.catch(e => {
|
||||
console.warn('Failed getting Identity token', e);
|
||||
return false;
|
||||
})) || false;
|
||||
}
|
||||
|
||||
return { auth: { status: auth }, api: { status: api, statusPage: STATUS_PAGE } };
|
||||
}
|
||||
|
||||
async getAuthClient() {
|
||||
if (this.authClient) {
|
||||
return this.authClient;
|
||||
}
|
||||
await initPromise;
|
||||
if (window.netlifyIdentity) {
|
||||
this.authClient = {
|
||||
logout: () => window.netlifyIdentity?.logout(),
|
||||
currentUser: () => window.netlifyIdentity?.currentUser(),
|
||||
clearStore: () => {
|
||||
const store = window.netlifyIdentity?.store;
|
||||
if (store) {
|
||||
store.user = null;
|
||||
store.modal.page = 'login';
|
||||
store.saving = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const goTrue = new GoTrue({ APIUrl: this.apiUrl });
|
||||
this.authClient = {
|
||||
logout: () => {
|
||||
const user = goTrue.currentUser();
|
||||
if (user) {
|
||||
return user.logout();
|
||||
}
|
||||
},
|
||||
currentUser: () => goTrue.currentUser(),
|
||||
login: goTrue.login.bind(goTrue),
|
||||
clearStore: () => undefined,
|
||||
};
|
||||
}
|
||||
return this.authClient;
|
||||
}
|
||||
|
||||
requestFunction = (req: ApiRequest) =>
|
||||
this.tokenPromise!()
|
||||
.then(
|
||||
token => unsentRequest.withHeaders({ Authorization: `Bearer ${token}` }, req) as ApiRequest,
|
||||
)
|
||||
.then(unsentRequest.performRequest);
|
||||
|
||||
authenticate(credentials: Credentials) {
|
||||
const user = credentials as GitGatewayUser;
|
||||
if (user.jwt) {
|
||||
// Netlify auth
|
||||
this.tokenPromise = async () => {
|
||||
try {
|
||||
const func = user.jwt.bind(user);
|
||||
return await func();
|
||||
} catch (error) {
|
||||
throw new AccessTokenError(`Failed getting access token: ${error.message}`);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// OAuth
|
||||
this.tokenPromise = async () => (typeof user.token === 'string' ? user.token : '');
|
||||
}
|
||||
|
||||
return this.tokenPromise!().then(async token => {
|
||||
if (!this.backendType) {
|
||||
const {
|
||||
github_enabled: githubEnabled,
|
||||
gitlab_enabled: gitlabEnabled,
|
||||
bitbucket_enabled: bitbucketEnabled,
|
||||
roles,
|
||||
} = await unsentRequest
|
||||
.fetchWithTimeout(`${this.gatewayUrl}/settings`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then(async res => {
|
||||
const contentType = res.headers.get('Content-Type') || '';
|
||||
if (!contentType.includes('application/json') && !contentType.includes('text/json')) {
|
||||
throw new APIError(
|
||||
`Your Git Gateway backend is not returning valid settings. Please make sure it is enabled.`,
|
||||
res.status,
|
||||
'Git Gateway',
|
||||
);
|
||||
}
|
||||
const body = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new APIError(
|
||||
`Git Gateway Error: ${body.message ? body.message : body}`,
|
||||
res.status,
|
||||
'Git Gateway',
|
||||
);
|
||||
}
|
||||
|
||||
return body;
|
||||
});
|
||||
this.acceptRoles = roles;
|
||||
if (githubEnabled) {
|
||||
this.backendType = 'github';
|
||||
} else if (gitlabEnabled) {
|
||||
this.backendType = 'gitlab';
|
||||
} else if (bitbucketEnabled) {
|
||||
this.backendType = 'bitbucket';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.acceptRoles && this.acceptRoles.length > 0) {
|
||||
const userRoles = get(jwtDecode(token), 'app_metadata.roles', []);
|
||||
const validRole = intersection(userRoles, this.acceptRoles).length > 0;
|
||||
if (!validRole) {
|
||||
throw new Error("You don't have sufficient permissions to access Decap CMS");
|
||||
}
|
||||
}
|
||||
|
||||
const userData = {
|
||||
name: user.user_metadata.full_name || user.email.split('@').shift()!,
|
||||
email: user.email,
|
||||
avatar_url: user.user_metadata.avatar_url,
|
||||
metadata: user.user_metadata,
|
||||
};
|
||||
const apiConfig = {
|
||||
apiRoot: `${this.gatewayUrl}/${this.backendType}`,
|
||||
branch: this.branch,
|
||||
tokenPromise: this.tokenPromise!,
|
||||
commitAuthor: pick(userData, ['name', 'email']),
|
||||
isLargeMedia: (filename: string) => this.isLargeMediaFile(filename),
|
||||
squashMerges: this.squashMerges,
|
||||
cmsLabelPrefix: this.cmsLabelPrefix,
|
||||
initialWorkflowStatus: this.options.initialWorkflowStatus,
|
||||
};
|
||||
|
||||
if (this.backendType === 'github') {
|
||||
this.api = new GitHubAPI(apiConfig);
|
||||
this.backend = new GitHubBackend(this.config, { ...this.options, API: this.api });
|
||||
} else if (this.backendType === 'gitlab') {
|
||||
this.api = new GitLabAPI(apiConfig);
|
||||
this.backend = new GitLabBackend(this.config, { ...this.options, API: this.api });
|
||||
} else if (this.backendType === 'bitbucket') {
|
||||
this.api = new BitBucketAPI({
|
||||
...apiConfig,
|
||||
requestFunction: this.requestFunction,
|
||||
hasWriteAccess: async () => true,
|
||||
});
|
||||
this.backend = new BitbucketBackend(this.config, { ...this.options, API: this.api });
|
||||
}
|
||||
|
||||
if (!(await this.api!.hasWriteAccess())) {
|
||||
throw new Error("You don't have sufficient permissions to access Decap CMS");
|
||||
}
|
||||
return {
|
||||
name: userData.name,
|
||||
login: userData.email,
|
||||
avatar_url: userData.avatar_url,
|
||||
} as unknown as User;
|
||||
});
|
||||
}
|
||||
async restoreUser() {
|
||||
const client = await this.getAuthClient();
|
||||
const user = client.currentUser();
|
||||
if (!user) return Promise.reject();
|
||||
return this.authenticate(user as Credentials);
|
||||
}
|
||||
authComponent() {
|
||||
return this.authType === 'pkce' ? PKCEAuthenticationPage : NetlifyAuthenticationPage;
|
||||
}
|
||||
|
||||
async logout() {
|
||||
const client = await this.getAuthClient();
|
||||
try {
|
||||
client.logout();
|
||||
} catch (e) {
|
||||
// due to a bug in the identity widget (gotrue-js actually) the store is not reset if logout fails
|
||||
// TODO: remove after https://github.com/netlify/gotrue-js/pull/83 is merged
|
||||
client.clearStore();
|
||||
}
|
||||
}
|
||||
getToken() {
|
||||
return this.tokenPromise!();
|
||||
}
|
||||
|
||||
async entriesByFolder(folder: string, extension: string, depth: number) {
|
||||
return this.backend!.entriesByFolder(folder, extension, depth);
|
||||
}
|
||||
allEntriesByFolder(folder: string, extension: string, depth: number, pathRegex?: RegExp) {
|
||||
return this.backend!.allEntriesByFolder(folder, extension, depth, pathRegex);
|
||||
}
|
||||
entriesByFiles(files: ImplementationFile[]) {
|
||||
return this.backend!.entriesByFiles(files);
|
||||
}
|
||||
getEntry(path: string) {
|
||||
return this.backend!.getEntry(path);
|
||||
}
|
||||
|
||||
async unpublishedEntryDataFile(collection: string, slug: string, path: string, id: string) {
|
||||
return this.backend!.unpublishedEntryDataFile(collection, slug, path, id);
|
||||
}
|
||||
|
||||
async isLargeMediaFile(path: string) {
|
||||
const client = await this.getLargeMediaClient();
|
||||
return client.enabled && client.matchPath(path);
|
||||
}
|
||||
|
||||
async unpublishedEntryMediaFile(collection: string, slug: string, path: string, id: string) {
|
||||
const isLargeMedia = await this.isLargeMediaFile(path);
|
||||
if (isLargeMedia) {
|
||||
const branch = this.backend!.getBranch(collection, slug);
|
||||
const { url, blob } = await this.getLargeMediaDisplayURL({ path, id }, branch);
|
||||
return {
|
||||
id,
|
||||
name: basename(path),
|
||||
path,
|
||||
url,
|
||||
displayURL: url,
|
||||
file: new File([blob], basename(path)),
|
||||
size: blob.size,
|
||||
};
|
||||
} else {
|
||||
return this.backend!.unpublishedEntryMediaFile(collection, slug, path, id);
|
||||
}
|
||||
}
|
||||
|
||||
getMedia(mediaFolder = this.mediaFolder) {
|
||||
return this.backend!.getMedia(mediaFolder);
|
||||
}
|
||||
|
||||
// this method memoizes this._getLargeMediaClient so that there can
|
||||
// only be one client at a time
|
||||
getLargeMediaClient() {
|
||||
if (this._largeMediaClientPromise) {
|
||||
return this._largeMediaClientPromise;
|
||||
}
|
||||
this._largeMediaClientPromise = this._getLargeMediaClient();
|
||||
return this._largeMediaClientPromise;
|
||||
}
|
||||
_getLargeMediaClient() {
|
||||
const netlifyLargeMediaEnabledPromise = this.api!.readFile('.lfsconfig')
|
||||
.then(config => ini.decode<{ lfs: { url: string } }>(config as string))
|
||||
.then(({ lfs: { url } }) => new URL(url))
|
||||
.then(lfsURL => ({
|
||||
enabled: lfsURL.hostname.endsWith('netlify.com') || lfsURL.hostname.endsWith('netlify.app'),
|
||||
}))
|
||||
.catch((err: Error) => ({ enabled: false, err }));
|
||||
|
||||
const lfsPatternsPromise = this.api!.readFile('.gitattributes')
|
||||
.then(attributes => getLargeMediaPatternsFromGitAttributesFile(attributes as string))
|
||||
.then((patterns: string[]) => ({ err: null, patterns }))
|
||||
.catch((err: Error) => {
|
||||
if (err.message.includes('404')) {
|
||||
console.log('This 404 was expected and handled appropriately.');
|
||||
return { err: null, patterns: [] as string[] };
|
||||
} else {
|
||||
return { err, patterns: [] as string[] };
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all([netlifyLargeMediaEnabledPromise, lfsPatternsPromise]).then(
|
||||
([{ enabled: maybeEnabled }, { patterns, err: patternsErr }]) => {
|
||||
const enabled = maybeEnabled && !patternsErr;
|
||||
|
||||
// We expect LFS patterns to exist when the .lfsconfig states
|
||||
// that we're using Netlify Large Media
|
||||
if (maybeEnabled && patternsErr) {
|
||||
console.error(patternsErr);
|
||||
}
|
||||
|
||||
return getClient({
|
||||
enabled,
|
||||
rootURL: this.netlifyLargeMediaURL,
|
||||
makeAuthorizedRequest: this.requestFunction,
|
||||
patterns,
|
||||
transformImages: this.transformImages ? { nf_resize: 'fit', w: 560, h: 320 } : false,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
async getLargeMediaDisplayURL(
|
||||
{ path, id }: { path: string; id: string | null },
|
||||
branch = this.branch,
|
||||
) {
|
||||
const readFile = (
|
||||
path: string,
|
||||
id: string | null | undefined,
|
||||
{ parseText }: { parseText: boolean },
|
||||
) => this.api!.readFile(path, id, { branch, parseText });
|
||||
|
||||
const items = await entriesByFiles(
|
||||
[{ path, id }],
|
||||
readFile,
|
||||
this.api!.readFileMetadata.bind(this.api),
|
||||
'Git-Gateway',
|
||||
);
|
||||
const entry = items[0];
|
||||
const pointerFile = parsePointerFile(entry.data);
|
||||
if (!pointerFile.sha) {
|
||||
console.warn(`Failed parsing pointer file ${path}`);
|
||||
return { url: path, blob: new Blob() };
|
||||
}
|
||||
|
||||
const client = await this.getLargeMediaClient();
|
||||
const { url, blob } = await client.getDownloadURL(pointerFile);
|
||||
return { url, blob };
|
||||
}
|
||||
|
||||
async getMediaDisplayURL(displayURL: DisplayURL) {
|
||||
const { path, id } = displayURL as DisplayURLObject;
|
||||
const isLargeMedia = await this.isLargeMediaFile(path);
|
||||
if (isLargeMedia) {
|
||||
const { url } = await this.getLargeMediaDisplayURL({ path, id });
|
||||
return url;
|
||||
}
|
||||
if (typeof displayURL === 'string') {
|
||||
return displayURL;
|
||||
}
|
||||
|
||||
const url = await this.backend!.getMediaDisplayURL(displayURL);
|
||||
return url;
|
||||
}
|
||||
|
||||
async getMediaFile(path: string) {
|
||||
const isLargeMedia = await this.isLargeMediaFile(path);
|
||||
if (isLargeMedia) {
|
||||
const { url, blob } = await this.getLargeMediaDisplayURL({ path, id: null });
|
||||
return {
|
||||
id: url,
|
||||
name: basename(path),
|
||||
path,
|
||||
url,
|
||||
displayURL: url,
|
||||
file: new File([blob], basename(path)),
|
||||
size: blob.size,
|
||||
};
|
||||
}
|
||||
return this.backend!.getMediaFile(path);
|
||||
}
|
||||
|
||||
async persistEntry(entry: Entry, options: PersistOptions) {
|
||||
const client = await this.getLargeMediaClient();
|
||||
if (client.enabled) {
|
||||
const assets = await getLargeMediaFilteredMediaFiles(client, entry.assets);
|
||||
return this.backend!.persistEntry({ ...entry, assets }, options);
|
||||
} else {
|
||||
return this.backend!.persistEntry(entry, options);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
const isLargeMedia = await this.isLargeMediaFile(fixedPath);
|
||||
if (isLargeMedia) {
|
||||
const persistMediaArgument = await getPointerFileForMediaFileObj(
|
||||
client,
|
||||
fileObj as File,
|
||||
path,
|
||||
);
|
||||
return {
|
||||
...(await this.backend!.persistMedia(persistMediaArgument, options)),
|
||||
displayURL,
|
||||
};
|
||||
}
|
||||
return await this.backend!.persistMedia(mediaFile, options);
|
||||
}
|
||||
deleteFiles(paths: string[], commitMessage: string) {
|
||||
return this.backend!.deleteFiles(paths, commitMessage);
|
||||
}
|
||||
async getDeployPreview(collection: string, slug: string) {
|
||||
let preview = await this.backend!.getDeployPreview(collection, slug);
|
||||
if (!preview) {
|
||||
try {
|
||||
// if the commit doesn't have a status, try to use Netlify API directly
|
||||
// this is useful when builds are queue up in Netlify and don't have a commit status yet
|
||||
// and only works with public logs at the moment
|
||||
// TODO: get Netlify API Token and use it to access private logs
|
||||
const siteId = new URL(localStorage.getItem('netlifySiteURL') || '').hostname;
|
||||
const site = await apiGet(siteId);
|
||||
const deploys: { state: string; commit_ref: string; deploy_url: string }[] = await apiGet(
|
||||
`${site.id}/deploys?per_page=100`,
|
||||
);
|
||||
if (deploys.length > 0) {
|
||||
const ref = await this.api!.getUnpublishedEntrySha(collection, slug);
|
||||
const deploy = deploys.find(d => d.commit_ref === ref);
|
||||
if (deploy) {
|
||||
preview = {
|
||||
status: deploy.state === 'ready' ? PreviewState.Success : PreviewState.Other,
|
||||
url: deploy.deploy_url,
|
||||
};
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
unpublishedEntries() {
|
||||
return this.backend!.unpublishedEntries();
|
||||
}
|
||||
unpublishedEntry({ id, collection, slug }: { id?: string; collection?: string; slug?: string }) {
|
||||
return this.backend!.unpublishedEntry({ id, collection, slug });
|
||||
}
|
||||
updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
return this.backend!.updateUnpublishedEntryStatus(collection, slug, newStatus);
|
||||
}
|
||||
deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
return this.backend!.deleteUnpublishedEntry(collection, slug);
|
||||
}
|
||||
publishUnpublishedEntry(collection: string, slug: string) {
|
||||
return this.backend!.publishUnpublishedEntry(collection, slug);
|
||||
}
|
||||
traverseCursor(cursor: Cursor, action: string) {
|
||||
return this.backend!.traverseCursor!(cursor, action);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import GitGatewayBackend from './implementation';
|
||||
|
||||
export const DecapCmsBackendGitGateway = {
|
||||
GitGatewayBackend,
|
||||
};
|
||||
export { GitGatewayBackend };
|
||||
@@ -0,0 +1,181 @@
|
||||
import { flow, fromPairs, map } from 'lodash/fp';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
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>;
|
||||
|
||||
type ImageTransformations = { nf_resize: string; w: number; h: number };
|
||||
|
||||
type ClientConfig = {
|
||||
rootURL: string;
|
||||
makeAuthorizedRequest: MakeAuthorizedRequest;
|
||||
patterns: string[];
|
||||
enabled: boolean;
|
||||
transformImages: ImageTransformations | boolean;
|
||||
};
|
||||
|
||||
export function matchPath({ patterns }: ClientConfig, path: string) {
|
||||
return patterns.some(pattern => minimatch(path, pattern, { matchBase: true }));
|
||||
}
|
||||
|
||||
//
|
||||
// API interactions
|
||||
|
||||
const defaultContentHeaders = {
|
||||
Accept: 'application/vnd.git-lfs+json',
|
||||
['Content-Type']: 'application/vnd.git-lfs+json',
|
||||
};
|
||||
|
||||
async function resourceExists(
|
||||
{ rootURL, makeAuthorizedRequest }: ClientConfig,
|
||||
{ sha, size }: PointerFile,
|
||||
) {
|
||||
const response = await makeAuthorizedRequest({
|
||||
url: `${rootURL}/verify`,
|
||||
method: 'POST',
|
||||
headers: defaultContentHeaders,
|
||||
body: JSON.stringify({ oid: sha, size }),
|
||||
});
|
||||
if (response.ok) {
|
||||
return true;
|
||||
}
|
||||
if (response.status === 404) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: what kind of error to throw here? APIError doesn't seem
|
||||
// to fit
|
||||
}
|
||||
|
||||
function getTransofrmationsParams(t: boolean | ImageTransformations) {
|
||||
if (isPlainObject(t) && !isEmpty(t)) {
|
||||
const { nf_resize: resize, w, h } = t as ImageTransformations;
|
||||
return `?nf_resize=${resize}&w=${w}&h=${h}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function getDownloadURL(
|
||||
{ rootURL, transformImages: t, makeAuthorizedRequest }: ClientConfig,
|
||||
{ sha }: PointerFile,
|
||||
) {
|
||||
try {
|
||||
const transformation = getTransofrmationsParams(t);
|
||||
const transformedPromise = makeAuthorizedRequest(`${rootURL}/origin/${sha}${transformation}`);
|
||||
const [transformed, original] = await Promise.all([
|
||||
transformedPromise,
|
||||
// if transformation is defined, we need to load the original so we have the correct meta data
|
||||
transformation ? makeAuthorizedRequest(`${rootURL}/origin/${sha}`) : transformedPromise,
|
||||
]);
|
||||
if (!transformed.ok) {
|
||||
const error = await transformed.json();
|
||||
throw new Error(
|
||||
`Failed getting large media for sha '${sha}': '${error.code} - ${error.msg}'`,
|
||||
);
|
||||
}
|
||||
|
||||
const transformedBlob = await transformed.blob();
|
||||
const url = URL.createObjectURL(transformedBlob);
|
||||
return { url, blob: transformation ? await original.blob() : transformedBlob };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { url: '', blob: new Blob() };
|
||||
}
|
||||
}
|
||||
|
||||
function uploadOperation(objects: PointerFile[]) {
|
||||
return {
|
||||
operation: 'upload',
|
||||
transfers: ['basic'],
|
||||
objects: objects.map(({ sha, ...rest }) => ({ ...rest, oid: sha })),
|
||||
};
|
||||
}
|
||||
|
||||
async function getResourceUploadURLs(
|
||||
{
|
||||
rootURL,
|
||||
makeAuthorizedRequest,
|
||||
}: { rootURL: string; makeAuthorizedRequest: MakeAuthorizedRequest },
|
||||
pointerFiles: PointerFile[],
|
||||
) {
|
||||
const response = await makeAuthorizedRequest({
|
||||
url: `${rootURL}/objects/batch`,
|
||||
method: 'POST',
|
||||
headers: defaultContentHeaders,
|
||||
body: JSON.stringify(uploadOperation(pointerFiles)),
|
||||
});
|
||||
|
||||
const { objects } = await response.json();
|
||||
const uploadUrls = objects.map(
|
||||
(object: { error?: { message: string }; actions: { upload: { href: string } } }) => {
|
||||
if (object.error) {
|
||||
throw new Error(object.error.message);
|
||||
}
|
||||
return object.actions.upload.href;
|
||||
},
|
||||
);
|
||||
return uploadUrls;
|
||||
}
|
||||
|
||||
function uploadBlob(uploadURL: string, blob: Blob) {
|
||||
return unsentRequest.fetchWithTimeout(uploadURL, {
|
||||
method: 'PUT',
|
||||
body: blob,
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadResource(
|
||||
clientConfig: ClientConfig,
|
||||
{ sha, size }: PointerFile,
|
||||
resource: Blob,
|
||||
) {
|
||||
const existingFile = await resourceExists(clientConfig, { sha, size });
|
||||
if (existingFile) {
|
||||
return sha;
|
||||
}
|
||||
const [uploadURL] = await getResourceUploadURLs(clientConfig, [{ sha, size }]);
|
||||
await uploadBlob(uploadURL, resource);
|
||||
return sha;
|
||||
}
|
||||
|
||||
//
|
||||
// Create Large Media client
|
||||
|
||||
function configureFn(config: ClientConfig, fn: Function) {
|
||||
return (...args: unknown[]) => fn(config, ...args);
|
||||
}
|
||||
|
||||
const clientFns: Record<string, Function> = {
|
||||
resourceExists,
|
||||
getResourceUploadURLs,
|
||||
getDownloadURL,
|
||||
uploadResource,
|
||||
matchPath,
|
||||
};
|
||||
|
||||
export type Client = {
|
||||
resourceExists: (pointer: PointerFile) => Promise<boolean | undefined>;
|
||||
getResourceUploadURLs: (objects: PointerFile[]) => Promise<string>;
|
||||
getDownloadURL: (pointer: PointerFile) => Promise<{ url: string; blob: Blob }>;
|
||||
uploadResource: (pointer: PointerFile, blob: Blob) => Promise<string>;
|
||||
matchPath: (path: string) => boolean;
|
||||
patterns: string[];
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export function getClient(clientConfig: ClientConfig) {
|
||||
return flow([
|
||||
Object.keys,
|
||||
map((key: string) => [key, configureFn(clientConfig, clientFns[key])]),
|
||||
fromPairs,
|
||||
configuredFns => ({
|
||||
...configuredFns,
|
||||
patterns: clientConfig.patterns,
|
||||
enabled: clientConfig.enabled,
|
||||
}),
|
||||
])(clientFns);
|
||||
}
|
||||
4
source/admin/packages/decap-cms-backend-git-gateway/src/types/ini.d.ts
vendored
Normal file
4
source/admin/packages/decap-cms-backend-git-gateway/src/types/ini.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module 'ini' {
|
||||
const ini: { decode: <T>(ini: string) => T };
|
||||
export default ini;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
const { getConfig } = require('../../scripts/webpack.js');
|
||||
|
||||
module.exports = getConfig();
|
||||
64
source/admin/packages/decap-cms-backend-gitea/CHANGELOG.md
Normal file
64
source/admin/packages/decap-cms-backend-gitea/CHANGELOG.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# 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-gitea@3.1.5...decap-cms-backend-gitea@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.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitea@3.1.5...decap-cms-backend-gitea@3.2.0) (2025-06-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitea
|
||||
|
||||
## [3.1.5](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitea@3.1.4...decap-cms-backend-gitea@3.1.5) (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.4](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitea@3.1.3...decap-cms-backend-gitea@3.1.4) (2024-08-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitea
|
||||
|
||||
## [3.1.3](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitea@3.1.2...decap-cms-backend-gitea@3.1.3) (2024-03-21)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitea
|
||||
|
||||
## [3.1.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitea@3.1.1...decap-cms-backend-gitea@3.1.2) (2024-03-08)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitea
|
||||
|
||||
## [3.1.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitea@3.1.0-beta.1...decap-cms-backend-gitea@3.1.1) (2024-02-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- gitlab pkce auth error ([#7110](https://github.com/decaporg/decap-cms/issues/7110)) ([bcd58d6](https://github.com/decaporg/decap-cms/commit/bcd58d6e117b4654b3e0dca173f7f8aaca8dabdf))
|
||||
|
||||
# [3.1.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitea@3.1.0-beta.1...decap-cms-backend-gitea@3.1.0) (2024-02-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitea
|
||||
|
||||
# [3.1.0-beta.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitea@3.1.0-beta.0...decap-cms-backend-gitea@3.1.0-beta.1) (2024-01-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitea
|
||||
|
||||
# [3.1.0-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitea@3.1.0...decap-cms-backend-gitea@3.1.0-beta.0) (2023-10-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([b89fc89](https://github.com/decaporg/decap-cms/commit/b89fc894dfbb5f4136b2e5427fd25a29378a58c6))
|
||||
|
||||
## 3.0.4 (2023-10-20)
|
||||
|
||||
### Features
|
||||
|
||||
- **backend:** add gitea backend ([#6808](https://github.com/decaporg/decap-cms/issues/6808)) ([0d89a58](https://github.com/decaporg/decap-cms/commit/0d89a58e93f64f868ff3e4e8f0945ccf166ad738))
|
||||
|
||||
## 3.0.3 (2023-10-20)
|
||||
|
||||
### Features
|
||||
|
||||
- **backend:** add gitea backend ([#6808](https://github.com/decaporg/decap-cms/issues/6808)) ([0d89a58](https://github.com/decaporg/decap-cms/commit/0d89a58e93f64f868ff3e4e8f0945ccf166ad738))
|
||||
36
source/admin/packages/decap-cms-backend-gitea/package.json
Normal file
36
source/admin/packages/decap-cms-backend-gitea/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "decap-cms-backend-gitea",
|
||||
"description": "Gitea backend for Decap CMS",
|
||||
"version": "3.3.0",
|
||||
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitea",
|
||||
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
||||
"license": "MIT",
|
||||
"module": "dist/esm/index.js",
|
||||
"main": "dist/decap-cms-backend-gitea.js",
|
||||
"keywords": [
|
||||
"decap-cms",
|
||||
"backend",
|
||||
"gitea"
|
||||
],
|
||||
"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": {
|
||||
"js-base64": "^3.0.0",
|
||||
"semaphore": "^1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"decap-cms-lib-auth": "^3.0.0",
|
||||
"decap-cms-lib-util": "^3.0.0",
|
||||
"decap-cms-ui-default": "^3.0.0",
|
||||
"immutable": "^3.7.6",
|
||||
"lodash": "^4.17.11",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
}
|
||||
468
source/admin/packages/decap-cms-backend-gitea/src/API.ts
Normal file
468
source/admin/packages/decap-cms-backend-gitea/src/API.ts
Normal file
@@ -0,0 +1,468 @@
|
||||
import { Base64 } from 'js-base64';
|
||||
import trimStart from 'lodash/trimStart';
|
||||
import trim from 'lodash/trim';
|
||||
import result from 'lodash/result';
|
||||
import partial from 'lodash/partial';
|
||||
import last from 'lodash/last';
|
||||
import initial from 'lodash/initial';
|
||||
import {
|
||||
APIError,
|
||||
basename,
|
||||
generateContentKey,
|
||||
getAllResponses,
|
||||
localForage,
|
||||
parseContentKey,
|
||||
readFileMetadata,
|
||||
requestWithBackoff,
|
||||
unsentRequest,
|
||||
} from 'decap-cms-lib-util';
|
||||
|
||||
import type {
|
||||
DataFile,
|
||||
PersistOptions,
|
||||
AssetProxy,
|
||||
ApiRequest,
|
||||
FetchError,
|
||||
} from 'decap-cms-lib-util';
|
||||
import type { Semaphore } from 'semaphore';
|
||||
import type {
|
||||
FilesResponse,
|
||||
GitGetBlobResponse,
|
||||
GitGetTreeResponse,
|
||||
GiteaUser,
|
||||
GiteaRepository,
|
||||
ReposListCommitsResponse,
|
||||
} from './types';
|
||||
|
||||
export const API_NAME = 'Gitea';
|
||||
|
||||
export interface Config {
|
||||
apiRoot?: string;
|
||||
token?: string;
|
||||
branch?: string;
|
||||
repo?: string;
|
||||
originRepo?: string;
|
||||
}
|
||||
|
||||
enum FileOperation {
|
||||
CREATE = 'create',
|
||||
DELETE = 'delete',
|
||||
UPDATE = 'update',
|
||||
}
|
||||
|
||||
export interface ChangeFileOperation {
|
||||
content?: string;
|
||||
from_path?: string;
|
||||
path: string;
|
||||
operation: FileOperation;
|
||||
sha?: string;
|
||||
}
|
||||
|
||||
interface MetaDataObjects {
|
||||
entry: { path: string; sha: string };
|
||||
files: MediaFile[];
|
||||
}
|
||||
|
||||
export interface Metadata {
|
||||
type: string;
|
||||
objects: MetaDataObjects;
|
||||
branch: string;
|
||||
status: string;
|
||||
collection: string;
|
||||
commitMessage: string;
|
||||
version?: string;
|
||||
user: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
timeStamp: string;
|
||||
}
|
||||
|
||||
export interface BlobArgs {
|
||||
sha: string;
|
||||
repoURL: string;
|
||||
parseText: boolean;
|
||||
}
|
||||
|
||||
type Param = string | number | undefined;
|
||||
|
||||
export type Options = RequestInit & {
|
||||
params?: Record<string, Param | Record<string, Param> | string[]>;
|
||||
};
|
||||
|
||||
type MediaFile = {
|
||||
sha: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export default class API {
|
||||
apiRoot: string;
|
||||
token: string;
|
||||
branch: string;
|
||||
repo: string;
|
||||
originRepo: string;
|
||||
repoOwner: string;
|
||||
repoName: string;
|
||||
originRepoOwner: string;
|
||||
originRepoName: string;
|
||||
repoURL: string;
|
||||
originRepoURL: string;
|
||||
|
||||
_userPromise?: Promise<GiteaUser>;
|
||||
_metadataSemaphore?: Semaphore;
|
||||
|
||||
commitAuthor?: {};
|
||||
|
||||
constructor(config: Config) {
|
||||
this.apiRoot = config.apiRoot || 'https://try.gitea.io/api/v1';
|
||||
this.token = config.token || '';
|
||||
this.branch = config.branch || 'master';
|
||||
this.repo = config.repo || '';
|
||||
this.originRepo = config.originRepo || this.repo;
|
||||
this.repoURL = `/repos/${this.repo}`;
|
||||
this.originRepoURL = `/repos/${this.originRepo}`;
|
||||
|
||||
const [repoParts, originRepoParts] = [this.repo.split('/'), this.originRepo.split('/')];
|
||||
this.repoOwner = repoParts[0];
|
||||
this.repoName = repoParts[1];
|
||||
|
||||
this.originRepoOwner = originRepoParts[0];
|
||||
this.originRepoName = originRepoParts[1];
|
||||
}
|
||||
|
||||
static DEFAULT_COMMIT_MESSAGE = 'Automatically generated by Static CMS';
|
||||
|
||||
user(): Promise<{ full_name: string; login: string; avatar_url: string }> {
|
||||
if (!this._userPromise) {
|
||||
this._userPromise = this.getUser();
|
||||
}
|
||||
return this._userPromise;
|
||||
}
|
||||
|
||||
getUser() {
|
||||
return this.request('/user') as Promise<GiteaUser>;
|
||||
}
|
||||
|
||||
async hasWriteAccess() {
|
||||
try {
|
||||
const result: GiteaRepository = await this.request(this.repoURL);
|
||||
// update config repoOwner to avoid case sensitivity issues with Gitea
|
||||
this.repoOwner = result.owner.login;
|
||||
return result.permissions.push;
|
||||
} catch (error) {
|
||||
console.error('Problem fetching repo data from Gitea');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
// no op
|
||||
}
|
||||
|
||||
requestHeaders(headers = {}) {
|
||||
const baseHeader: Record<string, string> = {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
...headers,
|
||||
};
|
||||
|
||||
if (this.token) {
|
||||
baseHeader.Authorization = `token ${this.token}`;
|
||||
return Promise.resolve(baseHeader);
|
||||
}
|
||||
|
||||
return Promise.resolve(baseHeader);
|
||||
}
|
||||
|
||||
async parseJsonResponse(response: Response) {
|
||||
const json = await response.json();
|
||||
if (!response.ok) {
|
||||
return Promise.reject(json);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
urlFor(path: string, options: Options) {
|
||||
const params = [];
|
||||
if (options.params) {
|
||||
for (const key in options.params) {
|
||||
params.push(`${key}=${encodeURIComponent(options.params[key] as string)}`);
|
||||
}
|
||||
}
|
||||
if (params.length) {
|
||||
path += `?${params.join('&')}`;
|
||||
}
|
||||
return this.apiRoot + path;
|
||||
}
|
||||
|
||||
parseResponse(response: Response) {
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
if (contentType && contentType.match(/json/)) {
|
||||
return this.parseJsonResponse(response);
|
||||
}
|
||||
const textPromise = response.text().then(text => {
|
||||
if (!response.ok) {
|
||||
return Promise.reject(text);
|
||||
}
|
||||
return text;
|
||||
});
|
||||
return textPromise;
|
||||
}
|
||||
|
||||
handleRequestError(error: FetchError, responseStatus: number) {
|
||||
throw new APIError(error.message, responseStatus, API_NAME);
|
||||
}
|
||||
|
||||
buildRequest(req: ApiRequest) {
|
||||
return req;
|
||||
}
|
||||
|
||||
async request(
|
||||
path: string,
|
||||
options: Options = {},
|
||||
parser = (response: Response) => this.parseResponse(response),
|
||||
) {
|
||||
options = { cache: 'no-cache', ...options };
|
||||
const headers = await this.requestHeaders(options.headers || {});
|
||||
const url = this.urlFor(path, options);
|
||||
let responseStatus = 500;
|
||||
|
||||
try {
|
||||
const req = unsentRequest.fromFetchArguments(url, {
|
||||
...options,
|
||||
headers,
|
||||
}) as unknown as ApiRequest;
|
||||
const response = await requestWithBackoff(this, req);
|
||||
responseStatus = response.status;
|
||||
const parsedResponse = await parser(response);
|
||||
return parsedResponse;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
return this.handleRequestError(error, responseStatus);
|
||||
}
|
||||
}
|
||||
|
||||
nextUrlProcessor() {
|
||||
return (url: string) => url;
|
||||
}
|
||||
|
||||
async requestAllPages<T>(url: string, options: Options = {}) {
|
||||
options = { cache: 'no-cache', ...options };
|
||||
const headers = await this.requestHeaders(options.headers || {});
|
||||
const processedURL = this.urlFor(url, options);
|
||||
const allResponses = await getAllResponses(
|
||||
processedURL,
|
||||
{ ...options, headers },
|
||||
'next',
|
||||
this.nextUrlProcessor(),
|
||||
);
|
||||
const pages: T[][] = await Promise.all(
|
||||
allResponses.map((res: Response) => this.parseResponse(res)),
|
||||
);
|
||||
return ([] as T[]).concat(...pages);
|
||||
}
|
||||
|
||||
generateContentKey(collectionName: string, slug: string) {
|
||||
return generateContentKey(collectionName, slug);
|
||||
}
|
||||
|
||||
parseContentKey(contentKey: string) {
|
||||
return parseContentKey(contentKey);
|
||||
}
|
||||
|
||||
async readFile(
|
||||
path: string,
|
||||
sha?: string | null,
|
||||
{
|
||||
branch = this.branch,
|
||||
repoURL = this.repoURL,
|
||||
parseText = true,
|
||||
}: {
|
||||
branch?: string;
|
||||
repoURL?: string;
|
||||
parseText?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
if (!sha) {
|
||||
sha = await this.getFileSha(path, { repoURL, branch });
|
||||
}
|
||||
const content = await this.fetchBlobContent({ sha: sha as string, repoURL, parseText });
|
||||
return content;
|
||||
}
|
||||
|
||||
async readFileMetadata(path: string, sha: string | null | undefined) {
|
||||
const fetchFileMetadata = async () => {
|
||||
try {
|
||||
const result: ReposListCommitsResponse = await this.request(
|
||||
`${this.originRepoURL}/commits`,
|
||||
{
|
||||
params: { path, sha: this.branch, stat: 'false' },
|
||||
},
|
||||
);
|
||||
const { commit } = result[0];
|
||||
return {
|
||||
author: commit.author.name || commit.author.email,
|
||||
updatedOn: commit.author.date,
|
||||
};
|
||||
} catch (e) {
|
||||
return { author: '', updatedOn: '' };
|
||||
}
|
||||
};
|
||||
const fileMetadata = await readFileMetadata(sha, fetchFileMetadata, localForage);
|
||||
return fileMetadata;
|
||||
}
|
||||
|
||||
async fetchBlobContent({ sha, repoURL, parseText }: BlobArgs) {
|
||||
const result: GitGetBlobResponse = await this.request(`${repoURL}/git/blobs/${sha}`, {
|
||||
cache: 'force-cache',
|
||||
});
|
||||
|
||||
if (parseText) {
|
||||
// treat content as a utf-8 string
|
||||
const content = Base64.decode(result.content);
|
||||
return content;
|
||||
} else {
|
||||
// treat content as binary and convert to blob
|
||||
const content = Base64.atob(result.content);
|
||||
const byteArray = new Uint8Array(content.length);
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
byteArray[i] = content.charCodeAt(i);
|
||||
}
|
||||
const blob = new Blob([byteArray]);
|
||||
return blob;
|
||||
}
|
||||
}
|
||||
|
||||
async listFiles(
|
||||
path: string,
|
||||
{ repoURL = this.repoURL, branch = this.branch, depth = 1 } = {},
|
||||
folderSupport?: boolean,
|
||||
): Promise<{ type: string; id: string; name: string; path: string; size: number }[]> {
|
||||
const folder = trim(path, '/');
|
||||
try {
|
||||
const result: GitGetTreeResponse = await this.request(
|
||||
`${repoURL}/git/trees/${branch}:${encodeURIComponent(folder)}`,
|
||||
{
|
||||
// Gitea API supports recursive=1 for getting the entire recursive tree
|
||||
// or omitting it to get the non-recursive tree
|
||||
params: depth > 1 ? { recursive: 1 } : {},
|
||||
},
|
||||
);
|
||||
return (
|
||||
result.tree
|
||||
// filter only files and/or folders up to the required depth
|
||||
.filter(
|
||||
file =>
|
||||
(!folderSupport ? file.type === 'blob' : true) &&
|
||||
decodeURIComponent(file.path).split('/').length <= depth,
|
||||
)
|
||||
.map(file => ({
|
||||
type: file.type,
|
||||
id: file.sha,
|
||||
name: basename(file.path),
|
||||
path: `${folder}/${file.path}`,
|
||||
size: file.size!,
|
||||
}))
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
if (err && err.status === 404) {
|
||||
console.info('[StaticCMS] This 404 was expected and handled appropriately.');
|
||||
return [];
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async persistFiles(dataFiles: DataFile[], mediaFiles: AssetProxy[], options: PersistOptions) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const files: (DataFile | AssetProxy)[] = mediaFiles.concat(dataFiles as any);
|
||||
const operations = await this.getChangeFileOperations(files, this.branch);
|
||||
return this.changeFiles(operations, options);
|
||||
}
|
||||
|
||||
async changeFiles(operations: ChangeFileOperation[], options: PersistOptions) {
|
||||
return (await this.request(`${this.repoURL}/contents`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
branch: this.branch,
|
||||
files: operations,
|
||||
message: options.commitMessage,
|
||||
}),
|
||||
})) as FilesResponse;
|
||||
}
|
||||
|
||||
async getChangeFileOperations(files: { path: string; newPath?: string }[], branch: string) {
|
||||
const items: ChangeFileOperation[] = await Promise.all(
|
||||
files.map(async file => {
|
||||
const content = await result(
|
||||
file,
|
||||
'toBase64',
|
||||
partial(this.toBase64, (file as DataFile).raw),
|
||||
);
|
||||
let sha;
|
||||
let operation;
|
||||
let from_path;
|
||||
let path = trimStart(file.path, '/');
|
||||
try {
|
||||
sha = await this.getFileSha(file.path, { branch });
|
||||
operation = FileOperation.UPDATE;
|
||||
from_path = file.newPath && path;
|
||||
path = file.newPath ? trimStart(file.newPath, '/') : path;
|
||||
} catch {
|
||||
sha = undefined;
|
||||
operation = FileOperation.CREATE;
|
||||
}
|
||||
|
||||
return {
|
||||
operation,
|
||||
content,
|
||||
path,
|
||||
from_path,
|
||||
sha,
|
||||
} as ChangeFileOperation;
|
||||
}),
|
||||
);
|
||||
return items;
|
||||
}
|
||||
|
||||
async getFileSha(path: string, { repoURL = this.repoURL, branch = this.branch } = {}) {
|
||||
/**
|
||||
* We need to request the tree first to get the SHA. We use extended SHA-1
|
||||
* syntax (<rev>:<path>) to get a blob from a tree without having to recurse
|
||||
* through the tree.
|
||||
*/
|
||||
|
||||
const pathArray = path.split('/');
|
||||
const filename = last(pathArray);
|
||||
const directory = initial(pathArray).join('/');
|
||||
const fileDataPath = encodeURIComponent(directory);
|
||||
const fileDataURL = `${repoURL}/git/trees/${branch}:${fileDataPath}`;
|
||||
|
||||
const result: GitGetTreeResponse = await this.request(fileDataURL);
|
||||
const file = result.tree.find(file => file.path === filename);
|
||||
if (file) {
|
||||
return file.sha;
|
||||
} else {
|
||||
throw new APIError('Not Found', 404, API_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFiles(paths: string[], message: string) {
|
||||
const operations: ChangeFileOperation[] = await Promise.all(
|
||||
paths.map(async path => {
|
||||
const sha = await this.getFileSha(path);
|
||||
|
||||
return {
|
||||
operation: FileOperation.DELETE,
|
||||
path,
|
||||
sha,
|
||||
} as ChangeFileOperation;
|
||||
}),
|
||||
);
|
||||
this.changeFiles(operations, { commitMessage: message });
|
||||
}
|
||||
|
||||
toBase64(str: string) {
|
||||
return Promise.resolve(Base64.encode(str));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from '@emotion/styled';
|
||||
import { PkceAuthenticator } from 'decap-cms-lib-auth';
|
||||
import { AuthenticationPage, Icon } from 'decap-cms-ui-default';
|
||||
|
||||
const LoginButtonIcon = styled(Icon)`
|
||||
margin-right: 18px;
|
||||
`;
|
||||
|
||||
export default class GiteaAuthenticationPage extends React.Component {
|
||||
static propTypes = {
|
||||
inProgress: PropTypes.bool,
|
||||
config: PropTypes.object.isRequired,
|
||||
onLogin: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
componentDidMount() {
|
||||
// Manually validate PropTypes - React 19 breaking change
|
||||
PropTypes.checkPropTypes(
|
||||
GiteaAuthenticationPage.propTypes,
|
||||
this.props,
|
||||
'prop',
|
||||
'GiteaAuthenticationPage',
|
||||
);
|
||||
|
||||
const { base_url = 'https://try.gitea.io', app_id = '' } = this.props.config.backend;
|
||||
this.auth = new PkceAuthenticator({
|
||||
base_url,
|
||||
auth_endpoint: 'login/oauth/authorize',
|
||||
app_id,
|
||||
auth_token_endpoint: 'login/oauth/access_token',
|
||||
auth_token_endpoint_content_type: 'application/json; charset=utf-8',
|
||||
});
|
||||
// Complete authentication if we were redirected back to from the provider.
|
||||
this.auth.completeAuth((err, data) => {
|
||||
if (err) {
|
||||
this.setState({ loginError: err.toString() });
|
||||
return;
|
||||
} else if (data) {
|
||||
this.props.onLogin(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleLogin = e => {
|
||||
e.preventDefault();
|
||||
this.auth.authenticate({ scope: 'repository' }, (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="gitea" />{' '}
|
||||
{inProgress ? t('auth.loggingIn') : t('auth.loginWithGitea')}
|
||||
</React.Fragment>
|
||||
)}
|
||||
t={t}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
import API from '../API';
|
||||
|
||||
global.fetch = jest.fn().mockRejectedValue(new Error('should not call fetch inside tests'));
|
||||
|
||||
describe('gitea API', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function mockAPI(api, responses) {
|
||||
api.request = jest.fn().mockImplementation((path, options = {}) => {
|
||||
const normalizedPath = path.indexOf('?') !== -1 ? path.slice(0, path.indexOf('?')) : path;
|
||||
const response = responses[normalizedPath];
|
||||
return typeof response === 'function'
|
||||
? Promise.resolve(response(options))
|
||||
: Promise.reject(new Error(`No response for path '${normalizedPath}'`));
|
||||
});
|
||||
}
|
||||
|
||||
describe('request', () => {
|
||||
const fetch = jest.fn();
|
||||
beforeEach(() => {
|
||||
global.fetch = fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should fetch url with authorization header', async () => {
|
||||
const api = new API({ branch: 'gh-pages', repo: 'my-repo', token: 'token' });
|
||||
|
||||
fetch.mockResolvedValue({
|
||||
text: jest.fn().mockResolvedValue('some response'),
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { get: () => '' },
|
||||
});
|
||||
const result = await api.request('/some-path');
|
||||
expect(result).toEqual('some response');
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(fetch).toHaveBeenCalledWith('https://try.gitea.io/api/v1/some-path', {
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
Authorization: 'token token',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error on not ok response', async () => {
|
||||
const api = new API({ branch: 'gt-pages', repo: 'my-repo', token: 'token' });
|
||||
|
||||
fetch.mockResolvedValue({
|
||||
text: jest.fn().mockResolvedValue({ message: 'some error' }),
|
||||
ok: false,
|
||||
status: 404,
|
||||
headers: { get: () => '' },
|
||||
});
|
||||
|
||||
await expect(api.request('some-path')).rejects.toThrow(
|
||||
expect.objectContaining({
|
||||
message: 'some error',
|
||||
name: 'API_ERROR',
|
||||
status: 404,
|
||||
api: 'Gitea',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow overriding requestHeaders to return a promise ', async () => {
|
||||
const api = new API({ branch: 'gt-pages', repo: 'my-repo', token: 'token' });
|
||||
|
||||
api.requestHeaders = jest.fn().mockResolvedValue({
|
||||
Authorization: 'promise-token',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
});
|
||||
|
||||
fetch.mockResolvedValue({
|
||||
text: jest.fn().mockResolvedValue('some response'),
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { get: () => '' },
|
||||
});
|
||||
const result = await api.request('/some-path');
|
||||
expect(result).toEqual('some response');
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(fetch).toHaveBeenCalledWith('https://try.gitea.io/api/v1/some-path', {
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
Authorization: 'promise-token',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('persistFiles', () => {
|
||||
it('should create a new commit', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const responses = {
|
||||
'/repos/owner/repo/git/trees/master:content%2Fposts': () => {
|
||||
return { tree: [{ path: 'update-post.md', sha: 'old-sha' }] };
|
||||
},
|
||||
|
||||
'/repos/owner/repo/contents': () => ({
|
||||
commit: { sha: 'new-sha' },
|
||||
files: [
|
||||
{
|
||||
path: 'content/posts/new-post.md',
|
||||
},
|
||||
{
|
||||
path: 'content/posts/update-post.md',
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
mockAPI(api, responses);
|
||||
|
||||
const entry = {
|
||||
dataFiles: [
|
||||
{
|
||||
slug: 'entry',
|
||||
path: 'content/posts/new-post.md',
|
||||
raw: 'content',
|
||||
},
|
||||
{
|
||||
slug: 'entry',
|
||||
sha: 'old-sha',
|
||||
path: 'content/posts/update-post.md',
|
||||
raw: 'content',
|
||||
},
|
||||
],
|
||||
assets: [],
|
||||
};
|
||||
await expect(
|
||||
api.persistFiles(entry.dataFiles, entry.assets, {
|
||||
commitMessage: 'commitMessage',
|
||||
newEntry: true,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
commit: { sha: 'new-sha' },
|
||||
files: [
|
||||
{
|
||||
path: 'content/posts/new-post.md',
|
||||
},
|
||||
{
|
||||
path: 'content/posts/update-post.md',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(api.request).toHaveBeenCalledTimes(3);
|
||||
|
||||
expect(api.request.mock.calls[0]).toEqual([
|
||||
'/repos/owner/repo/git/trees/master:content%2Fposts',
|
||||
]);
|
||||
|
||||
expect(api.request.mock.calls[1]).toEqual([
|
||||
'/repos/owner/repo/git/trees/master:content%2Fposts',
|
||||
]);
|
||||
|
||||
expect(api.request.mock.calls[2]).toEqual([
|
||||
'/repos/owner/repo/contents',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
branch: 'master',
|
||||
files: [
|
||||
{
|
||||
operation: 'create',
|
||||
content: Base64.encode(entry.dataFiles[0].raw),
|
||||
path: entry.dataFiles[0].path,
|
||||
},
|
||||
{
|
||||
operation: 'update',
|
||||
content: Base64.encode(entry.dataFiles[1].raw),
|
||||
path: entry.dataFiles[1].path,
|
||||
sha: entry.dataFiles[1].sha,
|
||||
},
|
||||
],
|
||||
message: 'commitMessage',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteFiles', () => {
|
||||
it('should check if files exist and delete them', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const responses = {
|
||||
'/repos/owner/repo/git/trees/master:content%2Fposts': () => {
|
||||
return {
|
||||
tree: [
|
||||
{ path: 'delete-post-1.md', sha: 'old-sha-1' },
|
||||
{ path: 'delete-post-2.md', sha: 'old-sha-2' },
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
'/repos/owner/repo/contents': () => ({
|
||||
commit: { sha: 'new-sha' },
|
||||
files: [
|
||||
{
|
||||
path: 'content/posts/delete-post-1.md',
|
||||
},
|
||||
{
|
||||
path: 'content/posts/delete-post-2.md',
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
mockAPI(api, responses);
|
||||
|
||||
const deleteFiles = ['content/posts/delete-post-1.md', 'content/posts/delete-post-2.md'];
|
||||
|
||||
await api.deleteFiles(deleteFiles, 'commitMessage');
|
||||
|
||||
expect(api.request).toHaveBeenCalledTimes(3);
|
||||
|
||||
expect(api.request.mock.calls[0]).toEqual([
|
||||
'/repos/owner/repo/git/trees/master:content%2Fposts',
|
||||
]);
|
||||
|
||||
expect(api.request.mock.calls[1]).toEqual([
|
||||
'/repos/owner/repo/git/trees/master:content%2Fposts',
|
||||
]);
|
||||
|
||||
expect(api.request.mock.calls[2]).toEqual([
|
||||
'/repos/owner/repo/contents',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
branch: 'master',
|
||||
files: [
|
||||
{
|
||||
operation: 'delete',
|
||||
path: deleteFiles[0],
|
||||
sha: 'old-sha-1',
|
||||
},
|
||||
{
|
||||
operation: 'delete',
|
||||
path: deleteFiles[1],
|
||||
sha: 'old-sha-2',
|
||||
},
|
||||
],
|
||||
message: 'commitMessage',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listFiles', () => {
|
||||
it('should get files by depth', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const tree = [
|
||||
{
|
||||
path: 'post.md',
|
||||
type: 'blob',
|
||||
},
|
||||
{
|
||||
path: 'dir1',
|
||||
type: 'tree',
|
||||
},
|
||||
{
|
||||
path: 'dir1/nested-post.md',
|
||||
type: 'blob',
|
||||
},
|
||||
{
|
||||
path: 'dir1/dir2',
|
||||
type: 'tree',
|
||||
},
|
||||
{
|
||||
path: 'dir1/dir2/nested-post.md',
|
||||
type: 'blob',
|
||||
},
|
||||
];
|
||||
api.request = jest.fn().mockResolvedValue({ tree });
|
||||
|
||||
await expect(api.listFiles('posts', { depth: 1 })).resolves.toEqual([
|
||||
{
|
||||
path: 'posts/post.md',
|
||||
type: 'blob',
|
||||
name: 'post.md',
|
||||
},
|
||||
]);
|
||||
expect(api.request).toHaveBeenCalledTimes(1);
|
||||
expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:posts', {
|
||||
params: {},
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
await expect(api.listFiles('posts', { depth: 2 })).resolves.toEqual([
|
||||
{
|
||||
path: 'posts/post.md',
|
||||
type: 'blob',
|
||||
name: 'post.md',
|
||||
},
|
||||
{
|
||||
path: 'posts/dir1/nested-post.md',
|
||||
type: 'blob',
|
||||
name: 'nested-post.md',
|
||||
},
|
||||
]);
|
||||
expect(api.request).toHaveBeenCalledTimes(1);
|
||||
expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:posts', {
|
||||
params: { recursive: 1 },
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
await expect(api.listFiles('posts', { depth: 3 })).resolves.toEqual([
|
||||
{
|
||||
path: 'posts/post.md',
|
||||
type: 'blob',
|
||||
name: 'post.md',
|
||||
},
|
||||
{
|
||||
path: 'posts/dir1/nested-post.md',
|
||||
type: 'blob',
|
||||
name: 'nested-post.md',
|
||||
},
|
||||
{
|
||||
path: 'posts/dir1/dir2/nested-post.md',
|
||||
type: 'blob',
|
||||
name: 'nested-post.md',
|
||||
},
|
||||
]);
|
||||
expect(api.request).toHaveBeenCalledTimes(1);
|
||||
expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:posts', {
|
||||
params: { recursive: 1 },
|
||||
});
|
||||
});
|
||||
it('should get files and folders', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const tree = [
|
||||
{
|
||||
path: 'image.png',
|
||||
type: 'blob',
|
||||
},
|
||||
{
|
||||
path: 'dir1',
|
||||
type: 'tree',
|
||||
},
|
||||
{
|
||||
path: 'dir1/nested-image.png',
|
||||
type: 'blob',
|
||||
},
|
||||
{
|
||||
path: 'dir1/dir2',
|
||||
type: 'tree',
|
||||
},
|
||||
{
|
||||
path: 'dir1/dir2/nested-image.png',
|
||||
type: 'blob',
|
||||
},
|
||||
];
|
||||
api.request = jest.fn().mockResolvedValue({ tree });
|
||||
|
||||
await expect(api.listFiles('media', {}, true)).resolves.toEqual([
|
||||
{
|
||||
path: 'media/image.png',
|
||||
type: 'blob',
|
||||
name: 'image.png',
|
||||
},
|
||||
{
|
||||
path: 'media/dir1',
|
||||
type: 'tree',
|
||||
name: 'dir1',
|
||||
},
|
||||
]);
|
||||
expect(api.request).toHaveBeenCalledTimes(1);
|
||||
expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:media', {
|
||||
params: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,284 @@
|
||||
import { Cursor, CURSOR_COMPATIBILITY_SYMBOL } from 'decap-cms-lib-util';
|
||||
|
||||
import GiteaImplementation from '../implementation';
|
||||
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
describe('gitea backend implementation', () => {
|
||||
const config = {
|
||||
backend: {
|
||||
repo: 'owner/repo',
|
||||
api_root: 'https://try.gitea.io/api/v1',
|
||||
},
|
||||
};
|
||||
|
||||
const createObjectURL = jest.fn();
|
||||
global.URL = {
|
||||
createObjectURL,
|
||||
};
|
||||
|
||||
createObjectURL.mockReturnValue('displayURL');
|
||||
|
||||
beforeAll(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('persistMedia', () => {
|
||||
const persistFiles = jest.fn();
|
||||
const mockAPI = {
|
||||
persistFiles,
|
||||
};
|
||||
|
||||
persistFiles.mockImplementation((_, files) => {
|
||||
files.forEach((file, index) => {
|
||||
file.sha = index;
|
||||
});
|
||||
});
|
||||
|
||||
it('should persist media file', async () => {
|
||||
const giteaImplementation = new GiteaImplementation(config);
|
||||
giteaImplementation.api = mockAPI;
|
||||
|
||||
const mediaFile = {
|
||||
fileObj: { size: 100, name: 'image.png' },
|
||||
path: '/media/image.png',
|
||||
};
|
||||
|
||||
expect.assertions(5);
|
||||
await expect(
|
||||
giteaImplementation.persistMedia(mediaFile, { commitMessage: 'Persisting media' }),
|
||||
).resolves.toEqual({
|
||||
id: 0,
|
||||
name: 'image.png',
|
||||
size: 100,
|
||||
displayURL: 'displayURL',
|
||||
path: 'media/image.png',
|
||||
});
|
||||
|
||||
expect(persistFiles).toHaveBeenCalledTimes(1);
|
||||
expect(persistFiles).toHaveBeenCalledWith([], [mediaFile], {
|
||||
commitMessage: 'Persisting media',
|
||||
});
|
||||
expect(createObjectURL).toHaveBeenCalledTimes(1);
|
||||
expect(createObjectURL).toHaveBeenCalledWith(mediaFile.fileObj);
|
||||
});
|
||||
|
||||
it('should log and throw error on "persistFiles" error', async () => {
|
||||
const giteaImplementation = new GiteaImplementation(config);
|
||||
giteaImplementation.api = mockAPI;
|
||||
|
||||
const error = new Error('failed to persist files');
|
||||
persistFiles.mockRejectedValue(error);
|
||||
|
||||
const mediaFile = {
|
||||
fileObj: { size: 100 },
|
||||
path: '/media/image.png',
|
||||
};
|
||||
|
||||
expect.assertions(5);
|
||||
await expect(
|
||||
giteaImplementation.persistMedia(mediaFile, { commitMessage: 'Persisting media' }),
|
||||
).rejects.toThrowError(error);
|
||||
|
||||
expect(persistFiles).toHaveBeenCalledTimes(1);
|
||||
expect(createObjectURL).toHaveBeenCalledTimes(0);
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
expect(console.error).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('entriesByFolder', () => {
|
||||
const listFiles = jest.fn();
|
||||
const readFile = jest.fn();
|
||||
const readFileMetadata = jest.fn(() => Promise.resolve({ author: '', updatedOn: '' }));
|
||||
|
||||
const mockAPI = {
|
||||
listFiles,
|
||||
readFile,
|
||||
readFileMetadata,
|
||||
originRepoURL: 'originRepoURL',
|
||||
};
|
||||
|
||||
it('should return entries and cursor', async () => {
|
||||
const giteaImplementation = new GiteaImplementation(config);
|
||||
giteaImplementation.api = mockAPI;
|
||||
|
||||
const files = [];
|
||||
const count = 1501;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const id = `${i}`.padStart(`${count}`.length, '0');
|
||||
files.push({
|
||||
id,
|
||||
path: `posts/post-${id}.md`,
|
||||
});
|
||||
}
|
||||
|
||||
listFiles.mockResolvedValue(files);
|
||||
readFile.mockImplementation((_path, id) => Promise.resolve(`${id}`));
|
||||
|
||||
const expectedEntries = files
|
||||
.slice(0, 20)
|
||||
.map(({ id, path }) => ({ data: id, file: { path, id, author: '', updatedOn: '' } }));
|
||||
|
||||
const expectedCursor = Cursor.create({
|
||||
actions: ['next', 'last'],
|
||||
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expectedEntries[CURSOR_COMPATIBILITY_SYMBOL] = expectedCursor;
|
||||
|
||||
const result = await giteaImplementation.entriesByFolder('posts', 'md', 1);
|
||||
|
||||
expect(result).toEqual(expectedEntries);
|
||||
expect(listFiles).toHaveBeenCalledTimes(1);
|
||||
expect(listFiles).toHaveBeenCalledWith('posts', { depth: 1, repoURL: 'originRepoURL' });
|
||||
expect(readFile).toHaveBeenCalledTimes(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('traverseCursor', () => {
|
||||
const listFiles = jest.fn();
|
||||
const readFile = jest.fn((_path, id) => Promise.resolve(`${id}`));
|
||||
const readFileMetadata = jest.fn(() => Promise.resolve({}));
|
||||
|
||||
const mockAPI = {
|
||||
listFiles,
|
||||
readFile,
|
||||
originRepoURL: 'originRepoURL',
|
||||
readFileMetadata,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const files = [];
|
||||
const count = 1501;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const id = `${i}`.padStart(`${count}`.length, '0');
|
||||
files.push({
|
||||
id,
|
||||
path: `posts/post-${id}.md`,
|
||||
});
|
||||
}
|
||||
|
||||
it('should handle next action', async () => {
|
||||
const giteaImplementation = new GiteaImplementation(config);
|
||||
giteaImplementation.api = mockAPI;
|
||||
|
||||
const cursor = Cursor.create({
|
||||
actions: ['next', 'last'],
|
||||
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const expectedEntries = files
|
||||
.slice(20, 40)
|
||||
.map(({ id, path }) => ({ data: id, file: { path, id } }));
|
||||
|
||||
const expectedCursor = Cursor.create({
|
||||
actions: ['prev', 'first', 'next', 'last'],
|
||||
meta: { page: 2, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const result = await giteaImplementation.traverseCursor(cursor, 'next');
|
||||
|
||||
expect(result).toEqual({
|
||||
entries: expectedEntries,
|
||||
cursor: expectedCursor,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle prev action', async () => {
|
||||
const giteaImplementation = new GiteaImplementation(config);
|
||||
giteaImplementation.api = mockAPI;
|
||||
|
||||
const cursor = Cursor.create({
|
||||
actions: ['prev', 'first', 'next', 'last'],
|
||||
meta: { page: 2, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const expectedEntries = files
|
||||
.slice(0, 20)
|
||||
.map(({ id, path }) => ({ data: id, file: { path, id } }));
|
||||
|
||||
const expectedCursor = Cursor.create({
|
||||
actions: ['next', 'last'],
|
||||
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const result = await giteaImplementation.traverseCursor(cursor, 'prev');
|
||||
|
||||
expect(result).toEqual({
|
||||
entries: expectedEntries,
|
||||
cursor: expectedCursor,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle last action', async () => {
|
||||
const giteaImplementation = new GiteaImplementation(config);
|
||||
giteaImplementation.api = mockAPI;
|
||||
|
||||
const cursor = Cursor.create({
|
||||
actions: ['next', 'last'],
|
||||
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const expectedEntries = files
|
||||
.slice(1500)
|
||||
.map(({ id, path }) => ({ data: id, file: { path, id } }));
|
||||
|
||||
const expectedCursor = Cursor.create({
|
||||
actions: ['prev', 'first'],
|
||||
meta: { page: 76, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const result = await giteaImplementation.traverseCursor(cursor, 'last');
|
||||
|
||||
expect(result).toEqual({
|
||||
entries: expectedEntries,
|
||||
cursor: expectedCursor,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle first action', async () => {
|
||||
const giteaImplementation = new GiteaImplementation(config);
|
||||
giteaImplementation.api = mockAPI;
|
||||
|
||||
const cursor = Cursor.create({
|
||||
actions: ['prev', 'first'],
|
||||
meta: { page: 76, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const expectedEntries = files
|
||||
.slice(0, 20)
|
||||
.map(({ id, path }) => ({ data: id, file: { path, id } }));
|
||||
|
||||
const expectedCursor = Cursor.create({
|
||||
actions: ['next', 'last'],
|
||||
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const result = await giteaImplementation.traverseCursor(cursor, 'first');
|
||||
|
||||
expect(result).toEqual({
|
||||
entries: expectedEntries,
|
||||
cursor: expectedCursor,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,450 @@
|
||||
import { stripIndent } from 'common-tags';
|
||||
import trimStart from 'lodash/trimStart';
|
||||
import semaphore from 'semaphore';
|
||||
import {
|
||||
asyncLock,
|
||||
basename,
|
||||
blobToFileObj,
|
||||
Cursor,
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
entriesByFiles,
|
||||
entriesByFolder,
|
||||
filterByExtension,
|
||||
getBlobSHA,
|
||||
getMediaAsBlob,
|
||||
getMediaDisplayURL,
|
||||
runWithLock,
|
||||
unsentRequest,
|
||||
} from 'decap-cms-lib-util';
|
||||
|
||||
import API, { API_NAME } from './API';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
|
||||
import type {
|
||||
AssetProxy,
|
||||
AsyncLock,
|
||||
Config,
|
||||
Credentials,
|
||||
DisplayURL,
|
||||
Entry,
|
||||
Implementation,
|
||||
ImplementationFile,
|
||||
PersistOptions,
|
||||
User,
|
||||
} from 'decap-cms-lib-util';
|
||||
import type { Semaphore } from 'semaphore';
|
||||
import type { GiteaUser } from './types';
|
||||
|
||||
const MAX_CONCURRENT_DOWNLOADS = 10;
|
||||
|
||||
type ApiFile = { id: string; type: string; name: string; path: string; size: number };
|
||||
|
||||
const { fetchWithTimeout: fetch } = unsentRequest;
|
||||
|
||||
export default class Gitea implements Implementation {
|
||||
lock: AsyncLock;
|
||||
api: API | null;
|
||||
options: {
|
||||
proxied: boolean;
|
||||
API: API | null;
|
||||
useWorkflow?: boolean;
|
||||
};
|
||||
originRepo: string;
|
||||
repo?: string;
|
||||
branch: string;
|
||||
apiRoot: string;
|
||||
mediaFolder?: string;
|
||||
token: string | null;
|
||||
_currentUserPromise?: Promise<GiteaUser>;
|
||||
_userIsOriginMaintainerPromises?: {
|
||||
[key: string]: Promise<boolean>;
|
||||
};
|
||||
_mediaDisplayURLSem?: Semaphore;
|
||||
|
||||
constructor(config: Config, options = {}) {
|
||||
this.options = {
|
||||
proxied: false,
|
||||
API: null,
|
||||
useWorkflow: false,
|
||||
...options,
|
||||
};
|
||||
|
||||
if (
|
||||
!this.options.proxied &&
|
||||
(config.backend.repo === null || config.backend.repo === undefined)
|
||||
) {
|
||||
throw new Error('The Gitea backend needs a "repo" in the backend configuration.');
|
||||
}
|
||||
|
||||
if (this.options.useWorkflow) {
|
||||
throw new Error('The Gitea backend does not support editorial workflow.');
|
||||
}
|
||||
|
||||
this.api = this.options.API || null;
|
||||
this.repo = this.originRepo = config.backend.repo || '';
|
||||
this.branch = config.backend.branch?.trim() || 'master';
|
||||
this.apiRoot = config.backend.api_root || 'https://try.gitea.io/api/v1';
|
||||
this.token = '';
|
||||
this.mediaFolder = config.media_folder;
|
||||
this.lock = asyncLock();
|
||||
}
|
||||
|
||||
isGitBackend() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async status() {
|
||||
const auth =
|
||||
(await this.api
|
||||
?.user()
|
||||
.then(user => !!user)
|
||||
.catch(e => {
|
||||
console.warn('[StaticCMS] Failed getting Gitea user', e);
|
||||
return false;
|
||||
})) || false;
|
||||
|
||||
return { auth: { status: auth }, api: { status: true, statusPage: '' } };
|
||||
}
|
||||
|
||||
authComponent() {
|
||||
return AuthenticationPage;
|
||||
}
|
||||
|
||||
restoreUser(user: User) {
|
||||
return this.authenticate(user);
|
||||
}
|
||||
|
||||
async currentUser({ token }: { token: string }) {
|
||||
if (!this._currentUserPromise) {
|
||||
this._currentUserPromise = fetch(`${this.apiRoot}/user`, {
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
return this._currentUserPromise;
|
||||
}
|
||||
|
||||
async userIsOriginMaintainer({
|
||||
username: usernameArg,
|
||||
token,
|
||||
}: {
|
||||
username?: string;
|
||||
token: string;
|
||||
}) {
|
||||
const username = usernameArg || (await this.currentUser({ token })).login;
|
||||
this._userIsOriginMaintainerPromises = this._userIsOriginMaintainerPromises || {};
|
||||
if (!this._userIsOriginMaintainerPromises[username]) {
|
||||
this._userIsOriginMaintainerPromises[username] = fetch(
|
||||
`${this.apiRoot}/repos/${this.originRepo}/collaborators/${username}/permission`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
.then(res => res.json())
|
||||
.then(({ permission }) => permission === 'admin' || permission === 'write');
|
||||
}
|
||||
return this._userIsOriginMaintainerPromises[username];
|
||||
}
|
||||
|
||||
async authenticate(state: Credentials) {
|
||||
this.token = state.token as string;
|
||||
const apiCtor = API;
|
||||
this.api = new apiCtor({
|
||||
token: this.token,
|
||||
branch: this.branch,
|
||||
repo: this.repo,
|
||||
originRepo: this.originRepo,
|
||||
apiRoot: this.apiRoot,
|
||||
});
|
||||
const user = await this.api!.user();
|
||||
const isCollab = await this.api!.hasWriteAccess().catch(error => {
|
||||
error.message = stripIndent`
|
||||
Repo "${this.repo}" not found.
|
||||
|
||||
Please ensure the repo information is spelled correctly.
|
||||
|
||||
If the repo is private, make sure you're logged into a Gitea account with access.
|
||||
|
||||
If your repo is under an organization, ensure the organization has granted access to Static
|
||||
CMS.
|
||||
`;
|
||||
throw error;
|
||||
});
|
||||
|
||||
// Unauthorized user
|
||||
if (!isCollab) {
|
||||
throw new Error('Your Gitea user account does not have access to this repo.');
|
||||
}
|
||||
|
||||
// Authorized user
|
||||
return {
|
||||
name: user.full_name,
|
||||
login: user.login,
|
||||
avatar_url: user.avatar_url,
|
||||
token: state.token as string,
|
||||
};
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.token = null;
|
||||
if (this.api && this.api.reset && typeof this.api.reset === 'function') {
|
||||
return this.api.reset();
|
||||
}
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return Promise.resolve(this.token);
|
||||
}
|
||||
|
||||
getCursorAndFiles = (files: ApiFile[], page: number) => {
|
||||
const pageSize = 20;
|
||||
const count = files.length;
|
||||
const pageCount = Math.ceil(files.length / pageSize);
|
||||
|
||||
const actions = [] as string[];
|
||||
if (page > 1) {
|
||||
actions.push('prev');
|
||||
actions.push('first');
|
||||
}
|
||||
if (page < pageCount) {
|
||||
actions.push('next');
|
||||
actions.push('last');
|
||||
}
|
||||
|
||||
const cursor = Cursor.create({
|
||||
actions,
|
||||
meta: { page, count, pageSize, pageCount },
|
||||
data: { files },
|
||||
});
|
||||
const pageFiles = files.slice((page - 1) * pageSize, page * pageSize);
|
||||
return { cursor, files: pageFiles };
|
||||
};
|
||||
|
||||
async entriesByFolder(folder: string, extension: string, depth: number) {
|
||||
const repoURL = this.api!.originRepoURL;
|
||||
|
||||
let cursor: Cursor;
|
||||
|
||||
const listFiles = () =>
|
||||
this.api!.listFiles(folder, {
|
||||
repoURL,
|
||||
depth,
|
||||
}).then(files => {
|
||||
const filtered = files.filter(file => filterByExtension(file, extension));
|
||||
const result = this.getCursorAndFiles(filtered, 1);
|
||||
cursor = result.cursor;
|
||||
return result.files;
|
||||
});
|
||||
|
||||
const readFile = (path: string, id: string | null | undefined) =>
|
||||
this.api!.readFile(path, id, { repoURL }) as Promise<string>;
|
||||
|
||||
const files = await entriesByFolder(
|
||||
listFiles,
|
||||
readFile,
|
||||
this.api!.readFileMetadata.bind(this.api),
|
||||
API_NAME,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
files[CURSOR_COMPATIBILITY_SYMBOL] = cursor;
|
||||
return files;
|
||||
}
|
||||
|
||||
async allEntriesByFolder(folder: string, extension: string, depth: number) {
|
||||
const repoURL = this.api!.originRepoURL;
|
||||
|
||||
const listFiles = () =>
|
||||
this.api!.listFiles(folder, {
|
||||
repoURL,
|
||||
depth,
|
||||
}).then(files => files.filter(file => filterByExtension(file, extension)));
|
||||
|
||||
const readFile = (path: string, id: string | null | undefined) => {
|
||||
return this.api!.readFile(path, id, { repoURL }) as Promise<string>;
|
||||
};
|
||||
|
||||
const files = await entriesByFolder(
|
||||
listFiles,
|
||||
readFile,
|
||||
this.api!.readFileMetadata.bind(this.api),
|
||||
API_NAME,
|
||||
);
|
||||
return files;
|
||||
}
|
||||
|
||||
entriesByFiles(files: ImplementationFile[]) {
|
||||
const repoURL = this.api!.repoURL;
|
||||
|
||||
const readFile = (path: string, id: string | null | undefined) =>
|
||||
this.api!.readFile(path, id, { repoURL }).catch(() => '') as Promise<string>;
|
||||
|
||||
return entriesByFiles(files, readFile, this.api!.readFileMetadata.bind(this.api), API_NAME);
|
||||
}
|
||||
|
||||
// Fetches a single entry.
|
||||
getEntry(path: string) {
|
||||
const repoURL = this.api!.originRepoURL;
|
||||
return this.api!.readFile(path, null, { repoURL })
|
||||
.then(data => ({
|
||||
file: { path, id: null },
|
||||
data: data as string,
|
||||
}))
|
||||
.catch(() => ({ file: { path, id: null }, data: '' }));
|
||||
}
|
||||
|
||||
async getMedia(mediaFolder = this.mediaFolder, folderSupport?: boolean) {
|
||||
if (!mediaFolder) {
|
||||
return [];
|
||||
}
|
||||
return this.api!.listFiles(mediaFolder, undefined, folderSupport).then(files =>
|
||||
files.map(({ id, name, size, path, type }) => {
|
||||
return { id, name, size, displayURL: { id, path }, path, isDirectory: type === 'tree' };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getMediaFile(path: string) {
|
||||
const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!));
|
||||
|
||||
const name = basename(path);
|
||||
const fileObj = blobToFileObj(name, blob);
|
||||
const url = URL.createObjectURL(fileObj);
|
||||
const id = await getBlobSHA(blob);
|
||||
|
||||
return {
|
||||
id,
|
||||
displayURL: url,
|
||||
path,
|
||||
name,
|
||||
size: fileObj.size,
|
||||
file: fileObj,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
getMediaDisplayURL(displayURL: DisplayURL) {
|
||||
this._mediaDisplayURLSem = this._mediaDisplayURLSem || semaphore(MAX_CONCURRENT_DOWNLOADS);
|
||||
return getMediaDisplayURL(
|
||||
displayURL,
|
||||
this.api!.readFile.bind(this.api!),
|
||||
this._mediaDisplayURLSem,
|
||||
);
|
||||
}
|
||||
|
||||
persistEntry(entry: Entry, options: PersistOptions) {
|
||||
// persistEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.persistFiles(entry.dataFiles, entry.assets, options),
|
||||
'Failed to acquire persist entry lock',
|
||||
);
|
||||
}
|
||||
|
||||
async persistMedia(mediaFile: AssetProxy, options: PersistOptions) {
|
||||
try {
|
||||
await this.api!.persistFiles([], [mediaFile], options);
|
||||
const { sha, path, fileObj } = mediaFile as AssetProxy & { sha: string };
|
||||
const displayURL = URL.createObjectURL(fileObj as Blob);
|
||||
return {
|
||||
id: sha,
|
||||
name: fileObj!.name,
|
||||
size: fileObj!.size,
|
||||
displayURL,
|
||||
path: trimStart(path, '/'),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
deleteFiles(paths: string[], commitMessage: string) {
|
||||
return this.api!.deleteFiles(paths, commitMessage);
|
||||
}
|
||||
|
||||
async traverseCursor(cursor: Cursor, action: string) {
|
||||
const meta = cursor.meta!;
|
||||
const files = cursor.data!.get('files')!.toJS() as ApiFile[];
|
||||
|
||||
let result: { cursor: Cursor; files: ApiFile[] };
|
||||
switch (action) {
|
||||
case 'first': {
|
||||
result = this.getCursorAndFiles(files, 1);
|
||||
break;
|
||||
}
|
||||
case 'last': {
|
||||
result = this.getCursorAndFiles(files, meta.get('pageCount'));
|
||||
break;
|
||||
}
|
||||
case 'next': {
|
||||
result = this.getCursorAndFiles(files, meta.get('page') + 1);
|
||||
break;
|
||||
}
|
||||
case 'prev': {
|
||||
result = this.getCursorAndFiles(files, meta.get('page') - 1);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
result = this.getCursorAndFiles(files, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const readFile = (path: string, id: string | null | undefined) =>
|
||||
this.api!.readFile(path, id, { repoURL: this.api!.originRepoURL }).catch(
|
||||
() => '',
|
||||
) as Promise<string>;
|
||||
|
||||
const entries = await entriesByFiles(
|
||||
result.files,
|
||||
readFile,
|
||||
this.api!.readFileMetadata.bind(this.api),
|
||||
API_NAME,
|
||||
);
|
||||
|
||||
return {
|
||||
entries,
|
||||
cursor: result.cursor,
|
||||
};
|
||||
}
|
||||
|
||||
async unpublishedEntries() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return {} as any;
|
||||
}
|
||||
|
||||
async unpublishedEntry() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return {} as any;
|
||||
}
|
||||
|
||||
async unpublishedEntryDataFile() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return {} as any;
|
||||
}
|
||||
|
||||
async unpublishedEntryMediaFile() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return {} as any;
|
||||
}
|
||||
|
||||
async updateUnpublishedEntryStatus() {
|
||||
return;
|
||||
}
|
||||
|
||||
async publishUnpublishedEntry() {
|
||||
return;
|
||||
}
|
||||
async deleteUnpublishedEntry() {
|
||||
return;
|
||||
}
|
||||
|
||||
async getDeployPreview() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return {} as any;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as GiteaBackend } from './implementation';
|
||||
export { default as API } from './API';
|
||||
export { default as AuthenticationPage } from './AuthenticationPage';
|
||||
260
source/admin/packages/decap-cms-backend-gitea/src/types.ts
Normal file
260
source/admin/packages/decap-cms-backend-gitea/src/types.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
export type GiteaUser = {
|
||||
active: boolean;
|
||||
avatar_url: string;
|
||||
created: string;
|
||||
description: string;
|
||||
email: string;
|
||||
followers_count: number;
|
||||
following_count: number;
|
||||
full_name: string;
|
||||
id: number;
|
||||
is_admin: boolean;
|
||||
language: string;
|
||||
last_login: string;
|
||||
location: string;
|
||||
login: string;
|
||||
login_name?: string;
|
||||
prohibit_login: boolean;
|
||||
restricted: boolean;
|
||||
starred_repos_count: number;
|
||||
visibility: string;
|
||||
website: string;
|
||||
};
|
||||
|
||||
export type GiteaTeam = {
|
||||
can_create_org_repo: boolean;
|
||||
description: string;
|
||||
id: number;
|
||||
includes_all_repositories: boolean;
|
||||
name: string;
|
||||
organization: GiteaOrganization;
|
||||
permission: string;
|
||||
units: Array<string>;
|
||||
units_map: Map<string, string>;
|
||||
};
|
||||
|
||||
export type GiteaOrganization = {
|
||||
avatar_url: string;
|
||||
description: string;
|
||||
full_name: string;
|
||||
id: number;
|
||||
location: string;
|
||||
name: string;
|
||||
repo_admin_change_team_access: boolean;
|
||||
username: string;
|
||||
visibility: string;
|
||||
website: string;
|
||||
};
|
||||
|
||||
type CommitUser = {
|
||||
date: string;
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type CommitMeta = {
|
||||
created: string;
|
||||
sha: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type PayloadUser = {
|
||||
email: string;
|
||||
name: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
type PayloadCommitVerification = {
|
||||
payload: string;
|
||||
reason: string;
|
||||
signature: string;
|
||||
signer: PayloadUser;
|
||||
verified: boolean;
|
||||
};
|
||||
|
||||
type ReposListCommitsResponseItemCommit = {
|
||||
author: CommitUser;
|
||||
committer: CommitUser;
|
||||
message: string;
|
||||
tree: CommitMeta;
|
||||
url: string;
|
||||
verification: PayloadCommitVerification;
|
||||
};
|
||||
|
||||
type GiteaRepositoryPermissions = {
|
||||
admin: boolean;
|
||||
pull: boolean;
|
||||
push: boolean;
|
||||
};
|
||||
|
||||
type GiteaRepositoryExternalTracker = {
|
||||
external_tracker_format: string;
|
||||
external_tracker_regexp_pattern: string;
|
||||
external_tracker_style: string;
|
||||
external_tracker_url: string;
|
||||
};
|
||||
|
||||
type GiteaRepositoryExternalWiki = {
|
||||
external_wiki_url: string;
|
||||
};
|
||||
|
||||
type GiteaRepositoryInternalTracker = {
|
||||
allow_only_contributors_to_track_time: boolean;
|
||||
enable_issue_dependencies: boolean;
|
||||
enable_time_tracker: boolean;
|
||||
};
|
||||
|
||||
type GiteaRepositoryRepoTransfer = {
|
||||
description: string;
|
||||
doer: GiteaUser;
|
||||
recipient: GiteaUser;
|
||||
teams: Array<GiteaTeam>;
|
||||
enable_issue_dependencies: boolean;
|
||||
enable_time_tracker: boolean;
|
||||
};
|
||||
|
||||
export type GiteaRepository = {
|
||||
allow_merge_commits: boolean;
|
||||
allow_rebase: boolean;
|
||||
allow_rebase_explicit: boolean;
|
||||
allow_rebase_update: boolean;
|
||||
allow_squash_merge: boolean;
|
||||
archived: boolean;
|
||||
avatar_url: string;
|
||||
clone_url: string;
|
||||
created_at: string;
|
||||
default_branch: string;
|
||||
default_delete_branch_after_merge: boolean;
|
||||
default_merge_style: boolean;
|
||||
description: string;
|
||||
empty: boolean;
|
||||
external_tracker: GiteaRepositoryExternalTracker;
|
||||
external_wiki: GiteaRepositoryExternalWiki;
|
||||
fork: boolean;
|
||||
forks_count: number;
|
||||
full_name: string;
|
||||
has_issues: boolean;
|
||||
has_projects: boolean;
|
||||
has_pull_requests: boolean;
|
||||
has_wiki: boolean;
|
||||
html_url: string;
|
||||
id: number;
|
||||
ignore_whitespace_conflicts: boolean;
|
||||
internal: boolean;
|
||||
internal_tracker: GiteaRepositoryInternalTracker;
|
||||
language: string;
|
||||
languages_url: string;
|
||||
mirror: boolean;
|
||||
mirror_interval: string;
|
||||
mirror_updated: string;
|
||||
name: string;
|
||||
open_issues_count: number;
|
||||
open_pr_counter: number;
|
||||
original_url: string;
|
||||
owner: GiteaUser;
|
||||
parent: null;
|
||||
permissions: GiteaRepositoryPermissions;
|
||||
private: boolean;
|
||||
release_counter: number;
|
||||
repo_transfer: GiteaRepositoryRepoTransfer;
|
||||
size: number;
|
||||
ssh_url: string;
|
||||
stars_count: number;
|
||||
template: boolean;
|
||||
updated_at: string;
|
||||
watchers_count: number;
|
||||
website: string;
|
||||
};
|
||||
|
||||
type ReposListCommitsResponseItemCommitAffectedFiles = {
|
||||
filename: string;
|
||||
};
|
||||
|
||||
type ReposListCommitsResponseItemCommitStats = {
|
||||
additions: number;
|
||||
deletions: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
type ReposListCommitsResponseItem = {
|
||||
author: GiteaUser;
|
||||
commit: ReposListCommitsResponseItemCommit;
|
||||
committer: GiteaUser;
|
||||
created: string;
|
||||
files: Array<ReposListCommitsResponseItemCommitAffectedFiles>;
|
||||
html_url: string;
|
||||
parents: Array<CommitMeta>;
|
||||
sha: string;
|
||||
stats: ReposListCommitsResponseItemCommitStats;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type ReposListCommitsResponse = Array<ReposListCommitsResponseItem>;
|
||||
|
||||
export type GitGetBlobResponse = {
|
||||
content: string;
|
||||
encoding: string;
|
||||
sha: string;
|
||||
size: number;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type GitGetTreeResponseTreeItem = {
|
||||
mode: string;
|
||||
path: string;
|
||||
sha: string;
|
||||
size?: number;
|
||||
type: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type GitGetTreeResponse = {
|
||||
page: number;
|
||||
sha: string;
|
||||
total_count: number;
|
||||
tree: Array<GitGetTreeResponseTreeItem>;
|
||||
truncated: boolean;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type FileLinksResponse = {
|
||||
git: string;
|
||||
html: string;
|
||||
self: string;
|
||||
};
|
||||
|
||||
type ContentsResponse = {
|
||||
_links: FileLinksResponse;
|
||||
content?: string | null;
|
||||
download_url: string;
|
||||
encoding?: string | null;
|
||||
git_url: string;
|
||||
html_url: string;
|
||||
last_commit_sha: string;
|
||||
name: string;
|
||||
path: string;
|
||||
sha: string;
|
||||
size: number;
|
||||
submodule_git_url?: string | null;
|
||||
target?: string | null;
|
||||
type: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type FileCommitResponse = {
|
||||
author: CommitUser;
|
||||
committer: CommitUser;
|
||||
created: string;
|
||||
html_url: string;
|
||||
message: string;
|
||||
parents: Array<CommitMeta>;
|
||||
sha: string;
|
||||
tree: CommitMeta;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type FilesResponse = {
|
||||
commit: FileCommitResponse;
|
||||
content: Array<ContentsResponse>;
|
||||
verification: PayloadCommitVerification;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
const { getConfig } = require('../../scripts/webpack.js');
|
||||
|
||||
module.exports = getConfig();
|
||||
555
source/admin/packages/decap-cms-backend-github/CHANGELOG.md
Normal file
555
source/admin/packages/decap-cms-backend-github/CHANGELOG.md
Normal file
@@ -0,0 +1,555 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.4.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.3.1...decap-cms-backend-github@3.4.0) (2025-07-15)
|
||||
|
||||
### Features
|
||||
|
||||
- add logo to header ([#7487](https://github.com/decaporg/decap-cms/issues/7487)) ([b540ace](https://github.com/decaporg/decap-cms/commit/b540acec943eb231df6aac7b1d515d9b4b84fa5d))
|
||||
|
||||
## [3.3.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.3.0...decap-cms-backend-github@3.3.1) (2025-07-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [3.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.2.2...decap-cms-backend-github@3.3.0) (2025-06-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [3.2.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.2.1...decap-cms-backend-github@3.2.2) (2024-08-13)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "Update dependencies (#7264)" ([22d483a](https://github.com/decaporg/decap-cms/commit/22d483a5b0c654071ae05735ac4f49abdc13d38c)), closes [#7264](https://github.com/decaporg/decap-cms/issues/7264)
|
||||
|
||||
## [3.2.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.2.0...decap-cms-backend-github@3.2.1) (2024-08-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [3.2.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.1.2...decap-cms-backend-github@3.2.0) (2024-08-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fetch GitHub PR author name, fixes [#7232](https://github.com/decaporg/decap-cms/issues/7232) ([#7253](https://github.com/decaporg/decap-cms/issues/7253)) ([0e5335d](https://github.com/decaporg/decap-cms/commit/0e5335daba1b67816b4a0c24d1a2d9a185e3b54f))
|
||||
|
||||
## [3.1.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.1.1...decap-cms-backend-github@3.1.2) (2024-04-03)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [3.1.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.1.0-beta.2...decap-cms-backend-github@3.1.1) (2024-03-21)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [3.1.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.1.0-beta.2...decap-cms-backend-github@3.1.0) (2024-02-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [3.1.0-beta.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.1.0-beta.1...decap-cms-backend-github@3.1.0-beta.2) (2024-01-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [3.1.0-beta.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.1.0-beta.0...decap-cms-backend-github@3.1.0-beta.1) (2023-11-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- used merge-upstream to sync fork & upstream ([#6504](https://github.com/decaporg/decap-cms/issues/6504)) ([931399d](https://github.com/decaporg/decap-cms/commit/931399dd6eb675e06d59ac57ecfefc1b82467271))
|
||||
|
||||
# [3.1.0-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.1.0...decap-cms-backend-github@3.1.0-beta.0) (2023-10-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([b89fc89](https://github.com/decaporg/decap-cms/commit/b89fc894dfbb5f4136b2e5427fd25a29378a58c6))
|
||||
|
||||
## [3.0.3](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.0.2...decap-cms-backend-github@3.0.3) (2023-10-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [3.0.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.0.1...decap-cms-backend-github@3.0.2) (2023-09-06)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
- filter by path when loading collection from github backend ([#6898](https://github.com/decaporg/decap-cms/issues/6898)) ([18ef773](https://github.com/decaporg/decap-cms/commit/18ef773f35db1b7ef3ab5a0f25527d87745b9c73))
|
||||
|
||||
## [3.0.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.0.0...decap-cms-backend-github@3.0.1) (2023-08-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update peer dependencies ([#6886](https://github.com/decaporg/decap-cms/issues/6886)) ([e580ce5](https://github.com/decaporg/decap-cms/commit/e580ce52ce5f80fa040e8fbcab7fed0744f4f695))
|
||||
|
||||
# [3.0.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@2.15.0...decap-cms-backend-github@3.0.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [2.15.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@2.15.0-beta.0...decap-cms-backend-github@2.15.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# 2.15.0-beta.0 (2023-08-18)
|
||||
|
||||
### Features
|
||||
|
||||
- rename packages ([#6863](https://github.com/decaporg/decap-cms/issues/6863)) ([d515e7b](https://github.com/decaporg/decap-cms/commit/d515e7bd33216a775d96887b08c4f7b1962941bb))
|
||||
|
||||
## [2.14.2-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@2.14.1...decap-cms-backend-github@2.14.2-beta.0) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.14.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@2.14.0...decap-cms-backend-github@2.14.1) (2022-04-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [2.14.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@2.13.5...decap-cms-backend-github@2.14.0) (2021-10-18)
|
||||
|
||||
### Features
|
||||
|
||||
- display author of changes in workflow tab ([#5780](https://github.com/decaporg/decap-cms/issues/5780)) ([3f607e4](https://github.com/decaporg/decap-cms/commit/3f607e41d9c4d8fe5329a9ab6841cada7742825e))
|
||||
|
||||
## [2.13.5](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@2.13.4...decap-cms-backend-github@2.13.5) (2021-10-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- remove "Don't fork the repo"-Button - fixes [#5723](https://github.com/decaporg/decap-cms/issues/5723) ([#5872](https://github.com/decaporg/decap-cms/issues/5872)) ([05d8923](https://github.com/decaporg/decap-cms/commit/05d89230dca315ddcc734b1dc6223df1d8dc1ede))
|
||||
|
||||
## [2.13.4](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@2.13.3...decap-cms-backend-github@2.13.4) (2021-07-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add updated_at to graphql query ([#5611](https://github.com/decaporg/decap-cms/issues/5611)) ([8989550](https://github.com/decaporg/decap-cms/commit/89895508b2ccc8f07019abb6bc2d0162c0d86266))
|
||||
|
||||
## [2.13.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.13.2...decap-cms-backend-github@2.13.3) (2021-06-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.13.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.13.1...decap-cms-backend-github@2.13.2) (2021-05-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.13.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.13.0...decap-cms-backend-github@2.13.1) (2021-05-19)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [2.13.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.12.0...decap-cms-backend-github@2.13.0) (2021-05-04)
|
||||
|
||||
### Features
|
||||
|
||||
- added react 17 as peer dependency in packages ([#5316](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/5316)) ([9e42380](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/9e423805707321396eec137f5b732a5b07a0dd3f))
|
||||
|
||||
# [2.12.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.11.9...decap-cms-backend-github@2.12.0) (2021-04-04)
|
||||
|
||||
### Features
|
||||
|
||||
- **open-authoring:** add alwaysFork option ([#5204](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/5204)) ([7b19e30](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/7b19e30dd2a310dbc20ccb6fcca45d5cbde1014b))
|
||||
|
||||
## [2.11.9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.11.8...decap-cms-backend-github@2.11.9) (2021-02-23)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.11.8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.11.7...decap-cms-backend-github@2.11.8) (2021-02-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.11.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.11.6...decap-cms-backend-github@2.11.7) (2020-12-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **large-media:** mark pointer files as binary ([#4678](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/4678)) ([7697b90](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/7697b907d7bae750f4ec041a184188aa46995320))
|
||||
|
||||
## [2.11.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.11.5...decap-cms-backend-github@2.11.6) (2020-09-20)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.11.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.11.4...decap-cms-backend-github@2.11.5) (2020-09-15)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## 2.11.4 (2020-09-08)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([828bb16](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/828bb16415b8c22a34caa19c50c38b24ffe9ceae))
|
||||
|
||||
## 2.11.3 (2020-08-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([8262487](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/82624879ccbcb16610090041db28f00714d924c8))
|
||||
|
||||
## 2.11.2 (2020-07-27)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([118d50a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/118d50a7a70295f25073e564b5161aa2b9883056))
|
||||
|
||||
## [2.11.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.11.0...decap-cms-backend-github@2.11.1) (2020-07-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** use workflow branch when listing files to move ([#4019](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/4019)) ([8720a42](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/8720a4233db16d91d6b86ee8653d05f8953cb430))
|
||||
|
||||
# [2.11.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.10.6...decap-cms-backend-github@2.11.0) (2020-06-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- handle token expiry ([#3847](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3847)) ([285c940](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/285c940562548d7bc88de244123ba87ff66fba65))
|
||||
|
||||
### Features
|
||||
|
||||
- add backend status down indicator ([#3889](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3889)) ([a50edc7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/a50edc70553ad6afa1acee6a51996ad226443f8c))
|
||||
- **backend-gitgateway:** improve deploy preview visibility ([#3882](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3882)) ([afc9bf4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/afc9bf4f3fe14ccb60851fc24e68922a6e4a85a9))
|
||||
|
||||
## [2.10.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.10.5...decap-cms-backend-github@2.10.6) (2020-05-19)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.10.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.10.4...decap-cms-backend-github@2.10.5) (2020-04-21)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.10.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.10.3...decap-cms-backend-github@2.10.4) (2020-04-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** add fallback for diff errors/warnings ([#3558](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3558)) ([1705c79](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/1705c79a9297d844d5421d685a7785e1e210e39e))
|
||||
|
||||
## [2.10.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.10.2...decap-cms-backend-github@2.10.3) (2020-04-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **open-authoring:** properly delete open authoring branches ([#3512](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3512)) ([cc89aa5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/cc89aa5c430a6bee51483cda91d0f92e7437f29e))
|
||||
|
||||
## [2.10.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.10.1...decap-cms-backend-github@2.10.2) (2020-04-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **open-authoring:** prevent workflow view from breaking on entry error ([#3508](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3508)) ([cbb3927](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/cbb39271012fc3beecfdf180e573e343ee48fe26))
|
||||
|
||||
## [2.10.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.10.0...decap-cms-backend-github@2.10.1) (2020-03-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- missing workflow timestamp ([#3445](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3445)) ([9616cdb](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/9616cdb8bb0a564771e5755bcd3718a07f2e2072))
|
||||
|
||||
# [2.10.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.9.3...decap-cms-backend-github@2.10.0) (2020-03-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** don't create new commits on empty diff when rebasing ([#3411](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3411)) ([70de9f6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/70de9f6b4b89dd8e23205929033745572562e8fc))
|
||||
- update repo owner from GitHub API to match casing ([#3410](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3410)) ([c2e7a24](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/c2e7a24dc20dfea5b1289c5705095d2cf8b04c54))
|
||||
|
||||
### Features
|
||||
|
||||
- **backend-github:** add pagination ([#3379](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3379)) ([39f1307](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/39f1307e3a36447da8c9b3ca79b1d7db52ea1a19))
|
||||
|
||||
## [2.9.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.9.2...decap-cms-backend-github@2.9.3) (2020-03-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **locale:** Remove hard coded string literals ([#3333](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3333)) ([7c45a3c](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/7c45a3cda983be427864a56e58791565eb9232e2))
|
||||
- **open-authoring:** use origin repo when calling compare API ([#3363](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3363)) ([e40b81a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/e40b81a5647d45487d6ddf17245beddd354e0f39))
|
||||
|
||||
## [2.9.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.9.1...decap-cms-backend-github@2.9.2) (2020-02-27)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.9.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.9.0...decap-cms-backend-github@2.9.1) (2020-02-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** fail workflow migrations gracefully ([#3325](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3325)) ([83e0383](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/83e0383b690fb452ea40cb165a56f65a695dc83c))
|
||||
|
||||
# [2.9.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.8.1...decap-cms-backend-github@2.9.0) (2020-02-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** improve workflow migration edge cases/messaging ([#3319](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3319)) ([684b79e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/684b79e43bebb63ce1e844eae5c8c0e76087687b))
|
||||
|
||||
### Features
|
||||
|
||||
- **core:** align GitHub metadata handling with other backends ([#3316](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3316)) ([7e0a8ad](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/7e0a8ad532012576dc5e40bd4e9d54522e307123)), closes [#3292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3292)
|
||||
|
||||
## [2.8.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.8.0...decap-cms-backend-github@2.8.1) (2020-02-22)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "feat(core): Align GitHub metadata handling with other backends (#3292)" ([5bdd3df](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/5bdd3df9ccbb5149c22d79987ebdcd6cab4b261f)), closes [#3292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3292)
|
||||
|
||||
# [2.8.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.7.1...decap-cms-backend-github@2.8.0) (2020-02-22)
|
||||
|
||||
### Features
|
||||
|
||||
- **core:** Align GitHub metadata handling with other backends ([#3292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3292)) ([8193b5a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/8193b5ace89d6f14a6c756235a50b186a763b6b1))
|
||||
|
||||
## [2.7.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.7.0...decap-cms-backend-github@2.7.1) (2020-02-17)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [2.7.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.6.6...decap-cms-backend-github@2.7.0) (2020-02-10)
|
||||
|
||||
### Features
|
||||
|
||||
- field based media/public folders ([#3208](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3208)) ([97bc0c8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/97bc0c8dc489e736f89d748ba832d78400fe4332))
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([a015d1d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/a015d1d92a4b1c0130c44fcef1c9ecdb157a0f07))
|
||||
|
||||
## [2.6.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.6.5...decap-cms-backend-github@2.6.6) (2020-02-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **locale:** remove hard coded strings ([#3193](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3193)) ([fc91bf8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/fc91bf8781e65ce1dc946363dbb10419a145c66b))
|
||||
|
||||
## [2.6.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.6.4...decap-cms-backend-github@2.6.5) (2020-01-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-git-gateway:** re-write GitHub pagination links ([#3135](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3135)) ([834f6b9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/834f6b9e457f3738ce0f240ddd4cc160aff9e2f5))
|
||||
|
||||
## [2.6.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.6.3...decap-cms-backend-github@2.6.4) (2020-01-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github-graphql:** handle trailing paths in collection folder ([#3099](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3099)) ([bc80804](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/bc808040661d345e65d49d64693cd6da3b6816fb))
|
||||
|
||||
## [2.6.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.6.2...decap-cms-backend-github@2.6.3) (2020-01-14)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.6.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.6.1...decap-cms-backend-github@2.6.2) (2020-01-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github-graphql:** return empty array on non existent folder ([#3079](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3079)) ([69b130a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/69b130a3f239590f828f0e4f6f6c0a872b17548b))
|
||||
|
||||
## [2.6.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.6.0...decap-cms-backend-github@2.6.1) (2020-01-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- trim '/' from folder ([#3052](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/3052)) ([4b6c8de](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/4b6c8de6b2e3de28f0989b9a012cb302d4de4358))
|
||||
|
||||
# [2.6.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.6.0-beta.0...decap-cms-backend-github@2.6.0) (2020-01-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- rebase open authoring branches ([#2975](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2975)) ([8c175f6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/8c175f6132fa18a13763cc563f7d3201c1e3580e))
|
||||
|
||||
# [2.6.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0...decap-cms-backend-github@2.6.0-beta.0) (2019-12-18)
|
||||
|
||||
### Features
|
||||
|
||||
- bundle assets with content ([#2958](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2958)) ([2b41d8a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/2b41d8a838a9c8a6b21cde2ddd16b9288334e298))
|
||||
|
||||
# [2.5.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.8...decap-cms-backend-github@2.5.0) (2019-12-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [2.5.0-beta.8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.7...decap-cms-backend-github@2.5.0-beta.8) (2019-12-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- don't fail on deleting non existent branch ([1e77d4b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/1e77d4b7688de795ab1b01c6ce2483a0383bbfb6))
|
||||
|
||||
# [2.5.0-beta.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.6...decap-cms-backend-github@2.5.0-beta.7) (2019-12-02)
|
||||
|
||||
### Features
|
||||
|
||||
- content in sub folders ([#2897](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2897)) ([afcfe5b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/afcfe5b6d5f32669e9061ec596bd35ad545d61a3))
|
||||
|
||||
# [2.5.0-beta.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.5...decap-cms-backend-github@2.5.0-beta.6) (2019-11-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** prepend collection name ([#2878](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2878)) ([465f463](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/465f4639597f258d5aa2c1b65e9d2c16023ee7ae))
|
||||
|
||||
### Features
|
||||
|
||||
- workflow unpublished entry ([#2914](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2914)) ([41bb9aa](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/41bb9aac0dd6fd9f8ff157bb0b29c85aa87fe04d))
|
||||
|
||||
# [2.5.0-beta.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.4...decap-cms-backend-github@2.5.0-beta.5) (2019-11-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** editorial workflow commits ([#2867](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2867)) ([86adca3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/86adca3a18f25ab74d1c6702bafab250f005ceec))
|
||||
- make forkExists name matching case-insensitive ([#2869](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2869)) ([9978769](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/9978769ece9262265d3efa77357f9e8b46ad9a1e))
|
||||
- **backend-github:** loaded entries limit ([#2873](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2873)) ([68a8c8a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/68a8c8a693646ebd33fae791aaaec47b050e0186))
|
||||
- **git-gateway:** unpublished entries not loaded for git-gateway(GitHub) ([#2856](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2856)) ([4a2328b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/4a2328b2f10ea678184391e4caf235b41323cd3e))
|
||||
|
||||
### Features
|
||||
|
||||
- commit media with post ([#2851](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2851)) ([6515dee](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/6515dee8715d8571ea19484a7dfab7cfd0cc40be))
|
||||
|
||||
# [2.5.0-beta.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.3...decap-cms-backend-github@2.5.0-beta.4) (2019-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **github-backend:** load media URLs via API ([#2817](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2817)) ([eaeaf44](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/eaeaf4483287a1f724ee60ef321ff749f1c20acf))
|
||||
- change default open authoring scope, make it configurable ([#2821](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2821)) ([002cdd7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/002cdd77a856bde3672e75dde6d3a2b246e1035f))
|
||||
- display UI to fork a repo only when fork doesn't exist ([#2802](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2802)) ([7f90d0e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/7f90d0e065315b9073d21fd733f42f3838ecfe09))
|
||||
|
||||
### Features
|
||||
|
||||
- add go back to site button ([#2538](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2538)) ([f206e7e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/f206e7e5a13fb48ec6b27dce0dbb3a59b61de8f9))
|
||||
|
||||
# [2.5.0-beta.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.2...decap-cms-backend-github@2.5.0-beta.3) (2019-09-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-github:** update Open Authoring branches with no PR ([#2618](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2618)) ([6817033](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/6817033))
|
||||
- **git-gateway:** pass api URL instead of constructing it from repo value ([#2631](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2631)) ([922c0f3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/922c0f3))
|
||||
- **github-backend:** handle race condition in editorial workflow ([#2658](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2658)) ([97f1f84](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/97f1f84))
|
||||
|
||||
# [2.5.0-beta.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.1...decap-cms-backend-github@2.5.0-beta.2) (2019-09-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **github-graphql:** use getMediaDisplayURL to load media with auth header ([#2652](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2652)) ([e674e43](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/e674e43))
|
||||
|
||||
### Features
|
||||
|
||||
- **backend-github:** GitHub GraphQL API support ([#2456](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2456)) ([ece136c](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/ece136c))
|
||||
|
||||
# [2.5.0-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.5.0-beta.0...decap-cms-backend-github@2.5.0-beta.1) (2019-08-24)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [2.5.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.4.2...decap-cms-backend-github@2.5.0-beta.0) (2019-07-24)
|
||||
|
||||
### Features
|
||||
|
||||
- **backend-github:** Open Authoring ([#2430](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2430)) ([edf0a3a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/edf0a3a))
|
||||
|
||||
## [2.4.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.4.2-beta.0...decap-cms-backend-github@2.4.2) (2019-04-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.4.2-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.4.1...decap-cms-backend-github@2.4.2-beta.0) (2019-04-05)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.4.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.4.1-beta.1...decap-cms-backend-github@2.4.1) (2019-03-29)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.4.1-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.4.1-beta.0...decap-cms-backend-github@2.4.1-beta.1) (2019-03-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- export on decap-cms and maps on esm ([#2244](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2244)) ([6ffd13b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/6ffd13b))
|
||||
|
||||
## [2.4.1-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.4.0...decap-cms-backend-github@2.4.1-beta.0) (2019-03-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update peer dep versions ([#2234](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2234)) ([7987091](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/7987091))
|
||||
|
||||
# [2.4.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.3.0...decap-cms-backend-github@2.4.0) (2019-03-22)
|
||||
|
||||
### Features
|
||||
|
||||
- add ES module builds ([#2215](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2215)) ([d142b32](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/d142b32))
|
||||
|
||||
# [2.3.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.3.0-beta.0...decap-cms-backend-github@2.3.0) (2019-03-22)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [2.3.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.2.3-beta.0...decap-cms-backend-github@2.3.0-beta.0) (2019-03-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix umd builds ([#2214](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2214)) ([e04f6be](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/e04f6be))
|
||||
|
||||
### Features
|
||||
|
||||
- provide usable UMD builds for all packages ([#2141](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2141)) ([82cc794](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/82cc794))
|
||||
|
||||
## [2.2.3-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.2.2...decap-cms-backend-github@2.2.3-beta.0) (2019-03-15)
|
||||
|
||||
### Features
|
||||
|
||||
- upgrade to Emotion 10 ([#2166](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2166)) ([ccef446](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/ccef446))
|
||||
|
||||
## [2.2.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.2.1...decap-cms-backend-github@2.2.2) (2019-03-08)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
## [2.2.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.2.0...decap-cms-backend-github@2.2.1) (2019-02-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
# [2.2.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.1.0...decap-cms-backend-github@2.2.0) (2019-02-08)
|
||||
|
||||
### Features
|
||||
|
||||
- **workflow:** add deploy preview links ([#2028](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/2028)) ([15d221d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/15d221d))
|
||||
|
||||
# [2.1.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.9...decap-cms-backend-github@2.1.0) (2018-11-12)
|
||||
|
||||
### Features
|
||||
|
||||
- allow custom logo on auth page ([#1818](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/1818)) ([c6ae1e8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/c6ae1e8))
|
||||
|
||||
<a name="2.0.9"></a>
|
||||
|
||||
## [2.0.9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.8...decap-cms-backend-github@2.0.9) (2018-09-17)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
<a name="2.0.8"></a>
|
||||
|
||||
## [2.0.8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.7...decap-cms-backend-github@2.0.8) (2018-09-06)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
<a name="2.0.7"></a>
|
||||
|
||||
## [2.0.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.6...decap-cms-backend-github@2.0.7) (2018-08-27)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
<a name="2.0.6"></a>
|
||||
|
||||
## [2.0.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.5...decap-cms-backend-github@2.0.6) (2018-08-24)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
<a name="2.0.5"></a>
|
||||
|
||||
## [2.0.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.4...decap-cms-backend-github@2.0.5) (2018-08-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backends:** fix commit message handling ([#1568](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/1568)) ([f7e7120](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/f7e7120))
|
||||
|
||||
<a name="2.0.4"></a>
|
||||
|
||||
## [2.0.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.3...decap-cms-backend-github@2.0.4) (2018-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **workflow:** enable workflow per method ([#1569](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/1569)) ([90b8156](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/90b8156))
|
||||
|
||||
<a name="2.0.3"></a>
|
||||
|
||||
## [2.0.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.2...decap-cms-backend-github@2.0.3) (2018-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **github:** fix image uploading ([#1561](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/1561)) ([ddc8f04](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/ddc8f04))
|
||||
- **workflow:** fix status not set on new workflow entries ([#1558](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/issues/1558)) ([0aa085f](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/commit/0aa085f))
|
||||
|
||||
<a name="2.0.2"></a>
|
||||
|
||||
## [2.0.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github/compare/decap-cms-backend-github@2.0.1...decap-cms-backend-github@2.0.2) (2018-07-28)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
|
||||
<a name="2.0.1"></a>
|
||||
|
||||
## 2.0.1 (2018-07-26)
|
||||
|
||||
<a name="2.0.0"></a>
|
||||
|
||||
# 2.0.0 (2018-07-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-github
|
||||
17
source/admin/packages/decap-cms-backend-github/README.md
Normal file
17
source/admin/packages/decap-cms-backend-github/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# GitHub backend
|
||||
|
||||
An abstraction layer between the CMS and [GitHub](https://docs.github.com/en/rest)
|
||||
|
||||
## Code structure
|
||||
|
||||
`Implementation` for [File Management System API](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-lib-util/README.md) based on `Api`.
|
||||
|
||||
`Api` - A wrapper for GitHub REST API.
|
||||
|
||||
`GraphQLApi` - `Api` with `ApolloClient`. [Api docs](https://docs.github.com/en/graphql) and [netlify docs](https://www.decapcms.org/docs/beta-features/#github-graphql-api).
|
||||
|
||||
`AuthenticationPage` - uses [lib-auth](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-lib-auth/README.md) to facilitate OAuth and implicit authentication.
|
||||
|
||||
`scripts` - use `createFragmentTypes.js` to create GitHub GraphQL API fragment types.
|
||||
|
||||
Look at tests or types for more info.
|
||||
47
source/admin/packages/decap-cms-backend-github/package.json
Normal file
47
source/admin/packages/decap-cms-backend-github/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "decap-cms-backend-github",
|
||||
"description": "GitHub backend for Decap CMS",
|
||||
"version": "3.4.0",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github",
|
||||
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
||||
"module": "dist/esm/index.js",
|
||||
"main": "dist/decap-cms-backend-github.js",
|
||||
"keywords": [
|
||||
"decap-cms",
|
||||
"backend",
|
||||
"github"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"develop": "npm run build:esm -- --watch",
|
||||
"build": "cross-env NODE_ENV=production webpack",
|
||||
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward --extensions \".js,.jsx,.ts,.tsx\"",
|
||||
"createFragmentTypes": "node scripts/createFragmentTypes.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"apollo-cache-inmemory": "^1.6.2",
|
||||
"apollo-client": "^2.6.3",
|
||||
"apollo-link-context": "^1.0.18",
|
||||
"apollo-link-http": "^1.5.15",
|
||||
"common-tags": "^1.8.0",
|
||||
"graphql": "^15.0.0",
|
||||
"graphql-tag": "^2.10.1",
|
||||
"js-base64": "^3.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"semaphore": "^1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"decap-cms-lib-auth": "^3.0.0",
|
||||
"decap-cms-lib-util": "^3.0.0",
|
||||
"decap-cms-ui-default": "^3.0.0",
|
||||
"lodash": "^4.17.11",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^19.1.0"
|
||||
},
|
||||
"browser": {
|
||||
"path": "path-browserify"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
const fetch = require('node-fetch');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const API_HOST = process.env.GITHUB_HOST || 'https://api.github.com';
|
||||
const API_TOKEN = process.env.GITHUB_API_TOKEN;
|
||||
|
||||
if (!API_TOKEN) {
|
||||
throw new Error('Missing environment variable GITHUB_API_TOKEN');
|
||||
}
|
||||
|
||||
fetch(`${API_HOST}/graphql`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `bearer ${API_TOKEN}` },
|
||||
body: JSON.stringify({
|
||||
variables: {},
|
||||
query: `
|
||||
{
|
||||
__schema {
|
||||
types {
|
||||
kind
|
||||
name
|
||||
possibleTypes {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
})
|
||||
.then(result => result.json())
|
||||
.then(result => {
|
||||
// here we're filtering out any type information unrelated to unions or interfaces
|
||||
const filteredData = result.data.__schema.types.filter(type => type.possibleTypes !== null);
|
||||
result.data.__schema.types = filteredData;
|
||||
fs.writeFile(
|
||||
path.join(__dirname, '..', 'src', 'fragmentTypes.js'),
|
||||
`module.exports = ${JSON.stringify(result.data)}`,
|
||||
err => {
|
||||
if (err) {
|
||||
console.error('Error writing fragmentTypes file', err);
|
||||
} else {
|
||||
console.log('Fragment types successfully extracted!');
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
1477
source/admin/packages/decap-cms-backend-github/src/API.ts
Normal file
1477
source/admin/packages/decap-cms-backend-github/src/API.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,162 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from '@emotion/styled';
|
||||
import { NetlifyAuthenticator } from 'decap-cms-lib-auth';
|
||||
import { AuthenticationPage, Icon } from 'decap-cms-ui-default';
|
||||
|
||||
const LoginButtonIcon = styled(Icon)`
|
||||
margin-right: 18px;
|
||||
`;
|
||||
|
||||
const ForkApprovalContainer = styled.div`
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: space-around;
|
||||
flex-grow: 0.2;
|
||||
`;
|
||||
const ForkButtonsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export default class GitHubAuthenticationPage extends React.Component {
|
||||
static propTypes = {
|
||||
onLogin: PropTypes.func.isRequired,
|
||||
inProgress: PropTypes.bool,
|
||||
base_url: PropTypes.string,
|
||||
siteId: PropTypes.string,
|
||||
authEndpoint: PropTypes.string,
|
||||
config: PropTypes.object.isRequired,
|
||||
clearHash: PropTypes.func,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
componentDidMount() {
|
||||
// Manually validate PropTypes - React 19 breaking change
|
||||
PropTypes.checkPropTypes(
|
||||
GitHubAuthenticationPage.propTypes,
|
||||
this.props,
|
||||
'prop',
|
||||
'GitHubAuthenticationPage',
|
||||
);
|
||||
}
|
||||
|
||||
getPermissionToFork = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.setState({
|
||||
requestingFork: true,
|
||||
approveFork: () => {
|
||||
this.setState({ requestingFork: false });
|
||||
resolve();
|
||||
},
|
||||
refuseFork: () => {
|
||||
this.setState({ requestingFork: false });
|
||||
reject();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
loginWithOpenAuthoring(data) {
|
||||
const { backend } = this.props;
|
||||
|
||||
this.setState({ findingFork: true });
|
||||
return backend
|
||||
.authenticateWithFork({ userData: data, getPermissionToFork: this.getPermissionToFork })
|
||||
.catch(err => {
|
||||
this.setState({ findingFork: false });
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
handleLogin = e => {
|
||||
e.preventDefault();
|
||||
const cfg = {
|
||||
base_url: this.props.base_url,
|
||||
site_id:
|
||||
document.location.host.split(':')[0] === 'localhost'
|
||||
? 'demo.decapcms.org'
|
||||
: this.props.siteId,
|
||||
auth_endpoint: this.props.authEndpoint,
|
||||
};
|
||||
const auth = new NetlifyAuthenticator(cfg);
|
||||
|
||||
const { open_authoring: openAuthoring = false, auth_scope: authScope = '' } =
|
||||
this.props.config.backend;
|
||||
|
||||
const scope = authScope || (openAuthoring ? 'public_repo' : 'repo');
|
||||
auth.authenticate({ provider: 'github', scope }, (err, data) => {
|
||||
if (err) {
|
||||
this.setState({ loginError: err.toString() });
|
||||
return;
|
||||
}
|
||||
if (openAuthoring) {
|
||||
return this.loginWithOpenAuthoring(data).then(() => this.props.onLogin(data));
|
||||
}
|
||||
this.props.onLogin(data);
|
||||
});
|
||||
};
|
||||
|
||||
renderLoginButton = () => {
|
||||
const { inProgress, t } = this.props;
|
||||
return inProgress || this.state.findingFork ? (
|
||||
t('auth.loggingIn')
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<LoginButtonIcon type="github" />
|
||||
{t('auth.loginWithGitHub')}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
getAuthenticationPageRenderArgs() {
|
||||
const { requestingFork } = this.state;
|
||||
|
||||
if (requestingFork) {
|
||||
const { approveFork, refuseFork } = this.state;
|
||||
return {
|
||||
renderPageContent: ({ LoginButton, TextButton, showAbortButton }) => (
|
||||
<ForkApprovalContainer>
|
||||
<p>
|
||||
Open Authoring is enabled: we need to use a fork on your github account. (If a fork
|
||||
already exists, we'll use that.)
|
||||
</p>
|
||||
<ForkButtonsContainer>
|
||||
<LoginButton onClick={approveFork}>Fork the repo</LoginButton>
|
||||
{showAbortButton && (
|
||||
<TextButton onClick={refuseFork}>Don't fork the repo</TextButton>
|
||||
)}
|
||||
</ForkButtonsContainer>
|
||||
</ForkApprovalContainer>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
renderButtonContent: this.renderLoginButton,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { inProgress, config, t } = this.props;
|
||||
const { loginError, requestingFork, findingFork } = this.state;
|
||||
|
||||
return (
|
||||
<AuthenticationPage
|
||||
onLogin={this.handleLogin}
|
||||
loginDisabled={inProgress || findingFork || requestingFork}
|
||||
loginErrorMessage={loginError}
|
||||
logoUrl={config.logo_url} // Deprecated, replaced by `logo.src`
|
||||
logo={config.logo}
|
||||
siteUrl={config.site_url}
|
||||
{...this.getAuthenticationPageRenderArgs()}
|
||||
t={t}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
710
source/admin/packages/decap-cms-backend-github/src/GraphQLAPI.ts
Normal file
710
source/admin/packages/decap-cms-backend-github/src/GraphQLAPI.ts
Normal file
@@ -0,0 +1,710 @@
|
||||
import { ApolloClient } from 'apollo-client';
|
||||
import {
|
||||
InMemoryCache,
|
||||
defaultDataIdFromObject,
|
||||
IntrospectionFragmentMatcher,
|
||||
} from 'apollo-cache-inmemory';
|
||||
import { createHttpLink } from 'apollo-link-http';
|
||||
import { setContext } from 'apollo-link-context';
|
||||
import {
|
||||
APIError,
|
||||
readFile,
|
||||
localForage,
|
||||
DEFAULT_PR_BODY,
|
||||
branchFromContentKey,
|
||||
CMS_BRANCH_PREFIX,
|
||||
throwOnConflictingBranches,
|
||||
} from 'decap-cms-lib-util';
|
||||
import trim from 'lodash/trim';
|
||||
import trimStart from 'lodash/trimStart';
|
||||
|
||||
import introspectionQueryResultData from './fragmentTypes';
|
||||
import API, { API_NAME, PullRequestState, MOCK_PULL_REQUEST } from './API';
|
||||
import * as queries from './queries';
|
||||
import * as mutations from './mutations';
|
||||
|
||||
import type { Config, BlobArgs } from './API';
|
||||
import type { NormalizedCacheObject } from 'apollo-cache-inmemory';
|
||||
import type { QueryOptions, MutationOptions, OperationVariables } from 'apollo-client';
|
||||
import type { GraphQLError } from 'graphql';
|
||||
import type { Octokit } from '@octokit/rest';
|
||||
|
||||
const NO_CACHE = 'no-cache';
|
||||
const CACHE_FIRST = 'cache-first';
|
||||
|
||||
const fragmentMatcher = new IntrospectionFragmentMatcher({
|
||||
introspectionQueryResultData,
|
||||
});
|
||||
|
||||
interface TreeEntry {
|
||||
object?: {
|
||||
entries: TreeEntry[];
|
||||
};
|
||||
type: 'blob' | 'tree';
|
||||
name: string;
|
||||
sha: string;
|
||||
blob?: {
|
||||
size: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface TreeFile {
|
||||
path: string;
|
||||
id: string;
|
||||
size: number;
|
||||
type: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
type GraphQLPullRequest = {
|
||||
id: string;
|
||||
baseRefName: string;
|
||||
baseRefOid: string;
|
||||
body: string;
|
||||
headRefName: string;
|
||||
headRefOid: string;
|
||||
number: number;
|
||||
state: string;
|
||||
title: string;
|
||||
mergedAt: string | null;
|
||||
updatedAt: string | null;
|
||||
labels: { nodes: { name: string }[] };
|
||||
repository: {
|
||||
id: string;
|
||||
isFork: boolean;
|
||||
};
|
||||
user: GraphQLPullsListResponseItemUser;
|
||||
};
|
||||
|
||||
type GraphQLPullsListResponseItemUser = {
|
||||
avatar_url: string;
|
||||
login: string;
|
||||
url: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
function transformPullRequest(pr: GraphQLPullRequest) {
|
||||
return {
|
||||
...pr,
|
||||
labels: pr.labels.nodes,
|
||||
head: { ref: pr.headRefName, sha: pr.headRefOid, repo: { fork: pr.repository.isFork } },
|
||||
base: { ref: pr.baseRefName, sha: pr.baseRefOid },
|
||||
};
|
||||
}
|
||||
|
||||
type Error = GraphQLError & { type: string };
|
||||
|
||||
export default class GraphQLAPI extends API {
|
||||
client: ApolloClient<NormalizedCacheObject>;
|
||||
|
||||
constructor(config: Config) {
|
||||
super(config);
|
||||
|
||||
this.client = this.getApolloClient();
|
||||
}
|
||||
|
||||
getApolloClient() {
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
return {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
...headers,
|
||||
authorization: this.token ? `${this.tokenKeyword} ${this.token}` : '',
|
||||
},
|
||||
};
|
||||
});
|
||||
const httpLink = createHttpLink({ uri: `${this.apiRoot}/graphql` });
|
||||
return new ApolloClient({
|
||||
link: authLink.concat(httpLink),
|
||||
cache: new InMemoryCache({ fragmentMatcher }),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: NO_CACHE,
|
||||
errorPolicy: 'ignore',
|
||||
},
|
||||
query: {
|
||||
fetchPolicy: NO_CACHE,
|
||||
errorPolicy: 'all',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
return this.client.resetStore();
|
||||
}
|
||||
|
||||
async getRepository(owner: string, name: string) {
|
||||
const { data } = await this.query({
|
||||
query: queries.repository,
|
||||
variables: { owner, name },
|
||||
fetchPolicy: CACHE_FIRST, // repository id doesn't change
|
||||
});
|
||||
return data.repository;
|
||||
}
|
||||
|
||||
query(options: QueryOptions<OperationVariables>) {
|
||||
return this.client.query(options).catch(error => {
|
||||
throw new APIError(error.message, 500, 'GitHub');
|
||||
});
|
||||
}
|
||||
|
||||
async mutate(options: MutationOptions<OperationVariables>) {
|
||||
try {
|
||||
const result = await this.client.mutate(options);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errors = error.graphQLErrors;
|
||||
if (Array.isArray(errors) && errors.some(e => e.message === 'Ref cannot be created.')) {
|
||||
const refName = options?.variables?.createRefInput?.name || '';
|
||||
const branchName = trimStart(refName, 'refs/heads/');
|
||||
if (branchName) {
|
||||
await throwOnConflictingBranches(branchName, name => this.getBranch(name), API_NAME);
|
||||
}
|
||||
} else if (
|
||||
Array.isArray(errors) &&
|
||||
errors.some(e =>
|
||||
new RegExp(
|
||||
`A ref named "refs/heads/${CMS_BRANCH_PREFIX}/.+?" already exists in the repository.`,
|
||||
).test(e.message),
|
||||
)
|
||||
) {
|
||||
const refName = options?.variables?.createRefInput?.name || '';
|
||||
const sha = options?.variables?.createRefInput?.oid || '';
|
||||
const branchName = trimStart(refName, 'refs/heads/');
|
||||
if (branchName && branchName.startsWith(`${CMS_BRANCH_PREFIX}/`) && sha) {
|
||||
try {
|
||||
// this can happen if the branch wasn't deleted when the PR was merged
|
||||
// we backup the existing branch just in case an re-run the mutation
|
||||
await this.backupBranch(branchName);
|
||||
await this.deleteBranch(branchName);
|
||||
const result = await this.client.mutate(options);
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new APIError(error.message, 500, 'GitHub');
|
||||
}
|
||||
}
|
||||
|
||||
async hasWriteAccess() {
|
||||
const { repoOwner: owner, repoName: name } = this;
|
||||
try {
|
||||
const { data } = await this.query({
|
||||
query: queries.repoPermission,
|
||||
variables: { owner, name },
|
||||
fetchPolicy: CACHE_FIRST, // we can assume permission doesn't change often
|
||||
});
|
||||
// https://developer.github.com/v4/enum/repositorypermission/
|
||||
const { viewerPermission } = data.repository;
|
||||
return ['ADMIN', 'MAINTAIN', 'WRITE'].includes(viewerPermission);
|
||||
} catch (error) {
|
||||
console.error('Problem fetching repo data from GitHub');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async user() {
|
||||
const { data } = await this.query({
|
||||
query: queries.user,
|
||||
fetchPolicy: CACHE_FIRST, // we can assume user details don't change often
|
||||
});
|
||||
return data.viewer;
|
||||
}
|
||||
|
||||
async retrieveBlobObject(owner: string, name: string, expression: string, options = {}) {
|
||||
const { data } = await this.query({
|
||||
query: queries.blob,
|
||||
variables: { owner, name, expression },
|
||||
...options,
|
||||
});
|
||||
// https://developer.github.com/v4/object/blob/
|
||||
if (data.repository.object) {
|
||||
const { is_binary: isBinary, text } = data.repository.object;
|
||||
return { isNull: false, isBinary, text };
|
||||
} else {
|
||||
return { isNull: true };
|
||||
}
|
||||
}
|
||||
|
||||
getOwnerAndNameFromRepoUrl(repoURL: string) {
|
||||
let { repoOwner: owner, repoName: name } = this;
|
||||
|
||||
if (repoURL === this.originRepoURL) {
|
||||
({ originRepoOwner: owner, originRepoName: name } = this);
|
||||
}
|
||||
|
||||
return { owner, name };
|
||||
}
|
||||
|
||||
async readFile(
|
||||
path: string,
|
||||
sha?: string | null,
|
||||
{
|
||||
branch = this.branch,
|
||||
repoURL = this.repoURL,
|
||||
parseText = true,
|
||||
}: {
|
||||
branch?: string;
|
||||
repoURL?: string;
|
||||
parseText?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
if (!sha) {
|
||||
sha = await this.getFileSha(path, { repoURL, branch });
|
||||
}
|
||||
const fetchContent = () => this.fetchBlobContent({ sha: sha as string, repoURL, parseText });
|
||||
const content = await readFile(sha, fetchContent, localForage, parseText);
|
||||
return content;
|
||||
}
|
||||
|
||||
async fetchBlobContent({ sha, repoURL, parseText }: BlobArgs) {
|
||||
if (!parseText) {
|
||||
return super.fetchBlobContent({ sha, repoURL, parseText });
|
||||
}
|
||||
const { owner, name } = this.getOwnerAndNameFromRepoUrl(repoURL);
|
||||
const { isNull, isBinary, text } = await this.retrieveBlobObject(
|
||||
owner,
|
||||
name,
|
||||
sha,
|
||||
{ fetchPolicy: CACHE_FIRST }, // blob sha is derived from file content
|
||||
);
|
||||
|
||||
if (isNull) {
|
||||
throw new APIError('Not Found', 404, 'GitHub');
|
||||
} else if (!isBinary) {
|
||||
return text;
|
||||
} else {
|
||||
return super.fetchBlobContent({ sha, repoURL, parseText });
|
||||
}
|
||||
}
|
||||
|
||||
async getPullRequestAuthor(pullRequest: Octokit.PullsListResponseItem) {
|
||||
const user = pullRequest.user as unknown as GraphQLPullsListResponseItemUser;
|
||||
return user?.name || user?.login;
|
||||
}
|
||||
|
||||
async getPullRequests(
|
||||
head: string | undefined,
|
||||
state: PullRequestState,
|
||||
predicate: (pr: Octokit.PullsListResponseItem) => boolean,
|
||||
) {
|
||||
const { originRepoOwner: owner, originRepoName: name } = this;
|
||||
let states;
|
||||
if (state === PullRequestState.Open) {
|
||||
states = ['OPEN'];
|
||||
} else if (state === PullRequestState.Closed) {
|
||||
states = ['CLOSED', 'MERGED'];
|
||||
} else {
|
||||
states = ['OPEN', 'CLOSED', 'MERGED'];
|
||||
}
|
||||
const { data } = await this.query({
|
||||
query: queries.pullRequests,
|
||||
variables: {
|
||||
owner,
|
||||
name,
|
||||
...(head ? { head } : {}),
|
||||
states,
|
||||
},
|
||||
});
|
||||
const {
|
||||
pullRequests,
|
||||
}: {
|
||||
pullRequests: {
|
||||
nodes: GraphQLPullRequest[];
|
||||
};
|
||||
} = data.repository;
|
||||
|
||||
const mapped = pullRequests.nodes.map(transformPullRequest);
|
||||
|
||||
return (mapped as unknown as Octokit.PullsListResponseItem[]).filter(
|
||||
pr => pr.head.ref.startsWith(`${CMS_BRANCH_PREFIX}/`) && predicate(pr),
|
||||
);
|
||||
}
|
||||
|
||||
async getOpenAuthoringBranches() {
|
||||
const { repoOwner: owner, repoName: name } = this;
|
||||
const { data } = await this.query({
|
||||
query: queries.openAuthoringBranches,
|
||||
variables: {
|
||||
owner,
|
||||
name,
|
||||
refPrefix: `refs/heads/cms/${this.repo}/`,
|
||||
},
|
||||
});
|
||||
|
||||
return data.repository.refs.nodes.map(({ name, prefix }: { name: string; prefix: string }) => ({
|
||||
ref: `${prefix}${name}`,
|
||||
}));
|
||||
}
|
||||
|
||||
async getStatuses(collectionName: string, slug: string) {
|
||||
const contentKey = this.generateContentKey(collectionName, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
const sha = pullRequest.head.sha;
|
||||
const { originRepoOwner: owner, originRepoName: name } = this;
|
||||
const { data } = await this.query({ query: queries.statues, variables: { owner, name, sha } });
|
||||
if (data.repository.object) {
|
||||
const { status } = data.repository.object;
|
||||
const { contexts } = status || { contexts: [] };
|
||||
return contexts;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
getAllFiles(entries: TreeEntry[], path: string) {
|
||||
const allFiles: TreeFile[] = entries.reduce((acc, item) => {
|
||||
if (item.type === 'tree') {
|
||||
const entries = item.object?.entries || [];
|
||||
return [...acc, ...this.getAllFiles(entries, `${path}/${item.name}`)];
|
||||
} else if (item.type === 'blob') {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
id: item.sha,
|
||||
path: `${path}/${item.name}`,
|
||||
size: item.blob ? item.blob.size : 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as TreeFile[]);
|
||||
return allFiles;
|
||||
}
|
||||
|
||||
async listFiles(path: string, { repoURL = this.repoURL, branch = this.branch, depth = 1 } = {}) {
|
||||
const { owner, name } = this.getOwnerAndNameFromRepoUrl(repoURL);
|
||||
const folder = trim(path, '/');
|
||||
const { data } = await this.query({
|
||||
query: queries.files(depth),
|
||||
variables: { owner, name, expression: `${branch}:${folder}` },
|
||||
});
|
||||
|
||||
if (data.repository.object) {
|
||||
const allFiles = this.getAllFiles(data.repository.object.entries, folder);
|
||||
return allFiles;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
getBranchQualifiedName(branch: string) {
|
||||
return `refs/heads/${branch}`;
|
||||
}
|
||||
|
||||
getBranchQuery(branch: string, owner: string, name: string) {
|
||||
return {
|
||||
query: queries.branch,
|
||||
variables: {
|
||||
owner,
|
||||
name,
|
||||
qualifiedName: this.getBranchQualifiedName(branch),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getDefaultBranch() {
|
||||
const { data } = await this.query({
|
||||
...this.getBranchQuery(this.branch, this.originRepoOwner, this.originRepoName),
|
||||
});
|
||||
return data.repository.branch;
|
||||
}
|
||||
|
||||
async getBranch(branch: string) {
|
||||
const { data } = await this.query({
|
||||
...this.getBranchQuery(branch, this.repoOwner, this.repoName),
|
||||
fetchPolicy: CACHE_FIRST,
|
||||
});
|
||||
if (!data.repository.branch) {
|
||||
throw new APIError('Branch not found', 404, API_NAME);
|
||||
}
|
||||
return data.repository.branch;
|
||||
}
|
||||
|
||||
async patchRef(type: string, name: string, sha: string, opts: { force?: boolean } = {}) {
|
||||
if (type !== 'heads') {
|
||||
return super.patchRef(type, name, sha, opts);
|
||||
}
|
||||
|
||||
const force = opts.force || false;
|
||||
|
||||
const branch = await this.getBranch(name);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.updateBranch,
|
||||
variables: {
|
||||
input: { oid: sha, refId: branch.id, force },
|
||||
},
|
||||
});
|
||||
return data!.updateRef.branch;
|
||||
}
|
||||
|
||||
async deleteBranch(branchName: string) {
|
||||
const branch = await this.getBranch(branchName);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.deleteBranch,
|
||||
variables: {
|
||||
deleteRefInput: { refId: branch.id },
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
update: (store: any) => store.data.delete(defaultDataIdFromObject(branch)),
|
||||
});
|
||||
|
||||
return data!.deleteRef;
|
||||
}
|
||||
|
||||
getPullRequestQuery(number: number) {
|
||||
const { originRepoOwner: owner, originRepoName: name } = this;
|
||||
|
||||
return {
|
||||
query: queries.pullRequest,
|
||||
variables: { owner, name, number },
|
||||
};
|
||||
}
|
||||
|
||||
async getPullRequest(number: number) {
|
||||
const { data } = await this.query({
|
||||
...this.getPullRequestQuery(number),
|
||||
fetchPolicy: CACHE_FIRST,
|
||||
});
|
||||
|
||||
// https://developer.github.com/v4/enum/pullrequeststate/
|
||||
// GraphQL state: [CLOSED, MERGED, OPEN]
|
||||
// REST API state: [closed, open]
|
||||
const state =
|
||||
data.repository.pullRequest.state === 'OPEN'
|
||||
? PullRequestState.Open
|
||||
: PullRequestState.Closed;
|
||||
return {
|
||||
...data.repository.pullRequest,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
getPullRequestAndBranchQuery(branch: string, number: number) {
|
||||
const { repoOwner: owner, repoName: name } = this;
|
||||
const { originRepoOwner, originRepoName } = this;
|
||||
return {
|
||||
query: queries.pullRequestAndBranch,
|
||||
variables: {
|
||||
owner,
|
||||
name,
|
||||
originRepoOwner,
|
||||
originRepoName,
|
||||
number,
|
||||
qualifiedName: this.getBranchQualifiedName(branch),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getPullRequestAndBranch(branch: string, number: number) {
|
||||
const { data } = await this.query({
|
||||
...this.getPullRequestAndBranchQuery(branch, number),
|
||||
fetchPolicy: CACHE_FIRST,
|
||||
});
|
||||
|
||||
const { repository, origin } = data;
|
||||
return { branch: repository.branch, pullRequest: origin.pullRequest };
|
||||
}
|
||||
|
||||
async openPR(number: number) {
|
||||
const pullRequest = await this.getPullRequest(number);
|
||||
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.reopenPullRequest,
|
||||
variables: {
|
||||
reopenPullRequestInput: { pullRequestId: pullRequest.id },
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { pullRequest } = mutationResult!.reopenPullRequest;
|
||||
const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestQuery(pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return data!.reopenPullRequest;
|
||||
}
|
||||
|
||||
async closePR(number: number) {
|
||||
const pullRequest = await this.getPullRequest(number);
|
||||
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.closePullRequest,
|
||||
variables: {
|
||||
closePullRequestInput: { pullRequestId: pullRequest.id },
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { pullRequest } = mutationResult!.closePullRequest;
|
||||
const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestQuery(pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return data!.closePullRequest;
|
||||
}
|
||||
|
||||
async deleteUnpublishedEntry(collectionName: string, slug: string) {
|
||||
try {
|
||||
const contentKey = this.generateContentKey(collectionName, slug);
|
||||
const branchName = branchFromContentKey(contentKey);
|
||||
const pr = await this.getBranchPullRequest(branchName);
|
||||
if (pr.number !== MOCK_PULL_REQUEST) {
|
||||
const { branch, pullRequest } = await this.getPullRequestAndBranch(branchName, pr.number);
|
||||
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.closePullRequestAndDeleteBranch,
|
||||
variables: {
|
||||
deleteRefInput: { refId: branch.id },
|
||||
closePullRequestInput: { pullRequestId: pullRequest.id },
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
update: (store: any) => {
|
||||
store.data.delete(defaultDataIdFromObject(branch));
|
||||
store.data.delete(defaultDataIdFromObject(pullRequest));
|
||||
},
|
||||
});
|
||||
|
||||
return data!.closePullRequest;
|
||||
} else {
|
||||
return await this.deleteBranch(branchName);
|
||||
}
|
||||
} catch (e) {
|
||||
const { graphQLErrors } = e;
|
||||
if (graphQLErrors && graphQLErrors.length > 0) {
|
||||
const branchNotFound = graphQLErrors.some((e: Error) => e.type === 'NOT_FOUND');
|
||||
if (branchNotFound) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async createPR(title: string, head: string) {
|
||||
const [repository, headReference] = await Promise.all([
|
||||
this.getRepository(this.originRepoOwner, this.originRepoName),
|
||||
this.useOpenAuthoring ? `${(await this.user()).login}:${head}` : head,
|
||||
]);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.createPullRequest,
|
||||
variables: {
|
||||
createPullRequestInput: {
|
||||
baseRefName: this.branch,
|
||||
body: DEFAULT_PR_BODY,
|
||||
title,
|
||||
headRefName: headReference,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { pullRequest } = mutationResult!.createPullRequest;
|
||||
const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestQuery(pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { pullRequest } = data!.createPullRequest;
|
||||
return { ...pullRequest, head: { sha: pullRequest.headRefOid } };
|
||||
}
|
||||
|
||||
async createBranch(branchName: string, sha: string) {
|
||||
const owner = this.repoOwner;
|
||||
const name = this.repoName;
|
||||
const repository = await this.getRepository(owner, name);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.createBranch,
|
||||
variables: {
|
||||
createRefInput: {
|
||||
name: this.getBranchQualifiedName(branchName),
|
||||
oid: sha,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { branch } = mutationResult!.createRef;
|
||||
const branchData = { repository: { ...branch.repository, branch } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getBranchQuery(branchName, owner, name),
|
||||
data: branchData,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { branch } = data!.createRef;
|
||||
return { ...branch, ref: `${branch.prefix}${branch.name}` };
|
||||
}
|
||||
|
||||
async createBranchAndPullRequest(branchName: string, sha: string, title: string) {
|
||||
const owner = this.originRepoOwner;
|
||||
const name = this.originRepoName;
|
||||
const repository = await this.getRepository(owner, name);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.createBranchAndPullRequest,
|
||||
variables: {
|
||||
createRefInput: {
|
||||
name: this.getBranchQualifiedName(branchName),
|
||||
oid: sha,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
createPullRequestInput: {
|
||||
baseRefName: this.branch,
|
||||
body: DEFAULT_PR_BODY,
|
||||
title,
|
||||
headRefName: branchName,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { branch } = mutationResult!.createRef;
|
||||
const { pullRequest } = mutationResult!.createPullRequest;
|
||||
const branchData = { repository: { ...branch.repository, branch } };
|
||||
const pullRequestData = {
|
||||
repository: { ...pullRequest.repository, branch },
|
||||
origin: { ...pullRequest.repository, pullRequest },
|
||||
};
|
||||
|
||||
store.writeQuery({
|
||||
...this.getBranchQuery(branchName, owner, name),
|
||||
data: branchData,
|
||||
});
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestAndBranchQuery(branchName, pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { pullRequest } = data!.createPullRequest;
|
||||
return transformPullRequest(pullRequest) as unknown as Octokit.PullsCreateResponse;
|
||||
}
|
||||
|
||||
async getFileSha(path: string, { repoURL = this.repoURL, branch = this.branch } = {}) {
|
||||
const { owner, name } = this.getOwnerAndNameFromRepoUrl(repoURL);
|
||||
const { data } = await this.query({
|
||||
query: queries.fileSha,
|
||||
variables: { owner, name, expression: `${branch}:${path}` },
|
||||
});
|
||||
|
||||
if (data.repository.file) {
|
||||
return data.repository.file.sha;
|
||||
}
|
||||
throw new APIError('Not Found', 404, API_NAME);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,833 @@
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
import API from '../API';
|
||||
|
||||
global.fetch = jest.fn().mockRejectedValue(new Error('should not call fetch inside tests'));
|
||||
|
||||
describe('github API', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
function mockAPI(api, responses) {
|
||||
api.request = jest.fn().mockImplementation((path, options = {}) => {
|
||||
const normalizedPath = path.indexOf('?') !== -1 ? path.slice(0, path.indexOf('?')) : path;
|
||||
const response = responses[normalizedPath];
|
||||
return typeof response === 'function'
|
||||
? Promise.resolve(response(options))
|
||||
: Promise.reject(new Error(`No response for path '${normalizedPath}'`));
|
||||
});
|
||||
}
|
||||
|
||||
describe('editorialWorkflowGit', () => {
|
||||
it('should create PR with correct base branch name when publishing with editorial workflow', () => {
|
||||
let prBaseBranch = null;
|
||||
let labels = null;
|
||||
const api = new API({
|
||||
branch: 'gh-pages',
|
||||
repo: 'owner/my-repo',
|
||||
initialWorkflowStatus: 'draft',
|
||||
});
|
||||
const responses = {
|
||||
'/repos/owner/my-repo/branches/gh-pages': () => ({ commit: { sha: 'def' } }),
|
||||
'/repos/owner/my-repo/git/trees/def': () => ({ tree: [] }),
|
||||
'/repos/owner/my-repo/git/trees': () => ({}),
|
||||
'/repos/owner/my-repo/git/commits': () => ({}),
|
||||
'/repos/owner/my-repo/git/refs': () => ({}),
|
||||
'/repos/owner/my-repo/pulls': req => {
|
||||
prBaseBranch = JSON.parse(req.body).base;
|
||||
return { head: { sha: 'cbd' }, labels: [], number: 1 };
|
||||
},
|
||||
'/repos/owner/my-repo/issues/1/labels': req => {
|
||||
labels = JSON.parse(req.body).labels;
|
||||
return {};
|
||||
},
|
||||
};
|
||||
mockAPI(api, responses);
|
||||
|
||||
return expect(
|
||||
api.editorialWorkflowGit([], { slug: 'entry', sha: 'abc' }, null, {}).then(() => ({
|
||||
prBaseBranch,
|
||||
labels,
|
||||
})),
|
||||
).resolves.toEqual({ prBaseBranch: 'gh-pages', labels: ['decap-cms/draft'] });
|
||||
});
|
||||
|
||||
it('should create PR with correct base branch name with custom prefix when publishing with editorial workflow', () => {
|
||||
let prBaseBranch = null;
|
||||
let labels = null;
|
||||
const api = new API({
|
||||
branch: 'gh-pages',
|
||||
repo: 'owner/my-repo',
|
||||
initialWorkflowStatus: 'draft',
|
||||
cmsLabelPrefix: 'other/',
|
||||
});
|
||||
const responses = {
|
||||
'/repos/owner/my-repo/branches/gh-pages': () => ({ commit: { sha: 'def' } }),
|
||||
'/repos/owner/my-repo/git/trees/def': () => ({ tree: [] }),
|
||||
'/repos/owner/my-repo/git/trees': () => ({}),
|
||||
'/repos/owner/my-repo/git/commits': () => ({}),
|
||||
'/repos/owner/my-repo/git/refs': () => ({}),
|
||||
'/repos/owner/my-repo/pulls': req => {
|
||||
prBaseBranch = JSON.parse(req.body).base;
|
||||
return { head: { sha: 'cbd' }, labels: [], number: 1 };
|
||||
},
|
||||
'/repos/owner/my-repo/issues/1/labels': req => {
|
||||
labels = JSON.parse(req.body).labels;
|
||||
return {};
|
||||
},
|
||||
};
|
||||
mockAPI(api, responses);
|
||||
|
||||
return expect(
|
||||
api.editorialWorkflowGit([], { slug: 'entry', sha: 'abc' }, null, {}).then(() => ({
|
||||
prBaseBranch,
|
||||
labels,
|
||||
})),
|
||||
).resolves.toEqual({ prBaseBranch: 'gh-pages', labels: ['other/draft'] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTree', () => {
|
||||
it('should create tree with nested paths', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
api.createTree = jest.fn().mockImplementation(() => Promise.resolve({ sha: 'newTreeSha' }));
|
||||
|
||||
const files = [
|
||||
{ path: '/static/media/new-image.jpeg', sha: null },
|
||||
{ path: 'content/posts/new-post.md', sha: 'new-post.md' },
|
||||
];
|
||||
|
||||
const baseTreeSha = 'baseTreeSha';
|
||||
|
||||
await expect(api.updateTree(baseTreeSha, files)).resolves.toEqual({
|
||||
sha: 'newTreeSha',
|
||||
parentSha: baseTreeSha,
|
||||
});
|
||||
|
||||
expect(api.createTree).toHaveBeenCalledTimes(1);
|
||||
expect(api.createTree).toHaveBeenCalledWith(baseTreeSha, [
|
||||
{
|
||||
path: 'static/media/new-image.jpeg',
|
||||
mode: '100644',
|
||||
type: 'blob',
|
||||
sha: null,
|
||||
},
|
||||
{
|
||||
path: 'content/posts/new-post.md',
|
||||
mode: '100644',
|
||||
type: 'blob',
|
||||
sha: 'new-post.md',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('request', () => {
|
||||
beforeEach(() => {
|
||||
const fetch = jest.fn();
|
||||
global.fetch = fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fetch url with authorization header', async () => {
|
||||
const api = new API({ branch: 'gh-pages', repo: 'my-repo', token: 'token' });
|
||||
|
||||
fetch.mockResolvedValue({
|
||||
text: jest.fn().mockResolvedValue('some response'),
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { get: () => '' },
|
||||
});
|
||||
const result = await api.request('/some-path');
|
||||
expect(result).toEqual('some response');
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(fetch).toHaveBeenCalledWith('https://api.github.com/some-path', {
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
Authorization: 'token token',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error on not ok response', async () => {
|
||||
const api = new API({ branch: 'gh-pages', repo: 'my-repo', token: 'token' });
|
||||
|
||||
fetch.mockResolvedValue({
|
||||
text: jest.fn().mockResolvedValue({ message: 'some error' }),
|
||||
ok: false,
|
||||
status: 404,
|
||||
headers: { get: () => '' },
|
||||
});
|
||||
|
||||
await expect(api.request('some-path')).rejects.toThrow(
|
||||
expect.objectContaining({
|
||||
message: 'some error',
|
||||
name: 'API_ERROR',
|
||||
status: 404,
|
||||
api: 'GitHub',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow overriding requestHeaders to return a promise ', async () => {
|
||||
const api = new API({ branch: 'gh-pages', repo: 'my-repo', token: 'token' });
|
||||
|
||||
api.requestHeaders = jest.fn().mockResolvedValue({
|
||||
Authorization: 'promise-token',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
});
|
||||
|
||||
fetch.mockResolvedValue({
|
||||
text: jest.fn().mockResolvedValue('some response'),
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { get: () => '' },
|
||||
});
|
||||
const result = await api.request('/some-path');
|
||||
expect(result).toEqual('some response');
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(fetch).toHaveBeenCalledWith('https://api.github.com/some-path', {
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
Authorization: 'promise-token',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('persistFiles', () => {
|
||||
it('should update tree, commit and patch branch when useWorkflow is false', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const responses = {
|
||||
// upload the file
|
||||
'/repos/owner/repo/git/blobs': () => ({ sha: 'new-file-sha' }),
|
||||
|
||||
// get the branch
|
||||
'/repos/owner/repo/branches/master': () => ({ commit: { sha: 'root' } }),
|
||||
|
||||
// create new tree
|
||||
'/repos/owner/repo/git/trees': options => {
|
||||
const data = JSON.parse(options.body);
|
||||
return { sha: data.base_tree };
|
||||
},
|
||||
|
||||
// update the commit with the tree
|
||||
'/repos/owner/repo/git/commits': () => ({ sha: 'commit-sha' }),
|
||||
|
||||
// patch the branch
|
||||
'/repos/owner/repo/git/refs/heads/master': () => ({}),
|
||||
};
|
||||
mockAPI(api, responses);
|
||||
|
||||
const entry = {
|
||||
dataFiles: [
|
||||
{
|
||||
slug: 'entry',
|
||||
sha: 'abc',
|
||||
path: 'content/posts/new-post.md',
|
||||
raw: 'content',
|
||||
},
|
||||
],
|
||||
assets: [],
|
||||
};
|
||||
await api.persistFiles(entry.dataFiles, entry.assets, { commitMessage: 'commitMessage' });
|
||||
|
||||
expect(api.request).toHaveBeenCalledTimes(5);
|
||||
|
||||
expect(api.request.mock.calls[0]).toEqual([
|
||||
'/repos/owner/repo/git/blobs',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
content: Base64.encode(entry.dataFiles[0].raw),
|
||||
encoding: 'base64',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
expect(api.request.mock.calls[1]).toEqual(['/repos/owner/repo/branches/master']);
|
||||
|
||||
expect(api.request.mock.calls[2]).toEqual([
|
||||
'/repos/owner/repo/git/trees',
|
||||
{
|
||||
body: JSON.stringify({
|
||||
base_tree: 'root',
|
||||
tree: [
|
||||
{
|
||||
path: 'content/posts/new-post.md',
|
||||
mode: '100644',
|
||||
type: 'blob',
|
||||
sha: 'new-file-sha',
|
||||
},
|
||||
],
|
||||
}),
|
||||
method: 'POST',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(api.request.mock.calls[3]).toEqual([
|
||||
'/repos/owner/repo/git/commits',
|
||||
{
|
||||
body: JSON.stringify({
|
||||
message: 'commitMessage',
|
||||
tree: 'root',
|
||||
parents: ['root'],
|
||||
}),
|
||||
method: 'POST',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(api.request.mock.calls[4]).toEqual([
|
||||
'/repos/owner/repo/git/refs/heads/master',
|
||||
{
|
||||
body: JSON.stringify({
|
||||
sha: 'commit-sha',
|
||||
force: false,
|
||||
}),
|
||||
method: 'PATCH',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call editorialWorkflowGit when useWorkflow is true', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
api.uploadBlob = jest.fn();
|
||||
api.editorialWorkflowGit = jest.fn();
|
||||
|
||||
const entry = {
|
||||
dataFiles: [
|
||||
{
|
||||
slug: 'entry',
|
||||
sha: 'abc',
|
||||
path: 'content/posts/new-post.md',
|
||||
raw: 'content',
|
||||
},
|
||||
],
|
||||
assets: [
|
||||
{
|
||||
path: '/static/media/image-1.png',
|
||||
sha: 'image-1.png',
|
||||
},
|
||||
{
|
||||
path: '/static/media/image-2.png',
|
||||
sha: 'image-2.png',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await api.persistFiles(entry.dataFiles, entry.assets, { useWorkflow: true });
|
||||
|
||||
expect(api.uploadBlob).toHaveBeenCalledTimes(3);
|
||||
expect(api.uploadBlob).toHaveBeenCalledWith(entry.dataFiles[0]);
|
||||
expect(api.uploadBlob).toHaveBeenCalledWith(entry.assets[0]);
|
||||
expect(api.uploadBlob).toHaveBeenCalledWith(entry.assets[1]);
|
||||
|
||||
expect(api.editorialWorkflowGit).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(api.editorialWorkflowGit).toHaveBeenCalledWith(
|
||||
entry.assets.concat(entry.dataFiles),
|
||||
entry.dataFiles[0].slug,
|
||||
[
|
||||
{ path: 'static/media/image-1.png', sha: 'image-1.png' },
|
||||
{ path: 'static/media/image-2.png', sha: 'image-2.png' },
|
||||
],
|
||||
{ useWorkflow: true },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('migratePullRequest', () => {
|
||||
it('should migrate to pull request labels when no version', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const pr = {
|
||||
head: { ref: 'cms/2019-11-11-post-title' },
|
||||
title: 'pr title',
|
||||
number: 1,
|
||||
labels: [],
|
||||
};
|
||||
const metadata = { type: 'PR' };
|
||||
api.retrieveMetadataOld = jest.fn().mockResolvedValue(metadata);
|
||||
const newBranch = 'cms/posts/2019-11-11-post-title';
|
||||
const migrateToVersion1Result = {
|
||||
metadata: { ...metadata, branch: newBranch, version: '1' },
|
||||
pullRequest: { ...pr, number: 2 },
|
||||
};
|
||||
api.migrateToVersion1 = jest.fn().mockResolvedValue(migrateToVersion1Result);
|
||||
api.migrateToPullRequestLabels = jest.fn();
|
||||
|
||||
await api.migratePullRequest(pr);
|
||||
|
||||
expect(api.migrateToVersion1).toHaveBeenCalledTimes(1);
|
||||
expect(api.migrateToVersion1).toHaveBeenCalledWith(pr, metadata);
|
||||
|
||||
expect(api.migrateToPullRequestLabels).toHaveBeenCalledTimes(1);
|
||||
expect(api.migrateToPullRequestLabels).toHaveBeenCalledWith(
|
||||
migrateToVersion1Result.pullRequest,
|
||||
migrateToVersion1Result.metadata,
|
||||
);
|
||||
|
||||
expect(api.retrieveMetadataOld).toHaveBeenCalledTimes(1);
|
||||
expect(api.retrieveMetadataOld).toHaveBeenCalledWith('2019-11-11-post-title');
|
||||
});
|
||||
|
||||
it('should migrate to pull request labels when version is 1', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
api.migrateToVersion1 = jest.fn();
|
||||
const pr = {
|
||||
head: { ref: 'cms/posts/2019-11-11-post-title' },
|
||||
title: 'pr title',
|
||||
number: 1,
|
||||
labels: [],
|
||||
};
|
||||
const metadata = { type: 'PR', version: '1' };
|
||||
api.retrieveMetadataOld = jest.fn().mockResolvedValue(metadata);
|
||||
api.migrateToPullRequestLabels = jest.fn().mockResolvedValue(pr, metadata);
|
||||
|
||||
await api.migratePullRequest(pr);
|
||||
|
||||
expect(api.migrateToVersion1).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(api.migrateToPullRequestLabels).toHaveBeenCalledTimes(1);
|
||||
expect(api.migrateToPullRequestLabels).toHaveBeenCalledWith(pr, metadata);
|
||||
|
||||
expect(api.retrieveMetadataOld).toHaveBeenCalledTimes(1);
|
||||
expect(api.retrieveMetadataOld).toHaveBeenCalledWith('posts/2019-11-11-post-title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateToVersion1', () => {
|
||||
it('should migrate to version 1', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const pr = {
|
||||
head: { ref: 'cms/2019-11-11-post-title', sha: 'pr_head' },
|
||||
title: 'pr title',
|
||||
number: 1,
|
||||
labels: [],
|
||||
};
|
||||
|
||||
const newBranch = { ref: 'refs/heads/cms/posts/2019-11-11-post-title' };
|
||||
api.createBranch = jest.fn().mockResolvedValue(newBranch);
|
||||
api.getBranch = jest.fn().mockRejectedValue(new Error('Branch not found'));
|
||||
|
||||
const newPr = { ...pr, number: 2 };
|
||||
api.createPR = jest.fn().mockResolvedValue(newPr);
|
||||
api.getPullRequests = jest.fn().mockResolvedValue([]);
|
||||
|
||||
api.storeMetadata = jest.fn();
|
||||
api.closePR = jest.fn();
|
||||
api.deleteBranch = jest.fn();
|
||||
api.deleteMetadata = jest.fn();
|
||||
|
||||
const branch = 'cms/2019-11-11-post-title';
|
||||
const metadata = {
|
||||
branch,
|
||||
type: 'PR',
|
||||
pr: { head: pr.head.sha },
|
||||
commitMessage: 'commitMessage',
|
||||
collection: 'posts',
|
||||
};
|
||||
|
||||
const expectedMetadata = {
|
||||
type: 'PR',
|
||||
pr: { head: newPr.head.sha, number: 2 },
|
||||
commitMessage: 'commitMessage',
|
||||
collection: 'posts',
|
||||
branch: 'cms/posts/2019-11-11-post-title',
|
||||
version: '1',
|
||||
};
|
||||
await expect(api.migrateToVersion1(pr, metadata)).resolves.toEqual({
|
||||
metadata: expectedMetadata,
|
||||
pullRequest: newPr,
|
||||
});
|
||||
|
||||
expect(api.getBranch).toHaveBeenCalledTimes(1);
|
||||
expect(api.getBranch).toHaveBeenCalledWith('cms/posts/2019-11-11-post-title');
|
||||
expect(api.createBranch).toHaveBeenCalledTimes(1);
|
||||
expect(api.createBranch).toHaveBeenCalledWith('cms/posts/2019-11-11-post-title', 'pr_head');
|
||||
|
||||
expect(api.getPullRequests).toHaveBeenCalledTimes(1);
|
||||
expect(api.getPullRequests).toHaveBeenCalledWith(
|
||||
'cms/posts/2019-11-11-post-title',
|
||||
'all',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(api.createPR).toHaveBeenCalledTimes(1);
|
||||
expect(api.createPR).toHaveBeenCalledWith('pr title', 'cms/posts/2019-11-11-post-title');
|
||||
|
||||
expect(api.storeMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(api.storeMetadata).toHaveBeenCalledWith(
|
||||
'posts/2019-11-11-post-title',
|
||||
expectedMetadata,
|
||||
);
|
||||
|
||||
expect(api.closePR).toHaveBeenCalledTimes(1);
|
||||
expect(api.closePR).toHaveBeenCalledWith(pr.number);
|
||||
|
||||
expect(api.deleteBranch).toHaveBeenCalledTimes(1);
|
||||
expect(api.deleteBranch).toHaveBeenCalledWith('cms/2019-11-11-post-title');
|
||||
|
||||
expect(api.deleteMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(api.deleteMetadata).toHaveBeenCalledWith('2019-11-11-post-title');
|
||||
});
|
||||
|
||||
it('should not create new branch if exists', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const pr = {
|
||||
head: { ref: 'cms/2019-11-11-post-title', sha: 'pr_head' },
|
||||
title: 'pr title',
|
||||
number: 1,
|
||||
labels: [],
|
||||
};
|
||||
|
||||
const newBranch = { ref: 'refs/heads/cms/posts/2019-11-11-post-title' };
|
||||
api.createBranch = jest.fn();
|
||||
api.getBranch = jest.fn().mockResolvedValue(newBranch);
|
||||
|
||||
const newPr = { ...pr, number: 2 };
|
||||
api.createPR = jest.fn().mockResolvedValue(newPr);
|
||||
api.getPullRequests = jest.fn().mockResolvedValue([]);
|
||||
|
||||
api.storeMetadata = jest.fn();
|
||||
api.closePR = jest.fn();
|
||||
api.deleteBranch = jest.fn();
|
||||
api.deleteMetadata = jest.fn();
|
||||
|
||||
const branch = 'cms/2019-11-11-post-title';
|
||||
const metadata = {
|
||||
branch,
|
||||
type: 'PR',
|
||||
pr: { head: pr.head.sha },
|
||||
commitMessage: 'commitMessage',
|
||||
collection: 'posts',
|
||||
};
|
||||
|
||||
const expectedMetadata = {
|
||||
type: 'PR',
|
||||
pr: { head: newPr.head.sha, number: 2 },
|
||||
commitMessage: 'commitMessage',
|
||||
collection: 'posts',
|
||||
branch: 'cms/posts/2019-11-11-post-title',
|
||||
version: '1',
|
||||
};
|
||||
await expect(api.migrateToVersion1(pr, metadata)).resolves.toEqual({
|
||||
metadata: expectedMetadata,
|
||||
pullRequest: newPr,
|
||||
});
|
||||
|
||||
expect(api.getBranch).toHaveBeenCalledTimes(1);
|
||||
expect(api.getBranch).toHaveBeenCalledWith('cms/posts/2019-11-11-post-title');
|
||||
expect(api.createBranch).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(api.getPullRequests).toHaveBeenCalledTimes(1);
|
||||
expect(api.getPullRequests).toHaveBeenCalledWith(
|
||||
'cms/posts/2019-11-11-post-title',
|
||||
'all',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(api.createPR).toHaveBeenCalledTimes(1);
|
||||
expect(api.createPR).toHaveBeenCalledWith('pr title', 'cms/posts/2019-11-11-post-title');
|
||||
|
||||
expect(api.storeMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(api.storeMetadata).toHaveBeenCalledWith(
|
||||
'posts/2019-11-11-post-title',
|
||||
expectedMetadata,
|
||||
);
|
||||
|
||||
expect(api.closePR).toHaveBeenCalledTimes(1);
|
||||
expect(api.closePR).toHaveBeenCalledWith(pr.number);
|
||||
|
||||
expect(api.deleteBranch).toHaveBeenCalledTimes(1);
|
||||
expect(api.deleteBranch).toHaveBeenCalledWith('cms/2019-11-11-post-title');
|
||||
|
||||
expect(api.deleteMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(api.deleteMetadata).toHaveBeenCalledWith('2019-11-11-post-title');
|
||||
});
|
||||
|
||||
it('should not create new pr if exists', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const pr = {
|
||||
head: { ref: 'cms/2019-11-11-post-title', sha: 'pr_head' },
|
||||
title: 'pr title',
|
||||
number: 1,
|
||||
labels: [],
|
||||
};
|
||||
|
||||
const newBranch = { ref: 'refs/heads/cms/posts/2019-11-11-post-title' };
|
||||
api.createBranch = jest.fn();
|
||||
api.getBranch = jest.fn().mockResolvedValue(newBranch);
|
||||
|
||||
const newPr = { ...pr, number: 2 };
|
||||
api.createPR = jest.fn();
|
||||
api.getPullRequests = jest.fn().mockResolvedValue([newPr]);
|
||||
|
||||
api.storeMetadata = jest.fn();
|
||||
api.closePR = jest.fn();
|
||||
api.deleteBranch = jest.fn();
|
||||
api.deleteMetadata = jest.fn();
|
||||
|
||||
const branch = 'cms/2019-11-11-post-title';
|
||||
const metadata = {
|
||||
branch,
|
||||
type: 'PR',
|
||||
pr: { head: pr.head.sha },
|
||||
commitMessage: 'commitMessage',
|
||||
collection: 'posts',
|
||||
};
|
||||
|
||||
const expectedMetadata = {
|
||||
type: 'PR',
|
||||
pr: { head: newPr.head.sha, number: 2 },
|
||||
commitMessage: 'commitMessage',
|
||||
collection: 'posts',
|
||||
branch: 'cms/posts/2019-11-11-post-title',
|
||||
version: '1',
|
||||
};
|
||||
await expect(api.migrateToVersion1(pr, metadata)).resolves.toEqual({
|
||||
metadata: expectedMetadata,
|
||||
pullRequest: newPr,
|
||||
});
|
||||
|
||||
expect(api.getBranch).toHaveBeenCalledTimes(1);
|
||||
expect(api.getBranch).toHaveBeenCalledWith('cms/posts/2019-11-11-post-title');
|
||||
expect(api.createBranch).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(api.getPullRequests).toHaveBeenCalledTimes(1);
|
||||
expect(api.getPullRequests).toHaveBeenCalledWith(
|
||||
'cms/posts/2019-11-11-post-title',
|
||||
'all',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(api.createPR).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(api.storeMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(api.storeMetadata).toHaveBeenCalledWith(
|
||||
'posts/2019-11-11-post-title',
|
||||
expectedMetadata,
|
||||
);
|
||||
|
||||
expect(api.closePR).toHaveBeenCalledTimes(1);
|
||||
expect(api.closePR).toHaveBeenCalledWith(pr.number);
|
||||
|
||||
expect(api.deleteBranch).toHaveBeenCalledTimes(1);
|
||||
expect(api.deleteBranch).toHaveBeenCalledWith('cms/2019-11-11-post-title');
|
||||
|
||||
expect(api.deleteMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(api.deleteMetadata).toHaveBeenCalledWith('2019-11-11-post-title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateToPullRequestLabels', () => {
|
||||
it('should migrate to pull request labels', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const pr = {
|
||||
head: { ref: 'cms/posts/2019-11-11-post-title', sha: 'pr_head' },
|
||||
title: 'pr title',
|
||||
number: 1,
|
||||
labels: [],
|
||||
};
|
||||
|
||||
api.setPullRequestStatus = jest.fn();
|
||||
api.deleteMetadata = jest.fn();
|
||||
|
||||
const metadata = {
|
||||
branch: pr.head.ref,
|
||||
type: 'PR',
|
||||
pr: { head: pr.head.sha },
|
||||
commitMessage: 'commitMessage',
|
||||
collection: 'posts',
|
||||
status: 'pending_review',
|
||||
};
|
||||
|
||||
await api.migrateToPullRequestLabels(pr, metadata);
|
||||
|
||||
expect(api.setPullRequestStatus).toHaveBeenCalledTimes(1);
|
||||
expect(api.setPullRequestStatus).toHaveBeenCalledWith(pr, 'pending_review');
|
||||
|
||||
expect(api.deleteMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(api.deleteMetadata).toHaveBeenCalledWith('posts/2019-11-11-post-title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('rebaseSingleCommit', () => {
|
||||
it('should create updated tree and commit', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
api.getDifferences = jest.fn().mockResolvedValueOnce({
|
||||
files: [
|
||||
{ filename: 'removed.md', status: 'removed', sha: 'removed_sha' },
|
||||
{
|
||||
filename: 'renamed.md',
|
||||
status: 'renamed',
|
||||
previous_filename: 'previous_filename.md',
|
||||
sha: 'renamed_sha',
|
||||
},
|
||||
{ filename: 'added.md', status: 'added', sha: 'added_sha' },
|
||||
],
|
||||
});
|
||||
|
||||
const newTree = { sha: 'new_tree_sha' };
|
||||
api.updateTree = jest.fn().mockResolvedValueOnce(newTree);
|
||||
|
||||
const newCommit = { sha: 'newCommit' };
|
||||
api.createCommit = jest.fn().mockResolvedValueOnce(newCommit);
|
||||
|
||||
const baseCommit = { sha: 'base_commit_sha' };
|
||||
const commit = {
|
||||
sha: 'sha',
|
||||
parents: [{ sha: 'parent_sha' }],
|
||||
commit: {
|
||||
message: 'message',
|
||||
author: { name: 'author' },
|
||||
committer: { name: 'committer' },
|
||||
},
|
||||
};
|
||||
|
||||
await expect(api.rebaseSingleCommit(baseCommit, commit)).resolves.toBe(newCommit);
|
||||
|
||||
expect(api.getDifferences).toHaveBeenCalledTimes(1);
|
||||
expect(api.getDifferences).toHaveBeenCalledWith('parent_sha', 'sha');
|
||||
|
||||
expect(api.updateTree).toHaveBeenCalledTimes(1);
|
||||
expect(api.updateTree).toHaveBeenCalledWith('base_commit_sha', [
|
||||
{ path: 'removed.md', sha: null },
|
||||
{ path: 'previous_filename.md', sha: null },
|
||||
{ path: 'renamed.md', sha: 'renamed_sha' },
|
||||
{ path: 'added.md', sha: 'added_sha' },
|
||||
]);
|
||||
|
||||
expect(api.createCommit).toHaveBeenCalledTimes(1);
|
||||
expect(api.createCommit).toHaveBeenCalledWith(
|
||||
'message',
|
||||
newTree.sha,
|
||||
[baseCommit.sha],
|
||||
{ name: 'author' },
|
||||
{ name: 'committer' },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listFiles', () => {
|
||||
it('should get files by depth', async () => {
|
||||
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
||||
|
||||
const tree = [
|
||||
{
|
||||
path: 'post.md',
|
||||
type: 'blob',
|
||||
},
|
||||
{
|
||||
path: 'dir1',
|
||||
type: 'tree',
|
||||
},
|
||||
{
|
||||
path: 'dir1/nested-post.md',
|
||||
type: 'blob',
|
||||
},
|
||||
{
|
||||
path: 'dir1/dir2',
|
||||
type: 'tree',
|
||||
},
|
||||
{
|
||||
path: 'dir1/dir2/nested-post.md',
|
||||
type: 'blob',
|
||||
},
|
||||
];
|
||||
api.request = jest.fn().mockResolvedValue({ tree });
|
||||
|
||||
await expect(api.listFiles('posts', { depth: 1 })).resolves.toEqual([
|
||||
{
|
||||
path: 'posts/post.md',
|
||||
type: 'blob',
|
||||
name: 'post.md',
|
||||
},
|
||||
]);
|
||||
expect(api.request).toHaveBeenCalledTimes(1);
|
||||
expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:posts', {
|
||||
params: {},
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
await expect(api.listFiles('posts', { depth: 2 })).resolves.toEqual([
|
||||
{
|
||||
path: 'posts/post.md',
|
||||
type: 'blob',
|
||||
name: 'post.md',
|
||||
},
|
||||
{
|
||||
path: 'posts/dir1/nested-post.md',
|
||||
type: 'blob',
|
||||
name: 'nested-post.md',
|
||||
},
|
||||
]);
|
||||
expect(api.request).toHaveBeenCalledTimes(1);
|
||||
expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:posts', {
|
||||
params: { recursive: 1 },
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
await expect(api.listFiles('posts', { depth: 3 })).resolves.toEqual([
|
||||
{
|
||||
path: 'posts/post.md',
|
||||
type: 'blob',
|
||||
name: 'post.md',
|
||||
},
|
||||
{
|
||||
path: 'posts/dir1/nested-post.md',
|
||||
type: 'blob',
|
||||
name: 'nested-post.md',
|
||||
},
|
||||
{
|
||||
path: 'posts/dir1/dir2/nested-post.md',
|
||||
type: 'blob',
|
||||
name: 'nested-post.md',
|
||||
},
|
||||
]);
|
||||
expect(api.request).toHaveBeenCalledTimes(1);
|
||||
expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:posts', {
|
||||
params: { recursive: 1 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should get preview statuses', async () => {
|
||||
const api = new API({ repo: 'repo' });
|
||||
|
||||
const statuses = [
|
||||
{ context: 'deploy', state: 'success', target_url: 'deploy-url' },
|
||||
{ context: 'build', state: 'error' },
|
||||
];
|
||||
|
||||
api.request = jest.fn(() => Promise.resolve({ statuses }));
|
||||
const sha = 'sha';
|
||||
api.getBranchPullRequest = jest.fn(() => Promise.resolve({ head: { sha } }));
|
||||
|
||||
const collection = 'collection';
|
||||
const slug = 'slug';
|
||||
await expect(api.getStatuses(collection, slug)).resolves.toEqual([
|
||||
{ context: 'deploy', state: 'success', target_url: 'deploy-url' },
|
||||
{ context: 'build', state: 'other' },
|
||||
]);
|
||||
|
||||
expect(api.getBranchPullRequest).toHaveBeenCalledTimes(1);
|
||||
expect(api.getBranchPullRequest).toHaveBeenCalledWith('cms/collection/slug');
|
||||
expect(api.request).toHaveBeenCalledTimes(1);
|
||||
expect(api.request).toHaveBeenCalledWith(`/repos/repo/commits/${sha}/status`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import GraphQLAPI from '../GraphQLAPI';
|
||||
|
||||
global.fetch = jest.fn().mockRejectedValue(new Error('should not call fetch inside tests'));
|
||||
|
||||
describe('github GraphQL API', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('editorialWorkflowGit', () => {
|
||||
it('should should flatten nested tree into a list of files', () => {
|
||||
const api = new GraphQLAPI({ branch: 'gh-pages', repo: 'owner/my-repo' });
|
||||
const entries = [
|
||||
{
|
||||
name: 'post-1.md',
|
||||
sha: 'sha-1',
|
||||
type: 'blob',
|
||||
blob: { size: 1 },
|
||||
},
|
||||
{
|
||||
name: 'post-2.md',
|
||||
sha: 'sha-2',
|
||||
type: 'blob',
|
||||
blob: { size: 2 },
|
||||
},
|
||||
{
|
||||
name: '2019',
|
||||
sha: 'dir-sha',
|
||||
type: 'tree',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
name: 'nested-post.md',
|
||||
sha: 'nested-post-sha',
|
||||
type: 'blob',
|
||||
blob: { size: 3 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
const path = 'posts';
|
||||
|
||||
expect(api.getAllFiles(entries, path)).toEqual([
|
||||
{
|
||||
name: 'post-1.md',
|
||||
id: 'sha-1',
|
||||
type: 'blob',
|
||||
size: 1,
|
||||
path: 'posts/post-1.md',
|
||||
},
|
||||
{
|
||||
name: 'post-2.md',
|
||||
id: 'sha-2',
|
||||
type: 'blob',
|
||||
size: 2,
|
||||
path: 'posts/post-2.md',
|
||||
},
|
||||
{
|
||||
name: 'nested-post.md',
|
||||
id: 'nested-post-sha',
|
||||
type: 'blob',
|
||||
size: 3,
|
||||
path: 'posts/2019/nested-post.md',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,361 @@
|
||||
import { Cursor, CURSOR_COMPATIBILITY_SYMBOL } from 'decap-cms-lib-util';
|
||||
|
||||
import GitHubImplementation from '../implementation';
|
||||
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
describe('github backend implementation', () => {
|
||||
const config = {
|
||||
backend: {
|
||||
repo: 'owner/repo',
|
||||
open_authoring: false,
|
||||
api_root: 'https://api.github.com',
|
||||
},
|
||||
};
|
||||
|
||||
const createObjectURL = jest.fn();
|
||||
global.URL = {
|
||||
createObjectURL,
|
||||
};
|
||||
|
||||
createObjectURL.mockReturnValue('displayURL');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('forkExists', () => {
|
||||
it('should return true when repo is fork and parent matches originRepo', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.currentUser = jest.fn().mockResolvedValue({ login: 'login' });
|
||||
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
// matching should be case-insensitive
|
||||
json: () => ({ fork: true, parent: { full_name: 'OWNER/REPO' } }),
|
||||
});
|
||||
|
||||
await expect(gitHubImplementation.forkExists({ token: 'token' })).resolves.toBe(true);
|
||||
|
||||
expect(gitHubImplementation.currentUser).toHaveBeenCalledTimes(1);
|
||||
expect(gitHubImplementation.currentUser).toHaveBeenCalledWith({ token: 'token' });
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://api.github.com/repos/login/repo', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'token token',
|
||||
},
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false when repo is not a fork', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.currentUser = jest.fn().mockResolvedValue({ login: 'login' });
|
||||
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
// matching should be case-insensitive
|
||||
json: () => ({ fork: false }),
|
||||
});
|
||||
|
||||
expect.assertions(1);
|
||||
await expect(gitHubImplementation.forkExists({ token: 'token' })).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when parent doesn't match originRepo", async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.currentUser = jest.fn().mockResolvedValue({ login: 'login' });
|
||||
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
json: () => ({ fork: true, parent: { full_name: 'owner/other_repo' } }),
|
||||
});
|
||||
|
||||
expect.assertions(1);
|
||||
await expect(gitHubImplementation.forkExists({ token: 'token' })).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('persistMedia', () => {
|
||||
const persistFiles = jest.fn();
|
||||
const mockAPI = {
|
||||
persistFiles,
|
||||
};
|
||||
|
||||
persistFiles.mockImplementation((_, files) => {
|
||||
files.forEach((file, index) => {
|
||||
file.sha = index;
|
||||
});
|
||||
});
|
||||
|
||||
it('should persist media file', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.api = mockAPI;
|
||||
|
||||
const mediaFile = {
|
||||
fileObj: { size: 100, name: 'image.png' },
|
||||
path: '/media/image.png',
|
||||
};
|
||||
|
||||
expect.assertions(5);
|
||||
await expect(gitHubImplementation.persistMedia(mediaFile, {})).resolves.toEqual({
|
||||
id: 0,
|
||||
name: 'image.png',
|
||||
size: 100,
|
||||
displayURL: 'displayURL',
|
||||
path: 'media/image.png',
|
||||
});
|
||||
|
||||
expect(persistFiles).toHaveBeenCalledTimes(1);
|
||||
expect(persistFiles).toHaveBeenCalledWith([], [mediaFile], {});
|
||||
expect(createObjectURL).toHaveBeenCalledTimes(1);
|
||||
expect(createObjectURL).toHaveBeenCalledWith(mediaFile.fileObj);
|
||||
});
|
||||
|
||||
it('should log and throw error on "persistFiles" error', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.api = mockAPI;
|
||||
|
||||
const error = new Error('failed to persist files');
|
||||
persistFiles.mockRejectedValue(error);
|
||||
|
||||
const mediaFile = {
|
||||
value: 'image.png',
|
||||
fileObj: { size: 100 },
|
||||
path: '/media/image.png',
|
||||
};
|
||||
|
||||
expect.assertions(5);
|
||||
await expect(gitHubImplementation.persistMedia(mediaFile)).rejects.toThrowError(error);
|
||||
|
||||
expect(persistFiles).toHaveBeenCalledTimes(1);
|
||||
expect(createObjectURL).toHaveBeenCalledTimes(0);
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
expect(console.error).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unpublishedEntry', () => {
|
||||
const generateContentKey = jest.fn();
|
||||
const retrieveUnpublishedEntryData = jest.fn();
|
||||
|
||||
const mockAPI = {
|
||||
generateContentKey,
|
||||
retrieveUnpublishedEntryData,
|
||||
};
|
||||
|
||||
it('should return unpublished entry data', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.api = mockAPI;
|
||||
gitHubImplementation.loadEntryMediaFiles = jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ path: 'image.png', id: 'sha' }]);
|
||||
|
||||
generateContentKey.mockReturnValue('contentKey');
|
||||
|
||||
const data = {
|
||||
collection: 'collection',
|
||||
slug: 'slug',
|
||||
status: 'draft',
|
||||
diffs: [],
|
||||
updatedAt: 'updatedAt',
|
||||
};
|
||||
retrieveUnpublishedEntryData.mockResolvedValue(data);
|
||||
|
||||
const collection = 'posts';
|
||||
const slug = 'slug';
|
||||
await expect(gitHubImplementation.unpublishedEntry({ collection, slug })).resolves.toEqual(
|
||||
data,
|
||||
);
|
||||
|
||||
expect(generateContentKey).toHaveBeenCalledTimes(1);
|
||||
expect(generateContentKey).toHaveBeenCalledWith('posts', 'slug');
|
||||
|
||||
expect(retrieveUnpublishedEntryData).toHaveBeenCalledTimes(1);
|
||||
expect(retrieveUnpublishedEntryData).toHaveBeenCalledWith('contentKey');
|
||||
});
|
||||
});
|
||||
|
||||
describe('entriesByFolder', () => {
|
||||
const listFiles = jest.fn();
|
||||
const readFile = jest.fn();
|
||||
const readFileMetadata = jest.fn(() => Promise.resolve({ author: '', updatedOn: '' }));
|
||||
|
||||
const mockAPI = {
|
||||
listFiles,
|
||||
readFile,
|
||||
readFileMetadata,
|
||||
originRepoURL: 'originRepoURL',
|
||||
};
|
||||
|
||||
it('should return entries and cursor', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.api = mockAPI;
|
||||
|
||||
const files = [];
|
||||
const count = 1501;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const id = `${i}`.padStart(`${count}`.length, '0');
|
||||
files.push({
|
||||
id,
|
||||
path: `posts/post-${id}.md`,
|
||||
});
|
||||
}
|
||||
|
||||
listFiles.mockResolvedValue(files);
|
||||
readFile.mockImplementation((path, id) => Promise.resolve(`${id}`));
|
||||
|
||||
const expectedEntries = files
|
||||
.slice(0, 20)
|
||||
.map(({ id, path }) => ({ data: id, file: { path, id, author: '', updatedOn: '' } }));
|
||||
|
||||
const expectedCursor = Cursor.create({
|
||||
actions: ['next', 'last'],
|
||||
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
expectedEntries[CURSOR_COMPATIBILITY_SYMBOL] = expectedCursor;
|
||||
|
||||
const result = await gitHubImplementation.entriesByFolder('posts', 'md', 1);
|
||||
|
||||
expect(result).toEqual(expectedEntries);
|
||||
expect(listFiles).toHaveBeenCalledTimes(1);
|
||||
expect(listFiles).toHaveBeenCalledWith('posts', { depth: 1, repoURL: 'originRepoURL' });
|
||||
expect(readFile).toHaveBeenCalledTimes(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('traverseCursor', () => {
|
||||
const listFiles = jest.fn();
|
||||
const readFile = jest.fn((path, id) => Promise.resolve(`${id}`));
|
||||
const readFileMetadata = jest.fn(() => Promise.resolve({}));
|
||||
|
||||
const mockAPI = {
|
||||
listFiles,
|
||||
readFile,
|
||||
originRepoURL: 'originRepoURL',
|
||||
readFileMetadata,
|
||||
};
|
||||
|
||||
const files = [];
|
||||
const count = 1501;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const id = `${i}`.padStart(`${count}`.length, '0');
|
||||
files.push({
|
||||
id,
|
||||
path: `posts/post-${id}.md`,
|
||||
});
|
||||
}
|
||||
|
||||
it('should handle next action', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.api = mockAPI;
|
||||
|
||||
const cursor = Cursor.create({
|
||||
actions: ['next', 'last'],
|
||||
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const expectedEntries = files
|
||||
.slice(20, 40)
|
||||
.map(({ id, path }) => ({ data: id, file: { path, id } }));
|
||||
|
||||
const expectedCursor = Cursor.create({
|
||||
actions: ['prev', 'first', 'next', 'last'],
|
||||
meta: { page: 2, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const result = await gitHubImplementation.traverseCursor(cursor, 'next');
|
||||
|
||||
expect(result).toEqual({
|
||||
entries: expectedEntries,
|
||||
cursor: expectedCursor,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle prev action', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.api = mockAPI;
|
||||
|
||||
const cursor = Cursor.create({
|
||||
actions: ['prev', 'first', 'next', 'last'],
|
||||
meta: { page: 2, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const expectedEntries = files
|
||||
.slice(0, 20)
|
||||
.map(({ id, path }) => ({ data: id, file: { path, id } }));
|
||||
|
||||
const expectedCursor = Cursor.create({
|
||||
actions: ['next', 'last'],
|
||||
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const result = await gitHubImplementation.traverseCursor(cursor, 'prev');
|
||||
|
||||
expect(result).toEqual({
|
||||
entries: expectedEntries,
|
||||
cursor: expectedCursor,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle last action', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.api = mockAPI;
|
||||
|
||||
const cursor = Cursor.create({
|
||||
actions: ['next', 'last'],
|
||||
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const expectedEntries = files
|
||||
.slice(1500)
|
||||
.map(({ id, path }) => ({ data: id, file: { path, id } }));
|
||||
|
||||
const expectedCursor = Cursor.create({
|
||||
actions: ['prev', 'first'],
|
||||
meta: { page: 76, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const result = await gitHubImplementation.traverseCursor(cursor, 'last');
|
||||
|
||||
expect(result).toEqual({
|
||||
entries: expectedEntries,
|
||||
cursor: expectedCursor,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle first action', async () => {
|
||||
const gitHubImplementation = new GitHubImplementation(config);
|
||||
gitHubImplementation.api = mockAPI;
|
||||
|
||||
const cursor = Cursor.create({
|
||||
actions: ['prev', 'first'],
|
||||
meta: { page: 76, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const expectedEntries = files
|
||||
.slice(0, 20)
|
||||
.map(({ id, path }) => ({ data: id, file: { path, id } }));
|
||||
|
||||
const expectedCursor = Cursor.create({
|
||||
actions: ['next', 'last'],
|
||||
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
|
||||
data: { files },
|
||||
});
|
||||
|
||||
const result = await gitHubImplementation.traverseCursor(cursor, 'first');
|
||||
|
||||
expect(result).toEqual({
|
||||
entries: expectedEntries,
|
||||
cursor: expectedCursor,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,92 @@
|
||||
import { gql } from 'graphql-tag';
|
||||
|
||||
export const repository = gql`
|
||||
fragment RepositoryParts on Repository {
|
||||
id
|
||||
isFork
|
||||
}
|
||||
`;
|
||||
|
||||
export const blobWithText = gql`
|
||||
fragment BlobWithTextParts on Blob {
|
||||
id
|
||||
text
|
||||
is_binary: isBinary
|
||||
}
|
||||
`;
|
||||
|
||||
export const object = gql`
|
||||
fragment ObjectParts on GitObject {
|
||||
id
|
||||
sha: oid
|
||||
}
|
||||
`;
|
||||
|
||||
export const branch = gql`
|
||||
fragment BranchParts on Ref {
|
||||
commit: target {
|
||||
...ObjectParts
|
||||
}
|
||||
id
|
||||
name
|
||||
prefix
|
||||
repository {
|
||||
...RepositoryParts
|
||||
}
|
||||
}
|
||||
${object}
|
||||
${repository}
|
||||
`;
|
||||
|
||||
export const pullRequest = gql`
|
||||
fragment PullRequestParts on PullRequest {
|
||||
id
|
||||
baseRefName
|
||||
baseRefOid
|
||||
body
|
||||
headRefName
|
||||
headRefOid
|
||||
number
|
||||
state
|
||||
title
|
||||
merged_at: mergedAt
|
||||
updated_at: updatedAt
|
||||
user: author {
|
||||
login
|
||||
... on User {
|
||||
name
|
||||
}
|
||||
}
|
||||
repository {
|
||||
...RepositoryParts
|
||||
}
|
||||
labels(last: 100) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
${repository}
|
||||
`;
|
||||
|
||||
export const treeEntry = gql`
|
||||
fragment TreeEntryParts on TreeEntry {
|
||||
path: name
|
||||
sha: oid
|
||||
type
|
||||
mode
|
||||
}
|
||||
`;
|
||||
|
||||
export const fileEntry = gql`
|
||||
fragment FileEntryParts on TreeEntry {
|
||||
name
|
||||
sha: oid
|
||||
type
|
||||
blob: object {
|
||||
... on Blob {
|
||||
size: byteSize
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,719 @@
|
||||
import * as React from 'react';
|
||||
import semaphore from 'semaphore';
|
||||
import trimStart from 'lodash/trimStart';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import {
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
Cursor,
|
||||
asyncLock,
|
||||
basename,
|
||||
getBlobSHA,
|
||||
entriesByFolder,
|
||||
entriesByFiles,
|
||||
unpublishedEntries,
|
||||
getMediaDisplayURL,
|
||||
getMediaAsBlob,
|
||||
filterByExtension,
|
||||
getPreviewStatus,
|
||||
runWithLock,
|
||||
blobToFileObj,
|
||||
contentKeyFromBranch,
|
||||
unsentRequest,
|
||||
branchFromContentKey,
|
||||
} from 'decap-cms-lib-util';
|
||||
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import API, { API_NAME } from './API';
|
||||
import GraphQLAPI from './GraphQLAPI';
|
||||
|
||||
import type { Octokit } from '@octokit/rest';
|
||||
import type {
|
||||
AsyncLock,
|
||||
Implementation,
|
||||
AssetProxy,
|
||||
PersistOptions,
|
||||
DisplayURL,
|
||||
User,
|
||||
Credentials,
|
||||
Config,
|
||||
ImplementationFile,
|
||||
UnpublishedEntryMediaFile,
|
||||
Entry,
|
||||
} from 'decap-cms-lib-util';
|
||||
import type { Semaphore } from 'semaphore';
|
||||
|
||||
export type GitHubUser = Octokit.UsersGetAuthenticatedResponse;
|
||||
|
||||
const MAX_CONCURRENT_DOWNLOADS = 10;
|
||||
|
||||
type ApiFile = { id: string; type: string; name: string; path: string; size: number };
|
||||
|
||||
const { fetchWithTimeout: fetch } = unsentRequest;
|
||||
|
||||
const STATUS_PAGE = 'https://www.githubstatus.com';
|
||||
const GITHUB_STATUS_ENDPOINT = `${STATUS_PAGE}/api/v2/components.json`;
|
||||
const GITHUB_OPERATIONAL_UNITS = ['API Requests', 'Issues, Pull Requests, Projects'];
|
||||
type GitHubStatusComponent = {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
export default class GitHub implements Implementation {
|
||||
lock: AsyncLock;
|
||||
api: API | null;
|
||||
options: {
|
||||
proxied: boolean;
|
||||
API: API | null;
|
||||
useWorkflow?: boolean;
|
||||
initialWorkflowStatus: string;
|
||||
};
|
||||
originRepo: string;
|
||||
isBranchConfigured: boolean;
|
||||
repo?: string;
|
||||
openAuthoringEnabled: boolean;
|
||||
useOpenAuthoring?: boolean;
|
||||
alwaysForkEnabled: boolean;
|
||||
branch: string;
|
||||
apiRoot: string;
|
||||
mediaFolder: string;
|
||||
previewContext: string;
|
||||
token: string | null;
|
||||
tokenKeyword: string;
|
||||
squashMerges: boolean;
|
||||
cmsLabelPrefix: string;
|
||||
useGraphql: boolean;
|
||||
baseUrl?: string;
|
||||
bypassWriteAccessCheckForAppTokens = false;
|
||||
_currentUserPromise?: Promise<GitHubUser>;
|
||||
_userIsOriginMaintainerPromises?: {
|
||||
[key: string]: Promise<boolean>;
|
||||
};
|
||||
_mediaDisplayURLSem?: Semaphore;
|
||||
|
||||
constructor(config: Config, options = {}) {
|
||||
this.options = {
|
||||
proxied: false,
|
||||
API: null,
|
||||
initialWorkflowStatus: '',
|
||||
...options,
|
||||
};
|
||||
|
||||
if (
|
||||
!this.options.proxied &&
|
||||
(config.backend.repo === null || config.backend.repo === undefined)
|
||||
) {
|
||||
throw new Error('The GitHub backend needs a "repo" in the backend configuration.');
|
||||
}
|
||||
|
||||
this.api = this.options.API || null;
|
||||
this.isBranchConfigured = config.backend.branch ? true : false;
|
||||
this.openAuthoringEnabled = config.backend.open_authoring || false;
|
||||
if (this.openAuthoringEnabled) {
|
||||
if (!this.options.useWorkflow) {
|
||||
throw new Error(
|
||||
'backend.open_authoring is true but publish_mode is not set to editorial_workflow.',
|
||||
);
|
||||
}
|
||||
this.originRepo = config.backend.repo || '';
|
||||
} else {
|
||||
this.repo = this.originRepo = config.backend.repo || '';
|
||||
}
|
||||
this.alwaysForkEnabled = config.backend.always_fork || false;
|
||||
this.branch = config.backend.branch?.trim() || 'master';
|
||||
this.apiRoot = config.backend.api_root || 'https://api.github.com';
|
||||
this.token = '';
|
||||
this.tokenKeyword = 'token';
|
||||
this.baseUrl = config.backend.base_url;
|
||||
this.squashMerges = config.backend.squash_merges || false;
|
||||
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
|
||||
this.useGraphql = config.backend.use_graphql || false;
|
||||
this.mediaFolder = config.media_folder;
|
||||
this.previewContext = config.backend.preview_context || '';
|
||||
this.lock = asyncLock();
|
||||
}
|
||||
|
||||
isGitBackend() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async status() {
|
||||
const api = await fetch(GITHUB_STATUS_ENDPOINT)
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
return res['components']
|
||||
.filter((statusComponent: GitHubStatusComponent) =>
|
||||
GITHUB_OPERATIONAL_UNITS.includes(statusComponent.name),
|
||||
)
|
||||
.every(
|
||||
(statusComponent: GitHubStatusComponent) => statusComponent.status === 'operational',
|
||||
);
|
||||
})
|
||||
.catch(e => {
|
||||
console.warn('Failed getting GitHub status', e);
|
||||
return true;
|
||||
});
|
||||
|
||||
let auth = false;
|
||||
// no need to check auth if api is down
|
||||
if (api) {
|
||||
auth =
|
||||
(await this.api
|
||||
?.getUser({ token: this.token ?? '' })
|
||||
.then(user => !!user)
|
||||
.catch(e => {
|
||||
console.warn('Failed getting GitHub user', e);
|
||||
return false;
|
||||
})) || false;
|
||||
}
|
||||
|
||||
return { auth: { status: auth }, api: { status: api, statusPage: STATUS_PAGE } };
|
||||
}
|
||||
|
||||
authComponent() {
|
||||
const wrappedAuthenticationPage = (props: Record<string, unknown>) => (
|
||||
<AuthenticationPage {...props} backend={this} />
|
||||
);
|
||||
wrappedAuthenticationPage.displayName = 'AuthenticationPage';
|
||||
return wrappedAuthenticationPage;
|
||||
}
|
||||
|
||||
restoreUser(user: User) {
|
||||
return this.openAuthoringEnabled
|
||||
? this.authenticateWithFork({ userData: user, getPermissionToFork: () => true }).then(() =>
|
||||
this.authenticate(user),
|
||||
)
|
||||
: this.authenticate(user);
|
||||
}
|
||||
|
||||
async pollUntilForkExists({ repo, token }: { repo: string; token: string }) {
|
||||
const pollDelay = 250; // milliseconds
|
||||
let repoExists = false;
|
||||
while (!repoExists) {
|
||||
repoExists = await fetch(`${this.apiRoot}/repos/${repo}`, {
|
||||
headers: { Authorization: `${this.tokenKeyword} ${token}` },
|
||||
})
|
||||
.then(() => true)
|
||||
.catch(err => {
|
||||
if (err && err.status === 404) {
|
||||
console.log('This 404 was expected and handled appropriately.');
|
||||
return false;
|
||||
} else {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
});
|
||||
// wait between polls
|
||||
if (!repoExists) {
|
||||
await new Promise(resolve => setTimeout(resolve, pollDelay));
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async currentUser({ token }: { token: string }) {
|
||||
if (!this._currentUserPromise) {
|
||||
this._currentUserPromise = fetch(`${this.apiRoot}/user`, {
|
||||
headers: {
|
||||
Authorization: `${this.tokenKeyword} ${token}`,
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
return this._currentUserPromise;
|
||||
}
|
||||
|
||||
async userIsOriginMaintainer({
|
||||
username: usernameArg,
|
||||
token,
|
||||
}: {
|
||||
username?: string;
|
||||
token: string;
|
||||
}) {
|
||||
const username = usernameArg || (await this.currentUser({ token })).login;
|
||||
this._userIsOriginMaintainerPromises = this._userIsOriginMaintainerPromises || {};
|
||||
if (!this._userIsOriginMaintainerPromises[username]) {
|
||||
this._userIsOriginMaintainerPromises[username] = fetch(
|
||||
`${this.apiRoot}/repos/${this.originRepo}/collaborators/${username}/permission`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `${this.tokenKeyword} ${token}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
.then(res => res.json())
|
||||
.then(({ permission }) => permission === 'admin' || permission === 'write');
|
||||
}
|
||||
return this._userIsOriginMaintainerPromises[username];
|
||||
}
|
||||
|
||||
async forkExists({ token }: { token: string }) {
|
||||
try {
|
||||
const currentUser = await this.currentUser({ token });
|
||||
const repoName = this.originRepo.split('/')[1];
|
||||
const repo = await fetch(`${this.apiRoot}/repos/${currentUser.login}/${repoName}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `${this.tokenKeyword} ${token}`,
|
||||
},
|
||||
}).then(res => res.json());
|
||||
|
||||
// https://developer.github.com/v3/repos/#get
|
||||
// The parent and source objects are present when the repository is a fork.
|
||||
// parent is the repository this repository was forked from, source is the ultimate source for the network.
|
||||
const forkExists =
|
||||
repo.fork === true &&
|
||||
repo.parent &&
|
||||
repo.parent.full_name.toLowerCase() === this.originRepo.toLowerCase();
|
||||
return forkExists;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async authenticateWithFork({
|
||||
userData,
|
||||
getPermissionToFork,
|
||||
}: {
|
||||
userData: User;
|
||||
getPermissionToFork: () => Promise<boolean> | boolean;
|
||||
}) {
|
||||
if (!this.openAuthoringEnabled) {
|
||||
throw new Error('Cannot authenticate with fork; Open Authoring is turned off.');
|
||||
}
|
||||
const token = userData.token as string;
|
||||
|
||||
// Origin maintainers should be able to use the CMS normally. If alwaysFork
|
||||
// is enabled we always fork (and avoid the origin maintainer check)
|
||||
if (!this.alwaysForkEnabled && (await this.userIsOriginMaintainer({ token }))) {
|
||||
this.repo = this.originRepo;
|
||||
this.useOpenAuthoring = false;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// If a fork exists merge it with upstream
|
||||
// otherwise create a new fork.
|
||||
const currentUser = await this.currentUser({ token });
|
||||
const repoName = this.originRepo.split('/')[1];
|
||||
this.repo = `${currentUser.login}/${repoName}`;
|
||||
this.useOpenAuthoring = true;
|
||||
|
||||
if (await this.forkExists({ token })) {
|
||||
return fetch(`${this.apiRoot}/repos/${this.repo}/merge-upstream`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `${this.tokenKeyword} ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
branch: this.branch,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
await getPermissionToFork();
|
||||
|
||||
const fork = await fetch(`${this.apiRoot}/repos/${this.originRepo}/forks`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `${this.tokenKeyword} ${token}`,
|
||||
},
|
||||
}).then(res => res.json());
|
||||
return this.pollUntilForkExists({ repo: fork.full_name, token });
|
||||
}
|
||||
}
|
||||
|
||||
async authenticate(state: Credentials) {
|
||||
this.token = state.token as string;
|
||||
// Query the default branch name when the `branch` property is missing
|
||||
// in the config file
|
||||
if (!this.isBranchConfigured) {
|
||||
const repoInfo = await fetch(`${this.apiRoot}/repos/${this.originRepo}`, {
|
||||
headers: { Authorization: `token ${this.token}` },
|
||||
})
|
||||
.then(res => res.json())
|
||||
.catch(() => null);
|
||||
if (repoInfo && repoInfo.default_branch) {
|
||||
this.branch = repoInfo.default_branch;
|
||||
}
|
||||
}
|
||||
const apiCtor = this.useGraphql ? GraphQLAPI : API;
|
||||
this.api = new apiCtor({
|
||||
token: this.token,
|
||||
tokenKeyword: this.tokenKeyword,
|
||||
branch: this.branch,
|
||||
repo: this.repo,
|
||||
originRepo: this.originRepo,
|
||||
apiRoot: this.apiRoot,
|
||||
squashMerges: this.squashMerges,
|
||||
cmsLabelPrefix: this.cmsLabelPrefix,
|
||||
useOpenAuthoring: this.useOpenAuthoring,
|
||||
initialWorkflowStatus: this.options.initialWorkflowStatus,
|
||||
baseUrl: this.baseUrl,
|
||||
getUser: this.currentUser,
|
||||
});
|
||||
const user = await this.api!.user();
|
||||
const isCollab = await this.api!.hasWriteAccess().catch(error => {
|
||||
error.message = stripIndent`
|
||||
Repo "${this.repo}" not found.
|
||||
|
||||
Please ensure the repo information is spelled correctly.
|
||||
|
||||
If the repo is private, make sure you're logged into a GitHub account with access.
|
||||
|
||||
If your repo is under an organization, ensure the organization has granted access to Decap CMS.
|
||||
`;
|
||||
throw error;
|
||||
});
|
||||
|
||||
// Unauthorized user
|
||||
if (!isCollab && !this.bypassWriteAccessCheckForAppTokens) {
|
||||
throw new Error('Your GitHub user account does not have access to this repo.');
|
||||
}
|
||||
|
||||
// if (!this.isBranchConfigured) {
|
||||
// const defaultBranchName = await this.api.getDefaultBranchName()
|
||||
// if (defaultBranchName) {
|
||||
// this.branch = defaultBranchName;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Authorized user
|
||||
return { ...user, token: state.token as string, useOpenAuthoring: this.useOpenAuthoring };
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.token = null;
|
||||
if (this.api && this.api.reset && typeof this.api.reset === 'function') {
|
||||
return this.api.reset();
|
||||
}
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return Promise.resolve(this.token);
|
||||
}
|
||||
|
||||
getCursorAndFiles = (files: ApiFile[], page: number) => {
|
||||
const pageSize = 20;
|
||||
const count = files.length;
|
||||
const pageCount = Math.ceil(files.length / pageSize);
|
||||
|
||||
const actions = [] as string[];
|
||||
if (page > 1) {
|
||||
actions.push('prev');
|
||||
actions.push('first');
|
||||
}
|
||||
if (page < pageCount) {
|
||||
actions.push('next');
|
||||
actions.push('last');
|
||||
}
|
||||
|
||||
const cursor = Cursor.create({
|
||||
actions,
|
||||
meta: { page, count, pageSize, pageCount },
|
||||
data: { files },
|
||||
});
|
||||
const pageFiles = files.slice((page - 1) * pageSize, page * pageSize);
|
||||
return { cursor, files: pageFiles };
|
||||
};
|
||||
|
||||
async entriesByFolder(folder: string, extension: string, depth: number) {
|
||||
const repoURL = this.api!.originRepoURL;
|
||||
|
||||
let cursor: Cursor;
|
||||
|
||||
const listFiles = () =>
|
||||
this.api!.listFiles(folder, {
|
||||
repoURL,
|
||||
depth,
|
||||
}).then(files => {
|
||||
const filtered = files.filter(file => filterByExtension(file, extension));
|
||||
const result = this.getCursorAndFiles(filtered, 1);
|
||||
cursor = result.cursor;
|
||||
return result.files;
|
||||
});
|
||||
|
||||
const readFile = (path: string, id: string | null | undefined) =>
|
||||
this.api!.readFile(path, id, { repoURL }) as Promise<string>;
|
||||
|
||||
const files = await entriesByFolder(
|
||||
listFiles,
|
||||
readFile,
|
||||
this.api!.readFileMetadata.bind(this.api),
|
||||
API_NAME,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
files[CURSOR_COMPATIBILITY_SYMBOL] = cursor;
|
||||
return files;
|
||||
}
|
||||
|
||||
async allEntriesByFolder(folder: string, extension: string, depth: number, pathRegex?: RegExp) {
|
||||
const repoURL = this.api!.originRepoURL;
|
||||
|
||||
const listFiles = () =>
|
||||
this.api!.listFiles(folder, {
|
||||
repoURL,
|
||||
depth,
|
||||
}).then(files =>
|
||||
files.filter(
|
||||
file => (!pathRegex || pathRegex.test(file.path)) && filterByExtension(file, extension),
|
||||
),
|
||||
);
|
||||
|
||||
const readFile = (path: string, id: string | null | undefined) => {
|
||||
return this.api!.readFile(path, id, { repoURL }) as Promise<string>;
|
||||
};
|
||||
|
||||
const files = await entriesByFolder(
|
||||
listFiles,
|
||||
readFile,
|
||||
this.api!.readFileMetadata.bind(this.api),
|
||||
API_NAME,
|
||||
);
|
||||
return files;
|
||||
}
|
||||
|
||||
entriesByFiles(files: ImplementationFile[]) {
|
||||
const repoURL = this.useOpenAuthoring ? this.api!.originRepoURL : this.api!.repoURL;
|
||||
|
||||
const readFile = (path: string, id: string | null | undefined) =>
|
||||
this.api!.readFile(path, id, { repoURL }).catch(() => '') as Promise<string>;
|
||||
|
||||
return entriesByFiles(files, readFile, this.api!.readFileMetadata.bind(this.api), API_NAME);
|
||||
}
|
||||
|
||||
// Fetches a single entry.
|
||||
getEntry(path: string) {
|
||||
const repoURL = this.api!.originRepoURL;
|
||||
return this.api!.readFile(path, null, { repoURL })
|
||||
.then(data => ({
|
||||
file: { path, id: null },
|
||||
data: data as string,
|
||||
}))
|
||||
.catch(() => ({ file: { path, id: null }, data: '' }));
|
||||
}
|
||||
|
||||
getMedia(mediaFolder = this.mediaFolder) {
|
||||
return this.api!.listFiles(mediaFolder).then(files =>
|
||||
files.map(({ id, name, size, path }) => {
|
||||
// load media using getMediaDisplayURL to avoid token expiration with GitHub raw content urls
|
||||
// for private repositories
|
||||
return { id, name, size, displayURL: { id, path }, path };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getMediaFile(path: string) {
|
||||
const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!));
|
||||
|
||||
const name = basename(path);
|
||||
const fileObj = blobToFileObj(name, blob);
|
||||
const url = URL.createObjectURL(fileObj);
|
||||
const id = await getBlobSHA(blob);
|
||||
|
||||
return {
|
||||
id,
|
||||
displayURL: url,
|
||||
path,
|
||||
name,
|
||||
size: fileObj.size,
|
||||
file: fileObj,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
getMediaDisplayURL(displayURL: DisplayURL) {
|
||||
this._mediaDisplayURLSem = this._mediaDisplayURLSem || semaphore(MAX_CONCURRENT_DOWNLOADS);
|
||||
return getMediaDisplayURL(
|
||||
displayURL,
|
||||
this.api!.readFile.bind(this.api!),
|
||||
this._mediaDisplayURLSem,
|
||||
);
|
||||
}
|
||||
|
||||
persistEntry(entry: Entry, options: PersistOptions) {
|
||||
// persistEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.persistFiles(entry.dataFiles, entry.assets, options),
|
||||
'Failed to acquire persist entry lock',
|
||||
);
|
||||
}
|
||||
|
||||
async persistMedia(mediaFile: AssetProxy, options: PersistOptions) {
|
||||
try {
|
||||
await this.api!.persistFiles([], [mediaFile], options);
|
||||
const { sha, path, fileObj } = mediaFile as AssetProxy & { sha: string };
|
||||
const displayURL = fileObj ? URL.createObjectURL(fileObj) : '';
|
||||
return {
|
||||
id: sha,
|
||||
name: fileObj!.name,
|
||||
size: fileObj!.size,
|
||||
displayURL,
|
||||
path: trimStart(path, '/'),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
deleteFiles(paths: string[], commitMessage: string) {
|
||||
return this.api!.deleteFiles(paths, commitMessage);
|
||||
}
|
||||
|
||||
async traverseCursor(cursor: Cursor, action: string) {
|
||||
const meta = cursor.meta!;
|
||||
const files = cursor.data!.get('files')!.toJS() as ApiFile[];
|
||||
|
||||
let result: { cursor: Cursor; files: ApiFile[] };
|
||||
switch (action) {
|
||||
case 'first': {
|
||||
result = this.getCursorAndFiles(files, 1);
|
||||
break;
|
||||
}
|
||||
case 'last': {
|
||||
result = this.getCursorAndFiles(files, meta.get('pageCount'));
|
||||
break;
|
||||
}
|
||||
case 'next': {
|
||||
result = this.getCursorAndFiles(files, meta.get('page') + 1);
|
||||
break;
|
||||
}
|
||||
case 'prev': {
|
||||
result = this.getCursorAndFiles(files, meta.get('page') - 1);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
result = this.getCursorAndFiles(files, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const readFile = (path: string, id: string | null | undefined) =>
|
||||
this.api!.readFile(path, id, { repoURL: this.api!.originRepoURL }).catch(
|
||||
() => '',
|
||||
) as Promise<string>;
|
||||
|
||||
const entries = await entriesByFiles(
|
||||
result.files,
|
||||
readFile,
|
||||
this.api!.readFileMetadata.bind(this.api),
|
||||
API_NAME,
|
||||
);
|
||||
|
||||
return {
|
||||
entries,
|
||||
cursor: result.cursor,
|
||||
};
|
||||
}
|
||||
|
||||
async loadMediaFile(branch: string, file: UnpublishedEntryMediaFile) {
|
||||
const readFile = (
|
||||
path: string,
|
||||
id: string | null | undefined,
|
||||
{ parseText }: { parseText: boolean },
|
||||
) => this.api!.readFile(path, id, { branch, parseText });
|
||||
|
||||
const blob = await getMediaAsBlob(file.path, file.id, readFile);
|
||||
const name = basename(file.path);
|
||||
const fileObj = blobToFileObj(name, blob);
|
||||
return {
|
||||
id: file.id,
|
||||
displayURL: URL.createObjectURL(fileObj),
|
||||
path: file.path,
|
||||
name,
|
||||
size: fileObj.size,
|
||||
file: fileObj,
|
||||
};
|
||||
}
|
||||
|
||||
async unpublishedEntries() {
|
||||
const listEntriesKeys = () =>
|
||||
this.api!.listUnpublishedBranches().then(branches =>
|
||||
branches.map(branch => contentKeyFromBranch(branch)),
|
||||
);
|
||||
|
||||
const ids = await unpublishedEntries(listEntriesKeys);
|
||||
return ids;
|
||||
}
|
||||
|
||||
async unpublishedEntry({
|
||||
id,
|
||||
collection,
|
||||
slug,
|
||||
}: {
|
||||
id?: string;
|
||||
collection?: string;
|
||||
slug?: string;
|
||||
}) {
|
||||
if (id) {
|
||||
const data = await this.api!.retrieveUnpublishedEntryData(id);
|
||||
return data;
|
||||
} else if (collection && slug) {
|
||||
const entryId = this.api!.generateContentKey(collection, slug);
|
||||
const data = await this.api!.retrieveUnpublishedEntryData(entryId);
|
||||
return data;
|
||||
} else {
|
||||
throw new Error('Missing unpublished entry id or collection and slug');
|
||||
}
|
||||
}
|
||||
|
||||
getBranch(collection: string, slug: string) {
|
||||
const contentKey = this.api!.generateContentKey(collection, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
return branch;
|
||||
}
|
||||
|
||||
async unpublishedEntryDataFile(collection: string, slug: string, path: string, id: string) {
|
||||
const branch = this.getBranch(collection, slug);
|
||||
const data = (await this.api!.readFile(path, id, { branch })) as string;
|
||||
return data;
|
||||
}
|
||||
|
||||
async unpublishedEntryMediaFile(collection: string, slug: string, path: string, id: string) {
|
||||
const branch = this.getBranch(collection, slug);
|
||||
const mediaFile = await this.loadMediaFile(branch, { path, id });
|
||||
return mediaFile;
|
||||
}
|
||||
|
||||
async getDeployPreview(collection: string, slug: string) {
|
||||
try {
|
||||
const statuses = await this.api!.getStatuses(collection, slug);
|
||||
const deployStatus = getPreviewStatus(statuses, this.previewContext);
|
||||
|
||||
if (deployStatus) {
|
||||
const { target_url: url, state } = deployStatus;
|
||||
return { url, status: state };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
// updateUnpublishedEntryStatus is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.updateUnpublishedEntryStatus(collection, slug, newStatus),
|
||||
'Failed to acquire update entry status lock',
|
||||
);
|
||||
}
|
||||
|
||||
deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
// deleteUnpublishedEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.deleteUnpublishedEntry(collection, slug),
|
||||
'Failed to acquire delete entry lock',
|
||||
);
|
||||
}
|
||||
|
||||
publishUnpublishedEntry(collection: string, slug: string) {
|
||||
// publishUnpublishedEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.publishUnpublishedEntry(collection, slug),
|
||||
'Failed to acquire publish entry lock',
|
||||
);
|
||||
}
|
||||
}
|
||||
10
source/admin/packages/decap-cms-backend-github/src/index.ts
Normal file
10
source/admin/packages/decap-cms-backend-github/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import GitHubBackend from './implementation';
|
||||
import API from './API';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
|
||||
export const DecapCmsBackendGithub = {
|
||||
GitHubBackend,
|
||||
API,
|
||||
AuthenticationPage,
|
||||
};
|
||||
export { GitHubBackend, API, AuthenticationPage };
|
||||
110
source/admin/packages/decap-cms-backend-github/src/mutations.ts
Normal file
110
source/admin/packages/decap-cms-backend-github/src/mutations.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { gql } from 'graphql-tag';
|
||||
|
||||
import * as fragments from './fragments';
|
||||
|
||||
// updateRef only works for branches at the moment
|
||||
export const updateBranch = gql`
|
||||
mutation updateRef($input: UpdateRefInput!) {
|
||||
updateRef(input: $input) {
|
||||
branch: ref {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
// deleteRef only works for branches at the moment
|
||||
const deleteRefMutationPart = `
|
||||
deleteRef(input: $deleteRefInput) {
|
||||
clientMutationId
|
||||
}
|
||||
`;
|
||||
export const deleteBranch = gql`
|
||||
mutation deleteRef($deleteRefInput: DeleteRefInput!) {
|
||||
${deleteRefMutationPart}
|
||||
}
|
||||
`;
|
||||
|
||||
const closePullRequestMutationPart = `
|
||||
closePullRequest(input: $closePullRequestInput) {
|
||||
clientMutationId
|
||||
pullRequest {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const closePullRequest = gql`
|
||||
mutation closePullRequestAndDeleteBranch($closePullRequestInput: ClosePullRequestInput!) {
|
||||
${closePullRequestMutationPart}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const closePullRequestAndDeleteBranch = gql`
|
||||
mutation closePullRequestAndDeleteBranch(
|
||||
$closePullRequestInput: ClosePullRequestInput!
|
||||
$deleteRefInput: DeleteRefInput!
|
||||
) {
|
||||
${closePullRequestMutationPart}
|
||||
${deleteRefMutationPart}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
const createPullRequestMutationPart = `
|
||||
createPullRequest(input: $createPullRequestInput) {
|
||||
clientMutationId
|
||||
pullRequest {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const createPullRequest = gql`
|
||||
mutation createPullRequest($createPullRequestInput: CreatePullRequestInput!) {
|
||||
${createPullRequestMutationPart}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const createBranch = gql`
|
||||
mutation createBranch($createRefInput: CreateRefInput!) {
|
||||
createRef(input: $createRefInput) {
|
||||
branch: ref {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
// createRef only works for branches at the moment
|
||||
export const createBranchAndPullRequest = gql`
|
||||
mutation createBranchAndPullRequest(
|
||||
$createRefInput: CreateRefInput!
|
||||
$createPullRequestInput: CreatePullRequestInput!
|
||||
) {
|
||||
createRef(input: $createRefInput) {
|
||||
branch: ref {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
${createPullRequestMutationPart}
|
||||
}
|
||||
${fragments.branch}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const reopenPullRequest = gql`
|
||||
mutation reopenPullRequest($reopenPullRequestInput: ReopenPullRequestInput!) {
|
||||
reopenPullRequest(input: $reopenPullRequestInput) {
|
||||
clientMutationId
|
||||
pullRequest {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
213
source/admin/packages/decap-cms-backend-github/src/queries.ts
Normal file
213
source/admin/packages/decap-cms-backend-github/src/queries.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { gql } from 'graphql-tag';
|
||||
import { oneLine } from 'common-tags';
|
||||
|
||||
import * as fragments from './fragments';
|
||||
|
||||
export const repoPermission = gql`
|
||||
query repoPermission($owner: String!, $name: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
viewerPermission
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
`;
|
||||
|
||||
export const user = gql`
|
||||
query {
|
||||
viewer {
|
||||
id
|
||||
avatar_url: avatarUrl
|
||||
name
|
||||
login
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const blob = gql`
|
||||
query blob($owner: String!, $name: String!, $expression: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
object(expression: $expression) {
|
||||
... on Blob {
|
||||
...BlobWithTextParts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.blobWithText}
|
||||
`;
|
||||
|
||||
export const statues = gql`
|
||||
query statues($owner: String!, $name: String!, $sha: GitObjectID!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
object(oid: $sha) {
|
||||
...ObjectParts
|
||||
... on Commit {
|
||||
status {
|
||||
id
|
||||
contexts {
|
||||
id
|
||||
context
|
||||
state
|
||||
target_url: targetUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.object}
|
||||
`;
|
||||
|
||||
function buildFilesQuery(depth = 1) {
|
||||
const PLACE_HOLDER = 'PLACE_HOLDER';
|
||||
let query = oneLine`
|
||||
...ObjectParts
|
||||
... on Tree {
|
||||
entries {
|
||||
...FileEntryParts
|
||||
${PLACE_HOLDER}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
for (let i = 0; i < depth - 1; i++) {
|
||||
query = query.replace(
|
||||
PLACE_HOLDER,
|
||||
oneLine`
|
||||
object {
|
||||
... on Tree {
|
||||
entries {
|
||||
...FileEntryParts
|
||||
${PLACE_HOLDER}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
query = query.replace(PLACE_HOLDER, '');
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
export function files(depth: number) {
|
||||
return gql`
|
||||
query files($owner: String!, $name: String!, $expression: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
object(expression: $expression) {
|
||||
${buildFilesQuery(depth)}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.object}
|
||||
${fragments.fileEntry}
|
||||
`;
|
||||
}
|
||||
|
||||
const branchQueryPart = `
|
||||
branch: ref(qualifiedName: $qualifiedName) {
|
||||
...BranchParts
|
||||
}
|
||||
`;
|
||||
|
||||
export const branch = gql`
|
||||
query branch($owner: String!, $name: String!, $qualifiedName: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
${branchQueryPart}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
export const openAuthoringBranches = gql`
|
||||
query openAuthoringBranches($owner: String!, $name: String!, $refPrefix: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
refs(refPrefix: $refPrefix, last: 100) {
|
||||
nodes {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
export const repository = gql`
|
||||
query repository($owner: String!, $name: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
`;
|
||||
|
||||
const pullRequestQueryPart = `
|
||||
pullRequest(number: $number) {
|
||||
...PullRequestParts
|
||||
}
|
||||
`;
|
||||
|
||||
export const pullRequest = gql`
|
||||
query pullRequest($owner: String!, $name: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
id
|
||||
${pullRequestQueryPart}
|
||||
}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const pullRequests = gql`
|
||||
query pullRequests($owner: String!, $name: String!, $head: String, $states: [PullRequestState!]) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
id
|
||||
pullRequests(last: 100, headRefName: $head, states: $states) {
|
||||
nodes {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const pullRequestAndBranch = gql`
|
||||
query pullRequestAndBranch($owner: String!, $name: String!, $originRepoOwner: String!, $originRepoName: String!, $qualifiedName: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
${branchQueryPart}
|
||||
}
|
||||
origin: repository(owner: $originRepoOwner, name: $originRepoName) {
|
||||
...RepositoryParts
|
||||
${pullRequestQueryPart}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.branch}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const fileSha = gql`
|
||||
query fileSha($owner: String!, $name: String!, $expression: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
file: object(expression: $expression) {
|
||||
...ObjectParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.object}
|
||||
`;
|
||||
5
source/admin/packages/decap-cms-backend-github/src/types/semaphore.d.ts
vendored
Normal file
5
source/admin/packages/decap-cms-backend-github/src/types/semaphore.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module 'semaphore' {
|
||||
export type Semaphore = { take: (f: Function) => void; leave: () => void };
|
||||
const semaphore: (count: number) => Semaphore;
|
||||
export default semaphore;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
const { getConfig } = require('../../scripts/webpack.js');
|
||||
|
||||
module.exports = getConfig();
|
||||
416
source/admin/packages/decap-cms-backend-gitlab/CHANGELOG.md
Normal file
416
source/admin/packages/decap-cms-backend-gitlab/CHANGELOG.md
Normal file
@@ -0,0 +1,416 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.4.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.3.1...decap-cms-backend-gitlab@3.4.0) (2025-07-15)
|
||||
|
||||
### Features
|
||||
|
||||
- add logo to header ([#7487](https://github.com/decaporg/decap-cms/issues/7487)) ([b540ace](https://github.com/decaporg/decap-cms/commit/b540acec943eb231df6aac7b1d515d9b4b84fa5d))
|
||||
|
||||
## [3.3.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.3.0...decap-cms-backend-gitlab@3.3.1) (2025-07-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
# [3.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.2.2...decap-cms-backend-gitlab@3.3.0) (2025-06-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [3.2.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.2.1...decap-cms-backend-gitlab@3.2.2) (2024-08-13)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "Update dependencies (#7264)" ([22d483a](https://github.com/decaporg/decap-cms/commit/22d483a5b0c654071ae05735ac4f49abdc13d38c)), closes [#7264](https://github.com/decaporg/decap-cms/issues/7264)
|
||||
|
||||
## [3.2.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.2.0...decap-cms-backend-gitlab@3.2.1) (2024-08-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
# [3.2.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.1.3...decap-cms-backend-gitlab@3.2.0) (2024-08-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend:** allow a custom API root for backend ([#7214](https://github.com/decaporg/decap-cms/issues/7214)) ([fae3e05](https://github.com/decaporg/decap-cms/commit/fae3e057f898f60fdbe80091acc833d6ac92696e)), closes [#7168](https://github.com/decaporg/decap-cms/issues/7168)
|
||||
|
||||
## [3.1.3](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.1.2...decap-cms-backend-gitlab@3.1.3) (2024-04-03)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [3.1.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.1.1...decap-cms-backend-gitlab@3.1.2) (2024-03-21)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [3.1.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.1.0-beta.1...decap-cms-backend-gitlab@3.1.1) (2024-02-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- gitlab pkce auth error ([#7110](https://github.com/decaporg/decap-cms/issues/7110)) ([bcd58d6](https://github.com/decaporg/decap-cms/commit/bcd58d6e117b4654b3e0dca173f7f8aaca8dabdf))
|
||||
|
||||
# [3.1.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.1.0-beta.1...decap-cms-backend-gitlab@3.1.0) (2024-02-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
# [3.1.0-beta.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.1.0-beta.0...decap-cms-backend-gitlab@3.1.0-beta.1) (2024-01-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
# [3.1.0-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.1.0...decap-cms-backend-gitlab@3.1.0-beta.0) (2023-10-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([b89fc89](https://github.com/decaporg/decap-cms/commit/b89fc894dfbb5f4136b2e5427fd25a29378a58c6))
|
||||
|
||||
## [3.0.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.0.1...decap-cms-backend-gitlab@3.0.2) (2023-10-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [3.0.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@3.0.0...decap-cms-backend-gitlab@3.0.1) (2023-08-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update peer dependencies ([#6886](https://github.com/decaporg/decap-cms/issues/6886)) ([e580ce5](https://github.com/decaporg/decap-cms/commit/e580ce52ce5f80fa040e8fbcab7fed0744f4f695))
|
||||
|
||||
# [3.0.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@2.14.0...decap-cms-backend-gitlab@3.0.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
# [2.14.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@2.14.0-beta.0...decap-cms-backend-gitlab@2.14.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
# 2.14.0-beta.0 (2023-08-18)
|
||||
|
||||
### Features
|
||||
|
||||
- rename packages ([#6863](https://github.com/decaporg/decap-cms/issues/6863)) ([d515e7b](https://github.com/decaporg/decap-cms/commit/d515e7bd33216a775d96887b08c4f7b1962941bb))
|
||||
|
||||
## [2.13.1-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@2.13.0...decap-cms-backend-gitlab@2.13.1-beta.0) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
# [2.13.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@2.12.0...decap-cms-backend-gitlab@2.13.0) (2021-12-28)
|
||||
|
||||
### Features
|
||||
|
||||
- **backend-gitlab:** initial GraphQL support ([#6059](https://github.com/decaporg/decap-cms/issues/6059)) ([1523a41](https://github.com/decaporg/decap-cms/commit/1523a4140a3d2f4cc01a1548514ae17bc1ad504e))
|
||||
|
||||
# [2.12.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitlab@2.11.4...decap-cms-backend-gitlab@2.12.0) (2021-10-18)
|
||||
|
||||
### Features
|
||||
|
||||
- display author of changes in workflow tab ([#5780](https://github.com/decaporg/decap-cms/issues/5780)) ([3f607e4](https://github.com/decaporg/decap-cms/commit/3f607e41d9c4d8fe5329a9ab6841cada7742825e))
|
||||
|
||||
## [2.11.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.11.3...decap-cms-backend-gitlab@2.11.4) (2021-06-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [2.11.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.11.2...decap-cms-backend-gitlab@2.11.3) (2021-05-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [2.11.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.11.1...decap-cms-backend-gitlab@2.11.2) (2021-05-19)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [2.11.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.11.0...decap-cms-backend-gitlab@2.11.1) (2021-05-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-gitlab:** increase merge requests fetched to 100 ([#5320](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/5320)) ([0a1b00d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/0a1b00d8b29a065b7d72fbb1744eb787da3b916e))
|
||||
|
||||
# [2.11.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.10.0...decap-cms-backend-gitlab@2.11.0) (2021-05-04)
|
||||
|
||||
### Features
|
||||
|
||||
- added react 17 as peer dependency in packages ([#5316](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/5316)) ([9e42380](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/9e423805707321396eec137f5b732a5b07a0dd3f))
|
||||
|
||||
# [2.10.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.9.9...decap-cms-backend-gitlab@2.10.0) (2021-04-14)
|
||||
|
||||
### Features
|
||||
|
||||
- Adds PKCE authentication for GitLab closes [#5236](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/5236) ([#5239](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/5239)) ([829409e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/829409e0bc03b4591ee6b59d9895adc4e7190037))
|
||||
|
||||
## [2.9.9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.9.8...decap-cms-backend-gitlab@2.9.9) (2021-02-23)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [2.9.8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.9.7...decap-cms-backend-gitlab@2.9.8) (2021-02-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [2.9.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.9.6...decap-cms-backend-gitlab@2.9.7) (2021-02-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-gitlab:** increase rebase timeout ([#4905](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/4905)) ([d9d6860](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/d9d686025fe848fe460917cdef1934dea3295c9d))
|
||||
|
||||
## [2.9.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.9.5...decap-cms-backend-gitlab@2.9.6) (2021-01-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-gitlab:** skip creating a CI pipeline when rebasing merge request ([#4802](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/4802)) ([ce11dd2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/ce11dd23753fd7bb502c299058b83701f20058b2)), closes [#4786](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/4786)
|
||||
|
||||
## [2.9.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.9.4...decap-cms-backend-gitlab@2.9.5) (2020-09-20)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [2.9.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.9.3...decap-cms-backend-gitlab@2.9.4) (2020-09-15)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## 2.9.3 (2020-09-08)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([828bb16](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/828bb16415b8c22a34caa19c50c38b24ffe9ceae))
|
||||
|
||||
## 2.9.2 (2020-08-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([8262487](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/82624879ccbcb16610090041db28f00714d924c8))
|
||||
|
||||
## 2.9.1 (2020-07-27)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([118d50a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/118d50a7a70295f25073e564b5161aa2b9883056))
|
||||
|
||||
# [2.9.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.8.4...decap-cms-backend-gitlab@2.9.0) (2020-06-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- handle token expiry ([#3847](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3847)) ([285c940](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/285c940562548d7bc88de244123ba87ff66fba65))
|
||||
|
||||
### Features
|
||||
|
||||
- add backend status down indicator ([#3889](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3889)) ([a50edc7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/a50edc70553ad6afa1acee6a51996ad226443f8c))
|
||||
- **backend-gitgateway:** improve deploy preview visibility ([#3882](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3882)) ([afc9bf4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/afc9bf4f3fe14ccb60851fc24e68922a6e4a85a9))
|
||||
|
||||
## [2.8.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.8.3...decap-cms-backend-gitlab@2.8.4) (2020-04-21)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [2.8.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.8.2...decap-cms-backend-gitlab@2.8.3) (2020-04-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [2.8.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.8.1...decap-cms-backend-gitlab@2.8.2) (2020-03-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- missing workflow timestamp ([#3445](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3445)) ([9616cdb](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/9616cdb8bb0a564771e5755bcd3718a07f2e2072))
|
||||
|
||||
## [2.8.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.8.0...decap-cms-backend-gitlab@2.8.1) (2020-03-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **locale:** Remove hard coded string literals ([#3333](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3333)) ([7c45a3c](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/7c45a3cda983be427864a56e58791565eb9232e2))
|
||||
|
||||
# [2.8.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.7.1...decap-cms-backend-gitlab@2.8.0) (2020-02-25)
|
||||
|
||||
### Features
|
||||
|
||||
- **core:** align GitHub metadata handling with other backends ([#3316](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3316)) ([7e0a8ad](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/7e0a8ad532012576dc5e40bd4e9d54522e307123)), closes [#3292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3292)
|
||||
|
||||
## [2.7.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.7.0...decap-cms-backend-gitlab@2.7.1) (2020-02-22)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "feat(core): Align GitHub metadata handling with other backends (#3292)" ([5bdd3df](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/5bdd3df9ccbb5149c22d79987ebdcd6cab4b261f)), closes [#3292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3292)
|
||||
|
||||
# [2.7.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.6.0...decap-cms-backend-gitlab@2.7.0) (2020-02-22)
|
||||
|
||||
### Features
|
||||
|
||||
- **core:** Align GitHub metadata handling with other backends ([#3292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3292)) ([8193b5a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/8193b5ace89d6f14a6c756235a50b186a763b6b1))
|
||||
|
||||
# [2.6.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.5.3...decap-cms-backend-gitlab@2.6.0) (2020-02-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- filter paginated results ([#3216](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3216)) ([0a482b1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/0a482b10049bcfa022b81165cabf4512d77e0b88))
|
||||
- workflow file collection ([#3207](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3207)) ([d22f7e6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/d22f7e680e7064e8607cf8b420571fa40a6c314e))
|
||||
|
||||
### Features
|
||||
|
||||
- field based media/public folders ([#3208](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3208)) ([97bc0c8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/97bc0c8dc489e736f89d748ba832d78400fe4332))
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([a015d1d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/a015d1d92a4b1c0130c44fcef1c9ecdb157a0f07))
|
||||
|
||||
## [2.5.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.5.2...decap-cms-backend-gitlab@2.5.3) (2020-02-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **locale:** remove hard coded strings ([#3193](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3193)) ([fc91bf8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/fc91bf8781e65ce1dc946363dbb10419a145c66b))
|
||||
|
||||
## [2.5.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.5.1...decap-cms-backend-gitlab@2.5.2) (2020-01-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-gitlab:** check for shared group permissions ([#3122](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3122)) ([f1739e9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/f1739e978f9dee1de42dd5479ec80a5d991a9bfe))
|
||||
- **git-gateway-gitlab:** fix large media support for editorial workflow ([#3105](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/3105)) ([038803c](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/038803c9f249de386812652372c35c4c53935295))
|
||||
|
||||
## [2.5.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.5.0...decap-cms-backend-gitlab@2.5.1) (2020-01-14)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
# [2.5.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.5.0-beta.0...decap-cms-backend-gitlab@2.5.0) (2020-01-07)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
# [2.5.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.4.0...decap-cms-backend-gitlab@2.5.0-beta.0) (2019-12-18)
|
||||
|
||||
### Features
|
||||
|
||||
- bundle assets with content ([#2958](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2958)) ([2b41d8a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/2b41d8a838a9c8a6b21cde2ddd16b9288334e298))
|
||||
|
||||
# [2.4.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.4.0-beta.1...decap-cms-backend-gitlab@2.4.0) (2019-12-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
# [2.4.0-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.4.0-beta.0...decap-cms-backend-gitlab@2.4.0-beta.1) (2019-12-02)
|
||||
|
||||
### Features
|
||||
|
||||
- content in sub folders ([#2897](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2897)) ([afcfe5b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/afcfe5b6d5f32669e9061ec596bd35ad545d61a3))
|
||||
|
||||
# [2.4.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.3.3-beta.1...decap-cms-backend-gitlab@2.4.0-beta.0) (2019-11-07)
|
||||
|
||||
### Features
|
||||
|
||||
- add go back to site button ([#2538](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2538)) ([f206e7e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/f206e7e5a13fb48ec6b27dce0dbb3a59b61de8f9))
|
||||
- enable specifying custom open authoring commit message ([#2810](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2810)) ([2841ff9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/2841ff9ffe58afcf4dba45514a84a262ad370f1d))
|
||||
|
||||
## [2.3.3-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.3.3-beta.0...decap-cms-backend-gitlab@2.3.3-beta.1) (2019-09-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-gitlab:** exclude directories in paginated results ([#2668](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2668)) ([3903acb](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/3903acb))
|
||||
|
||||
## [2.3.3-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.3.2...decap-cms-backend-gitlab@2.3.3-beta.0) (2019-08-24)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [2.3.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.3.2-beta.0...decap-cms-backend-gitlab@2.3.2) (2019-04-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [2.3.2-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.3.1...decap-cms-backend-gitlab@2.3.2-beta.0) (2019-04-05)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [2.3.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.3.1-beta.1...decap-cms-backend-gitlab@2.3.1) (2019-03-29)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [2.3.1-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.3.1-beta.0...decap-cms-backend-gitlab@2.3.1-beta.1) (2019-03-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- export on decap-cms and maps on esm ([#2244](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2244)) ([6ffd13b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/6ffd13b))
|
||||
|
||||
## [2.3.1-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.3.0...decap-cms-backend-gitlab@2.3.1-beta.0) (2019-03-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update peer dep versions ([#2234](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2234)) ([7987091](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/7987091))
|
||||
|
||||
# [2.3.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.2.0...decap-cms-backend-gitlab@2.3.0) (2019-03-22)
|
||||
|
||||
### Features
|
||||
|
||||
- add ES module builds ([#2215](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2215)) ([d142b32](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/d142b32))
|
||||
|
||||
# [2.2.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.2.0-beta.0...decap-cms-backend-gitlab@2.2.0) (2019-03-22)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
# [2.2.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.1.4-beta.0...decap-cms-backend-gitlab@2.2.0-beta.0) (2019-03-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix umd builds ([#2214](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2214)) ([e04f6be](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/e04f6be))
|
||||
|
||||
### Features
|
||||
|
||||
- provide usable UMD builds for all packages ([#2141](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2141)) ([82cc794](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/82cc794))
|
||||
|
||||
## [2.1.4-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.1.3...decap-cms-backend-gitlab@2.1.4-beta.0) (2019-03-15)
|
||||
|
||||
### Features
|
||||
|
||||
- upgrade to Emotion 10 ([#2166](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/2166)) ([ccef446](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/ccef446))
|
||||
|
||||
## [2.1.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.1.2...decap-cms-backend-gitlab@2.1.3) (2019-03-08)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [2.1.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.1.1...decap-cms-backend-gitlab@2.1.2) (2019-02-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
## [2.1.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.1.0...decap-cms-backend-gitlab@2.1.1) (2018-12-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-gitlab:** show svg previews ([#1946](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/1946)) ([c3adebe](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/c3adebe))
|
||||
|
||||
# [2.1.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.0.6...decap-cms-backend-gitlab@2.1.0) (2018-11-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-gitlab:** support folder names with whitespace ([#1799](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/1799)) ([a9f69f9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/a9f69f9))
|
||||
|
||||
### Features
|
||||
|
||||
- allow custom logo on auth page ([#1818](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/1818)) ([c6ae1e8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/c6ae1e8))
|
||||
|
||||
<a name="2.0.6"></a>
|
||||
|
||||
## [2.0.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.0.5...decap-cms-backend-gitlab@2.0.6) (2018-08-27)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
<a name="2.0.5"></a>
|
||||
|
||||
## [2.0.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.0.4...decap-cms-backend-gitlab@2.0.5) (2018-08-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **gitlab:** fetch media library images through API ([#1433](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/1433)) ([83d2adc](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/83d2adc))
|
||||
|
||||
<a name="2.0.4"></a>
|
||||
|
||||
## [2.0.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.0.3...decap-cms-backend-gitlab@2.0.4) (2018-08-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backends:** fix commit message handling ([#1568](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/1568)) ([f7e7120](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/f7e7120))
|
||||
|
||||
<a name="2.0.3"></a>
|
||||
|
||||
## [2.0.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.0.2...decap-cms-backend-gitlab@2.0.3) (2018-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **gitlab:** fix uploads ([#1566](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/issues/1566)) ([d59c990](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/commit/d59c990))
|
||||
|
||||
<a name="2.0.2"></a>
|
||||
|
||||
## [2.0.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab/compare/decap-cms-backend-gitlab@2.0.1...decap-cms-backend-gitlab@2.0.2) (2018-07-28)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
|
||||
<a name="2.0.1"></a>
|
||||
|
||||
## 2.0.1 (2018-07-26)
|
||||
|
||||
<a name="2.0.0"></a>
|
||||
|
||||
# 2.0.0 (2018-07-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-gitlab
|
||||
13
source/admin/packages/decap-cms-backend-gitlab/README.md
Normal file
13
source/admin/packages/decap-cms-backend-gitlab/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# GitLab backend
|
||||
|
||||
An abstraction layer between the CMS and [GitLab](https://docs.gitlab.com/ee/api/README.html)
|
||||
|
||||
## Code structure
|
||||
|
||||
`Implementation` for [File Management System API](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-lib-util/README.md) based on `Api`. With [Editorial Workflow](https://www.decapcms.org/docs/beta-features/#gitlab-and-bitbucket-editorial-workflow-support) uses merge requests labels to track unpublished entries statuses.
|
||||
|
||||
`Api` - A wrapper for GitLab REST API.
|
||||
|
||||
`AuthenticationPage` - A component uses [lib-auth](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-lib-auth/README.md) to facilitate OAuth, PKCE and implicit authentication.
|
||||
|
||||
Look at tests or types for more info.
|
||||
44
source/admin/packages/decap-cms-backend-gitlab/package.json
Normal file
44
source/admin/packages/decap-cms-backend-gitlab/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "decap-cms-backend-gitlab",
|
||||
"description": "GitLab backend for Decap CMS",
|
||||
"version": "3.4.0",
|
||||
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab",
|
||||
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
||||
"license": "MIT",
|
||||
"module": "dist/esm/index.js",
|
||||
"main": "dist/decap-cms-backend-gitlab.js",
|
||||
"keywords": [
|
||||
"decap-cms",
|
||||
"backend",
|
||||
"gitlab"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"develop": "npm run build:esm -- --watch",
|
||||
"build": "cross-env NODE_ENV=production webpack",
|
||||
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward --extensions \".js,.jsx,.ts,.tsx\""
|
||||
},
|
||||
"dependencies": {
|
||||
"apollo-cache-inmemory": "^1.6.2",
|
||||
"apollo-client": "^2.6.3",
|
||||
"apollo-link-context": "^1.0.18",
|
||||
"apollo-link-http": "^1.5.15",
|
||||
"js-base64": "^3.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"semaphore": "^1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"decap-cms-lib-auth": "^3.0.0",
|
||||
"decap-cms-lib-util": "^3.0.0",
|
||||
"decap-cms-ui-default": "^3.0.0",
|
||||
"immutable": "^3.7.6",
|
||||
"lodash": "^4.17.11",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^19.1.0"
|
||||
},
|
||||
"browser": {
|
||||
"path": "path-browserify"
|
||||
}
|
||||
}
|
||||
1021
source/admin/packages/decap-cms-backend-gitlab/src/API.ts
Normal file
1021
source/admin/packages/decap-cms-backend-gitlab/src/API.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
NetlifyAuthenticator,
|
||||
ImplicitAuthenticator,
|
||||
PkceAuthenticator,
|
||||
} from 'decap-cms-lib-auth';
|
||||
import { AuthenticationPage, Icon } from 'decap-cms-ui-default';
|
||||
|
||||
const LoginButtonIcon = styled(Icon)`
|
||||
margin-right: 18px;
|
||||
`;
|
||||
|
||||
const clientSideAuthenticators = {
|
||||
pkce: ({
|
||||
base_url,
|
||||
auth_endpoint,
|
||||
app_id,
|
||||
auth_token_endpoint}) =>
|
||||
new PkceAuthenticator({
|
||||
base_url,
|
||||
auth_endpoint,
|
||||
app_id,
|
||||
auth_token_endpoint,
|
||||
auth_token_endpoint_content_type: 'application/json; charset=utf-8',
|
||||
}),
|
||||
|
||||
implicit: ({
|
||||
base_url,
|
||||
auth_endpoint,
|
||||
app_id,
|
||||
clearHash }) =>
|
||||
new ImplicitAuthenticator({
|
||||
base_url,
|
||||
auth_endpoint,
|
||||
app_id,
|
||||
clearHash,
|
||||
}),
|
||||
};
|
||||
|
||||
export default class GitLabAuthenticationPage extends React.Component {
|
||||
static propTypes = {
|
||||
onLogin: PropTypes.func.isRequired,
|
||||
inProgress: PropTypes.bool,
|
||||
base_url: PropTypes.string,
|
||||
siteId: PropTypes.string,
|
||||
authEndpoint: PropTypes.string,
|
||||
config: PropTypes.object.isRequired,
|
||||
clearHash: PropTypes.func,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
componentDidMount() {
|
||||
// Manually validate PropTypes - React 19 breaking change
|
||||
PropTypes.checkPropTypes(GitLabAuthenticationPage.propTypes, this.props, 'prop', 'GitLabAuthenticationPage');
|
||||
|
||||
const {
|
||||
auth_type: authType = '',
|
||||
base_url = 'https://gitlab.com',
|
||||
auth_endpoint = 'oauth/authorize',
|
||||
app_id = '',
|
||||
} = this.props.config.backend;
|
||||
|
||||
if (clientSideAuthenticators[authType]) {
|
||||
this.auth = clientSideAuthenticators[authType]({
|
||||
base_url,
|
||||
auth_endpoint,
|
||||
app_id,
|
||||
auth_token_endpoint: 'oauth/token',
|
||||
clearHash: this.props.clearHash,
|
||||
});
|
||||
// Complete implicit authentication if we were redirected back to from the provider.
|
||||
this.auth.completeAuth((err, data) => {
|
||||
if (err) {
|
||||
this.setState({ loginError: err.toString() });
|
||||
return;
|
||||
}
|
||||
this.props.onLogin(data);
|
||||
});
|
||||
} else {
|
||||
this.auth = new NetlifyAuthenticator({
|
||||
base_url: this.props.base_url,
|
||||
site_id:
|
||||
document.location.host.split(':')[0] === 'localhost'
|
||||
? 'demo.decapcms.org'
|
||||
: this.props.siteId,
|
||||
auth_endpoint: this.props.authEndpoint,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleLogin = e => {
|
||||
e.preventDefault();
|
||||
this.auth.authenticate({ provider: 'gitlab', scope: 'api' }, (err, data) => {
|
||||
if (err) {
|
||||
this.setState({ loginError: err.toString() });
|
||||
return;
|
||||
}
|
||||
this.props.onLogin(data);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { inProgress, config, t } = this.props;
|
||||
return (
|
||||
<AuthenticationPage
|
||||
onLogin={this.handleLogin}
|
||||
loginDisabled={inProgress}
|
||||
loginErrorMessage={this.state.loginError}
|
||||
logoUrl={config.logo_url} // Deprecated, replaced by `logo.src`
|
||||
logo={config.logo}
|
||||
siteUrl={config.site_url}
|
||||
renderButtonContent={() => (
|
||||
<React.Fragment>
|
||||
<LoginButtonIcon type="gitlab" />{' '}
|
||||
{inProgress ? t('auth.loggingIn') : t('auth.loginWithGitLab')}
|
||||
</React.Fragment>
|
||||
)}
|
||||
t={t}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import API, { getMaxAccess } from '../API';
|
||||
|
||||
global.fetch = jest.fn().mockRejectedValue(new Error('should not call fetch inside tests'));
|
||||
|
||||
jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
|
||||
describe('GitLab API', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('hasWriteAccess', () => {
|
||||
test('should return true on project access_level >= 30', async () => {
|
||||
const api = new API({ repo: 'repo' });
|
||||
|
||||
api.requestJSON = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ permissions: { project_access: { access_level: 30 } } });
|
||||
|
||||
await expect(api.hasWriteAccess()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
test('should return false on project access_level < 30', async () => {
|
||||
const api = new API({ repo: 'repo' });
|
||||
|
||||
api.requestJSON = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ permissions: { project_access: { access_level: 10 } } });
|
||||
|
||||
await expect(api.hasWriteAccess()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
test('should return true on group access_level >= 30', async () => {
|
||||
const api = new API({ repo: 'repo' });
|
||||
|
||||
api.requestJSON = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ permissions: { group_access: { access_level: 30 } } });
|
||||
|
||||
await expect(api.hasWriteAccess()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
test('should return false on group access_level < 30', async () => {
|
||||
const api = new API({ repo: 'repo' });
|
||||
|
||||
api.requestJSON = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ permissions: { group_access: { access_level: 10 } } });
|
||||
|
||||
await expect(api.hasWriteAccess()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
test('should return true on shared group access_level >= 40', async () => {
|
||||
const api = new API({ repo: 'repo' });
|
||||
api.requestJSON = jest.fn().mockResolvedValueOnce({
|
||||
permissions: { project_access: null, group_access: null },
|
||||
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 40 }],
|
||||
});
|
||||
|
||||
await expect(api.hasWriteAccess()).resolves.toBe(true);
|
||||
|
||||
expect(api.requestJSON).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should return true on shared group access_level >= 30, developers can merge and push', async () => {
|
||||
const api = new API({ repo: 'repo' });
|
||||
|
||||
api.requestJSON = jest.fn();
|
||||
api.requestJSON.mockResolvedValueOnce({
|
||||
permissions: { project_access: null, group_access: null },
|
||||
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 30 }],
|
||||
});
|
||||
api.requestJSON.mockResolvedValueOnce({
|
||||
developers_can_merge: true,
|
||||
developers_can_push: true,
|
||||
});
|
||||
|
||||
await expect(api.hasWriteAccess()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
test('should return false on shared group access_level < 30,', async () => {
|
||||
const api = new API({ repo: 'repo' });
|
||||
|
||||
api.requestJSON = jest.fn();
|
||||
api.requestJSON.mockResolvedValueOnce({
|
||||
permissions: { project_access: null, group_access: null },
|
||||
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 20 }],
|
||||
});
|
||||
api.requestJSON.mockResolvedValueOnce({
|
||||
developers_can_merge: true,
|
||||
developers_can_push: true,
|
||||
});
|
||||
|
||||
await expect(api.hasWriteAccess()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
test("should return false on shared group access_level >= 30, developers can't merge", async () => {
|
||||
const api = new API({ repo: 'repo' });
|
||||
|
||||
api.requestJSON = jest.fn();
|
||||
api.requestJSON.mockResolvedValueOnce({
|
||||
permissions: { project_access: null, group_access: null },
|
||||
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 30 }],
|
||||
});
|
||||
api.requestJSON.mockResolvedValueOnce({
|
||||
developers_can_merge: false,
|
||||
developers_can_push: true,
|
||||
});
|
||||
|
||||
await expect(api.hasWriteAccess()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
test("should return false on shared group access_level >= 30, developers can't push", async () => {
|
||||
const api = new API({ repo: 'repo' });
|
||||
|
||||
api.requestJSON = jest.fn();
|
||||
api.requestJSON.mockResolvedValueOnce({
|
||||
permissions: { project_access: null, group_access: null },
|
||||
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 30 }],
|
||||
});
|
||||
api.requestJSON.mockResolvedValueOnce({
|
||||
developers_can_merge: true,
|
||||
developers_can_push: false,
|
||||
});
|
||||
|
||||
await expect(api.hasWriteAccess()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
test('should return false on shared group access_level >= 30, error getting branch', async () => {
|
||||
const api = new API({ repo: 'repo' });
|
||||
|
||||
api.requestJSON = jest.fn();
|
||||
api.requestJSON.mockResolvedValueOnce({
|
||||
permissions: { project_access: null, group_access: null },
|
||||
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 30 }],
|
||||
});
|
||||
|
||||
const error = new Error('Not Found');
|
||||
api.requestJSON.mockRejectedValue(error);
|
||||
|
||||
await expect(api.hasWriteAccess()).resolves.toBe(false);
|
||||
|
||||
expect(console.log).toHaveBeenCalledTimes(1);
|
||||
expect(console.log).toHaveBeenCalledWith('Failed getting default branch', error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatuses', () => {
|
||||
test('should get preview statuses', async () => {
|
||||
const api = new API({ repo: 'repo' });
|
||||
|
||||
const mr = { sha: 'sha' };
|
||||
const statuses = [
|
||||
{ name: 'deploy', status: 'success', target_url: 'deploy-url' },
|
||||
{ name: 'build', status: 'pending' },
|
||||
];
|
||||
|
||||
api.getBranchMergeRequest = jest.fn(() => Promise.resolve(mr));
|
||||
api.getMergeRequestStatues = jest.fn(() => Promise.resolve(statuses));
|
||||
|
||||
const collectionName = 'posts';
|
||||
const slug = 'title';
|
||||
await expect(api.getStatuses(collectionName, slug)).resolves.toEqual([
|
||||
{ context: 'deploy', state: 'success', target_url: 'deploy-url' },
|
||||
{ context: 'build', state: 'other' },
|
||||
]);
|
||||
|
||||
expect(api.getBranchMergeRequest).toHaveBeenCalledTimes(1);
|
||||
expect(api.getBranchMergeRequest).toHaveBeenCalledWith('cms/posts/title');
|
||||
|
||||
expect(api.getMergeRequestStatues).toHaveBeenCalledTimes(1);
|
||||
expect(api.getMergeRequestStatues).toHaveBeenCalledWith(mr, 'cms/posts/title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMaxAccess', () => {
|
||||
it('should return group with max access level', () => {
|
||||
const groups = [
|
||||
{ group_access_level: 10 },
|
||||
{ group_access_level: 5 },
|
||||
{ group_access_level: 100 },
|
||||
{ group_access_level: 1 },
|
||||
];
|
||||
expect(getMaxAccess(groups)).toBe(groups[2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,552 @@
|
||||
jest.mock('decap-cms-core/src/backend');
|
||||
import { fromJS } from 'immutable';
|
||||
import { oneLine, stripIndent } from 'common-tags';
|
||||
import nock from 'nock';
|
||||
import { Cursor } from 'decap-cms-lib-util';
|
||||
|
||||
import Gitlab from '../implementation';
|
||||
import AuthenticationPage from '../AuthenticationPage';
|
||||
|
||||
const { Backend, LocalStorageAuthStore } = jest.requireActual('decap-cms-core/src/backend');
|
||||
|
||||
function generateEntries(path, length) {
|
||||
const entries = Array.from({ length }, (val, idx) => {
|
||||
const count = idx + 1;
|
||||
const id = `00${count}`.slice(-3);
|
||||
const fileName = `test${id}.md`;
|
||||
return { id, fileName, filePath: `${path}/${fileName}` };
|
||||
});
|
||||
|
||||
return {
|
||||
tree: entries.map(({ id, fileName, filePath }) => ({
|
||||
id: `d8345753a1d935fa47a26317a503e73e1192d${id}`,
|
||||
name: fileName,
|
||||
type: 'blob',
|
||||
path: filePath,
|
||||
mode: '100644',
|
||||
})),
|
||||
files: entries.reduce(
|
||||
(acc, { id, filePath }) => ({
|
||||
...acc,
|
||||
[filePath]: stripIndent`
|
||||
---
|
||||
title: test ${id}
|
||||
---
|
||||
# test ${id}
|
||||
`,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const manyEntries = generateEntries('many-entries', 500);
|
||||
|
||||
const mockRepo = {
|
||||
tree: {
|
||||
'/': [
|
||||
{
|
||||
id: '5d0620ebdbc92068a3e866866e928cc373f18429',
|
||||
name: 'content',
|
||||
type: 'tree',
|
||||
path: 'content',
|
||||
mode: '040000',
|
||||
},
|
||||
],
|
||||
content: [
|
||||
{
|
||||
id: 'b1a200e48be54fde12b636f9563d659d44c206a5',
|
||||
name: 'test1.md',
|
||||
type: 'blob',
|
||||
path: 'content/test1.md',
|
||||
mode: '100644',
|
||||
},
|
||||
{
|
||||
id: 'd8345753a1d935fa47a26317a503e73e1192d623',
|
||||
name: 'test2.md',
|
||||
type: 'blob',
|
||||
path: 'content/test2.md',
|
||||
mode: '100644',
|
||||
},
|
||||
],
|
||||
'many-entries': manyEntries.tree,
|
||||
},
|
||||
files: {
|
||||
'content/test1.md': stripIndent`
|
||||
---
|
||||
title: test
|
||||
---
|
||||
# test
|
||||
`,
|
||||
'content/test2.md': stripIndent`
|
||||
---
|
||||
title: test2
|
||||
---
|
||||
# test 2
|
||||
`,
|
||||
...manyEntries.files,
|
||||
},
|
||||
};
|
||||
|
||||
const resp = {
|
||||
user: {
|
||||
success: {
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
branch: {
|
||||
success: {
|
||||
name: 'master',
|
||||
commit: {
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
project: {
|
||||
success: {
|
||||
permissions: {
|
||||
project_access: {
|
||||
access_level: 30,
|
||||
},
|
||||
},
|
||||
default_branch: 'main',
|
||||
},
|
||||
readOnly: {
|
||||
permissions: {
|
||||
project_access: {
|
||||
access_level: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('gitlab backend', () => {
|
||||
let authStore;
|
||||
let backend;
|
||||
const repo = 'foo/bar';
|
||||
const defaultConfig = {
|
||||
backend: {
|
||||
name: 'gitlab',
|
||||
repo,
|
||||
},
|
||||
};
|
||||
const collectionContentConfig = {
|
||||
name: 'foo',
|
||||
folder: 'content',
|
||||
fields: [{ name: 'title' }],
|
||||
// TODO: folder_based_collection is an internal string, we should not
|
||||
// be depending on it here
|
||||
type: 'folder_based_collection',
|
||||
};
|
||||
const collectionManyEntriesConfig = {
|
||||
name: 'foo',
|
||||
folder: 'many-entries',
|
||||
fields: [{ name: 'title' }],
|
||||
// TODO: folder_based_collection is an internal string, we should not
|
||||
// be depending on it here
|
||||
type: 'folder_based_collection',
|
||||
};
|
||||
const collectionFilesConfig = {
|
||||
name: 'foo',
|
||||
files: [
|
||||
{
|
||||
label: 'foo',
|
||||
name: 'foo',
|
||||
file: 'content/test1.md',
|
||||
fields: [{ name: 'title' }],
|
||||
},
|
||||
{
|
||||
label: 'bar',
|
||||
name: 'bar',
|
||||
file: 'content/test2.md',
|
||||
fields: [{ name: 'title' }],
|
||||
},
|
||||
],
|
||||
type: 'file_based_collection',
|
||||
};
|
||||
const mockCredentials = { token: 'MOCK_TOKEN' };
|
||||
const expectedRepo = encodeURIComponent(repo);
|
||||
const expectedRepoUrl = `/projects/${expectedRepo}`;
|
||||
|
||||
function resolveBackend(config = {}) {
|
||||
authStore = new LocalStorageAuthStore();
|
||||
return new Backend(
|
||||
{
|
||||
init: (...args) => new Gitlab(...args),
|
||||
},
|
||||
{
|
||||
backendName: 'gitlab',
|
||||
config,
|
||||
authStore,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function mockApi(backend) {
|
||||
return nock(backend.implementation.apiRoot);
|
||||
}
|
||||
|
||||
function interceptAuth(backend, { userResponse, projectResponse } = {}) {
|
||||
const api = mockApi(backend);
|
||||
api
|
||||
.get('/user')
|
||||
.query(true)
|
||||
.reply(200, userResponse || resp.user.success);
|
||||
|
||||
api
|
||||
// The `authenticate` method of the API class from netlify-cms-backend-gitlab
|
||||
// calls the same endpoint twice for gettng a single project.
|
||||
// First time through `this.api.hasWriteAccess()
|
||||
// Second time through the method `getDefaultBranchName` from lib-util
|
||||
// As a result, we need to repeat the same response twice.
|
||||
// Otherwise, we'll get an error: "No match for request to
|
||||
// https://gitlab.com/api/v4"
|
||||
|
||||
.get(expectedRepoUrl)
|
||||
.times(2)
|
||||
.query(true)
|
||||
.reply(200, projectResponse || resp.project.success);
|
||||
}
|
||||
|
||||
function interceptBranch(backend, { branch = 'master' } = {}) {
|
||||
const api = mockApi(backend);
|
||||
api
|
||||
.get(`${expectedRepoUrl}/repository/branches/${encodeURIComponent(branch)}`)
|
||||
.query(true)
|
||||
.reply(200, resp.branch.success);
|
||||
}
|
||||
|
||||
function parseQuery(uri) {
|
||||
const query = uri.split('?')[1];
|
||||
if (!query) {
|
||||
return {};
|
||||
}
|
||||
return query.split('&').reduce((acc, q) => {
|
||||
const [key, value] = q.split('=');
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function createHeaders(backend, { basePath, path, page, perPage, pageCount, totalCount }) {
|
||||
const pageNum = parseInt(page, 10);
|
||||
const pageCountNum = parseInt(pageCount, 10);
|
||||
const url = `${backend.implementation.apiRoot}${basePath}`;
|
||||
|
||||
function link(linkPage) {
|
||||
return `<${url}?id=${expectedRepo}&page=${linkPage}&path=${path}&per_page=${perPage}&recursive=false>`;
|
||||
}
|
||||
|
||||
const linkHeader = oneLine`
|
||||
${link(1)}; rel="first",
|
||||
${link(pageCount)}; rel="last",
|
||||
${pageNum === 1 ? '' : `${link(pageNum - 1)}; rel="prev",`}
|
||||
${pageNum === pageCountNum ? '' : `${link(pageNum + 1)}; rel="next",`}
|
||||
`.slice(0, -1);
|
||||
|
||||
return {
|
||||
'X-Page': page,
|
||||
'X-Total-Pages': pageCount,
|
||||
'X-Per-Page': perPage,
|
||||
'X-Total': totalCount,
|
||||
Link: linkHeader,
|
||||
};
|
||||
}
|
||||
|
||||
function interceptCollection(
|
||||
backend,
|
||||
collection,
|
||||
{ verb = 'get', repeat = 1, page: expectedPage } = {},
|
||||
) {
|
||||
const api = mockApi(backend);
|
||||
const url = `${expectedRepoUrl}/repository/tree`;
|
||||
const { folder } = collection;
|
||||
const tree = mockRepo.tree[folder];
|
||||
api[verb](url)
|
||||
.query(({ path, page }) => {
|
||||
if (path !== folder) {
|
||||
return false;
|
||||
}
|
||||
if (expectedPage && page && parseInt(page, 10) !== parseInt(expectedPage, 10)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.times(repeat)
|
||||
.reply(uri => {
|
||||
const { page = 1, per_page = 20 } = parseQuery(uri);
|
||||
const pageCount = tree.length <= per_page ? 1 : Math.round(tree.length / per_page);
|
||||
const pageLastIndex = page * per_page;
|
||||
const pageFirstIndex = pageLastIndex - per_page;
|
||||
const resp = tree.slice(pageFirstIndex, pageLastIndex);
|
||||
return [
|
||||
200,
|
||||
verb === 'head' ? null : resp,
|
||||
createHeaders(backend, {
|
||||
basePath: url,
|
||||
path: folder,
|
||||
page,
|
||||
perPage: per_page,
|
||||
pageCount,
|
||||
totalCount: tree.length,
|
||||
}),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
function interceptFiles(backend, path) {
|
||||
const api = mockApi(backend);
|
||||
const url = `${expectedRepoUrl}/repository/files/${encodeURIComponent(path)}/raw`;
|
||||
api.get(url).query(true).reply(200, mockRepo.files[path]);
|
||||
|
||||
api
|
||||
.get(`${expectedRepoUrl}/repository/commits`)
|
||||
.query(({ path }) => path === path)
|
||||
.reply(200, [
|
||||
{
|
||||
author_name: 'author_name',
|
||||
author_email: 'author_email',
|
||||
authored_date: 'authored_date',
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function sharedSetup() {
|
||||
beforeEach(async () => {
|
||||
backend = resolveBackend(defaultConfig);
|
||||
interceptAuth(backend);
|
||||
await backend.authenticate(mockCredentials);
|
||||
interceptCollection(backend, collectionManyEntriesConfig, { verb: 'head' });
|
||||
interceptCollection(backend, collectionContentConfig, { verb: 'head' });
|
||||
});
|
||||
}
|
||||
|
||||
it('throws if configuration does not include repo', () => {
|
||||
expect(() => resolveBackend({ backend: {} })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"The GitLab backend needs a \\"repo\\" in the backend configuration."`,
|
||||
);
|
||||
});
|
||||
|
||||
describe('authComponent', () => {
|
||||
it('returns authentication page component', () => {
|
||||
backend = resolveBackend(defaultConfig);
|
||||
expect(backend.authComponent()).toEqual(AuthenticationPage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authenticate', () => {
|
||||
it('throws if user does not have access to project', async () => {
|
||||
backend = resolveBackend(defaultConfig);
|
||||
interceptAuth(backend, { projectResponse: resp.project.readOnly });
|
||||
await expect(
|
||||
backend.authenticate(mockCredentials),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Your GitLab user account does not have access to this repo."`,
|
||||
);
|
||||
});
|
||||
|
||||
it('stores and returns user object on success', async () => {
|
||||
const backendName = defaultConfig.backend.name;
|
||||
backend = resolveBackend(defaultConfig);
|
||||
interceptAuth(backend);
|
||||
const user = await backend.authenticate(mockCredentials);
|
||||
expect(authStore.retrieve()).toEqual(user);
|
||||
expect(user).toEqual({ ...resp.user.success, ...mockCredentials, backendName });
|
||||
});
|
||||
});
|
||||
|
||||
describe('currentUser', () => {
|
||||
it('returns null if no user', async () => {
|
||||
backend = resolveBackend(defaultConfig);
|
||||
const user = await backend.currentUser();
|
||||
expect(user).toEqual(null);
|
||||
});
|
||||
|
||||
it('returns the stored user if exists', async () => {
|
||||
const backendName = defaultConfig.backend.name;
|
||||
backend = resolveBackend(defaultConfig);
|
||||
interceptAuth(backend);
|
||||
await backend.authenticate(mockCredentials);
|
||||
const user = await backend.currentUser();
|
||||
expect(user).toEqual({ ...resp.user.success, ...mockCredentials, backendName });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getToken', () => {
|
||||
it('returns the token for the current user', async () => {
|
||||
backend = resolveBackend(defaultConfig);
|
||||
interceptAuth(backend);
|
||||
await backend.authenticate(mockCredentials);
|
||||
const token = await backend.getToken();
|
||||
expect(token).toEqual(mockCredentials.token);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
it('sets token to null', async () => {
|
||||
backend = resolveBackend(defaultConfig);
|
||||
interceptAuth(backend);
|
||||
await backend.authenticate(mockCredentials);
|
||||
await backend.logout();
|
||||
const token = await backend.getToken();
|
||||
expect(token).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEntry', () => {
|
||||
sharedSetup();
|
||||
|
||||
it('returns an entry from folder collection', async () => {
|
||||
const entryTree = mockRepo.tree[collectionContentConfig.folder][0];
|
||||
const slug = entryTree.path.split('/').pop().replace('.md', '');
|
||||
|
||||
interceptFiles(backend, entryTree.path);
|
||||
interceptCollection(backend, collectionContentConfig);
|
||||
|
||||
const entry = await backend.getEntry(
|
||||
{
|
||||
config: {},
|
||||
integrations: fromJS([]),
|
||||
entryDraft: fromJS({}),
|
||||
mediaLibrary: fromJS({}),
|
||||
},
|
||||
fromJS(collectionContentConfig),
|
||||
slug,
|
||||
);
|
||||
|
||||
expect(entry).toEqual(expect.objectContaining({ path: entryTree.path }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('listEntries', () => {
|
||||
sharedSetup();
|
||||
|
||||
it('returns entries from folder collection', async () => {
|
||||
const tree = mockRepo.tree[collectionContentConfig.folder];
|
||||
tree.forEach(file => interceptFiles(backend, file.path));
|
||||
|
||||
interceptCollection(backend, collectionContentConfig);
|
||||
const entries = await backend.listEntries(fromJS(collectionContentConfig));
|
||||
|
||||
expect(entries).toEqual({
|
||||
cursor: expect.any(Cursor),
|
||||
pagination: 1,
|
||||
entries: expect.arrayContaining(
|
||||
tree.map(file => expect.objectContaining({ path: file.path })),
|
||||
),
|
||||
});
|
||||
expect(entries.entries).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('returns all entries from folder collection', async () => {
|
||||
const tree = mockRepo.tree[collectionManyEntriesConfig.folder];
|
||||
interceptBranch(backend);
|
||||
tree.forEach(file => interceptFiles(backend, file.path));
|
||||
|
||||
interceptCollection(backend, collectionManyEntriesConfig, { repeat: 5 });
|
||||
const entries = await backend.listAllEntries(fromJS(collectionManyEntriesConfig));
|
||||
|
||||
expect(entries).toEqual(
|
||||
expect.arrayContaining(tree.map(file => expect.objectContaining({ path: file.path }))),
|
||||
);
|
||||
expect(entries).toHaveLength(500);
|
||||
}, 7000);
|
||||
|
||||
it('returns entries from file collection', async () => {
|
||||
const { files } = collectionFilesConfig;
|
||||
files.forEach(file => interceptFiles(backend, file.file));
|
||||
const entries = await backend.listEntries(fromJS(collectionFilesConfig));
|
||||
|
||||
expect(entries).toEqual({
|
||||
cursor: expect.any(Cursor),
|
||||
entries: expect.arrayContaining(
|
||||
files.map(file => expect.objectContaining({ path: file.file })),
|
||||
),
|
||||
});
|
||||
expect(entries.entries).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('returns first page from paginated folder collection tree', async () => {
|
||||
const tree = mockRepo.tree[collectionManyEntriesConfig.folder];
|
||||
const pageTree = tree.slice(0, 20);
|
||||
pageTree.forEach(file => interceptFiles(backend, file.path));
|
||||
interceptCollection(backend, collectionManyEntriesConfig, { page: 1 });
|
||||
const entries = await backend.listEntries(fromJS(collectionManyEntriesConfig));
|
||||
|
||||
expect(entries.entries).toEqual(
|
||||
expect.arrayContaining(pageTree.map(file => expect.objectContaining({ path: file.path }))),
|
||||
);
|
||||
expect(entries.entries).toHaveLength(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('traverseCursor', () => {
|
||||
sharedSetup();
|
||||
|
||||
it('returns complete last page of paginated tree', async () => {
|
||||
const tree = mockRepo.tree[collectionManyEntriesConfig.folder];
|
||||
tree.slice(0, 20).forEach(file => interceptFiles(backend, file.path));
|
||||
interceptCollection(backend, collectionManyEntriesConfig, { page: 1 });
|
||||
const entries = await backend.listEntries(fromJS(collectionManyEntriesConfig));
|
||||
|
||||
const nextPageTree = tree.slice(20, 40);
|
||||
nextPageTree.forEach(file => interceptFiles(backend, file.path));
|
||||
interceptCollection(backend, collectionManyEntriesConfig, { page: 2 });
|
||||
const nextPage = await backend.traverseCursor(entries.cursor, 'next');
|
||||
|
||||
expect(nextPage.entries).toEqual(
|
||||
expect.arrayContaining(
|
||||
nextPageTree.map(file => expect.objectContaining({ path: file.path })),
|
||||
),
|
||||
);
|
||||
expect(nextPage.entries).toHaveLength(20);
|
||||
|
||||
const lastPageTree = tree.slice(-20);
|
||||
lastPageTree.forEach(file => interceptFiles(backend, file.path));
|
||||
interceptCollection(backend, collectionManyEntriesConfig, { page: 25 });
|
||||
const lastPage = await backend.traverseCursor(nextPage.cursor, 'last');
|
||||
expect(lastPage.entries).toEqual(
|
||||
expect.arrayContaining(
|
||||
lastPageTree.map(file => expect.objectContaining({ path: file.path })),
|
||||
),
|
||||
);
|
||||
expect(lastPage.entries).toHaveLength(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterFile', () => {
|
||||
it('should return true for nested file with matching depth', () => {
|
||||
backend = resolveBackend(defaultConfig);
|
||||
|
||||
expect(
|
||||
backend.implementation.filterFile(
|
||||
'content/posts',
|
||||
{ name: 'index.md', path: 'content/posts/dir1/dir2/index.md' },
|
||||
'md',
|
||||
3,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for nested file with non matching depth', () => {
|
||||
backend = resolveBackend(defaultConfig);
|
||||
|
||||
expect(
|
||||
backend.implementation.filterFile(
|
||||
'content/posts',
|
||||
{ name: 'index.md', path: 'content/posts/dir1/dir2/index.md' },
|
||||
'md',
|
||||
2,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
authStore.logout();
|
||||
backend = null;
|
||||
expect(authStore.retrieve()).toEqual(null);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,470 @@
|
||||
import trimStart from 'lodash/trimStart';
|
||||
import semaphore from 'semaphore';
|
||||
import trim from 'lodash/trim';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import {
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
basename,
|
||||
entriesByFolder,
|
||||
entriesByFiles,
|
||||
getMediaDisplayURL,
|
||||
getMediaAsBlob,
|
||||
unpublishedEntries,
|
||||
getPreviewStatus,
|
||||
asyncLock,
|
||||
runWithLock,
|
||||
getBlobSHA,
|
||||
blobToFileObj,
|
||||
contentKeyFromBranch,
|
||||
generateContentKey,
|
||||
localForage,
|
||||
allEntriesByFolder,
|
||||
filterByExtension,
|
||||
branchFromContentKey,
|
||||
getDefaultBranchName,
|
||||
} from 'decap-cms-lib-util';
|
||||
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import API, { API_NAME } from './API';
|
||||
|
||||
import type {
|
||||
Entry,
|
||||
AssetProxy,
|
||||
PersistOptions,
|
||||
Cursor,
|
||||
Implementation,
|
||||
DisplayURL,
|
||||
User,
|
||||
Credentials,
|
||||
Config,
|
||||
ImplementationFile,
|
||||
UnpublishedEntryMediaFile,
|
||||
AsyncLock,
|
||||
} from 'decap-cms-lib-util';
|
||||
import type { Semaphore } from 'semaphore';
|
||||
|
||||
const MAX_CONCURRENT_DOWNLOADS = 10;
|
||||
|
||||
export default class GitLab implements Implementation {
|
||||
lock: AsyncLock;
|
||||
api: API | null;
|
||||
options: {
|
||||
proxied: boolean;
|
||||
API: API | null;
|
||||
initialWorkflowStatus: string;
|
||||
};
|
||||
repo: string;
|
||||
isBranchConfigured: boolean;
|
||||
branch: string;
|
||||
apiRoot: string;
|
||||
token: string | null;
|
||||
squashMerges: boolean;
|
||||
cmsLabelPrefix: string;
|
||||
mediaFolder: string;
|
||||
previewContext: string;
|
||||
useGraphQL: boolean;
|
||||
graphQLAPIRoot: string;
|
||||
|
||||
_mediaDisplayURLSem?: Semaphore;
|
||||
|
||||
constructor(config: Config, options = {}) {
|
||||
this.options = {
|
||||
proxied: false,
|
||||
API: null,
|
||||
initialWorkflowStatus: '',
|
||||
...options,
|
||||
};
|
||||
|
||||
if (
|
||||
!this.options.proxied &&
|
||||
(config.backend.repo === null || config.backend.repo === undefined)
|
||||
) {
|
||||
throw new Error('The GitLab backend needs a "repo" in the backend configuration.');
|
||||
}
|
||||
|
||||
this.api = this.options.API || null;
|
||||
|
||||
this.repo = config.backend.repo || '';
|
||||
this.branch = config.backend.branch || 'master';
|
||||
this.isBranchConfigured = config.backend.branch ? true : false;
|
||||
this.apiRoot = config.backend.api_root || 'https://gitlab.com/api/v4';
|
||||
this.token = '';
|
||||
this.squashMerges = config.backend.squash_merges || false;
|
||||
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
|
||||
this.mediaFolder = config.media_folder;
|
||||
this.previewContext = config.backend.preview_context || '';
|
||||
this.useGraphQL = config.backend.use_graphql || false;
|
||||
this.graphQLAPIRoot = config.backend.graphql_api_root || 'https://gitlab.com/api/graphql';
|
||||
this.lock = asyncLock();
|
||||
}
|
||||
|
||||
isGitBackend() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async status() {
|
||||
const auth =
|
||||
(await this.api
|
||||
?.user()
|
||||
.then(user => !!user)
|
||||
.catch(e => {
|
||||
console.warn('Failed getting GitLab user', e);
|
||||
return false;
|
||||
})) || false;
|
||||
|
||||
return { auth: { status: auth }, api: { status: true, statusPage: '' } };
|
||||
}
|
||||
|
||||
authComponent() {
|
||||
return AuthenticationPage;
|
||||
}
|
||||
|
||||
restoreUser(user: User) {
|
||||
return this.authenticate(user);
|
||||
}
|
||||
|
||||
async authenticate(state: Credentials) {
|
||||
this.token = state.token as string;
|
||||
this.api = new API({
|
||||
token: this.token,
|
||||
branch: this.branch,
|
||||
repo: this.repo,
|
||||
apiRoot: this.apiRoot,
|
||||
squashMerges: this.squashMerges,
|
||||
cmsLabelPrefix: this.cmsLabelPrefix,
|
||||
initialWorkflowStatus: this.options.initialWorkflowStatus,
|
||||
useGraphQL: this.useGraphQL,
|
||||
graphQLAPIRoot: this.graphQLAPIRoot,
|
||||
});
|
||||
const user = await this.api.user();
|
||||
const isCollab = await this.api.hasWriteAccess().catch((error: Error) => {
|
||||
error.message = stripIndent`
|
||||
Repo "${this.repo}" not found.
|
||||
|
||||
Please ensure the repo information is spelled correctly.
|
||||
|
||||
If the repo is private, make sure you're logged into a GitLab account with access.
|
||||
`;
|
||||
throw error;
|
||||
});
|
||||
|
||||
// Unauthorized user
|
||||
if (!isCollab) {
|
||||
throw new Error('Your GitLab user account does not have access to this repo.');
|
||||
}
|
||||
|
||||
if (!this.isBranchConfigured) {
|
||||
const defaultBranchName = await getDefaultBranchName({
|
||||
backend: 'gitlab',
|
||||
repo: this.repo,
|
||||
token: this.token,
|
||||
apiRoot: this.apiRoot,
|
||||
});
|
||||
if (defaultBranchName) {
|
||||
this.branch = defaultBranchName;
|
||||
}
|
||||
}
|
||||
// Authorized user
|
||||
return { ...user, login: user.username, token: state.token as string };
|
||||
}
|
||||
|
||||
async logout() {
|
||||
this.token = null;
|
||||
return;
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return Promise.resolve(this.token);
|
||||
}
|
||||
|
||||
filterFile(
|
||||
folder: string,
|
||||
file: { path: string; name: string },
|
||||
extension: string,
|
||||
depth: number,
|
||||
) {
|
||||
// gitlab paths include the root folder
|
||||
const fileFolder = trim(file.path.split(folder)[1] || '/', '/');
|
||||
return filterByExtension(file, extension) && fileFolder.split('/').length <= depth;
|
||||
}
|
||||
|
||||
async entriesByFolder(folder: string, extension: string, depth: number) {
|
||||
let cursor: Cursor;
|
||||
|
||||
const listFiles = () =>
|
||||
this.api!.listFiles(folder, depth > 1).then(({ files, cursor: c }) => {
|
||||
cursor = c.mergeMeta({ folder, extension, depth });
|
||||
return files.filter(file => this.filterFile(folder, file, extension, depth));
|
||||
});
|
||||
|
||||
const files = await entriesByFolder(
|
||||
listFiles,
|
||||
this.api!.readFile.bind(this.api!),
|
||||
this.api!.readFileMetadata.bind(this.api),
|
||||
API_NAME,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
files[CURSOR_COMPATIBILITY_SYMBOL] = cursor;
|
||||
return files;
|
||||
}
|
||||
|
||||
async listAllFiles(folder: string, extension: string, depth: number) {
|
||||
const files = await this.api!.listAllFiles(folder, depth > 1);
|
||||
const filtered = files.filter(file => this.filterFile(folder, file, extension, depth));
|
||||
return filtered;
|
||||
}
|
||||
|
||||
async allEntriesByFolder(folder: string, extension: string, depth: number) {
|
||||
const files = await allEntriesByFolder({
|
||||
listAllFiles: () => this.listAllFiles(folder, extension, depth),
|
||||
readFile: this.api!.readFile.bind(this.api!),
|
||||
readFileMetadata: this.api!.readFileMetadata.bind(this.api),
|
||||
apiName: API_NAME,
|
||||
branch: this.branch,
|
||||
localForage,
|
||||
folder,
|
||||
extension,
|
||||
depth,
|
||||
getDefaultBranch: () =>
|
||||
this.api!.getDefaultBranch().then(b => ({ name: b.name, sha: b.commit.id })),
|
||||
isShaExistsInBranch: this.api!.isShaExistsInBranch.bind(this.api!),
|
||||
getDifferences: (to, from) => this.api!.getDifferences(to, from),
|
||||
getFileId: path => this.api!.getFileId(path, this.branch),
|
||||
filterFile: file => this.filterFile(folder, file, extension, depth),
|
||||
customFetch: this.useGraphQL ? files => this.api!.readFilesGraphQL(files) : undefined,
|
||||
});
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
entriesByFiles(files: ImplementationFile[]) {
|
||||
return entriesByFiles(
|
||||
files,
|
||||
this.api!.readFile.bind(this.api!),
|
||||
this.api!.readFileMetadata.bind(this.api),
|
||||
API_NAME,
|
||||
);
|
||||
}
|
||||
|
||||
// Fetches a single entry.
|
||||
getEntry(path: string) {
|
||||
return this.api!.readFile(path).then(data => ({
|
||||
file: { path, id: null },
|
||||
data: data as string,
|
||||
}));
|
||||
}
|
||||
|
||||
getMedia(mediaFolder = this.mediaFolder) {
|
||||
return this.api!.listAllFiles(mediaFolder).then(files =>
|
||||
files.map(({ id, name, path }) => {
|
||||
return { id, name, path, displayURL: { id, name, path } };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
getMediaDisplayURL(displayURL: DisplayURL) {
|
||||
this._mediaDisplayURLSem = this._mediaDisplayURLSem || semaphore(MAX_CONCURRENT_DOWNLOADS);
|
||||
return getMediaDisplayURL(
|
||||
displayURL,
|
||||
this.api!.readFile.bind(this.api!),
|
||||
this._mediaDisplayURLSem,
|
||||
);
|
||||
}
|
||||
|
||||
async getMediaFile(path: string) {
|
||||
const name = basename(path);
|
||||
const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!));
|
||||
const fileObj = blobToFileObj(name, blob);
|
||||
const url = URL.createObjectURL(fileObj);
|
||||
const id = await getBlobSHA(blob);
|
||||
|
||||
return {
|
||||
id,
|
||||
displayURL: url,
|
||||
path,
|
||||
name,
|
||||
size: fileObj.size,
|
||||
file: fileObj,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
async persistEntry(entry: Entry, options: PersistOptions) {
|
||||
// persistEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.persistFiles(entry.dataFiles, entry.assets, options),
|
||||
'Failed to acquire persist entry lock',
|
||||
);
|
||||
}
|
||||
|
||||
async persistMedia(mediaFile: AssetProxy, options: PersistOptions) {
|
||||
const fileObj = mediaFile.fileObj as File;
|
||||
|
||||
const [id] = await Promise.all([
|
||||
getBlobSHA(fileObj),
|
||||
this.api!.persistFiles([], [mediaFile], options),
|
||||
]);
|
||||
|
||||
const { path } = mediaFile;
|
||||
const url = URL.createObjectURL(fileObj);
|
||||
|
||||
return {
|
||||
displayURL: url,
|
||||
path: trimStart(path, '/'),
|
||||
name: fileObj!.name,
|
||||
size: fileObj!.size,
|
||||
file: fileObj,
|
||||
url,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
deleteFiles(paths: string[], commitMessage: string) {
|
||||
return this.api!.deleteFiles(paths, commitMessage);
|
||||
}
|
||||
|
||||
traverseCursor(cursor: Cursor, action: string) {
|
||||
return this.api!.traverseCursor(cursor, action).then(async ({ entries, cursor: newCursor }) => {
|
||||
const [folder, depth, extension] = [
|
||||
cursor.meta?.get('folder') as string,
|
||||
cursor.meta?.get('depth') as number,
|
||||
cursor.meta?.get('extension') as string,
|
||||
];
|
||||
if (folder && depth && extension) {
|
||||
entries = entries.filter(f => this.filterFile(folder, f, extension, depth));
|
||||
newCursor = newCursor.mergeMeta({ folder, extension, depth });
|
||||
}
|
||||
const entriesWithData = await entriesByFiles(
|
||||
entries,
|
||||
this.api!.readFile.bind(this.api!),
|
||||
this.api!.readFileMetadata.bind(this.api)!,
|
||||
API_NAME,
|
||||
);
|
||||
return {
|
||||
entries: entriesWithData,
|
||||
cursor: newCursor,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
loadMediaFile(branch: string, file: UnpublishedEntryMediaFile) {
|
||||
const readFile = (
|
||||
path: string,
|
||||
id: string | null | undefined,
|
||||
{ parseText }: { parseText: boolean },
|
||||
) => this.api!.readFile(path, id, { branch, parseText });
|
||||
|
||||
return getMediaAsBlob(file.path, null, readFile).then(blob => {
|
||||
const name = basename(file.path);
|
||||
const fileObj = blobToFileObj(name, blob);
|
||||
return {
|
||||
id: file.path,
|
||||
displayURL: URL.createObjectURL(fileObj),
|
||||
path: file.path,
|
||||
name,
|
||||
size: fileObj.size,
|
||||
file: fileObj,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async loadEntryMediaFiles(branch: string, files: UnpublishedEntryMediaFile[]) {
|
||||
const mediaFiles = await Promise.all(files.map(file => this.loadMediaFile(branch, file)));
|
||||
|
||||
return mediaFiles;
|
||||
}
|
||||
|
||||
async unpublishedEntries() {
|
||||
const listEntriesKeys = () =>
|
||||
this.api!.listUnpublishedBranches().then(branches =>
|
||||
branches.map(branch => contentKeyFromBranch(branch)),
|
||||
);
|
||||
|
||||
const ids = await unpublishedEntries(listEntriesKeys);
|
||||
return ids;
|
||||
}
|
||||
|
||||
async unpublishedEntry({
|
||||
id,
|
||||
collection,
|
||||
slug,
|
||||
}: {
|
||||
id?: string;
|
||||
collection?: string;
|
||||
slug?: string;
|
||||
}) {
|
||||
if (id) {
|
||||
const data = await this.api!.retrieveUnpublishedEntryData(id);
|
||||
return data;
|
||||
} else if (collection && slug) {
|
||||
const entryId = generateContentKey(collection, slug);
|
||||
const data = await this.api!.retrieveUnpublishedEntryData(entryId);
|
||||
return data;
|
||||
} else {
|
||||
throw new Error('Missing unpublished entry id or collection and slug');
|
||||
}
|
||||
}
|
||||
|
||||
getBranch(collection: string, slug: string) {
|
||||
const contentKey = generateContentKey(collection, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
return branch;
|
||||
}
|
||||
|
||||
async unpublishedEntryDataFile(collection: string, slug: string, path: string, id: string) {
|
||||
const branch = this.getBranch(collection, slug);
|
||||
const data = (await this.api!.readFile(path, id, { branch })) as string;
|
||||
return data;
|
||||
}
|
||||
|
||||
async unpublishedEntryMediaFile(collection: string, slug: string, path: string, id: string) {
|
||||
const branch = this.getBranch(collection, slug);
|
||||
const mediaFile = await this.loadMediaFile(branch, { path, id });
|
||||
return mediaFile;
|
||||
}
|
||||
|
||||
async updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
// updateUnpublishedEntryStatus is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.updateUnpublishedEntryStatus(collection, slug, newStatus),
|
||||
'Failed to acquire update entry status lock',
|
||||
);
|
||||
}
|
||||
|
||||
async deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
// deleteUnpublishedEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.deleteUnpublishedEntry(collection, slug),
|
||||
'Failed to acquire delete entry lock',
|
||||
);
|
||||
}
|
||||
|
||||
async publishUnpublishedEntry(collection: string, slug: string) {
|
||||
// publishUnpublishedEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.publishUnpublishedEntry(collection, slug),
|
||||
'Failed to acquire publish entry lock',
|
||||
);
|
||||
}
|
||||
|
||||
async getDeployPreview(collection: string, slug: string) {
|
||||
try {
|
||||
const statuses = await this.api!.getStatuses(collection, slug);
|
||||
const deployStatus = getPreviewStatus(statuses, this.previewContext);
|
||||
|
||||
if (deployStatus) {
|
||||
const { target_url: url, state } = deployStatus;
|
||||
return { url, status: state };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
source/admin/packages/decap-cms-backend-gitlab/src/index.ts
Normal file
10
source/admin/packages/decap-cms-backend-gitlab/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import GitLabBackend from './implementation';
|
||||
import API from './API';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
|
||||
export const DecapCmsBackendGitlab = {
|
||||
GitLabBackend,
|
||||
API,
|
||||
AuthenticationPage,
|
||||
};
|
||||
export { GitLabBackend, API, AuthenticationPage };
|
||||
@@ -0,0 +1,73 @@
|
||||
import { gql } from 'graphql-tag';
|
||||
import { oneLine } from 'common-tags';
|
||||
|
||||
export const files = gql`
|
||||
query files($repo: ID!, $branch: String!, $path: String!, $recursive: Boolean!, $cursor: String) {
|
||||
project(fullPath: $repo) {
|
||||
repository {
|
||||
tree(ref: $branch, path: $path, recursive: $recursive) {
|
||||
blobs(after: $cursor) {
|
||||
nodes {
|
||||
type
|
||||
id: sha
|
||||
path
|
||||
name
|
||||
}
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const blobs = gql`
|
||||
query blobs($repo: ID!, $branch: String!, $paths: [String!]!) {
|
||||
project(fullPath: $repo) {
|
||||
repository {
|
||||
blobs(ref: $branch, paths: $paths) {
|
||||
nodes {
|
||||
id
|
||||
data: rawBlob
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function lastCommits(paths: string[]) {
|
||||
const tree = paths
|
||||
.map(
|
||||
(path, index) => oneLine`
|
||||
tree${index}: tree(ref: $branch, path: "${path}") {
|
||||
lastCommit {
|
||||
authorName
|
||||
authoredDate
|
||||
author {
|
||||
id
|
||||
username
|
||||
name
|
||||
publicEmail
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
const query = gql`
|
||||
query lastCommits($repo: ID!, $branch: String!) {
|
||||
project(fullPath: $repo) {
|
||||
repository {
|
||||
${tree}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return query;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
const { getConfig } = require('../../scripts/webpack.js');
|
||||
|
||||
module.exports = getConfig();
|
||||
168
source/admin/packages/decap-cms-backend-proxy/CHANGELOG.md
Normal file
168
source/admin/packages/decap-cms-backend-proxy/CHANGELOG.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# 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-proxy@3.1.4...decap-cms-backend-proxy@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.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-proxy@3.1.4...decap-cms-backend-proxy@3.2.0) (2025-06-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
## [3.1.4](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-proxy@3.1.3...decap-cms-backend-proxy@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-proxy@3.1.2...decap-cms-backend-proxy@3.1.3) (2024-08-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
## [3.1.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-proxy@3.1.1...decap-cms-backend-proxy@3.1.2) (2024-06-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- display custom logo when using a proxy server ([#7235](https://github.com/decaporg/decap-cms/issues/7235)) ([7feca52](https://github.com/decaporg/decap-cms/commit/7feca52605315a764dfbe1b65c6782a644191dd4))
|
||||
|
||||
## [3.1.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-proxy@3.1.0-beta.1...decap-cms-backend-proxy@3.1.1) (2024-03-21)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
# [3.1.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-proxy@3.1.0-beta.1...decap-cms-backend-proxy@3.1.0) (2024-02-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
# [3.1.0-beta.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-proxy@3.1.0-beta.0...decap-cms-backend-proxy@3.1.0-beta.1) (2024-01-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
# [3.1.0-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-proxy@3.1.0...decap-cms-backend-proxy@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-proxy@3.0.1...decap-cms-backend-proxy@3.0.2) (2023-10-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
## [3.0.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-proxy@3.0.0...decap-cms-backend-proxy@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-proxy@1.3.0...decap-cms-backend-proxy@3.0.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
# [1.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-proxy@1.3.0-beta.0...decap-cms-backend-proxy@1.3.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
# 1.3.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))
|
||||
|
||||
## [1.2.4-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-proxy@1.2.3...decap-cms-backend-proxy@1.2.4-beta.0) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
## [1.2.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/compare/decap-cms-backend-proxy@1.2.2...decap-cms-backend-proxy@1.2.3) (2021-06-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
## [1.2.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/compare/decap-cms-backend-proxy@1.2.1...decap-cms-backend-proxy@1.2.2) (2021-05-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
## [1.2.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/compare/decap-cms-backend-proxy@1.2.0...decap-cms-backend-proxy@1.2.1) (2021-05-19)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
# [1.2.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/compare/decap-cms-backend-proxy@1.1.7...decap-cms-backend-proxy@1.2.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-proxy/issues/5316)) ([9e42380](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/commit/9e423805707321396eec137f5b732a5b07a0dd3f))
|
||||
|
||||
## [1.1.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/compare/decap-cms-backend-proxy@1.1.6...decap-cms-backend-proxy@1.1.7) (2021-02-23)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
## [1.1.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/compare/decap-cms-backend-proxy@1.1.5...decap-cms-backend-proxy@1.1.6) (2021-02-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
## [1.1.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/compare/decap-cms-backend-proxy@1.1.4...decap-cms-backend-proxy@1.1.5) (2020-09-20)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
## [1.1.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/compare/decap-cms-backend-proxy@1.1.3...decap-cms-backend-proxy@1.1.4) (2020-09-15)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
## 1.1.3 (2020-09-08)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([828bb16](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/commit/828bb16415b8c22a34caa19c50c38b24ffe9ceae))
|
||||
|
||||
## 1.1.2 (2020-08-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([8262487](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/commit/82624879ccbcb16610090041db28f00714d924c8))
|
||||
|
||||
## 1.1.1 (2020-07-27)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([118d50a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/commit/118d50a7a70295f25073e564b5161aa2b9883056))
|
||||
|
||||
# [1.1.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/compare/decap-cms-backend-proxy@1.0.7...decap-cms-backend-proxy@1.1.0) (2020-06-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- handle token expiry ([#3847](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/issues/3847)) ([285c940](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/commit/285c940562548d7bc88de244123ba87ff66fba65))
|
||||
|
||||
### Features
|
||||
|
||||
- add backend status down indicator ([#3889](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/issues/3889)) ([a50edc7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/commit/a50edc70553ad6afa1acee6a51996ad226443f8c))
|
||||
|
||||
## [1.0.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/compare/decap-cms-backend-proxy@1.0.6...decap-cms-backend-proxy@1.0.7) (2020-05-19)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
## [1.0.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/compare/decap-cms-backend-proxy@1.0.5...decap-cms-backend-proxy@1.0.6) (2020-04-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-proxy:** fix error reporting ([#3527](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/issues/3527)) ([f94dea3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/commit/f94dea386ce89f0f92744d0c4196416706999ea0))
|
||||
|
||||
## [1.0.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/compare/decap-cms-backend-proxy@1.0.4...decap-cms-backend-proxy@1.0.5) (2020-04-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
|
||||
## [1.0.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/compare/decap-cms-backend-proxy@1.0.3...decap-cms-backend-proxy@1.0.4) (2020-03-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **locale:** Remove hard coded string literals ([#3333](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/issues/3333)) ([7c45a3c](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/commit/7c45a3cda983be427864a56e58791565eb9232e2))
|
||||
|
||||
## [1.0.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/compare/decap-cms-backend-proxy@1.0.2...decap-cms-backend-proxy@1.0.3) (2020-02-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **locale:** remove hard coded strings ([#3193](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/issues/3193)) ([fc91bf8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy/commit/fc91bf8781e65ce1dc946363dbb10419a145c66b))
|
||||
|
||||
## 1.0.2 (2020-01-22)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-proxy
|
||||
9
source/admin/packages/decap-cms-backend-proxy/README.md
Normal file
9
source/admin/packages/decap-cms-backend-proxy/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Proxy backend
|
||||
|
||||
Facilitates [local development](https://www.decapcms.org/docs/beta-features/#working-with-a-local-git-repository).
|
||||
|
||||
## Code structure
|
||||
|
||||
`Implementation` for [File Management System API](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-lib-util/README.md). An `RPC` wrapper for `decap-server`.
|
||||
|
||||
`AuthenticationPage` - a mock authentication page
|
||||
28
source/admin/packages/decap-cms-backend-proxy/package.json
Normal file
28
source/admin/packages/decap-cms-backend-proxy/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "decap-cms-backend-proxy",
|
||||
"description": "Proxy backend for Decap CMS",
|
||||
"version": "3.3.0",
|
||||
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-proxy",
|
||||
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
||||
"license": "MIT",
|
||||
"module": "dist/esm/index.js",
|
||||
"main": "dist/decap-cms-backend-proxy.js",
|
||||
"keywords": [
|
||||
"decap-cms",
|
||||
"backend"
|
||||
],
|
||||
"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\""
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"decap-cms-lib-util": "^3.0.0",
|
||||
"decap-cms-ui-default": "^3.0.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from '@emotion/styled';
|
||||
import { Icon, buttons, shadows, GoBackButton, renderPageLogo } from 'decap-cms-ui-default';
|
||||
|
||||
const StyledAuthenticationPage = styled.section`
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
`;
|
||||
|
||||
const LoginButton = styled.button`
|
||||
${buttons.button};
|
||||
${shadows.dropDeep};
|
||||
${buttons.default};
|
||||
${buttons.gray};
|
||||
|
||||
padding: 0 30px;
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
${Icon} {
|
||||
margin-right: 18px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default class AuthenticationPage extends React.Component {
|
||||
static propTypes = {
|
||||
onLogin: PropTypes.func.isRequired,
|
||||
inProgress: PropTypes.bool,
|
||||
config: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// Manually validate PropTypes - React 19 breaking change
|
||||
PropTypes.checkPropTypes(
|
||||
AuthenticationPage.propTypes,
|
||||
this.props,
|
||||
'prop',
|
||||
'AuthenticationPage',
|
||||
);
|
||||
}
|
||||
|
||||
handleLogin = e => {
|
||||
e.preventDefault();
|
||||
this.props.onLogin(this.state);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { config, inProgress, t } = this.props;
|
||||
|
||||
return (
|
||||
<StyledAuthenticationPage>
|
||||
{renderPageLogo(config.logo?.src || config.logo_url)}
|
||||
<LoginButton disabled={inProgress} onClick={this.handleLogin}>
|
||||
{inProgress ? t('auth.loggingIn') : t('auth.login')}
|
||||
</LoginButton>
|
||||
{config.site_url && <GoBackButton href={config.site_url} t={t}></GoBackButton>}
|
||||
</StyledAuthenticationPage>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import {
|
||||
EditorialWorkflowError,
|
||||
APIError,
|
||||
unsentRequest,
|
||||
blobToFileObj,
|
||||
} from 'decap-cms-lib-util';
|
||||
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
|
||||
import type {
|
||||
Entry,
|
||||
AssetProxy,
|
||||
PersistOptions,
|
||||
User,
|
||||
Config,
|
||||
Implementation,
|
||||
ImplementationFile,
|
||||
UnpublishedEntry,
|
||||
} from 'decap-cms-lib-util';
|
||||
|
||||
async function serializeAsset(assetProxy: AssetProxy) {
|
||||
const base64content = await assetProxy.toBase64!();
|
||||
return { path: assetProxy.path, content: base64content, encoding: 'base64' };
|
||||
}
|
||||
|
||||
type MediaFile = {
|
||||
id: string;
|
||||
content: string;
|
||||
encoding: string;
|
||||
name: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
function deserializeMediaFile({ id, content, encoding, path, name }: MediaFile) {
|
||||
let byteArray = new Uint8Array(0);
|
||||
if (encoding !== 'base64') {
|
||||
console.error(`Unsupported encoding '${encoding}' for file '${path}'`);
|
||||
} else {
|
||||
const decodedContent = atob(content);
|
||||
byteArray = new Uint8Array(decodedContent.length);
|
||||
for (let i = 0; i < decodedContent.length; i++) {
|
||||
byteArray[i] = decodedContent.charCodeAt(i);
|
||||
}
|
||||
}
|
||||
const blob = new Blob([byteArray]);
|
||||
const file = blobToFileObj(name, blob);
|
||||
const url = URL.createObjectURL(file);
|
||||
return { id, name, path, file, size: file.size, url, displayURL: url };
|
||||
}
|
||||
|
||||
export default class ProxyBackend implements Implementation {
|
||||
proxyUrl: string;
|
||||
mediaFolder: string;
|
||||
options: { initialWorkflowStatus?: string };
|
||||
branch: string;
|
||||
cmsLabelPrefix?: string;
|
||||
|
||||
constructor(config: Config, options = {}) {
|
||||
if (!config.backend.proxy_url) {
|
||||
throw new Error('The Proxy backend needs a "proxy_url" in the backend configuration.');
|
||||
}
|
||||
|
||||
this.branch = config.backend.branch || 'master';
|
||||
this.proxyUrl = config.backend.proxy_url;
|
||||
this.mediaFolder = config.media_folder;
|
||||
this.options = options;
|
||||
this.cmsLabelPrefix = config.backend.cms_label_prefix;
|
||||
}
|
||||
|
||||
isGitBackend() {
|
||||
return false;
|
||||
}
|
||||
|
||||
status() {
|
||||
return Promise.resolve({ auth: { status: true }, api: { status: true, statusPage: '' } });
|
||||
}
|
||||
|
||||
authComponent() {
|
||||
return AuthenticationPage;
|
||||
}
|
||||
|
||||
restoreUser() {
|
||||
return this.authenticate();
|
||||
}
|
||||
|
||||
authenticate() {
|
||||
return Promise.resolve() as unknown as Promise<User>;
|
||||
}
|
||||
|
||||
logout() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return Promise.resolve('');
|
||||
}
|
||||
|
||||
async request(payload: { action: string; params: Record<string, unknown> }) {
|
||||
const response = await unsentRequest.fetchWithTimeout(this.proxyUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
body: JSON.stringify({ branch: this.branch, ...payload }),
|
||||
});
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
return json;
|
||||
} else {
|
||||
throw new APIError(json.error, response.status, 'Proxy');
|
||||
}
|
||||
}
|
||||
|
||||
entriesByFolder(folder: string, extension: string, depth: number) {
|
||||
return this.request({
|
||||
action: 'entriesByFolder',
|
||||
params: { branch: this.branch, folder, extension, depth },
|
||||
});
|
||||
}
|
||||
|
||||
entriesByFiles(files: ImplementationFile[]) {
|
||||
return this.request({
|
||||
action: 'entriesByFiles',
|
||||
params: { branch: this.branch, files },
|
||||
});
|
||||
}
|
||||
|
||||
getEntry(path: string) {
|
||||
return this.request({
|
||||
action: 'getEntry',
|
||||
params: { branch: this.branch, path },
|
||||
});
|
||||
}
|
||||
|
||||
unpublishedEntries() {
|
||||
return this.request({
|
||||
action: 'unpublishedEntries',
|
||||
params: { branch: this.branch },
|
||||
});
|
||||
}
|
||||
|
||||
async unpublishedEntry({
|
||||
id,
|
||||
collection,
|
||||
slug,
|
||||
}: {
|
||||
id?: string;
|
||||
collection?: string;
|
||||
slug?: string;
|
||||
}) {
|
||||
try {
|
||||
const entry: UnpublishedEntry = await this.request({
|
||||
action: 'unpublishedEntry',
|
||||
params: { branch: this.branch, id, collection, slug, cmsLabelPrefix: this.cmsLabelPrefix },
|
||||
});
|
||||
|
||||
return entry;
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
throw new EditorialWorkflowError('content is not under editorial workflow', true);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async unpublishedEntryDataFile(collection: string, slug: string, path: string, id: string) {
|
||||
const { data } = await this.request({
|
||||
action: 'unpublishedEntryDataFile',
|
||||
params: { branch: this.branch, collection, slug, path, id },
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
async unpublishedEntryMediaFile(collection: string, slug: string, path: string, id: string) {
|
||||
const file = await this.request({
|
||||
action: 'unpublishedEntryMediaFile',
|
||||
params: { branch: this.branch, collection, slug, path, id },
|
||||
});
|
||||
return deserializeMediaFile(file);
|
||||
}
|
||||
|
||||
deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
return this.request({
|
||||
action: 'deleteUnpublishedEntry',
|
||||
params: { branch: this.branch, collection, slug },
|
||||
});
|
||||
}
|
||||
|
||||
async persistEntry(entry: Entry, options: PersistOptions) {
|
||||
const assets = await Promise.all(entry.assets.map(serializeAsset));
|
||||
return this.request({
|
||||
action: 'persistEntry',
|
||||
params: {
|
||||
branch: this.branch,
|
||||
dataFiles: entry.dataFiles,
|
||||
assets,
|
||||
options: { ...options, status: options.status || this.options.initialWorkflowStatus },
|
||||
cmsLabelPrefix: this.cmsLabelPrefix,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
return this.request({
|
||||
action: 'updateUnpublishedEntryStatus',
|
||||
params: {
|
||||
branch: this.branch,
|
||||
collection,
|
||||
slug,
|
||||
newStatus,
|
||||
cmsLabelPrefix: this.cmsLabelPrefix,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
publishUnpublishedEntry(collection: string, slug: string) {
|
||||
return this.request({
|
||||
action: 'publishUnpublishedEntry',
|
||||
params: { branch: this.branch, collection, slug },
|
||||
});
|
||||
}
|
||||
|
||||
async getMedia(mediaFolder = this.mediaFolder) {
|
||||
const files: MediaFile[] = await this.request({
|
||||
action: 'getMedia',
|
||||
params: { branch: this.branch, mediaFolder },
|
||||
});
|
||||
|
||||
return files.map(deserializeMediaFile);
|
||||
}
|
||||
|
||||
async getMediaFile(path: string) {
|
||||
const file = await this.request({
|
||||
action: 'getMediaFile',
|
||||
params: { branch: this.branch, path },
|
||||
});
|
||||
return deserializeMediaFile(file);
|
||||
}
|
||||
|
||||
async persistMedia(assetProxy: AssetProxy, options: PersistOptions) {
|
||||
const asset = await serializeAsset(assetProxy);
|
||||
const file: MediaFile = await this.request({
|
||||
action: 'persistMedia',
|
||||
params: { branch: this.branch, asset, options: { commitMessage: options.commitMessage } },
|
||||
});
|
||||
|
||||
return deserializeMediaFile(file);
|
||||
}
|
||||
|
||||
deleteFiles(paths: string[], commitMessage: string) {
|
||||
return this.request({
|
||||
action: 'deleteFiles',
|
||||
params: { branch: this.branch, paths, options: { commitMessage } },
|
||||
});
|
||||
}
|
||||
|
||||
getDeployPreview(collection: string, slug: string) {
|
||||
return this.request({
|
||||
action: 'getDeployPreview',
|
||||
params: { branch: this.branch, collection, slug },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import ProxyBackend from './implementation';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
|
||||
export const DecapCmsBackendProxy = {
|
||||
ProxyBackend,
|
||||
AuthenticationPage,
|
||||
};
|
||||
export { ProxyBackend, AuthenticationPage };
|
||||
@@ -0,0 +1,3 @@
|
||||
const { getConfig } = require('../../scripts/webpack.js');
|
||||
|
||||
module.exports = getConfig();
|
||||
305
source/admin/packages/decap-cms-backend-test/CHANGELOG.md
Normal file
305
source/admin/packages/decap-cms-backend-test/CHANGELOG.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.2.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-test@3.2.0...decap-cms-backend-test@3.2.1) (2025-07-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
# [3.2.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-test@3.1.3...decap-cms-backend-test@3.2.0) (2025-06-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
## [3.1.3](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-test@3.1.2...decap-cms-backend-test@3.1.3) (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.2](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-test@3.1.1...decap-cms-backend-test@3.1.2) (2024-08-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
## [3.1.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-test@3.1.0-beta.1...decap-cms-backend-test@3.1.1) (2024-03-21)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
# [3.1.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-test@3.1.0-beta.1...decap-cms-backend-test@3.1.0) (2024-02-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
# [3.1.0-beta.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-test@3.1.0-beta.0...decap-cms-backend-test@3.1.0-beta.1) (2024-01-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
# [3.1.0-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-test@3.1.0...decap-cms-backend-test@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-test@3.0.1...decap-cms-backend-test@3.0.2) (2023-10-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
## [3.0.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-test@3.0.0...decap-cms-backend-test@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-test@2.12.0...decap-cms-backend-test@3.0.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
# [2.12.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-test@2.12.0-beta.0...decap-cms-backend-test@2.12.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
# 2.12.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.11.4-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-test@2.11.3...decap-cms-backend-test@2.11.4-beta.0) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
## [2.11.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.11.2...decap-cms-backend-test@2.11.3) (2021-06-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
## [2.11.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.11.1...decap-cms-backend-test@2.11.2) (2021-05-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
## [2.11.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.11.0...decap-cms-backend-test@2.11.1) (2021-05-19)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
# [2.11.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.10.7...decap-cms-backend-test@2.11.0) (2021-05-04)
|
||||
|
||||
### Features
|
||||
|
||||
- added react 17 as peer dependency in packages ([#5316](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/5316)) ([9e42380](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/9e423805707321396eec137f5b732a5b07a0dd3f))
|
||||
|
||||
## [2.10.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.10.6...decap-cms-backend-test@2.10.7) (2021-02-23)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
## [2.10.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.10.5...decap-cms-backend-test@2.10.6) (2021-02-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
## [2.10.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.10.4...decap-cms-backend-test@2.10.5) (2020-09-20)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
## [2.10.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.10.3...decap-cms-backend-test@2.10.4) (2020-09-15)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
## 2.10.3 (2020-09-08)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([828bb16](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/828bb16415b8c22a34caa19c50c38b24ffe9ceae))
|
||||
|
||||
## 2.10.2 (2020-08-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([8262487](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/82624879ccbcb16610090041db28f00714d924c8))
|
||||
|
||||
## 2.10.1 (2020-07-27)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([118d50a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/118d50a7a70295f25073e564b5161aa2b9883056))
|
||||
|
||||
# [2.10.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.9.0...decap-cms-backend-test@2.10.0) (2020-06-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- handle token expiry ([#3847](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/3847)) ([285c940](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/285c940562548d7bc88de244123ba87ff66fba65))
|
||||
|
||||
### Features
|
||||
|
||||
- add backend status down indicator ([#3889](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/3889)) ([a50edc7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/a50edc70553ad6afa1acee6a51996ad226443f8c))
|
||||
|
||||
# [2.9.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.8.1...decap-cms-backend-test@2.9.0) (2020-06-01)
|
||||
|
||||
### Features
|
||||
|
||||
- add pre save/ post save hooks ([#3812](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/3812)) ([812716e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/812716e18b09a716547f128b783c8e6f3d54cc5b))
|
||||
|
||||
## [2.8.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.8.0...decap-cms-backend-test@2.8.1) (2020-04-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
# [2.8.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.7.3...decap-cms-backend-test@2.8.0) (2020-03-30)
|
||||
|
||||
### Features
|
||||
|
||||
- add publish configuration option to collections ([#3467](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/3467)) ([df33bc6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/df33bc64a996eedcb10835064a7cab8e7862e94d))
|
||||
|
||||
## [2.7.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.7.2...decap-cms-backend-test@2.7.3) (2020-03-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **locale:** Remove hard coded string literals ([#3333](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/3333)) ([7c45a3c](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/7c45a3cda983be427864a56e58791565eb9232e2))
|
||||
|
||||
## [2.7.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.7.1...decap-cms-backend-test@2.7.2) (2020-02-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **locale:** remove hard coded strings ([#3193](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/3193)) ([fc91bf8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/fc91bf8781e65ce1dc946363dbb10419a145c66b))
|
||||
|
||||
## [2.7.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.7.0...decap-cms-backend-test@2.7.1) (2020-01-14)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
# [2.7.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.7.0-beta.0...decap-cms-backend-test@2.7.0) (2020-01-07)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
# [2.7.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.6.0...decap-cms-backend-test@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-test/issues/2958)) ([2b41d8a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/2b41d8a838a9c8a6b21cde2ddd16b9288334e298))
|
||||
|
||||
# [2.6.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.6.0-beta.0...decap-cms-backend-test@2.6.0) (2019-12-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
# [2.6.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.5.0...decap-cms-backend-test@2.6.0-beta.0) (2019-12-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend-test:** delete nested file path ([#2930](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/2930)) ([b0fba6d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/b0fba6dc9ab89784e72d69a71752f68e0255a7e0))
|
||||
- keep editor slug path ([#2934](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/2934)) ([3c4865f](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/3c4865f2a76e6b0f156ab801081251eb620495b2))
|
||||
|
||||
### Features
|
||||
|
||||
- content in sub folders ([#2897](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/2897)) ([afcfe5b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/afcfe5b6d5f32669e9061ec596bd35ad545d61a3))
|
||||
|
||||
# [2.5.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.4.0...decap-cms-backend-test@2.5.0) (2019-11-26)
|
||||
|
||||
### Features
|
||||
|
||||
- workflow unpublished entry ([#2914](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/2914)) ([41bb9aa](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/41bb9aac0dd6fd9f8ff157bb0b29c85aa87fe04d))
|
||||
|
||||
# [2.4.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.3.0...decap-cms-backend-test@2.4.0) (2019-11-18)
|
||||
|
||||
### Features
|
||||
|
||||
- commit media with post ([#2851](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/2851)) ([6515dee](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/6515dee8715d8571ea19484a7dfab7cfd0cc40be))
|
||||
|
||||
# [2.3.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.2.3...decap-cms-backend-test@2.3.0) (2019-11-07)
|
||||
|
||||
### Features
|
||||
|
||||
- add go back to site button ([#2538](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/2538)) ([f206e7e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/f206e7e5a13fb48ec6b27dce0dbb3a59b61de8f9))
|
||||
|
||||
## [2.2.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.2.2...decap-cms-backend-test@2.2.3) (2019-07-24)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
## [2.2.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.2.2-beta.0...decap-cms-backend-test@2.2.2) (2019-04-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
## [2.2.2-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.2.1...decap-cms-backend-test@2.2.2-beta.0) (2019-04-05)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
## [2.2.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.2.1-beta.1...decap-cms-backend-test@2.2.1) (2019-03-29)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
## [2.2.1-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.2.1-beta.0...decap-cms-backend-test@2.2.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-test/issues/2244)) ([6ffd13b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/6ffd13b))
|
||||
|
||||
## [2.2.1-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.2.0...decap-cms-backend-test@2.2.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-test/issues/2234)) ([7987091](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/7987091))
|
||||
|
||||
# [2.2.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.1.0...decap-cms-backend-test@2.2.0) (2019-03-22)
|
||||
|
||||
### Features
|
||||
|
||||
- add ES module builds ([#2215](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/2215)) ([d142b32](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/d142b32))
|
||||
|
||||
# [2.1.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.1.0-beta.0...decap-cms-backend-test@2.1.0) (2019-03-22)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
# [2.1.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.0.7-beta.0...decap-cms-backend-test@2.1.0-beta.0) (2019-03-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix umd builds ([#2214](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/2214)) ([e04f6be](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/e04f6be))
|
||||
|
||||
### Features
|
||||
|
||||
- provide usable UMD builds for all packages ([#2141](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/2141)) ([82cc794](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/82cc794))
|
||||
|
||||
## [2.0.7-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.0.6...decap-cms-backend-test@2.0.7-beta.0) (2019-03-15)
|
||||
|
||||
### Features
|
||||
|
||||
- upgrade to Emotion 10 ([#2166](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/2166)) ([ccef446](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/ccef446))
|
||||
|
||||
<a name="2.0.6"></a>
|
||||
|
||||
## [2.0.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.0.5...decap-cms-backend-test@2.0.6) (2018-08-24)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
<a name="2.0.5"></a>
|
||||
|
||||
## [2.0.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.0.4...decap-cms-backend-test@2.0.5) (2018-08-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backends:** fix commit message handling ([#1568](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/1568)) ([f7e7120](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/f7e7120))
|
||||
|
||||
<a name="2.0.4"></a>
|
||||
|
||||
## [2.0.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.0.3...decap-cms-backend-test@2.0.4) (2018-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **workflow:** enable workflow per method ([#1569](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/1569)) ([90b8156](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/90b8156))
|
||||
|
||||
<a name="2.0.3"></a>
|
||||
|
||||
## [2.0.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.0.2...decap-cms-backend-test@2.0.3) (2018-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **workflow:** fix status not set on new workflow entries ([#1558](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/issues/1558)) ([0aa085f](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/commit/0aa085f))
|
||||
|
||||
<a name="2.0.2"></a>
|
||||
|
||||
## [2.0.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test/compare/decap-cms-backend-test@2.0.1...decap-cms-backend-test@2.0.2) (2018-07-28)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
|
||||
<a name="2.0.1"></a>
|
||||
|
||||
## 2.0.1 (2018-07-26)
|
||||
|
||||
<a name="2.0.0"></a>
|
||||
|
||||
# 2.0.0 (2018-07-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-backend-test
|
||||
17
source/admin/packages/decap-cms-backend-test/README.md
Normal file
17
source/admin/packages/decap-cms-backend-test/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Test backend
|
||||
|
||||
The backend behind https://demo.decapcms.org/.
|
||||
Used for demo purposes only.
|
||||
|
||||
## 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 simple JS objects:
|
||||
|
||||
```js
|
||||
window.repoFiles // json file-system tree
|
||||
window.repoFilesUnpublished // flat file list
|
||||
```
|
||||
|
||||
`AuthenticationPage` - A component which allow skip `login screen` for demo purposes.
|
||||
|
||||
Look at tests or types for more info.
|
||||
36
source/admin/packages/decap-cms-backend-test/package.json
Normal file
36
source/admin/packages/decap-cms-backend-test/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "decap-cms-backend-test",
|
||||
"description": "Development testing backend for Decap CMS",
|
||||
"version": "3.2.1",
|
||||
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-test",
|
||||
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
||||
"license": "MIT",
|
||||
"module": "dist/esm/index.js",
|
||||
"main": "dist/decap-cms-backend-test.js",
|
||||
"keywords": [
|
||||
"decap-cms",
|
||||
"backend"
|
||||
],
|
||||
"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": {
|
||||
"path-browserify": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"decap-cms-lib-util": "^3.0.0",
|
||||
"decap-cms-ui-default": "^3.0.0",
|
||||
"lodash": "^4.17.11",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^19.1.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"browser": {
|
||||
"path": "path-browserify"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from '@emotion/styled';
|
||||
import { Icon, buttons, shadows, GoBackButton } from 'decap-cms-ui-default';
|
||||
|
||||
const StyledAuthenticationPage = styled.section`
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 50px;
|
||||
height: 100vh;
|
||||
`;
|
||||
|
||||
const PageLogoIcon = styled(Icon)`
|
||||
height: auto;
|
||||
`;
|
||||
|
||||
const LoginButton = styled.button`
|
||||
${buttons.button};
|
||||
${shadows.dropDeep};
|
||||
${buttons.default};
|
||||
${buttons.gray};
|
||||
|
||||
padding: 0 30px;
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
${Icon} {
|
||||
margin-right: 18px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default class AuthenticationPage extends React.Component {
|
||||
static propTypes = {
|
||||
onLogin: PropTypes.func.isRequired,
|
||||
inProgress: PropTypes.bool,
|
||||
config: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// Manually validate PropTypes - React 19 breaking change
|
||||
PropTypes.checkPropTypes(
|
||||
AuthenticationPage.propTypes,
|
||||
this.props,
|
||||
'prop',
|
||||
'AuthenticationPage',
|
||||
);
|
||||
|
||||
/**
|
||||
* Allow login screen to be skipped for demo purposes.
|
||||
*/
|
||||
const skipLogin = this.props.config.backend.login === false;
|
||||
if (skipLogin) {
|
||||
this.props.onLogin(this.state);
|
||||
}
|
||||
}
|
||||
|
||||
handleLogin = e => {
|
||||
e.preventDefault();
|
||||
this.props.onLogin(this.state);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { config, inProgress, t } = this.props;
|
||||
|
||||
return (
|
||||
<StyledAuthenticationPage>
|
||||
<PageLogoIcon size="300px" type="decap-cms" />
|
||||
<LoginButton disabled={inProgress} onClick={this.handleLogin}>
|
||||
{inProgress ? t('auth.loggingIn') : t('auth.login')}
|
||||
</LoginButton>
|
||||
{config.site_url && <GoBackButton href={config.site_url} t={t}></GoBackButton>}
|
||||
</StyledAuthenticationPage>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import TestBackend, { getFolderFiles } from '../implementation';
|
||||
|
||||
describe('test backend implementation', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
describe('getEntry', () => {
|
||||
it('should get entry by path', async () => {
|
||||
window.repoFiles = {
|
||||
posts: {
|
||||
'some-post.md': {
|
||||
content: 'post content',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const backend = new TestBackend({});
|
||||
|
||||
await expect(backend.getEntry('posts/some-post.md')).resolves.toEqual({
|
||||
file: { path: 'posts/some-post.md', id: null },
|
||||
data: 'post content',
|
||||
});
|
||||
});
|
||||
|
||||
it('should get entry by nested path', async () => {
|
||||
window.repoFiles = {
|
||||
posts: {
|
||||
dir1: {
|
||||
dir2: {
|
||||
'some-post.md': {
|
||||
content: 'post content',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const backend = new TestBackend({});
|
||||
|
||||
await expect(backend.getEntry('posts/dir1/dir2/some-post.md')).resolves.toEqual({
|
||||
file: { path: 'posts/dir1/dir2/some-post.md', id: null },
|
||||
data: 'post content',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('persistEntry', () => {
|
||||
it('should persist entry', async () => {
|
||||
window.repoFiles = {};
|
||||
|
||||
const backend = new TestBackend({});
|
||||
|
||||
const entry = {
|
||||
dataFiles: [{ path: 'posts/some-post.md', raw: 'content', slug: 'some-post.md' }],
|
||||
assets: [],
|
||||
};
|
||||
await backend.persistEntry(entry, { newEntry: true });
|
||||
|
||||
expect(window.repoFiles).toEqual({
|
||||
posts: {
|
||||
'some-post.md': {
|
||||
content: 'content',
|
||||
path: 'posts/some-post.md',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should persist entry and keep existing unrelated entries', async () => {
|
||||
window.repoFiles = {
|
||||
pages: {
|
||||
'other-page.md': {
|
||||
content: 'content',
|
||||
},
|
||||
},
|
||||
posts: {
|
||||
'other-post.md': {
|
||||
content: 'content',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const backend = new TestBackend({});
|
||||
|
||||
const entry = {
|
||||
dataFiles: [{ path: 'posts/new-post.md', raw: 'content', slug: 'new-post.md' }],
|
||||
assets: [],
|
||||
};
|
||||
await backend.persistEntry(entry, { newEntry: true });
|
||||
|
||||
expect(window.repoFiles).toEqual({
|
||||
pages: {
|
||||
'other-page.md': {
|
||||
content: 'content',
|
||||
},
|
||||
},
|
||||
posts: {
|
||||
'new-post.md': {
|
||||
content: 'content',
|
||||
path: 'posts/new-post.md',
|
||||
},
|
||||
'other-post.md': {
|
||||
content: 'content',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should persist nested entry', async () => {
|
||||
window.repoFiles = {};
|
||||
|
||||
const backend = new TestBackend({});
|
||||
|
||||
const slug = 'dir1/dir2/some-post.md';
|
||||
const path = `posts/${slug}`;
|
||||
const entry = { dataFiles: [{ path, raw: 'content', slug }], assets: [] };
|
||||
await backend.persistEntry(entry, { newEntry: true });
|
||||
|
||||
expect(window.repoFiles).toEqual({
|
||||
posts: {
|
||||
dir1: {
|
||||
dir2: {
|
||||
'some-post.md': {
|
||||
content: 'content',
|
||||
path: 'posts/dir1/dir2/some-post.md',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should update existing nested entry', async () => {
|
||||
window.repoFiles = {
|
||||
posts: {
|
||||
dir1: {
|
||||
dir2: {
|
||||
'some-post.md': {
|
||||
mediaFiles: ['file1'],
|
||||
content: 'content',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const backend = new TestBackend({});
|
||||
|
||||
const slug = 'dir1/dir2/some-post.md';
|
||||
const path = `posts/${slug}`;
|
||||
const entry = { dataFiles: [{ path, raw: 'new content', slug }], assets: [] };
|
||||
await backend.persistEntry(entry, { newEntry: false });
|
||||
|
||||
expect(window.repoFiles).toEqual({
|
||||
posts: {
|
||||
dir1: {
|
||||
dir2: {
|
||||
'some-post.md': {
|
||||
path: 'posts/dir1/dir2/some-post.md',
|
||||
content: 'new content',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteFiles', () => {
|
||||
it('should delete entry by path', async () => {
|
||||
window.repoFiles = {
|
||||
posts: {
|
||||
'some-post.md': {
|
||||
content: 'post content',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const backend = new TestBackend({});
|
||||
|
||||
await backend.deleteFiles(['posts/some-post.md']);
|
||||
expect(window.repoFiles).toEqual({
|
||||
posts: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete entry by nested path', async () => {
|
||||
window.repoFiles = {
|
||||
posts: {
|
||||
dir1: {
|
||||
dir2: {
|
||||
'some-post.md': {
|
||||
content: 'post content',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const backend = new TestBackend({});
|
||||
|
||||
await backend.deleteFiles(['posts/dir1/dir2/some-post.md']);
|
||||
expect(window.repoFiles).toEqual({
|
||||
posts: {
|
||||
dir1: {
|
||||
dir2: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFolderFiles', () => {
|
||||
it('should get files by depth', () => {
|
||||
const tree = {
|
||||
pages: {
|
||||
'root-page.md': {
|
||||
content: 'root page content',
|
||||
},
|
||||
dir1: {
|
||||
'nested-page-1.md': {
|
||||
content: 'nested page 1 content',
|
||||
},
|
||||
dir2: {
|
||||
'nested-page-2.md': {
|
||||
content: 'nested page 2 content',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getFolderFiles(tree, 'pages', 'md', 1)).toEqual([
|
||||
{
|
||||
path: 'pages/root-page.md',
|
||||
content: 'root page content',
|
||||
},
|
||||
]);
|
||||
expect(getFolderFiles(tree, 'pages', 'md', 2)).toEqual([
|
||||
{
|
||||
path: 'pages/dir1/nested-page-1.md',
|
||||
content: 'nested page 1 content',
|
||||
},
|
||||
{
|
||||
path: 'pages/root-page.md',
|
||||
content: 'root page content',
|
||||
},
|
||||
]);
|
||||
expect(getFolderFiles(tree, 'pages', 'md', 3)).toEqual([
|
||||
{
|
||||
path: 'pages/dir1/dir2/nested-page-2.md',
|
||||
content: 'nested page 2 content',
|
||||
},
|
||||
{
|
||||
path: 'pages/dir1/nested-page-1.md',
|
||||
content: 'nested page 1 content',
|
||||
},
|
||||
{
|
||||
path: 'pages/root-page.md',
|
||||
content: 'root page content',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,434 @@
|
||||
import attempt from 'lodash/attempt';
|
||||
import isError from 'lodash/isError';
|
||||
import take from 'lodash/take';
|
||||
import unset from 'lodash/unset';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import {
|
||||
EditorialWorkflowError,
|
||||
Cursor,
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
basename,
|
||||
} from 'decap-cms-lib-util';
|
||||
import { extname, dirname } from 'path';
|
||||
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
|
||||
import type {
|
||||
Implementation,
|
||||
Entry,
|
||||
ImplementationEntry,
|
||||
AssetProxy,
|
||||
PersistOptions,
|
||||
User,
|
||||
Config,
|
||||
ImplementationFile,
|
||||
DataFile,
|
||||
} from 'decap-cms-lib-util';
|
||||
|
||||
type RepoFile = { path: string; content: string | AssetProxy };
|
||||
type RepoTree = { [key: string]: RepoFile | RepoTree };
|
||||
|
||||
type Diff = {
|
||||
id: string;
|
||||
originalPath?: string;
|
||||
path: string;
|
||||
newFile: boolean;
|
||||
status: string;
|
||||
content: string | AssetProxy;
|
||||
};
|
||||
|
||||
type UnpublishedRepoEntry = {
|
||||
slug: string;
|
||||
collection: string;
|
||||
status: string;
|
||||
diffs: Diff[];
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
repoFiles: RepoTree;
|
||||
repoFilesUnpublished: { [key: string]: UnpublishedRepoEntry };
|
||||
}
|
||||
}
|
||||
|
||||
window.repoFiles = window.repoFiles || {};
|
||||
window.repoFilesUnpublished = window.repoFilesUnpublished || [];
|
||||
|
||||
function getFile(path: string, tree: RepoTree) {
|
||||
const segments = path.split('/');
|
||||
let obj: RepoTree = tree;
|
||||
while (obj && segments.length) {
|
||||
obj = obj[segments.shift() as string] as RepoTree;
|
||||
}
|
||||
return (obj as unknown as RepoFile) || {};
|
||||
}
|
||||
|
||||
function writeFile(path: string, content: string | AssetProxy, tree: RepoTree) {
|
||||
const segments = path.split('/');
|
||||
let obj = tree;
|
||||
while (segments.length > 1) {
|
||||
const segment = segments.shift() as string;
|
||||
obj[segment] = obj[segment] || {};
|
||||
obj = obj[segment] as RepoTree;
|
||||
}
|
||||
(obj[segments.shift() as string] as RepoFile) = { content, path };
|
||||
}
|
||||
|
||||
function deleteFile(path: string, tree: RepoTree) {
|
||||
unset(tree, path.split('/'));
|
||||
}
|
||||
|
||||
const pageSize = 10;
|
||||
|
||||
function getCursor(
|
||||
folder: string,
|
||||
extension: string,
|
||||
entries: ImplementationEntry[],
|
||||
index: number,
|
||||
depth: number,
|
||||
) {
|
||||
const count = entries.length;
|
||||
const pageCount = Math.floor(count / pageSize);
|
||||
return Cursor.create({
|
||||
actions: [
|
||||
...(index < pageCount ? ['next', 'last'] : []),
|
||||
...(index > 0 ? ['prev', 'first'] : []),
|
||||
],
|
||||
meta: { index, count, pageSize, pageCount },
|
||||
data: { folder, extension, index, pageCount, depth },
|
||||
});
|
||||
}
|
||||
|
||||
export function getFolderFiles(
|
||||
tree: RepoTree,
|
||||
folder: string,
|
||||
extension: string,
|
||||
depth: number,
|
||||
files = [] as RepoFile[],
|
||||
path = folder,
|
||||
) {
|
||||
if (depth <= 0) {
|
||||
return files;
|
||||
}
|
||||
|
||||
Object.keys(tree[folder] || {}).forEach(key => {
|
||||
if (extname(key)) {
|
||||
const file = (tree[folder] as RepoTree)[key] as RepoFile;
|
||||
if (!extension || key.endsWith(`.${extension}`)) {
|
||||
files.unshift({ content: file.content, path: `${path}/${key}` });
|
||||
}
|
||||
} else {
|
||||
const subTree = tree[folder] as RepoTree;
|
||||
return getFolderFiles(subTree, key, extension, depth - 1, files, `${path}/${key}`);
|
||||
}
|
||||
});
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
export default class TestBackend implements Implementation {
|
||||
mediaFolder: string;
|
||||
options: { initialWorkflowStatus?: string };
|
||||
|
||||
constructor(config: Config, options = {}) {
|
||||
this.options = options;
|
||||
this.mediaFolder = config.media_folder;
|
||||
}
|
||||
|
||||
isGitBackend() {
|
||||
return false;
|
||||
}
|
||||
|
||||
status() {
|
||||
return Promise.resolve({ auth: { status: true }, api: { status: true, statusPage: '' } });
|
||||
}
|
||||
|
||||
authComponent() {
|
||||
return AuthenticationPage;
|
||||
}
|
||||
|
||||
restoreUser() {
|
||||
return this.authenticate();
|
||||
}
|
||||
|
||||
authenticate() {
|
||||
return Promise.resolve() as unknown as Promise<User>;
|
||||
}
|
||||
|
||||
logout() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return Promise.resolve('');
|
||||
}
|
||||
|
||||
traverseCursor(cursor: Cursor, action: string) {
|
||||
const { folder, extension, index, pageCount, depth } = cursor.data!.toObject() as {
|
||||
folder: string;
|
||||
extension: string;
|
||||
index: number;
|
||||
pageCount: number;
|
||||
depth: number;
|
||||
};
|
||||
const newIndex = (() => {
|
||||
if (action === 'next') {
|
||||
return (index as number) + 1;
|
||||
}
|
||||
if (action === 'prev') {
|
||||
return (index as number) - 1;
|
||||
}
|
||||
if (action === 'first') {
|
||||
return 0;
|
||||
}
|
||||
if (action === 'last') {
|
||||
return pageCount;
|
||||
}
|
||||
return 0;
|
||||
})();
|
||||
// TODO: stop assuming cursors are for collections
|
||||
const allFiles = getFolderFiles(window.repoFiles, folder, extension, depth);
|
||||
const allEntries = allFiles.map(f => ({
|
||||
data: f.content as string,
|
||||
file: { path: f.path, id: f.path },
|
||||
}));
|
||||
const entries = allEntries.slice(newIndex * pageSize, newIndex * pageSize + pageSize);
|
||||
const newCursor = getCursor(folder, extension, allEntries, newIndex, depth);
|
||||
return Promise.resolve({ entries, cursor: newCursor });
|
||||
}
|
||||
|
||||
entriesByFolder(folder: string, extension: string, depth: number) {
|
||||
const files = folder ? getFolderFiles(window.repoFiles, folder, extension, depth) : [];
|
||||
const entries = files.map(f => ({
|
||||
data: f.content as string,
|
||||
file: { path: f.path, id: f.path },
|
||||
}));
|
||||
const cursor = getCursor(folder, extension, entries, 0, depth);
|
||||
const ret = take(entries, pageSize);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
ret[CURSOR_COMPATIBILITY_SYMBOL] = cursor;
|
||||
return Promise.resolve(ret);
|
||||
}
|
||||
|
||||
entriesByFiles(files: ImplementationFile[]) {
|
||||
return Promise.all(
|
||||
files.map(file => ({
|
||||
file,
|
||||
data: getFile(file.path, window.repoFiles).content as string,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
getEntry(path: string) {
|
||||
return Promise.resolve({
|
||||
file: { path, id: null },
|
||||
data: getFile(path, window.repoFiles).content as string,
|
||||
});
|
||||
}
|
||||
|
||||
unpublishedEntries() {
|
||||
return Promise.resolve(Object.keys(window.repoFilesUnpublished));
|
||||
}
|
||||
|
||||
unpublishedEntry({ id, collection, slug }: { id?: string; collection?: string; slug?: string }) {
|
||||
if (id) {
|
||||
const parts = id.split('/');
|
||||
collection = parts[0];
|
||||
slug = parts[1];
|
||||
}
|
||||
const entry = window.repoFilesUnpublished[`${collection}/${slug}`];
|
||||
if (!entry) {
|
||||
return Promise.reject(
|
||||
new EditorialWorkflowError('content is not under editorial workflow', true),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve(entry);
|
||||
}
|
||||
|
||||
async unpublishedEntryDataFile(collection: string, slug: string, path: string) {
|
||||
const entry = window.repoFilesUnpublished[`${collection}/${slug}`];
|
||||
const file = entry.diffs.find(d => d.path === path);
|
||||
return file?.content as string;
|
||||
}
|
||||
|
||||
async unpublishedEntryMediaFile(collection: string, slug: string, path: string) {
|
||||
const entry = window.repoFilesUnpublished[`${collection}/${slug}`];
|
||||
const file = entry.diffs.find(d => d.path === path);
|
||||
return this.normalizeAsset(file?.content as AssetProxy);
|
||||
}
|
||||
|
||||
deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
delete window.repoFilesUnpublished[`${collection}/${slug}`];
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async addOrUpdateUnpublishedEntry(
|
||||
key: string,
|
||||
dataFiles: DataFile[],
|
||||
assetProxies: AssetProxy[],
|
||||
slug: string,
|
||||
collection: string,
|
||||
status: string,
|
||||
) {
|
||||
const diffs: Diff[] = [];
|
||||
dataFiles.forEach(dataFile => {
|
||||
const { path, newPath, raw } = dataFile;
|
||||
const currentDataFile = window.repoFilesUnpublished[key]?.diffs.find(d => d.path === path);
|
||||
const originalPath = currentDataFile ? currentDataFile.originalPath : path;
|
||||
diffs.push({
|
||||
originalPath,
|
||||
id: newPath || path,
|
||||
path: newPath || path,
|
||||
newFile: isEmpty(getFile(originalPath as string, window.repoFiles)),
|
||||
status: 'added',
|
||||
content: raw,
|
||||
});
|
||||
});
|
||||
assetProxies.forEach(a => {
|
||||
const asset = this.normalizeAsset(a);
|
||||
diffs.push({
|
||||
id: asset.id,
|
||||
path: asset.path,
|
||||
newFile: true,
|
||||
status: 'added',
|
||||
content: asset,
|
||||
});
|
||||
});
|
||||
window.repoFilesUnpublished[key] = {
|
||||
slug,
|
||||
collection,
|
||||
status,
|
||||
diffs,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async persistEntry(entry: Entry, options: PersistOptions) {
|
||||
if (options.useWorkflow) {
|
||||
const slug = entry.dataFiles[0].slug;
|
||||
const key = `${options.collectionName}/${slug}`;
|
||||
const currentEntry = window.repoFilesUnpublished[key];
|
||||
const status =
|
||||
currentEntry?.status || options.status || (this.options.initialWorkflowStatus as string);
|
||||
|
||||
this.addOrUpdateUnpublishedEntry(
|
||||
key,
|
||||
entry.dataFiles,
|
||||
entry.assets,
|
||||
slug,
|
||||
options.collectionName as string,
|
||||
status,
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
entry.dataFiles.forEach(dataFile => {
|
||||
const { path, raw } = dataFile;
|
||||
writeFile(path, raw, window.repoFiles);
|
||||
});
|
||||
entry.assets.forEach(a => {
|
||||
writeFile(a.path, a, window.repoFiles);
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
window.repoFilesUnpublished[`${collection}/${slug}`].status = newStatus;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
publishUnpublishedEntry(collection: string, slug: string) {
|
||||
const key = `${collection}/${slug}`;
|
||||
const unpubEntry = window.repoFilesUnpublished[key];
|
||||
|
||||
delete window.repoFilesUnpublished[key];
|
||||
|
||||
const tree = window.repoFiles;
|
||||
unpubEntry.diffs.forEach(d => {
|
||||
if (d.originalPath && !d.newFile) {
|
||||
const originalPath = d.originalPath;
|
||||
const sourceDir = dirname(originalPath);
|
||||
const destDir = dirname(d.path);
|
||||
const toMove = getFolderFiles(tree, originalPath.split('/')[0], '', 100).filter(f =>
|
||||
f.path.startsWith(sourceDir),
|
||||
);
|
||||
toMove.forEach(f => {
|
||||
deleteFile(f.path, tree);
|
||||
writeFile(f.path.replace(sourceDir, destDir), f.content, tree);
|
||||
});
|
||||
}
|
||||
writeFile(d.path, d.content, tree);
|
||||
});
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getMedia(mediaFolder = this.mediaFolder) {
|
||||
const files = getFolderFiles(window.repoFiles, mediaFolder.split('/')[0], '', 100).filter(f =>
|
||||
f.path.startsWith(mediaFolder),
|
||||
);
|
||||
const assets = files.map(f => this.normalizeAsset(f.content as AssetProxy));
|
||||
return Promise.resolve(assets);
|
||||
}
|
||||
|
||||
async getMediaFile(path: string) {
|
||||
const asset = getFile(path, window.repoFiles).content as AssetProxy;
|
||||
|
||||
const url = asset.toString();
|
||||
const name = basename(path);
|
||||
const blob = await fetch(url).then(res => res.blob());
|
||||
const fileObj = new File([blob], name);
|
||||
|
||||
return {
|
||||
id: url,
|
||||
displayURL: url,
|
||||
path,
|
||||
name,
|
||||
size: fileObj.size,
|
||||
file: fileObj,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
normalizeAsset(assetProxy: AssetProxy) {
|
||||
const fileObj = assetProxy.fileObj as File;
|
||||
const { name, size } = fileObj;
|
||||
const objectUrl = attempt(window.URL.createObjectURL, fileObj);
|
||||
const url = isError(objectUrl) ? '' : objectUrl;
|
||||
const normalizedAsset = {
|
||||
id: uuid(),
|
||||
name,
|
||||
size,
|
||||
path: assetProxy.path,
|
||||
url,
|
||||
displayURL: url,
|
||||
fileObj,
|
||||
};
|
||||
|
||||
return normalizedAsset;
|
||||
}
|
||||
|
||||
persistMedia(assetProxy: AssetProxy) {
|
||||
const normalizedAsset = this.normalizeAsset(assetProxy);
|
||||
|
||||
writeFile(assetProxy.path, assetProxy, window.repoFiles);
|
||||
|
||||
return Promise.resolve(normalizedAsset);
|
||||
}
|
||||
|
||||
deleteFiles(paths: string[]) {
|
||||
paths.forEach(path => {
|
||||
deleteFile(path, window.repoFiles);
|
||||
});
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async getDeployPreview() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import TestBackend from './implementation';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
|
||||
export const DecapCmsBackendTest = {
|
||||
TestBackend,
|
||||
AuthenticationPage,
|
||||
};
|
||||
export { TestBackend, AuthenticationPage };
|
||||
@@ -0,0 +1,3 @@
|
||||
const { getConfig } = require('../../scripts/webpack.js');
|
||||
|
||||
module.exports = getConfig();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user