@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,470 @@
|
|
|
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 {
|
|
6
|
+
CURSOR_COMPATIBILITY_SYMBOL,
|
|
7
|
+
basename,
|
|
8
|
+
entriesByFolder,
|
|
9
|
+
entriesByFiles,
|
|
10
|
+
getMediaDisplayURL,
|
|
11
|
+
getMediaAsBlob,
|
|
12
|
+
unpublishedEntries,
|
|
13
|
+
getPreviewStatus,
|
|
14
|
+
asyncLock,
|
|
15
|
+
runWithLock,
|
|
16
|
+
getBlobSHA,
|
|
17
|
+
blobToFileObj,
|
|
18
|
+
contentKeyFromBranch,
|
|
19
|
+
generateContentKey,
|
|
20
|
+
localForage,
|
|
21
|
+
allEntriesByFolder,
|
|
22
|
+
filterByExtension,
|
|
23
|
+
branchFromContentKey,
|
|
24
|
+
getDefaultBranchName,
|
|
25
|
+
} from 'decap-cms-lib-util';
|
|
26
|
+
|
|
27
|
+
import AuthenticationPage from './AuthenticationPage';
|
|
28
|
+
import API, { API_NAME } from './API';
|
|
29
|
+
|
|
30
|
+
import type {
|
|
31
|
+
Entry,
|
|
32
|
+
AssetProxy,
|
|
33
|
+
PersistOptions,
|
|
34
|
+
Cursor,
|
|
35
|
+
Implementation,
|
|
36
|
+
DisplayURL,
|
|
37
|
+
User,
|
|
38
|
+
Credentials,
|
|
39
|
+
Config,
|
|
40
|
+
ImplementationFile,
|
|
41
|
+
UnpublishedEntryMediaFile,
|
|
42
|
+
AsyncLock,
|
|
43
|
+
} from 'decap-cms-lib-util';
|
|
44
|
+
import type { Semaphore } from 'semaphore';
|
|
45
|
+
|
|
46
|
+
const MAX_CONCURRENT_DOWNLOADS = 10;
|
|
47
|
+
|
|
48
|
+
export default class GitLab implements Implementation {
|
|
49
|
+
lock: AsyncLock;
|
|
50
|
+
api: API | null;
|
|
51
|
+
options: {
|
|
52
|
+
proxied: boolean;
|
|
53
|
+
API: API | null;
|
|
54
|
+
initialWorkflowStatus: string;
|
|
55
|
+
};
|
|
56
|
+
repo: string;
|
|
57
|
+
isBranchConfigured: boolean;
|
|
58
|
+
branch: string;
|
|
59
|
+
apiRoot: string;
|
|
60
|
+
token: string | null;
|
|
61
|
+
squashMerges: boolean;
|
|
62
|
+
cmsLabelPrefix: string;
|
|
63
|
+
mediaFolder: string;
|
|
64
|
+
previewContext: string;
|
|
65
|
+
useGraphQL: boolean;
|
|
66
|
+
graphQLAPIRoot: string;
|
|
67
|
+
|
|
68
|
+
_mediaDisplayURLSem?: Semaphore;
|
|
69
|
+
|
|
70
|
+
constructor(config: Config, options = {}) {
|
|
71
|
+
this.options = {
|
|
72
|
+
proxied: false,
|
|
73
|
+
API: null,
|
|
74
|
+
initialWorkflowStatus: '',
|
|
75
|
+
...options,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
!this.options.proxied &&
|
|
80
|
+
(config.backend.repo === null || config.backend.repo === undefined)
|
|
81
|
+
) {
|
|
82
|
+
throw new Error('The GitLab backend needs a "repo" in the backend configuration.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.api = this.options.API || null;
|
|
86
|
+
|
|
87
|
+
this.repo = config.backend.repo || '';
|
|
88
|
+
this.branch = config.backend.branch || 'master';
|
|
89
|
+
this.isBranchConfigured = config.backend.branch ? true : false;
|
|
90
|
+
this.apiRoot = config.backend.api_root || 'https://gitlab.com/api/v4';
|
|
91
|
+
this.token = '';
|
|
92
|
+
this.squashMerges = config.backend.squash_merges || false;
|
|
93
|
+
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
|
|
94
|
+
this.mediaFolder = config.media_folder;
|
|
95
|
+
this.previewContext = config.backend.preview_context || '';
|
|
96
|
+
this.useGraphQL = config.backend.use_graphql || false;
|
|
97
|
+
this.graphQLAPIRoot = config.backend.graphql_api_root || 'https://gitlab.com/api/graphql';
|
|
98
|
+
this.lock = asyncLock();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
isGitBackend() {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async status() {
|
|
106
|
+
const auth =
|
|
107
|
+
(await this.api
|
|
108
|
+
?.user()
|
|
109
|
+
.then(user => !!user)
|
|
110
|
+
.catch(e => {
|
|
111
|
+
console.warn('Failed getting GitLab user', e);
|
|
112
|
+
return false;
|
|
113
|
+
})) || false;
|
|
114
|
+
|
|
115
|
+
return { auth: { status: auth }, api: { status: true, statusPage: '' } };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
authComponent() {
|
|
119
|
+
return AuthenticationPage;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
restoreUser(user: User) {
|
|
123
|
+
return this.authenticate(user);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async authenticate(state: Credentials) {
|
|
127
|
+
this.token = state.token as string;
|
|
128
|
+
this.api = new API({
|
|
129
|
+
token: this.token,
|
|
130
|
+
branch: this.branch,
|
|
131
|
+
repo: this.repo,
|
|
132
|
+
apiRoot: this.apiRoot,
|
|
133
|
+
squashMerges: this.squashMerges,
|
|
134
|
+
cmsLabelPrefix: this.cmsLabelPrefix,
|
|
135
|
+
initialWorkflowStatus: this.options.initialWorkflowStatus,
|
|
136
|
+
useGraphQL: this.useGraphQL,
|
|
137
|
+
graphQLAPIRoot: this.graphQLAPIRoot,
|
|
138
|
+
});
|
|
139
|
+
const user = await this.api.user();
|
|
140
|
+
const isCollab = await this.api.hasWriteAccess().catch((error: Error) => {
|
|
141
|
+
error.message = stripIndent`
|
|
142
|
+
Repo "${this.repo}" not found.
|
|
143
|
+
|
|
144
|
+
Please ensure the repo information is spelled correctly.
|
|
145
|
+
|
|
146
|
+
If the repo is private, make sure you're logged into a GitLab account with access.
|
|
147
|
+
`;
|
|
148
|
+
throw error;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Unauthorized user
|
|
152
|
+
if (!isCollab) {
|
|
153
|
+
throw new Error('Your GitLab user account does not have access to this repo.');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!this.isBranchConfigured) {
|
|
157
|
+
const defaultBranchName = await getDefaultBranchName({
|
|
158
|
+
backend: 'gitlab',
|
|
159
|
+
repo: this.repo,
|
|
160
|
+
token: this.token,
|
|
161
|
+
apiRoot: this.apiRoot,
|
|
162
|
+
});
|
|
163
|
+
if (defaultBranchName) {
|
|
164
|
+
this.branch = defaultBranchName;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Authorized user
|
|
168
|
+
return { ...user, login: user.username, token: state.token as string };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async logout() {
|
|
172
|
+
this.token = null;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getToken() {
|
|
177
|
+
return Promise.resolve(this.token);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
filterFile(
|
|
181
|
+
folder: string,
|
|
182
|
+
file: { path: string; name: string },
|
|
183
|
+
extension: string,
|
|
184
|
+
depth: number,
|
|
185
|
+
) {
|
|
186
|
+
// gitlab paths include the root folder
|
|
187
|
+
const fileFolder = trim(file.path.split(folder)[1] || '/', '/');
|
|
188
|
+
return filterByExtension(file, extension) && fileFolder.split('/').length <= depth;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async entriesByFolder(folder: string, extension: string, depth: number) {
|
|
192
|
+
let cursor: Cursor;
|
|
193
|
+
|
|
194
|
+
const listFiles = () =>
|
|
195
|
+
this.api!.listFiles(folder, depth > 1).then(({ files, cursor: c }) => {
|
|
196
|
+
cursor = c.mergeMeta({ folder, extension, depth });
|
|
197
|
+
return files.filter(file => this.filterFile(folder, file, extension, depth));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const files = await entriesByFolder(
|
|
201
|
+
listFiles,
|
|
202
|
+
this.api!.readFile.bind(this.api!),
|
|
203
|
+
this.api!.readFileMetadata.bind(this.api),
|
|
204
|
+
API_NAME,
|
|
205
|
+
);
|
|
206
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
207
|
+
// @ts-ignore
|
|
208
|
+
files[CURSOR_COMPATIBILITY_SYMBOL] = cursor;
|
|
209
|
+
return files;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async listAllFiles(folder: string, extension: string, depth: number) {
|
|
213
|
+
const files = await this.api!.listAllFiles(folder, depth > 1);
|
|
214
|
+
const filtered = files.filter(file => this.filterFile(folder, file, extension, depth));
|
|
215
|
+
return filtered;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async allEntriesByFolder(folder: string, extension: string, depth: number) {
|
|
219
|
+
const files = await allEntriesByFolder({
|
|
220
|
+
listAllFiles: () => this.listAllFiles(folder, extension, depth),
|
|
221
|
+
readFile: this.api!.readFile.bind(this.api!),
|
|
222
|
+
readFileMetadata: this.api!.readFileMetadata.bind(this.api),
|
|
223
|
+
apiName: API_NAME,
|
|
224
|
+
branch: this.branch,
|
|
225
|
+
localForage,
|
|
226
|
+
folder,
|
|
227
|
+
extension,
|
|
228
|
+
depth,
|
|
229
|
+
getDefaultBranch: () =>
|
|
230
|
+
this.api!.getDefaultBranch().then(b => ({ name: b.name, sha: b.commit.id })),
|
|
231
|
+
isShaExistsInBranch: this.api!.isShaExistsInBranch.bind(this.api!),
|
|
232
|
+
getDifferences: (to, from) => this.api!.getDifferences(to, from),
|
|
233
|
+
getFileId: path => this.api!.getFileId(path, this.branch),
|
|
234
|
+
filterFile: file => this.filterFile(folder, file, extension, depth),
|
|
235
|
+
customFetch: this.useGraphQL ? files => this.api!.readFilesGraphQL(files) : undefined,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return files;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
entriesByFiles(files: ImplementationFile[]) {
|
|
242
|
+
return entriesByFiles(
|
|
243
|
+
files,
|
|
244
|
+
this.api!.readFile.bind(this.api!),
|
|
245
|
+
this.api!.readFileMetadata.bind(this.api),
|
|
246
|
+
API_NAME,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Fetches a single entry.
|
|
251
|
+
getEntry(path: string) {
|
|
252
|
+
return this.api!.readFile(path).then(data => ({
|
|
253
|
+
file: { path, id: null },
|
|
254
|
+
data: data as string,
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
getMedia(mediaFolder = this.mediaFolder) {
|
|
259
|
+
return this.api!.listAllFiles(mediaFolder).then(files =>
|
|
260
|
+
files.map(({ id, name, path }) => {
|
|
261
|
+
return { id, name, path, displayURL: { id, name, path } };
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
getMediaDisplayURL(displayURL: DisplayURL) {
|
|
267
|
+
this._mediaDisplayURLSem = this._mediaDisplayURLSem || semaphore(MAX_CONCURRENT_DOWNLOADS);
|
|
268
|
+
return getMediaDisplayURL(
|
|
269
|
+
displayURL,
|
|
270
|
+
this.api!.readFile.bind(this.api!),
|
|
271
|
+
this._mediaDisplayURLSem,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async getMediaFile(path: string) {
|
|
276
|
+
const name = basename(path);
|
|
277
|
+
const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!));
|
|
278
|
+
const fileObj = blobToFileObj(name, blob);
|
|
279
|
+
const url = URL.createObjectURL(fileObj);
|
|
280
|
+
const id = await getBlobSHA(blob);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
id,
|
|
284
|
+
displayURL: url,
|
|
285
|
+
path,
|
|
286
|
+
name,
|
|
287
|
+
size: fileObj.size,
|
|
288
|
+
file: fileObj,
|
|
289
|
+
url,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async persistEntry(entry: Entry, options: PersistOptions) {
|
|
294
|
+
// persistEntry is a transactional operation
|
|
295
|
+
return runWithLock(
|
|
296
|
+
this.lock,
|
|
297
|
+
() => this.api!.persistFiles(entry.dataFiles, entry.assets, options),
|
|
298
|
+
'Failed to acquire persist entry lock',
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async persistMedia(mediaFile: AssetProxy, options: PersistOptions) {
|
|
303
|
+
const fileObj = mediaFile.fileObj as File;
|
|
304
|
+
|
|
305
|
+
const [id] = await Promise.all([
|
|
306
|
+
getBlobSHA(fileObj),
|
|
307
|
+
this.api!.persistFiles([], [mediaFile], options),
|
|
308
|
+
]);
|
|
309
|
+
|
|
310
|
+
const { path } = mediaFile;
|
|
311
|
+
const url = URL.createObjectURL(fileObj);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
displayURL: url,
|
|
315
|
+
path: trimStart(path, '/'),
|
|
316
|
+
name: fileObj!.name,
|
|
317
|
+
size: fileObj!.size,
|
|
318
|
+
file: fileObj,
|
|
319
|
+
url,
|
|
320
|
+
id,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
deleteFiles(paths: string[], commitMessage: string) {
|
|
325
|
+
return this.api!.deleteFiles(paths, commitMessage);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
traverseCursor(cursor: Cursor, action: string) {
|
|
329
|
+
return this.api!.traverseCursor(cursor, action).then(async ({ entries, cursor: newCursor }) => {
|
|
330
|
+
const [folder, depth, extension] = [
|
|
331
|
+
cursor.meta?.get('folder') as string,
|
|
332
|
+
cursor.meta?.get('depth') as number,
|
|
333
|
+
cursor.meta?.get('extension') as string,
|
|
334
|
+
];
|
|
335
|
+
if (folder && depth && extension) {
|
|
336
|
+
entries = entries.filter(f => this.filterFile(folder, f, extension, depth));
|
|
337
|
+
newCursor = newCursor.mergeMeta({ folder, extension, depth });
|
|
338
|
+
}
|
|
339
|
+
const entriesWithData = await entriesByFiles(
|
|
340
|
+
entries,
|
|
341
|
+
this.api!.readFile.bind(this.api!),
|
|
342
|
+
this.api!.readFileMetadata.bind(this.api)!,
|
|
343
|
+
API_NAME,
|
|
344
|
+
);
|
|
345
|
+
return {
|
|
346
|
+
entries: entriesWithData,
|
|
347
|
+
cursor: newCursor,
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
loadMediaFile(branch: string, file: UnpublishedEntryMediaFile) {
|
|
353
|
+
const readFile = (
|
|
354
|
+
path: string,
|
|
355
|
+
id: string | null | undefined,
|
|
356
|
+
{ parseText }: { parseText: boolean },
|
|
357
|
+
) => this.api!.readFile(path, id, { branch, parseText });
|
|
358
|
+
|
|
359
|
+
return getMediaAsBlob(file.path, null, readFile).then(blob => {
|
|
360
|
+
const name = basename(file.path);
|
|
361
|
+
const fileObj = blobToFileObj(name, blob);
|
|
362
|
+
return {
|
|
363
|
+
id: file.path,
|
|
364
|
+
displayURL: URL.createObjectURL(fileObj),
|
|
365
|
+
path: file.path,
|
|
366
|
+
name,
|
|
367
|
+
size: fileObj.size,
|
|
368
|
+
file: fileObj,
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async loadEntryMediaFiles(branch: string, files: UnpublishedEntryMediaFile[]) {
|
|
374
|
+
const mediaFiles = await Promise.all(files.map(file => this.loadMediaFile(branch, file)));
|
|
375
|
+
|
|
376
|
+
return mediaFiles;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async unpublishedEntries() {
|
|
380
|
+
const listEntriesKeys = () =>
|
|
381
|
+
this.api!.listUnpublishedBranches().then(branches =>
|
|
382
|
+
branches.map(branch => contentKeyFromBranch(branch)),
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const ids = await unpublishedEntries(listEntriesKeys);
|
|
386
|
+
return ids;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async unpublishedEntry({
|
|
390
|
+
id,
|
|
391
|
+
collection,
|
|
392
|
+
slug,
|
|
393
|
+
}: {
|
|
394
|
+
id?: string;
|
|
395
|
+
collection?: string;
|
|
396
|
+
slug?: string;
|
|
397
|
+
}) {
|
|
398
|
+
if (id) {
|
|
399
|
+
const data = await this.api!.retrieveUnpublishedEntryData(id);
|
|
400
|
+
return data;
|
|
401
|
+
} else if (collection && slug) {
|
|
402
|
+
const entryId = generateContentKey(collection, slug);
|
|
403
|
+
const data = await this.api!.retrieveUnpublishedEntryData(entryId);
|
|
404
|
+
return data;
|
|
405
|
+
} else {
|
|
406
|
+
throw new Error('Missing unpublished entry id or collection and slug');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
getBranch(collection: string, slug: string) {
|
|
411
|
+
const contentKey = generateContentKey(collection, slug);
|
|
412
|
+
const branch = branchFromContentKey(contentKey);
|
|
413
|
+
return branch;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async unpublishedEntryDataFile(collection: string, slug: string, path: string, id: string) {
|
|
417
|
+
const branch = this.getBranch(collection, slug);
|
|
418
|
+
const data = (await this.api!.readFile(path, id, { branch })) as string;
|
|
419
|
+
return data;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async unpublishedEntryMediaFile(collection: string, slug: string, path: string, id: string) {
|
|
423
|
+
const branch = this.getBranch(collection, slug);
|
|
424
|
+
const mediaFile = await this.loadMediaFile(branch, { path, id });
|
|
425
|
+
return mediaFile;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
|
429
|
+
// updateUnpublishedEntryStatus is a transactional operation
|
|
430
|
+
return runWithLock(
|
|
431
|
+
this.lock,
|
|
432
|
+
() => this.api!.updateUnpublishedEntryStatus(collection, slug, newStatus),
|
|
433
|
+
'Failed to acquire update entry status lock',
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async deleteUnpublishedEntry(collection: string, slug: string) {
|
|
438
|
+
// deleteUnpublishedEntry is a transactional operation
|
|
439
|
+
return runWithLock(
|
|
440
|
+
this.lock,
|
|
441
|
+
() => this.api!.deleteUnpublishedEntry(collection, slug),
|
|
442
|
+
'Failed to acquire delete entry lock',
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async publishUnpublishedEntry(collection: string, slug: string) {
|
|
447
|
+
// publishUnpublishedEntry is a transactional operation
|
|
448
|
+
return runWithLock(
|
|
449
|
+
this.lock,
|
|
450
|
+
() => this.api!.publishUnpublishedEntry(collection, slug),
|
|
451
|
+
'Failed to acquire publish entry lock',
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async getDeployPreview(collection: string, slug: string) {
|
|
456
|
+
try {
|
|
457
|
+
const statuses = await this.api!.getStatuses(collection, slug);
|
|
458
|
+
const deployStatus = getPreviewStatus(statuses, this.previewContext);
|
|
459
|
+
|
|
460
|
+
if (deployStatus) {
|
|
461
|
+
const { target_url: url, state } = deployStatus;
|
|
462
|
+
return { url, status: state };
|
|
463
|
+
} else {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
} catch (e) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import GitLabBackend from './implementation';
|
|
2
|
+
import API from './API';
|
|
3
|
+
import AuthenticationPage from './AuthenticationPage';
|
|
4
|
+
|
|
5
|
+
export const DecapCmsBackendGitlab = {
|
|
6
|
+
GitLabBackend,
|
|
7
|
+
API,
|
|
8
|
+
AuthenticationPage,
|
|
9
|
+
};
|
|
10
|
+
export { GitLabBackend, API, AuthenticationPage };
|
package/src/queries.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { gql } from 'graphql-tag';
|
|
2
|
+
import { oneLine } from 'common-tags';
|
|
3
|
+
|
|
4
|
+
export const files = gql`
|
|
5
|
+
query files($repo: ID!, $branch: String!, $path: String!, $recursive: Boolean!, $cursor: String) {
|
|
6
|
+
project(fullPath: $repo) {
|
|
7
|
+
repository {
|
|
8
|
+
tree(ref: $branch, path: $path, recursive: $recursive) {
|
|
9
|
+
blobs(after: $cursor) {
|
|
10
|
+
nodes {
|
|
11
|
+
type
|
|
12
|
+
id: sha
|
|
13
|
+
path
|
|
14
|
+
name
|
|
15
|
+
}
|
|
16
|
+
pageInfo {
|
|
17
|
+
endCursor
|
|
18
|
+
hasNextPage
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
export const blobs = gql`
|
|
28
|
+
query blobs($repo: ID!, $branch: String!, $paths: [String!]!) {
|
|
29
|
+
project(fullPath: $repo) {
|
|
30
|
+
repository {
|
|
31
|
+
blobs(ref: $branch, paths: $paths) {
|
|
32
|
+
nodes {
|
|
33
|
+
id
|
|
34
|
+
data: rawBlob
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
export function lastCommits(paths: string[]) {
|
|
43
|
+
const tree = paths
|
|
44
|
+
.map(
|
|
45
|
+
(path, index) => oneLine`
|
|
46
|
+
tree${index}: tree(ref: $branch, path: "${path}") {
|
|
47
|
+
lastCommit {
|
|
48
|
+
authorName
|
|
49
|
+
authoredDate
|
|
50
|
+
author {
|
|
51
|
+
id
|
|
52
|
+
username
|
|
53
|
+
name
|
|
54
|
+
publicEmail
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
`,
|
|
59
|
+
)
|
|
60
|
+
.join('\n');
|
|
61
|
+
|
|
62
|
+
const query = gql`
|
|
63
|
+
query lastCommits($repo: ID!, $branch: String!) {
|
|
64
|
+
project(fullPath: $repo) {
|
|
65
|
+
repository {
|
|
66
|
+
${tree}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
return query;
|
|
73
|
+
}
|