@pranaysahith/decap-cms-backend-gitlab 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +416 -0
- package/README.md +13 -0
- package/dist/@pranaysahith/decap-cms-backend-gitlab.js +37 -0
- package/dist/@pranaysahith/decap-cms-backend-gitlab.js.LICENSE.txt +23 -0
- package/dist/@pranaysahith/decap-cms-backend-gitlab.js.map +1 -0
- package/dist/decap-cms-backend-gitlab.js +64 -0
- package/dist/decap-cms-backend-gitlab.js.LICENSE.txt +23 -0
- package/dist/decap-cms-backend-gitlab.js.map +1 -0
- package/dist/esm/API.js +802 -0
- package/dist/esm/AuthenticationPage.js +126 -0
- package/dist/esm/implementation.js +358 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/queries.js +64 -0
- package/package.json +47 -0
- package/src/API.ts +1029 -0
- package/src/AuthenticationPage.js +126 -0
- package/src/__tests__/API.spec.js +187 -0
- package/src/__tests__/gitlab.spec.js +552 -0
- package/src/implementation.ts +470 -0
- package/src/index.ts +10 -0
- package/src/queries.ts +73 -0
- package/webpack.config.js +3 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import _styled from "@emotion/styled/base";
|
|
2
|
+
function _EMOTION_STRINGIFIED_CSS_ERROR__() { return "You have tried to stringify object returned from `css` function. It isn't supposed to be used directly (e.g. as value of the `className` prop), but rather handed to emotion so it can handle it (e.g. as value of `css` prop)."; }
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import PropTypes from 'prop-types';
|
|
5
|
+
import { NetlifyAuthenticator, ImplicitAuthenticator, PkceAuthenticator } from 'decap-cms-lib-auth';
|
|
6
|
+
import { AuthenticationPage, Icon } from 'decap-cms-ui-default';
|
|
7
|
+
import { jsx as ___EmotionJSX } from "@emotion/react";
|
|
8
|
+
const LoginButtonIcon = /*#__PURE__*/_styled(Icon, {
|
|
9
|
+
target: "e80yw6v0",
|
|
10
|
+
label: "LoginButtonIcon"
|
|
11
|
+
})(process.env.NODE_ENV === "production" ? {
|
|
12
|
+
name: "1gnqu05",
|
|
13
|
+
styles: "margin-right:18px"
|
|
14
|
+
} : {
|
|
15
|
+
name: "1gnqu05",
|
|
16
|
+
styles: "margin-right:18px/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9BdXRoZW50aWNhdGlvblBhZ2UuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBVW9DIiwiZmlsZSI6Ii4uLy4uL3NyYy9BdXRoZW50aWNhdGlvblBhZ2UuanMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnO1xuaW1wb3J0IFByb3BUeXBlcyBmcm9tICdwcm9wLXR5cGVzJztcbmltcG9ydCBzdHlsZWQgZnJvbSAnQGVtb3Rpb24vc3R5bGVkJztcbmltcG9ydCB7XG4gIE5ldGxpZnlBdXRoZW50aWNhdG9yLFxuICBJbXBsaWNpdEF1dGhlbnRpY2F0b3IsXG4gIFBrY2VBdXRoZW50aWNhdG9yLFxufSBmcm9tICdkZWNhcC1jbXMtbGliLWF1dGgnO1xuaW1wb3J0IHsgQXV0aGVudGljYXRpb25QYWdlLCBJY29uIH0gZnJvbSAnZGVjYXAtY21zLXVpLWRlZmF1bHQnO1xuXG5jb25zdCBMb2dpbkJ1dHRvbkljb24gPSBzdHlsZWQoSWNvbilgXG4gIG1hcmdpbi1yaWdodDogMThweDtcbmA7XG5cbmNvbnN0IGNsaWVudFNpZGVBdXRoZW50aWNhdG9ycyA9IHtcbiAgcGtjZTogKHtcbiAgICBiYXNlX3VybCxcbiAgICBhdXRoX2VuZHBvaW50LFxuICAgIGFwcF9pZCxcbiAgICBhdXRoX3Rva2VuX2VuZHBvaW50fSkgPT5cbiAgICBuZXcgUGtjZUF1dGhlbnRpY2F0b3Ioe1xuICAgICAgYmFzZV91cmwsXG4gICAgICBhdXRoX2VuZHBvaW50LFxuICAgICAgYXBwX2lkLFxuICAgICAgYXV0aF90b2tlbl9lbmRwb2ludCxcbiAgICAgIGF1dGhfdG9rZW5fZW5kcG9pbnRfY29udGVudF90eXBlOiAnYXBwbGljYXRpb24vanNvbjsgY2hhcnNldD11dGYtOCcsXG4gICAgfSksXG5cbiAgaW1wbGljaXQ6ICh7XG4gICAgYmFzZV91cmwsXG4gICAgYXV0aF9lbmRwb2ludCxcbiAgICBhcHBfaWQsXG4gICAgY2xlYXJIYXNoIH0pID0+XG4gICAgbmV3IEltcGxpY2l0QXV0aGVudGljYXRvcih7XG4gICAgICBiYXNlX3VybCxcbiAgICAgIGF1dGhfZW5kcG9pbnQsXG4gICAgICBhcHBfaWQsXG4gICAgICBjbGVhckhhc2gsXG4gICAgfSksXG59O1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBHaXRMYWJBdXRoZW50aWNhdGlvblBhZ2UgZXh0ZW5kcyBSZWFjdC5Db21wb25lbnQge1xuICBzdGF0aWMgcHJvcFR5cGVzID0ge1xuICAgIG9uTG9naW46IFByb3BUeXBlcy5mdW5jLmlzUmVxdWlyZWQsXG4gICAgaW5Qcm9ncmVzczogUHJvcFR5cGVzLmJvb2wsXG4gICAgYmFzZV91cmw6IFByb3BUeXBlcy5zdHJpbmcsXG4gICAgc2l0ZUlkOiBQcm9wVHlwZXMuc3RyaW5nLFxuICAgIGF1dGhFbmRwb2ludDogUHJvcFR5cGVzLnN0cmluZyxcbiAgICBjb25maWc6IFByb3BUeXBlcy5vYmplY3QuaXNSZXF1aXJlZCxcbiAgICBjbGVhckhhc2g6IFByb3BUeXBlcy5mdW5jLFxuICAgIHQ6IFByb3BUeXBlcy5mdW5jLmlzUmVxdWlyZWQsXG4gIH07XG5cbiAgc3RhdGUgPSB7fTtcblxuICBjb21wb25lbnREaWRNb3VudCgpIHtcbiAgICAvLyBNYW51YWxseSB2YWxpZGF0ZSBQcm9wVHlwZXMgLSBSZWFjdCAxOSBicmVha2luZyBjaGFuZ2VcbiAgICBQcm9wVHlwZXMuY2hlY2tQcm9wVHlwZXMoR2l0TGFiQXV0aGVudGljYXRpb25QYWdlLnByb3BUeXBlcywgdGhpcy5wcm9wcywgJ3Byb3AnLCAnR2l0TGFiQXV0aGVudGljYXRpb25QYWdlJyk7XG5cbiAgICBjb25zdCB7XG4gICAgICBhdXRoX3R5cGU6IGF1dGhUeXBlID0gJycsXG4gICAgICBiYXNlX3VybCA9ICdodHRwczovL2dpdGxhYi5jb20nLFxuICAgICAgYXV0aF9lbmRwb2ludCA9ICdvYXV0aC9hdXRob3JpemUnLFxuICAgICAgYXBwX2lkID0gJycsXG4gICAgfSA9IHRoaXMucHJvcHMuY29uZmlnLmJhY2tlbmQ7XG5cbiAgICBpZiAoY2xpZW50U2lkZUF1dGhlbnRpY2F0b3JzW2F1dGhUeXBlXSkge1xuICAgICAgdGhpcy5hdXRoID0gY2xpZW50U2lkZUF1dGhlbnRpY2F0b3JzW2F1dGhUeXBlXSh7XG4gICAgICAgIGJhc2VfdXJsLFxuICAgICAgICBhdXRoX2VuZHBvaW50LFxuICAgICAgICBhcHBfaWQsXG4gICAgICAgIGF1dGhfdG9rZW5fZW5kcG9pbnQ6ICdvYXV0aC90b2tlbicsXG4gICAgICAgIGNsZWFySGFzaDogdGhpcy5wcm9wcy5jbGVhckhhc2gsXG4gICAgICB9KTtcbiAgICAgIC8vIENvbXBsZXRlIGltcGxpY2l0IGF1dGhlbnRpY2F0aW9uIGlmIHdlIHdlcmUgcmVkaXJlY3RlZCBiYWNrIHRvIGZyb20gdGhlIHByb3ZpZGVyLlxuICAgICAgdGhpcy5hdXRoLmNvbXBsZXRlQXV0aCgoZXJyLCBkYXRhKSA9PiB7XG4gICAgICAgIGlmIChlcnIpIHtcbiAgICAgICAgICB0aGlzLnNldFN0YXRlKHsgbG9naW5FcnJvcjogZXJyLnRvU3RyaW5nKCkgfSk7XG4gICAgICAgICAgcmV0dXJuO1xuICAgICAgICB9XG4gICAgICAgIHRoaXMucHJvcHMub25Mb2dpbihkYXRhKTtcbiAgICAgIH0pO1xuICAgIH0gZWxzZSB7XG4gICAgICB0aGlzLmF1dGggPSBuZXcgTmV0bGlmeUF1dGhlbnRpY2F0b3Ioe1xuICAgICAgICBiYXNlX3VybDogdGhpcy5wcm9wcy5iYXNlX3VybCxcbiAgICAgICAgc2l0ZV9pZDpcbiAgICAgICAgICBkb2N1bWVudC5sb2NhdGlvbi5ob3N0LnNwbGl0KCc6JylbMF0gPT09ICdsb2NhbGhvc3QnXG4gICAgICAgICAgICA/ICdkZW1vLmRlY2FwY21zLm9yZydcbiAgICAgICAgICAgIDogdGhpcy5wcm9wcy5zaXRlSWQsXG4gICAgICAgIGF1dGhfZW5kcG9pbnQ6IHRoaXMucHJvcHMuYXV0aEVuZHBvaW50LFxuICAgICAgfSk7XG4gICAgfVxuICB9XG5cbiAgaGFuZGxlTG9naW4gPSBlID0+IHtcbiAgICBlLnByZXZlbnREZWZhdWx0KCk7XG4gICAgdGhpcy5hdXRoLmF1dGhlbnRpY2F0ZSh7IHByb3ZpZGVyOiAnZ2l0bGFiJywgc2NvcGU6ICdhcGknIH0sIChlcnIsIGRhdGEpID0+IHtcbiAgICAgIGlmIChlcnIpIHtcbiAgICAgICAgdGhpcy5zZXRTdGF0ZSh7IGxvZ2luRXJyb3I6IGVyci50b1N0cmluZygpIH0pO1xuICAgICAgICByZXR1cm47XG4gICAgICB9XG4gICAgICB0aGlzLnByb3BzLm9uTG9naW4oZGF0YSk7XG4gICAgfSk7XG4gIH07XG5cbiAgcmVuZGVyKCkge1xuICAgIGNvbnN0IHsgaW5Qcm9ncmVzcywgY29uZmlnLCB0IH0gPSB0aGlzLnByb3BzO1xuICAgIHJldHVybiAoXG4gICAgICA8QXV0aGVudGljYXRpb25QYWdlXG4gICAgICAgIG9uTG9naW49e3RoaXMuaGFuZGxlTG9naW59XG4gICAgICAgIGxvZ2luRGlzYWJsZWQ9e2luUHJvZ3Jlc3N9XG4gICAgICAgIGxvZ2luRXJyb3JNZXNzYWdlPXt0aGlzLnN0YXRlLmxvZ2luRXJyb3J9XG4gICAgICAgIGxvZ29Vcmw9e2NvbmZpZy5sb2dvX3VybH0gLy8gRGVwcmVjYXRlZCwgcmVwbGFjZWQgYnkgYGxvZ28uc3JjYFxuICAgICAgICBsb2dvPXtjb25maWcubG9nb31cbiAgICAgICAgc2l0ZVVybD17Y29uZmlnLnNpdGVfdXJsfVxuICAgICAgICByZW5kZXJCdXR0b25Db250ZW50PXsoKSA9PiAoXG4gICAgICAgICAgPFJlYWN0LkZyYWdtZW50PlxuICAgICAgICAgICAgPExvZ2luQnV0dG9uSWNvbiB0eXBlPVwiZ2l0bGFiXCIgLz57JyAnfVxuICAgICAgICAgICAge2luUHJvZ3Jlc3MgPyB0KCdhdXRoLmxvZ2dpbmdJbicpIDogdCgnYXV0aC5sb2dpbldpdGhHaXRMYWInKX1cbiAgICAgICAgICA8L1JlYWN0LkZyYWdtZW50PlxuICAgICAgICApfVxuICAgICAgICB0PXt0fVxuICAgICAgLz5cbiAgICApO1xuICB9XG59XG4iXX0= */",
|
|
17
|
+
toString: _EMOTION_STRINGIFIED_CSS_ERROR__
|
|
18
|
+
});
|
|
19
|
+
const clientSideAuthenticators = {
|
|
20
|
+
pkce: ({
|
|
21
|
+
base_url,
|
|
22
|
+
auth_endpoint,
|
|
23
|
+
app_id,
|
|
24
|
+
auth_token_endpoint
|
|
25
|
+
}) => new PkceAuthenticator({
|
|
26
|
+
base_url,
|
|
27
|
+
auth_endpoint,
|
|
28
|
+
app_id,
|
|
29
|
+
auth_token_endpoint,
|
|
30
|
+
auth_token_endpoint_content_type: 'application/json; charset=utf-8'
|
|
31
|
+
}),
|
|
32
|
+
implicit: ({
|
|
33
|
+
base_url,
|
|
34
|
+
auth_endpoint,
|
|
35
|
+
app_id,
|
|
36
|
+
clearHash
|
|
37
|
+
}) => new ImplicitAuthenticator({
|
|
38
|
+
base_url,
|
|
39
|
+
auth_endpoint,
|
|
40
|
+
app_id,
|
|
41
|
+
clearHash
|
|
42
|
+
})
|
|
43
|
+
};
|
|
44
|
+
export default class GitLabAuthenticationPage extends React.Component {
|
|
45
|
+
static propTypes = {
|
|
46
|
+
onLogin: PropTypes.func.isRequired,
|
|
47
|
+
inProgress: PropTypes.bool,
|
|
48
|
+
base_url: PropTypes.string,
|
|
49
|
+
siteId: PropTypes.string,
|
|
50
|
+
authEndpoint: PropTypes.string,
|
|
51
|
+
config: PropTypes.object.isRequired,
|
|
52
|
+
clearHash: PropTypes.func,
|
|
53
|
+
t: PropTypes.func.isRequired
|
|
54
|
+
};
|
|
55
|
+
state = {};
|
|
56
|
+
componentDidMount() {
|
|
57
|
+
// Manually validate PropTypes - React 19 breaking change
|
|
58
|
+
PropTypes.checkPropTypes(GitLabAuthenticationPage.propTypes, this.props, 'prop', 'GitLabAuthenticationPage');
|
|
59
|
+
const {
|
|
60
|
+
auth_type: authType = '',
|
|
61
|
+
base_url = 'https://gitlab.com',
|
|
62
|
+
auth_endpoint = 'oauth/authorize',
|
|
63
|
+
app_id = ''
|
|
64
|
+
} = this.props.config.backend;
|
|
65
|
+
if (clientSideAuthenticators[authType]) {
|
|
66
|
+
this.auth = clientSideAuthenticators[authType]({
|
|
67
|
+
base_url,
|
|
68
|
+
auth_endpoint,
|
|
69
|
+
app_id,
|
|
70
|
+
auth_token_endpoint: 'oauth/token',
|
|
71
|
+
clearHash: this.props.clearHash
|
|
72
|
+
});
|
|
73
|
+
// Complete implicit authentication if we were redirected back to from the provider.
|
|
74
|
+
this.auth.completeAuth((err, data) => {
|
|
75
|
+
if (err) {
|
|
76
|
+
this.setState({
|
|
77
|
+
loginError: err.toString()
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
this.props.onLogin(data);
|
|
82
|
+
});
|
|
83
|
+
} else {
|
|
84
|
+
this.auth = new NetlifyAuthenticator({
|
|
85
|
+
base_url: this.props.base_url,
|
|
86
|
+
site_id: document.location.host.split(':')[0] === 'localhost' ? 'demo.decapcms.org' : this.props.siteId,
|
|
87
|
+
auth_endpoint: this.props.authEndpoint
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
handleLogin = e => {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
this.auth.authenticate({
|
|
94
|
+
provider: 'gitlab',
|
|
95
|
+
scope: 'api'
|
|
96
|
+
}, (err, data) => {
|
|
97
|
+
if (err) {
|
|
98
|
+
this.setState({
|
|
99
|
+
loginError: err.toString()
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.props.onLogin(data);
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
render() {
|
|
107
|
+
const {
|
|
108
|
+
inProgress,
|
|
109
|
+
config,
|
|
110
|
+
t
|
|
111
|
+
} = this.props;
|
|
112
|
+
return ___EmotionJSX(AuthenticationPage, {
|
|
113
|
+
onLogin: this.handleLogin,
|
|
114
|
+
loginDisabled: inProgress,
|
|
115
|
+
loginErrorMessage: this.state.loginError,
|
|
116
|
+
logoUrl: config.logo_url // Deprecated, replaced by `logo.src`
|
|
117
|
+
,
|
|
118
|
+
logo: config.logo,
|
|
119
|
+
siteUrl: config.site_url,
|
|
120
|
+
renderButtonContent: () => ___EmotionJSX(React.Fragment, null, ___EmotionJSX(LoginButtonIcon, {
|
|
121
|
+
type: "gitlab"
|
|
122
|
+
}), ' ', inProgress ? t('auth.loggingIn') : t('auth.loginWithGitLab')),
|
|
123
|
+
t: t
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import trimStart from 'lodash/trimStart';
|
|
2
|
+
import semaphore from 'semaphore';
|
|
3
|
+
import trim from 'lodash/trim';
|
|
4
|
+
import { stripIndent } from 'common-tags';
|
|
5
|
+
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';
|
|
6
|
+
import AuthenticationPage from './AuthenticationPage';
|
|
7
|
+
import API, { API_NAME } from './API';
|
|
8
|
+
const MAX_CONCURRENT_DOWNLOADS = 10;
|
|
9
|
+
export default class GitLab {
|
|
10
|
+
constructor(config, options = {}) {
|
|
11
|
+
this.options = {
|
|
12
|
+
proxied: false,
|
|
13
|
+
API: null,
|
|
14
|
+
initialWorkflowStatus: '',
|
|
15
|
+
...options
|
|
16
|
+
};
|
|
17
|
+
if (!this.options.proxied && (config.backend.repo === null || config.backend.repo === undefined)) {
|
|
18
|
+
throw new Error('The GitLab backend needs a "repo" in the backend configuration.');
|
|
19
|
+
}
|
|
20
|
+
this.api = this.options.API || null;
|
|
21
|
+
this.repo = config.backend.repo || '';
|
|
22
|
+
this.branch = config.backend.branch || 'master';
|
|
23
|
+
this.isBranchConfigured = config.backend.branch ? true : false;
|
|
24
|
+
this.apiRoot = config.backend.api_root || 'https://gitlab.com/api/v4';
|
|
25
|
+
this.token = '';
|
|
26
|
+
this.squashMerges = config.backend.squash_merges || false;
|
|
27
|
+
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
|
|
28
|
+
this.mediaFolder = config.media_folder;
|
|
29
|
+
this.previewContext = config.backend.preview_context || '';
|
|
30
|
+
this.useGraphQL = config.backend.use_graphql || false;
|
|
31
|
+
this.graphQLAPIRoot = config.backend.graphql_api_root || 'https://gitlab.com/api/graphql';
|
|
32
|
+
this.lock = asyncLock();
|
|
33
|
+
}
|
|
34
|
+
isGitBackend() {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
async status() {
|
|
38
|
+
const auth = (await this.api?.user().then(user => !!user).catch(e => {
|
|
39
|
+
console.warn('Failed getting GitLab user', e);
|
|
40
|
+
return false;
|
|
41
|
+
})) || false;
|
|
42
|
+
return {
|
|
43
|
+
auth: {
|
|
44
|
+
status: auth
|
|
45
|
+
},
|
|
46
|
+
api: {
|
|
47
|
+
status: true,
|
|
48
|
+
statusPage: ''
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
authComponent() {
|
|
53
|
+
return AuthenticationPage;
|
|
54
|
+
}
|
|
55
|
+
restoreUser(user) {
|
|
56
|
+
return this.authenticate(user);
|
|
57
|
+
}
|
|
58
|
+
async authenticate(state) {
|
|
59
|
+
this.token = state.token;
|
|
60
|
+
this.api = new API({
|
|
61
|
+
token: this.token,
|
|
62
|
+
branch: this.branch,
|
|
63
|
+
repo: this.repo,
|
|
64
|
+
apiRoot: this.apiRoot,
|
|
65
|
+
squashMerges: this.squashMerges,
|
|
66
|
+
cmsLabelPrefix: this.cmsLabelPrefix,
|
|
67
|
+
initialWorkflowStatus: this.options.initialWorkflowStatus,
|
|
68
|
+
useGraphQL: this.useGraphQL,
|
|
69
|
+
graphQLAPIRoot: this.graphQLAPIRoot
|
|
70
|
+
});
|
|
71
|
+
const user = await this.api.user();
|
|
72
|
+
const isCollab = await this.api.hasWriteAccess().catch(error => {
|
|
73
|
+
error.message = stripIndent`
|
|
74
|
+
Repo "${this.repo}" not found.
|
|
75
|
+
|
|
76
|
+
Please ensure the repo information is spelled correctly.
|
|
77
|
+
|
|
78
|
+
If the repo is private, make sure you're logged into a GitLab account with access.
|
|
79
|
+
`;
|
|
80
|
+
throw error;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Unauthorized user
|
|
84
|
+
if (!isCollab) {
|
|
85
|
+
throw new Error('Your GitLab user account does not have access to this repo.');
|
|
86
|
+
}
|
|
87
|
+
if (!this.isBranchConfigured) {
|
|
88
|
+
const defaultBranchName = await getDefaultBranchName({
|
|
89
|
+
backend: 'gitlab',
|
|
90
|
+
repo: this.repo,
|
|
91
|
+
token: this.token,
|
|
92
|
+
apiRoot: this.apiRoot
|
|
93
|
+
});
|
|
94
|
+
if (defaultBranchName) {
|
|
95
|
+
this.branch = defaultBranchName;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Authorized user
|
|
99
|
+
return {
|
|
100
|
+
...user,
|
|
101
|
+
login: user.username,
|
|
102
|
+
token: state.token
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async logout() {
|
|
106
|
+
this.token = null;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
getToken() {
|
|
110
|
+
return Promise.resolve(this.token);
|
|
111
|
+
}
|
|
112
|
+
filterFile(folder, file, extension, depth) {
|
|
113
|
+
// gitlab paths include the root folder
|
|
114
|
+
const fileFolder = trim(file.path.split(folder)[1] || '/', '/');
|
|
115
|
+
return filterByExtension(file, extension) && fileFolder.split('/').length <= depth;
|
|
116
|
+
}
|
|
117
|
+
async entriesByFolder(folder, extension, depth) {
|
|
118
|
+
let cursor;
|
|
119
|
+
const listFiles = () => this.api.listFiles(folder, depth > 1).then(({
|
|
120
|
+
files,
|
|
121
|
+
cursor: c
|
|
122
|
+
}) => {
|
|
123
|
+
cursor = c.mergeMeta({
|
|
124
|
+
folder,
|
|
125
|
+
extension,
|
|
126
|
+
depth
|
|
127
|
+
});
|
|
128
|
+
return files.filter(file => this.filterFile(folder, file, extension, depth));
|
|
129
|
+
});
|
|
130
|
+
const files = await entriesByFolder(listFiles, this.api.readFile.bind(this.api), this.api.readFileMetadata.bind(this.api), API_NAME);
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
132
|
+
// @ts-ignore
|
|
133
|
+
files[CURSOR_COMPATIBILITY_SYMBOL] = cursor;
|
|
134
|
+
return files;
|
|
135
|
+
}
|
|
136
|
+
async listAllFiles(folder, extension, depth) {
|
|
137
|
+
const files = await this.api.listAllFiles(folder, depth > 1);
|
|
138
|
+
const filtered = files.filter(file => this.filterFile(folder, file, extension, depth));
|
|
139
|
+
return filtered;
|
|
140
|
+
}
|
|
141
|
+
async allEntriesByFolder(folder, extension, depth) {
|
|
142
|
+
const files = await allEntriesByFolder({
|
|
143
|
+
listAllFiles: () => this.listAllFiles(folder, extension, depth),
|
|
144
|
+
readFile: this.api.readFile.bind(this.api),
|
|
145
|
+
readFileMetadata: this.api.readFileMetadata.bind(this.api),
|
|
146
|
+
apiName: API_NAME,
|
|
147
|
+
branch: this.branch,
|
|
148
|
+
localForage,
|
|
149
|
+
folder,
|
|
150
|
+
extension,
|
|
151
|
+
depth,
|
|
152
|
+
getDefaultBranch: () => this.api.getDefaultBranch().then(b => ({
|
|
153
|
+
name: b.name,
|
|
154
|
+
sha: b.commit.id
|
|
155
|
+
})),
|
|
156
|
+
isShaExistsInBranch: this.api.isShaExistsInBranch.bind(this.api),
|
|
157
|
+
getDifferences: (to, from) => this.api.getDifferences(to, from),
|
|
158
|
+
getFileId: path => this.api.getFileId(path, this.branch),
|
|
159
|
+
filterFile: file => this.filterFile(folder, file, extension, depth),
|
|
160
|
+
customFetch: this.useGraphQL ? files => this.api.readFilesGraphQL(files) : undefined
|
|
161
|
+
});
|
|
162
|
+
return files;
|
|
163
|
+
}
|
|
164
|
+
entriesByFiles(files) {
|
|
165
|
+
return entriesByFiles(files, this.api.readFile.bind(this.api), this.api.readFileMetadata.bind(this.api), API_NAME);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Fetches a single entry.
|
|
169
|
+
getEntry(path) {
|
|
170
|
+
return this.api.readFile(path).then(data => ({
|
|
171
|
+
file: {
|
|
172
|
+
path,
|
|
173
|
+
id: null
|
|
174
|
+
},
|
|
175
|
+
data: data
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
getMedia(mediaFolder = this.mediaFolder) {
|
|
179
|
+
return this.api.listAllFiles(mediaFolder).then(files => files.map(({
|
|
180
|
+
id,
|
|
181
|
+
name,
|
|
182
|
+
path
|
|
183
|
+
}) => {
|
|
184
|
+
return {
|
|
185
|
+
id,
|
|
186
|
+
name,
|
|
187
|
+
path,
|
|
188
|
+
displayURL: {
|
|
189
|
+
id,
|
|
190
|
+
name,
|
|
191
|
+
path
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
getMediaDisplayURL(displayURL) {
|
|
197
|
+
this._mediaDisplayURLSem = this._mediaDisplayURLSem || semaphore(MAX_CONCURRENT_DOWNLOADS);
|
|
198
|
+
return getMediaDisplayURL(displayURL, this.api.readFile.bind(this.api), this._mediaDisplayURLSem);
|
|
199
|
+
}
|
|
200
|
+
async getMediaFile(path) {
|
|
201
|
+
const name = basename(path);
|
|
202
|
+
const blob = await getMediaAsBlob(path, null, this.api.readFile.bind(this.api));
|
|
203
|
+
const fileObj = blobToFileObj(name, blob);
|
|
204
|
+
const url = URL.createObjectURL(fileObj);
|
|
205
|
+
const id = await getBlobSHA(blob);
|
|
206
|
+
return {
|
|
207
|
+
id,
|
|
208
|
+
displayURL: url,
|
|
209
|
+
path,
|
|
210
|
+
name,
|
|
211
|
+
size: fileObj.size,
|
|
212
|
+
file: fileObj,
|
|
213
|
+
url
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
async persistEntry(entry, options) {
|
|
217
|
+
// persistEntry is a transactional operation
|
|
218
|
+
return runWithLock(this.lock, () => this.api.persistFiles(entry.dataFiles, entry.assets, options), 'Failed to acquire persist entry lock');
|
|
219
|
+
}
|
|
220
|
+
async persistMedia(mediaFile, options) {
|
|
221
|
+
const fileObj = mediaFile.fileObj;
|
|
222
|
+
const [id] = await Promise.all([getBlobSHA(fileObj), this.api.persistFiles([], [mediaFile], options)]);
|
|
223
|
+
const {
|
|
224
|
+
path
|
|
225
|
+
} = mediaFile;
|
|
226
|
+
const url = URL.createObjectURL(fileObj);
|
|
227
|
+
return {
|
|
228
|
+
displayURL: url,
|
|
229
|
+
path: trimStart(path, '/'),
|
|
230
|
+
name: fileObj.name,
|
|
231
|
+
size: fileObj.size,
|
|
232
|
+
file: fileObj,
|
|
233
|
+
url,
|
|
234
|
+
id
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
deleteFiles(paths, commitMessage) {
|
|
238
|
+
return this.api.deleteFiles(paths, commitMessage);
|
|
239
|
+
}
|
|
240
|
+
traverseCursor(cursor, action) {
|
|
241
|
+
return this.api.traverseCursor(cursor, action).then(async ({
|
|
242
|
+
entries,
|
|
243
|
+
cursor: newCursor
|
|
244
|
+
}) => {
|
|
245
|
+
const [folder, depth, extension] = [cursor.meta?.get('folder'), cursor.meta?.get('depth'), cursor.meta?.get('extension')];
|
|
246
|
+
if (folder && depth && extension) {
|
|
247
|
+
entries = entries.filter(f => this.filterFile(folder, f, extension, depth));
|
|
248
|
+
newCursor = newCursor.mergeMeta({
|
|
249
|
+
folder,
|
|
250
|
+
extension,
|
|
251
|
+
depth
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
const entriesWithData = await entriesByFiles(entries, this.api.readFile.bind(this.api), this.api.readFileMetadata.bind(this.api), API_NAME);
|
|
255
|
+
return {
|
|
256
|
+
entries: entriesWithData,
|
|
257
|
+
cursor: newCursor
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
loadMediaFile(branch, file) {
|
|
262
|
+
const readFile = (path, id, {
|
|
263
|
+
parseText
|
|
264
|
+
}) => this.api.readFile(path, id, {
|
|
265
|
+
branch,
|
|
266
|
+
parseText
|
|
267
|
+
});
|
|
268
|
+
return getMediaAsBlob(file.path, null, readFile).then(blob => {
|
|
269
|
+
const name = basename(file.path);
|
|
270
|
+
const fileObj = blobToFileObj(name, blob);
|
|
271
|
+
return {
|
|
272
|
+
id: file.path,
|
|
273
|
+
displayURL: URL.createObjectURL(fileObj),
|
|
274
|
+
path: file.path,
|
|
275
|
+
name,
|
|
276
|
+
size: fileObj.size,
|
|
277
|
+
file: fileObj
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
async loadEntryMediaFiles(branch, files) {
|
|
282
|
+
const mediaFiles = await Promise.all(files.map(file => this.loadMediaFile(branch, file)));
|
|
283
|
+
return mediaFiles;
|
|
284
|
+
}
|
|
285
|
+
async unpublishedEntries() {
|
|
286
|
+
const listEntriesKeys = () => this.api.listUnpublishedBranches().then(branches => branches.map(branch => contentKeyFromBranch(branch)));
|
|
287
|
+
const ids = await unpublishedEntries(listEntriesKeys);
|
|
288
|
+
return ids;
|
|
289
|
+
}
|
|
290
|
+
async unpublishedEntry({
|
|
291
|
+
id,
|
|
292
|
+
collection,
|
|
293
|
+
slug
|
|
294
|
+
}) {
|
|
295
|
+
if (id) {
|
|
296
|
+
const data = await this.api.retrieveUnpublishedEntryData(id);
|
|
297
|
+
return data;
|
|
298
|
+
} else if (collection && slug) {
|
|
299
|
+
const entryId = generateContentKey(collection, slug);
|
|
300
|
+
const data = await this.api.retrieveUnpublishedEntryData(entryId);
|
|
301
|
+
return data;
|
|
302
|
+
} else {
|
|
303
|
+
throw new Error('Missing unpublished entry id or collection and slug');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
getBranch(collection, slug) {
|
|
307
|
+
const contentKey = generateContentKey(collection, slug);
|
|
308
|
+
const branch = branchFromContentKey(contentKey);
|
|
309
|
+
return branch;
|
|
310
|
+
}
|
|
311
|
+
async unpublishedEntryDataFile(collection, slug, path, id) {
|
|
312
|
+
const branch = this.getBranch(collection, slug);
|
|
313
|
+
const data = await this.api.readFile(path, id, {
|
|
314
|
+
branch
|
|
315
|
+
});
|
|
316
|
+
return data;
|
|
317
|
+
}
|
|
318
|
+
async unpublishedEntryMediaFile(collection, slug, path, id) {
|
|
319
|
+
const branch = this.getBranch(collection, slug);
|
|
320
|
+
const mediaFile = await this.loadMediaFile(branch, {
|
|
321
|
+
path,
|
|
322
|
+
id
|
|
323
|
+
});
|
|
324
|
+
return mediaFile;
|
|
325
|
+
}
|
|
326
|
+
async updateUnpublishedEntryStatus(collection, slug, newStatus) {
|
|
327
|
+
// updateUnpublishedEntryStatus is a transactional operation
|
|
328
|
+
return runWithLock(this.lock, () => this.api.updateUnpublishedEntryStatus(collection, slug, newStatus), 'Failed to acquire update entry status lock');
|
|
329
|
+
}
|
|
330
|
+
async deleteUnpublishedEntry(collection, slug) {
|
|
331
|
+
// deleteUnpublishedEntry is a transactional operation
|
|
332
|
+
return runWithLock(this.lock, () => this.api.deleteUnpublishedEntry(collection, slug), 'Failed to acquire delete entry lock');
|
|
333
|
+
}
|
|
334
|
+
async publishUnpublishedEntry(collection, slug) {
|
|
335
|
+
// publishUnpublishedEntry is a transactional operation
|
|
336
|
+
return runWithLock(this.lock, () => this.api.publishUnpublishedEntry(collection, slug), 'Failed to acquire publish entry lock');
|
|
337
|
+
}
|
|
338
|
+
async getDeployPreview(collection, slug) {
|
|
339
|
+
try {
|
|
340
|
+
const statuses = await this.api.getStatuses(collection, slug);
|
|
341
|
+
const deployStatus = getPreviewStatus(statuses, this.previewContext);
|
|
342
|
+
if (deployStatus) {
|
|
343
|
+
const {
|
|
344
|
+
target_url: url,
|
|
345
|
+
state
|
|
346
|
+
} = deployStatus;
|
|
347
|
+
return {
|
|
348
|
+
url,
|
|
349
|
+
status: state
|
|
350
|
+
};
|
|
351
|
+
} else {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
} catch (e) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import GitLabBackend from './implementation';
|
|
2
|
+
import API from './API';
|
|
3
|
+
import AuthenticationPage from './AuthenticationPage';
|
|
4
|
+
export const DecapCmsBackendGitlab = {
|
|
5
|
+
GitLabBackend,
|
|
6
|
+
API,
|
|
7
|
+
AuthenticationPage
|
|
8
|
+
};
|
|
9
|
+
export { GitLabBackend, API, AuthenticationPage };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { gql } from 'graphql-tag';
|
|
2
|
+
import { oneLine } from 'common-tags';
|
|
3
|
+
export const files = gql`
|
|
4
|
+
query files($repo: ID!, $branch: String!, $path: String!, $recursive: Boolean!, $cursor: String) {
|
|
5
|
+
project(fullPath: $repo) {
|
|
6
|
+
repository {
|
|
7
|
+
tree(ref: $branch, path: $path, recursive: $recursive) {
|
|
8
|
+
blobs(after: $cursor) {
|
|
9
|
+
nodes {
|
|
10
|
+
type
|
|
11
|
+
id: sha
|
|
12
|
+
path
|
|
13
|
+
name
|
|
14
|
+
}
|
|
15
|
+
pageInfo {
|
|
16
|
+
endCursor
|
|
17
|
+
hasNextPage
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
export const blobs = gql`
|
|
26
|
+
query blobs($repo: ID!, $branch: String!, $paths: [String!]!) {
|
|
27
|
+
project(fullPath: $repo) {
|
|
28
|
+
repository {
|
|
29
|
+
blobs(ref: $branch, paths: $paths) {
|
|
30
|
+
nodes {
|
|
31
|
+
id
|
|
32
|
+
data: rawBlob
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
export function lastCommits(paths) {
|
|
40
|
+
const tree = paths.map((path, index) => oneLine`
|
|
41
|
+
tree${index}: tree(ref: $branch, path: "${path}") {
|
|
42
|
+
lastCommit {
|
|
43
|
+
authorName
|
|
44
|
+
authoredDate
|
|
45
|
+
author {
|
|
46
|
+
id
|
|
47
|
+
username
|
|
48
|
+
name
|
|
49
|
+
publicEmail
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
`).join('\n');
|
|
54
|
+
const query = gql`
|
|
55
|
+
query lastCommits($repo: ID!, $branch: String!) {
|
|
56
|
+
project(fullPath: $repo) {
|
|
57
|
+
repository {
|
|
58
|
+
${tree}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
return query;
|
|
64
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pranaysahith/decap-cms-backend-gitlab",
|
|
3
|
+
"description": "GitLab backend for Decap CMS",
|
|
4
|
+
"version": "3.4.0",
|
|
5
|
+
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab",
|
|
6
|
+
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"module": "dist/esm/index.js",
|
|
9
|
+
"main": "dist/decap-cms-backend-gitlab.js",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"decap-cms",
|
|
12
|
+
"backend",
|
|
13
|
+
"gitlab"
|
|
14
|
+
],
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"scripts": {
|
|
17
|
+
"develop": "npm run build:esm -- --watch",
|
|
18
|
+
"build": "cross-env NODE_ENV=production webpack",
|
|
19
|
+
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward --extensions \".js,.jsx,.ts,.tsx\""
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"apollo-cache-inmemory": "^1.6.2",
|
|
23
|
+
"apollo-client": "^2.6.3",
|
|
24
|
+
"apollo-link-context": "^1.0.18",
|
|
25
|
+
"apollo-link-http": "^1.5.15",
|
|
26
|
+
"js-base64": "^3.0.0",
|
|
27
|
+
"path-browserify": "^1.0.1",
|
|
28
|
+
"semaphore": "^1.1.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@emotion/react": "^11.11.1",
|
|
32
|
+
"@emotion/styled": "^11.11.0",
|
|
33
|
+
"@pranaysahith/decap-cms-lib-auth": "^3.0.0",
|
|
34
|
+
"@pranaysahith/decap-cms-lib-util": "^3.0.0",
|
|
35
|
+
"@pranaysahith/decap-cms-ui-default": "^3.0.0",
|
|
36
|
+
"immutable": "^3.7.6",
|
|
37
|
+
"lodash": "^4.17.11",
|
|
38
|
+
"prop-types": "^15.7.2",
|
|
39
|
+
"react": "^19.1.0"
|
|
40
|
+
},
|
|
41
|
+
"browser": {
|
|
42
|
+
"path": "path-browserify"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
}
|
|
47
|
+
}
|