feat(core): Align GitHub metadata handling with other backends (#3292)

* refactor: code cleanup for GitLab and BitBucket

* feat(backend-github): use prs labels for metadata - initial commit

* refactor(backend-github): code cleanup, fix tests

* fix(backend-github): bug fixes

* fix(backend-github): open authoring fixes

* fix(backend-github): open authoring fixes

* fix(backend-github): fix graphql

* test(e2e): update Octokit usage

* test(e2e): re-record test data, fix bugs

* test(e2e): re-record test data, fix bugs

* test(e2e): fix tests

* refactor: code cleanup

* fix(backend-github): fix migration code

* fix(backend-github): don't fail on non existing files collections files

* fix(backend-github): use patch attribute instead of slug to get path

* fix(backend-github): use compare api to force push a pr

* fix(backend-github): fix git-gateway and re-record tests

* fix(backend-github): use first commit message as open authoring pr title

* refactor(backend-github): code cleanup

* test(e2e): fix tests
This commit is contained in:
Erez Rokah 2020-02-22 15:51:30 +01:00 committed by GitHub
parent 036ea06d5e
commit 8193b5ace8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 26572 additions and 36237 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
const Octokit = require('@octokit/rest'); const { Octokit } = require('@octokit/rest');
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path'); const path = require('path');
const { const {

View File

@ -200,7 +200,7 @@ function flushClockAndSave() {
function populateEntry(entry, onDone = flushClockAndSave) { function populateEntry(entry, onDone = flushClockAndSave) {
const keys = Object.keys(entry); const keys = Object.keys(entry);
for (let key of keys) { for (const key of keys) {
const value = entry[key]; const value = entry[key];
if (key === 'body') { if (key === 'body') {
cy.getMarkdownEditor() cy.getMarkdownEditor()

View File

@ -23,6 +23,7 @@ import {
PreviewState, PreviewState,
FetchError, FetchError,
parseContentKey, parseContentKey,
branchFromContentKey,
} from 'netlify-cms-lib-util'; } from 'netlify-cms-lib-util';
import { oneLine } from 'common-tags'; import { oneLine } from 'common-tags';
import { parse } from 'what-the-diff'; import { parse } from 'what-the-diff';
@ -449,8 +450,8 @@ export default class API {
} }
async editorialWorkflowGit(files: (Entry | AssetProxy)[], entry: Entry, options: PersistOptions) { async editorialWorkflowGit(files: (Entry | AssetProxy)[], entry: Entry, options: PersistOptions) {
const contentKey = this.generateContentKey(options.collectionName as string, entry.slug); const contentKey = generateContentKey(options.collectionName as string, entry.slug);
const branch = this.branchFromContentKey(contentKey); const branch = branchFromContentKey(contentKey);
const unpublished = options.unpublished || false; const unpublished = options.unpublished || false;
if (!unpublished) { if (!unpublished) {
const defaultBranchSha = await this.branchCommitSha(this.branch); const defaultBranchSha = await this.branchCommitSha(this.branch);
@ -497,18 +498,6 @@ export default class API {
); );
}; };
generateContentKey(collectionName: string, slug: string) {
return generateContentKey(collectionName, slug);
}
contentKeyFromBranch(branch: string) {
return branch.substring(`${CMS_BRANCH_PREFIX}/`.length);
}
branchFromContentKey(contentKey: string) {
return `${CMS_BRANCH_PREFIX}/${contentKey}`;
}
async isFileExists(path: string, branch: string) { async isFileExists(path: string, branch: string) {
const fileExists = await this.readFile(path, null, { branch }) const fileExists = await this.readFile(path, null, { branch })
.then(() => true) .then(() => true)
@ -559,7 +548,7 @@ export default class API {
async retrieveMetadata(contentKey: string) { async retrieveMetadata(contentKey: string) {
const { collection, slug } = parseContentKey(contentKey); const { collection, slug } = parseContentKey(contentKey);
const branch = this.branchFromContentKey(contentKey); const branch = branchFromContentKey(contentKey);
const pullRequest = await this.getBranchPullRequest(branch); const pullRequest = await this.getBranchPullRequest(branch);
const diff = await this.getDifferences(branch); const diff = await this.getDifferences(branch);
const { newPath: path, newFile } = diff.find(d => !d.binary) as { const { newPath: path, newFile } = diff.find(d => !d.binary) as {
@ -609,8 +598,8 @@ export default class API {
} }
async updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) { async updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
const contentKey = this.generateContentKey(collection, slug); const contentKey = generateContentKey(collection, slug);
const branch = this.branchFromContentKey(contentKey); const branch = branchFromContentKey(contentKey);
const pullRequest = await this.getBranchPullRequest(branch); const pullRequest = await this.getBranchPullRequest(branch);
await this.addPullRequestComment(pullRequest, statusToLabel(newStatus)); await this.addPullRequestComment(pullRequest, statusToLabel(newStatus));
@ -632,8 +621,8 @@ export default class API {
} }
async publishUnpublishedEntry(collectionName: string, slug: string) { async publishUnpublishedEntry(collectionName: string, slug: string) {
const contentKey = this.generateContentKey(collectionName, slug); const contentKey = generateContentKey(collectionName, slug);
const branch = this.branchFromContentKey(contentKey); const branch = branchFromContentKey(contentKey);
const pullRequest = await this.getBranchPullRequest(branch); const pullRequest = await this.getBranchPullRequest(branch);
await this.mergePullRequest(pullRequest); await this.mergePullRequest(pullRequest);
@ -654,8 +643,8 @@ export default class API {
} }
async deleteUnpublishedEntry(collectionName: string, slug: string) { async deleteUnpublishedEntry(collectionName: string, slug: string) {
const contentKey = this.generateContentKey(collectionName, slug); const contentKey = generateContentKey(collectionName, slug);
const branch = this.branchFromContentKey(contentKey); const branch = branchFromContentKey(contentKey);
const pullRequest = await this.getBranchPullRequest(branch); const pullRequest = await this.getBranchPullRequest(branch);
await this.declinePullRequest(pullRequest); await this.declinePullRequest(pullRequest);
@ -674,8 +663,8 @@ export default class API {
} }
async getStatuses(collectionName: string, slug: string) { async getStatuses(collectionName: string, slug: string) {
const contentKey = this.generateContentKey(collectionName, slug); const contentKey = generateContentKey(collectionName, slug);
const branch = this.branchFromContentKey(contentKey); const branch = branchFromContentKey(contentKey);
const pullRequest = await this.getBranchPullRequest(branch); const pullRequest = await this.getBranchPullRequest(branch);
const statuses = await this.getPullRequestStatuses(pullRequest); const statuses = await this.getPullRequestStatuses(pullRequest);

View File

@ -34,6 +34,8 @@ import {
getLargeMediaFilteredMediaFiles, getLargeMediaFilteredMediaFiles,
FetchError, FetchError,
blobToFileObj, blobToFileObj,
contentKeyFromBranch,
generateContentKey,
} from 'netlify-cms-lib-util'; } from 'netlify-cms-lib-util';
import NetlifyAuthenticator from 'netlify-cms-lib-auth'; import NetlifyAuthenticator from 'netlify-cms-lib-auth';
import AuthenticationPage from './AuthenticationPage'; import AuthenticationPage from './AuthenticationPage';
@ -445,7 +447,7 @@ export default class BitbucketBackend implements Implementation {
async unpublishedEntries() { async unpublishedEntries() {
const listEntriesKeys = () => const listEntriesKeys = () =>
this.api!.listUnpublishedBranches().then(branches => this.api!.listUnpublishedBranches().then(branches =>
branches.map(branch => this.api!.contentKeyFromBranch(branch)), branches.map(branch => contentKeyFromBranch(branch)),
); );
const readUnpublishedBranchFile = (contentKey: string) => const readUnpublishedBranchFile = (contentKey: string) =>
@ -462,7 +464,7 @@ export default class BitbucketBackend implements Implementation {
this.loadEntryMediaFiles(branch, files), this.loadEntryMediaFiles(branch, files),
} = {}, } = {},
) { ) {
const contentKey = this.api!.generateContentKey(collection, slug); const contentKey = generateContentKey(collection, slug);
const data = await this.api!.readUnpublishedBranchFile(contentKey); const data = await this.api!.readUnpublishedBranchFile(contentKey);
const mediaFiles = await loadEntryMediaFiles( const mediaFiles = await loadEntryMediaFiles(
data.metaData.branch, data.metaData.branch,

View File

@ -71,6 +71,20 @@ export default class API extends GithubAPI {
return Promise.resolve({ login: '', ...this.commitAuthor }); 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 }) { commit(message: string, changeTree: { parentSha?: string; sha: string }) {
const commitParams: { const commitParams: {
message: string; message: string;

File diff suppressed because it is too large Load Diff

View File

@ -9,17 +9,18 @@ import { createHttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context'; import { setContext } from 'apollo-link-context';
import { import {
APIError, APIError,
EditorialWorkflowError,
readFile, readFile,
localForage, localForage,
DEFAULT_PR_BODY, DEFAULT_PR_BODY,
branchFromContentKey,
} from 'netlify-cms-lib-util'; } from 'netlify-cms-lib-util';
import { trim } from 'lodash'; import { trim } from 'lodash';
import introspectionQueryResultData from './fragmentTypes'; import introspectionQueryResultData from './fragmentTypes';
import API, { Config, BlobArgs, PR, API_NAME } from './API'; import API, { Config, BlobArgs, API_NAME, PullRequestState, MOCK_PULL_REQUEST } from './API';
import * as queries from './queries'; import * as queries from './queries';
import * as mutations from './mutations'; import * as mutations from './mutations';
import { GraphQLError } from 'graphql'; import { GraphQLError } from 'graphql';
import { Octokit } from '@octokit/rest';
const NO_CACHE = 'no-cache'; const NO_CACHE = 'no-cache';
const CACHE_FIRST = 'cache-first'; const CACHE_FIRST = 'cache-first';
@ -48,25 +49,37 @@ interface TreeFile {
name: 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;
labels: { nodes: { name: string }[] };
};
const transformPullRequest = (pr: GraphQLPullRequest) => {
return {
...pr,
labels: pr.labels.nodes,
head: { ref: pr.headRefName, sha: pr.headRefOid },
base: { ref: pr.baseRefName, sha: pr.baseRefOid },
};
};
type Error = GraphQLError & { type: string }; type Error = GraphQLError & { type: string };
export default class GraphQLAPI extends API { export default class GraphQLAPI extends API {
repoOwner: string;
repoName: string;
originRepoOwner: string;
originRepoName: string;
client: ApolloClient<NormalizedCacheObject>; client: ApolloClient<NormalizedCacheObject>;
constructor(config: Config) { constructor(config: Config) {
super(config); super(config);
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];
this.client = this.getApolloClient(); this.client = this.getApolloClient();
} }
@ -214,7 +227,62 @@ export default class GraphQLAPI extends API {
} }
} }
async getStatuses(sha: string) { 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(predicate);
}
async getCmsBranches() {
const { repoOwner: owner, repoName: name } = this;
const { data } = await this.query({
query: queries.cmsBranches,
variables: {
owner,
name,
},
});
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 { originRepoOwner: owner, originRepoName: name } = this;
const { data } = await this.query({ query: queries.statues, variables: { owner, name, sha } }); const { data } = await this.query({ query: queries.statues, variables: { owner, name, sha } });
if (data.repository.object) { if (data.repository.object) {
@ -265,76 +333,6 @@ export default class GraphQLAPI extends API {
} }
} }
async listUnpublishedBranches() {
if (this.useOpenAuthoring) {
return super.listUnpublishedBranches();
}
console.log(
'%c Checking for Unpublished entries',
'line-height: 30px;text-align: center;font-weight: bold',
);
const { repoOwner: owner, repoName: name } = this;
const { data } = await this.query({
query: queries.unpublishedPrBranches,
variables: { owner, name },
});
const { nodes } = data.repository.refs as {
nodes: {
associatedPullRequests: { nodes: { headRef: { prefix: string; name: string } }[] };
}[];
};
if (nodes.length > 0) {
const branches = [] as { ref: string }[];
nodes.forEach(({ associatedPullRequests }) => {
associatedPullRequests.nodes.forEach(({ headRef }) => {
branches.push({ ref: `${headRef.prefix}${headRef.name}` });
});
});
return await Promise.all(branches.map(branch => this.migrateBranch(branch)));
} else {
console.log(
'%c No Unpublished entries',
'line-height: 30px;text-align: center;font-weight: bold',
);
throw new APIError('Not Found', 404, 'GitHub');
}
}
async readUnpublishedBranchFile(contentKey: string) {
// retrieveMetadata(contentKey) rejects in case of no metadata
const metaData = await this.retrieveMetadata(contentKey).catch(() => null);
if (metaData && metaData.objects && metaData.objects.entry && metaData.objects.entry.path) {
const { path } = metaData.objects.entry;
const { repoOwner: headOwner, repoName: headRepoName } = this;
const { originRepoOwner: baseOwner, originRepoName: baseRepoName } = this;
const { data } = await this.query({
query: queries.unpublishedBranchFile,
variables: {
headOwner,
headRepoName,
headExpression: `${metaData.branch}:${path}`,
baseOwner,
baseRepoName,
baseExpression: `${this.branch}:${path}`,
},
});
if (!data.head.object) {
throw new EditorialWorkflowError('content is not under editorial workflow', true);
}
const result = {
metaData,
fileData: data.head.object.text,
isModification: !!data.base.object,
slug: this.slugFromContentKey(contentKey, metaData.collection),
};
return result;
} else {
throw new EditorialWorkflowError('content is not under editorial workflow', true);
}
}
getBranchQualifiedName(branch: string) { getBranchQualifiedName(branch: string) {
return `refs/heads/${branch}`; return `refs/heads/${branch}`;
} }
@ -414,7 +412,10 @@ export default class GraphQLAPI extends API {
// https://developer.github.com/v4/enum/pullrequeststate/ // https://developer.github.com/v4/enum/pullrequeststate/
// GraphQL state: [CLOSED, MERGED, OPEN] // GraphQL state: [CLOSED, MERGED, OPEN]
// REST API state: [closed, open] // REST API state: [closed, open]
const state = data.repository.pullRequest.state === 'OPEN' ? 'open' : 'closed'; const state =
data.repository.pullRequest.state === 'OPEN'
? PullRequestState.Open
: PullRequestState.Closed;
return { return {
...data.repository.pullRequest, ...data.repository.pullRequest,
state, state,
@ -424,7 +425,6 @@ export default class GraphQLAPI extends API {
getPullRequestAndBranchQuery(branch: string, number: number) { getPullRequestAndBranchQuery(branch: string, number: number) {
const { repoOwner: owner, repoName: name } = this; const { repoOwner: owner, repoName: name } = this;
const { originRepoOwner, originRepoName } = this; const { originRepoOwner, originRepoName } = this;
return { return {
query: queries.pullRequestAndBranch, query: queries.pullRequestAndBranch,
variables: { variables: {
@ -448,7 +448,7 @@ export default class GraphQLAPI extends API {
return { branch: repository.branch, pullRequest: origin.pullRequest }; return { branch: repository.branch, pullRequest: origin.pullRequest };
} }
async openPR({ number }: PR) { async openPR(number: number) {
const pullRequest = await this.getPullRequest(number); const pullRequest = await this.getPullRequest(number);
const { data } = await this.mutate({ const { data } = await this.mutate({
@ -467,10 +467,10 @@ export default class GraphQLAPI extends API {
}, },
}); });
return data!.closePullRequest; return data!.reopenPullRequest;
} }
async closePR({ number }: PR) { async closePR(number: number) {
const pullRequest = await this.getPullRequest(number); const pullRequest = await this.getPullRequest(number);
const { data } = await this.mutate({ const { data } = await this.mutate({
@ -495,13 +495,12 @@ export default class GraphQLAPI extends API {
async deleteUnpublishedEntry(collectionName: string, slug: string) { async deleteUnpublishedEntry(collectionName: string, slug: string) {
try { try {
const contentKey = this.generateContentKey(collectionName, slug); const contentKey = this.generateContentKey(collectionName, slug);
const branchName = this.generateBranchName(contentKey); const branchName = branchFromContentKey(contentKey);
const metadata = await this.retrieveMetadata(contentKey); const metadata = await this.retrieveMetadata(contentKey);
if (metadata && metadata.pr) { if (metadata.pullRequest.number !== MOCK_PULL_REQUEST) {
const { branch, pullRequest } = await this.getPullRequestAndBranch( const { branch, pullRequest } = await this.getPullRequestAndBranch(
branchName, branchName,
metadata.pr.number, metadata.pullRequest.number,
); );
const { data } = await this.mutate({ const { data } = await this.mutate({
@ -631,7 +630,7 @@ export default class GraphQLAPI extends API {
}, },
}); });
const { pullRequest } = data!.createPullRequest; const { pullRequest } = data!.createPullRequest;
return { ...pullRequest, head: { sha: pullRequest.headRefOid } }; return (transformPullRequest(pullRequest) as unknown) as Octokit.PullsCreateResponse;
} }
async getFileSha(path: string, { repoURL = this.repoURL, branch = this.branch } = {}) { async getFileSha(path: string, { repoURL = this.repoURL, branch = this.branch } = {}) {

View File

@ -21,28 +21,35 @@ describe('github API', () => {
describe('editorialWorkflowGit', () => { describe('editorialWorkflowGit', () => {
it('should create PR with correct base branch name when publishing with editorial workflow', () => { it('should create PR with correct base branch name when publishing with editorial workflow', () => {
let prBaseBranch = null; let prBaseBranch = null;
const api = new API({ branch: 'gh-pages', repo: 'my-repo' }); let labels = null;
const api = new API({
branch: 'gh-pages',
repo: 'owner/my-repo',
initialWorkflowStatus: 'draft',
});
const responses = { const responses = {
'/repos/my-repo/branches/gh-pages': () => ({ commit: { sha: 'def' } }), '/repos/owner/my-repo/branches/gh-pages': () => ({ commit: { sha: 'def' } }),
'/repos/my-repo/git/trees/def': () => ({ tree: [] }), '/repos/owner/my-repo/git/trees/def': () => ({ tree: [] }),
'/repos/my-repo/git/trees': () => ({}), '/repos/owner/my-repo/git/trees': () => ({}),
'/repos/my-repo/git/commits': () => ({}), '/repos/owner/my-repo/git/commits': () => ({}),
'/repos/my-repo/git/refs': () => ({}), '/repos/owner/my-repo/git/refs': () => ({}),
'/repos/my-repo/pulls': pullRequest => { '/repos/owner/my-repo/pulls': req => {
prBaseBranch = JSON.parse(pullRequest.body).base; prBaseBranch = JSON.parse(req.body).base;
return { head: { sha: 'cbd' } }; return { head: { sha: 'cbd' }, labels: [], number: 1 };
},
'/repos/owner/my-repo/issues/1/labels': req => {
labels = JSON.parse(req.body).labels;
return {};
}, },
'/user': () => ({}),
'/repos/my-repo/git/blobs': () => ({}),
'/repos/my-repo/git/refs/meta/_netlify_cms': () => ({ object: {} }),
}; };
mockAPI(api, responses); mockAPI(api, responses);
return expect( return expect(
api api.editorialWorkflowGit([], { slug: 'entry', sha: 'abc' }, null, {}).then(() => ({
.editorialWorkflowGit([], { slug: 'entry', sha: 'abc' }, null, {}) prBaseBranch,
.then(() => prBaseBranch), labels,
).resolves.toEqual('gh-pages'); })),
).resolves.toEqual({ prBaseBranch: 'gh-pages', labels: ['netlify-cms/draft'] });
}); });
}); });
@ -291,39 +298,64 @@ describe('github API', () => {
}); });
}); });
describe('migrateBranch', () => { describe('migratePullRequest', () => {
it('should migrate to version 1 when no version', async () => { it('should migrate to pull request labels when no version', async () => {
const api = new API({ branch: 'master', repo: 'owner/repo' }); const api = new API({ branch: 'master', repo: 'owner/repo' });
const newBranch = { ref: 'refs/heads/cms/posts/2019-11-11-post-title' }; const pr = {
api.migrateToVersion1 = jest.fn().mockResolvedValue(newBranch); head: { ref: 'cms/2019-11-11-post-title' },
title: 'pr title',
number: 1,
labels: [],
};
const metadata = { type: 'PR' }; const metadata = { type: 'PR' };
api.retrieveMetadata = jest.fn().mockResolvedValue(metadata); 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();
const branch = { ref: 'refs/heads/cms/2019-11-11-post-title' }; await api.migratePullRequest(pr);
await expect(api.migrateBranch(branch)).resolves.toBe(newBranch);
expect(api.migrateToVersion1).toHaveBeenCalledTimes(1); expect(api.migrateToVersion1).toHaveBeenCalledTimes(1);
expect(api.migrateToVersion1).toHaveBeenCalledWith(branch, metadata); expect(api.migrateToVersion1).toHaveBeenCalledWith(pr, metadata);
expect(api.retrieveMetadata).toHaveBeenCalledTimes(1); expect(api.migrateToPullRequestLabels).toHaveBeenCalledTimes(1);
expect(api.retrieveMetadata).toHaveBeenCalledWith('2019-11-11-post-title'); expect(api.migrateToPullRequestLabels).toHaveBeenCalledWith(
migrateToVersion1Result.pullRequest,
migrateToVersion1Result.metadata,
);
expect(api.retrieveMetadataOld).toHaveBeenCalledTimes(1);
expect(api.retrieveMetadataOld).toHaveBeenCalledWith('2019-11-11-post-title');
}); });
it('should not migrate to version 1 when version is 1', async () => { it('should migrate to pull request labels when version is 1', async () => {
const api = new API({ branch: 'master', repo: 'owner/repo' }); const api = new API({ branch: 'master', repo: 'owner/repo' });
api.migrateToVersion1 = jest.fn(); 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' }; const metadata = { type: 'PR', version: '1' };
api.retrieveMetadata = jest.fn().mockResolvedValue(metadata); api.retrieveMetadataOld = jest.fn().mockResolvedValue(metadata);
api.migrateToPullRequestLabels = jest.fn().mockResolvedValue(pr, metadata);
const branch = { ref: 'refs/heads/cms/posts/2019-11-11-post-title' }; await api.migratePullRequest(pr);
await expect(api.migrateBranch(branch)).resolves.toBe(branch);
expect(api.migrateToVersion1).toHaveBeenCalledTimes(0); expect(api.migrateToVersion1).toHaveBeenCalledTimes(0);
expect(api.retrieveMetadata).toHaveBeenCalledTimes(1); expect(api.migrateToPullRequestLabels).toHaveBeenCalledTimes(1);
expect(api.retrieveMetadata).toHaveBeenCalledWith('posts/2019-11-11-post-title'); expect(api.migrateToPullRequestLabels).toHaveBeenCalledWith(pr, metadata);
expect(api.retrieveMetadataOld).toHaveBeenCalledTimes(1);
expect(api.retrieveMetadataOld).toHaveBeenCalledWith('posts/2019-11-11-post-title');
}); });
}); });
@ -331,10 +363,17 @@ describe('github API', () => {
it('should migrate to version 1', async () => { it('should migrate to version 1', async () => {
const api = new API({ branch: 'master', repo: 'owner/repo' }); 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' }; const newBranch = { ref: 'refs/heads/cms/posts/2019-11-11-post-title' };
api.createBranch = jest.fn().mockResolvedValue(newBranch); api.createBranch = jest.fn().mockResolvedValue(newBranch);
const newPr = { number: 2, head: { sha: 'new_head' } }; const newPr = { ...pr, number: 2 };
api.createPR = jest.fn().mockResolvedValue(newPr); api.createPR = jest.fn().mockResolvedValue(newPr);
api.storeMetadata = jest.fn(); api.storeMetadata = jest.fn();
@ -342,35 +381,42 @@ describe('github API', () => {
api.deleteBranch = jest.fn(); api.deleteBranch = jest.fn();
api.deleteMetadata = jest.fn(); api.deleteMetadata = jest.fn();
const branch = { ref: 'refs/heads/cms/2019-11-11-post-title' }; const branch = 'cms/2019-11-11-post-title';
const metadata = { const metadata = {
branch: 'cms/2019-11-11-post-title', branch,
type: 'PR', type: 'PR',
pr: { head: 'old_head' }, pr: { head: pr.head.sha },
commitMessage: 'commitMessage', commitMessage: 'commitMessage',
collection: 'posts', collection: 'posts',
}; };
await expect(api.migrateToVersion1(branch, metadata)).resolves.toBe(newBranch); const expectedMetadata = {
expect(api.createBranch).toHaveBeenCalledTimes(1);
expect(api.createBranch).toHaveBeenCalledWith('cms/posts/2019-11-11-post-title', 'old_head');
expect(api.createPR).toHaveBeenCalledTimes(1);
expect(api.createPR).toHaveBeenCalledWith('commitMessage', 'cms/posts/2019-11-11-post-title');
expect(api.storeMetadata).toHaveBeenCalledTimes(1);
expect(api.storeMetadata).toHaveBeenCalledWith('posts/2019-11-11-post-title', {
type: 'PR', type: 'PR',
pr: { head: 'new_head', number: 2 }, pr: { head: newPr.head.sha, number: 2 },
commitMessage: 'commitMessage', commitMessage: 'commitMessage',
collection: 'posts', collection: 'posts',
branch: 'cms/posts/2019-11-11-post-title', branch: 'cms/posts/2019-11-11-post-title',
version: '1', version: '1',
};
await expect(api.migrateToVersion1(pr, metadata)).resolves.toEqual({
metadata: expectedMetadata,
pullRequest: newPr,
}); });
expect(api.createBranch).toHaveBeenCalledTimes(1);
expect(api.createBranch).toHaveBeenCalledWith('cms/posts/2019-11-11-post-title', 'pr_head');
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).toHaveBeenCalledTimes(1);
expect(api.closePR).toHaveBeenCalledWith(metadata.pr); expect(api.closePR).toHaveBeenCalledWith(metadata.pr.number);
expect(api.deleteBranch).toHaveBeenCalledTimes(1); expect(api.deleteBranch).toHaveBeenCalledTimes(1);
expect(api.deleteBranch).toHaveBeenCalledWith('cms/2019-11-11-post-title'); expect(api.deleteBranch).toHaveBeenCalledWith('cms/2019-11-11-post-title');
@ -380,20 +426,55 @@ describe('github API', () => {
}); });
}); });
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', () => { describe('rebaseSingleCommit', () => {
it('should create updated tree and commit', async () => { it('should create updated tree and commit', async () => {
const api = new API({ branch: 'master', repo: 'owner/repo' }); const api = new API({ branch: 'master', repo: 'owner/repo' });
api.getCommitsDiff = jest.fn().mockResolvedValueOnce([ api.getDifferences = jest.fn().mockResolvedValueOnce({
{ filename: 'removed.md', status: 'removed', sha: 'removed_sha' }, files: [
{ { filename: 'removed.md', status: 'removed', sha: 'removed_sha' },
filename: 'renamed.md', {
status: 'renamed', filename: 'renamed.md',
previous_filename: 'previous_filename.md', status: 'renamed',
sha: 'renamed_sha', previous_filename: 'previous_filename.md',
}, sha: 'renamed_sha',
{ filename: 'added.md', status: 'added', sha: 'added_sha' }, },
]); { filename: 'added.md', status: 'added', sha: 'added_sha' },
],
});
const newTree = { sha: 'new_tree_sha' }; const newTree = { sha: 'new_tree_sha' };
api.updateTree = jest.fn().mockResolvedValueOnce(newTree); api.updateTree = jest.fn().mockResolvedValueOnce(newTree);
@ -414,8 +495,8 @@ describe('github API', () => {
await expect(api.rebaseSingleCommit(baseCommit, commit)).resolves.toBe(newCommit); await expect(api.rebaseSingleCommit(baseCommit, commit)).resolves.toBe(newCommit);
expect(api.getCommitsDiff).toHaveBeenCalledTimes(1); expect(api.getDifferences).toHaveBeenCalledTimes(1);
expect(api.getCommitsDiff).toHaveBeenCalledWith('parent_sha', 'sha'); expect(api.getDifferences).toHaveBeenCalledWith('parent_sha', 'sha', '/repos/owner/repo');
expect(api.updateTree).toHaveBeenCalledTimes(1); expect(api.updateTree).toHaveBeenCalledTimes(1);
expect(api.updateTree).toHaveBeenCalledWith('base_commit_sha', [ expect(api.updateTree).toHaveBeenCalledWith('base_commit_sha', [
@ -528,13 +609,18 @@ describe('github API', () => {
]; ];
api.request = jest.fn(() => Promise.resolve({ statuses })); api.request = jest.fn(() => Promise.resolve({ statuses }));
const sha = 'sha'; const sha = 'sha';
await expect(api.getStatuses(sha)).resolves.toEqual([ 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: 'deploy', state: 'success', target_url: 'deploy-url' },
{ context: 'build', state: 'other' }, { context: 'build', state: 'other' },
]); ]);
expect(api.getBranchPullRequest).toHaveBeenCalledTimes(1);
expect(api.getBranchPullRequest).toHaveBeenCalledWith('cms/collection/slug');
expect(api.request).toHaveBeenCalledTimes(1); expect(api.request).toHaveBeenCalledTimes(1);
expect(api.request).toHaveBeenCalledWith(`/repos/repo/commits/${sha}/status`); expect(api.request).toHaveBeenCalledWith(`/repos/repo/commits/${sha}/status`);
}); });

View File

@ -9,7 +9,7 @@ describe('github GraphQL API', () => {
describe('editorialWorkflowGit', () => { describe('editorialWorkflowGit', () => {
it('should should flatten nested tree into a list of files', () => { it('should should flatten nested tree into a list of files', () => {
const api = new GraphQLAPI({ branch: 'gh-pages', repo: 'my-repo' }); const api = new GraphQLAPI({ branch: 'gh-pages', repo: 'owner/my-repo' });
const entries = [ const entries = [
{ {
name: 'post-1.md', name: 'post-1.md',

View File

@ -185,7 +185,9 @@ describe('github backend implementation', () => {
isModification: true, isModification: true,
metaData: { metaData: {
branch: 'branch', branch: 'branch',
objects: { entry: { path: 'entry-path' }, files: [{ path: 'image.png', sha: 'sha' }] }, objects: {
entry: { path: 'entry-path', mediaFiles: [{ path: 'image.png', id: 'sha' }] },
},
}, },
}; };
readUnpublishedBranchFile.mockResolvedValue(data); readUnpublishedBranchFile.mockResolvedValue(data);

View File

@ -41,6 +41,7 @@ export const pullRequest = gql`
fragment PullRequestParts on PullRequest { fragment PullRequestParts on PullRequest {
id id
baseRefName baseRefName
baseRefOid
body body
headRefName headRefName
headRefOid headRefOid
@ -51,6 +52,11 @@ export const pullRequest = gql`
repository { repository {
...RepositoryParts ...RepositoryParts
} }
labels(last: 100) {
nodes {
name
}
}
} }
${repository} ${repository}
`; `;

View File

@ -25,6 +25,7 @@ import {
UnpublishedEntryMediaFile, UnpublishedEntryMediaFile,
runWithLock, runWithLock,
blobToFileObj, blobToFileObj,
contentKeyFromBranch,
} from 'netlify-cms-lib-util'; } from 'netlify-cms-lib-util';
import AuthenticationPage from './AuthenticationPage'; import AuthenticationPage from './AuthenticationPage';
import { Octokit } from '@octokit/rest'; import { Octokit } from '@octokit/rest';
@ -299,7 +300,7 @@ export default class GitHub implements Implementation {
const repoURL = this.useOpenAuthoring ? this.api!.originRepoURL : this.api!.repoURL; const repoURL = this.useOpenAuthoring ? this.api!.originRepoURL : this.api!.repoURL;
const readFile = (path: string, id: string | null | undefined) => const readFile = (path: string, id: string | null | undefined) =>
this.api!.readFile(path, id, { repoURL }) as Promise<string>; this.api!.readFile(path, id, { repoURL }).catch(() => '') as Promise<string>;
return entriesByFiles(files, readFile, 'GitHub'); return entriesByFiles(files, readFile, 'GitHub');
} }
@ -307,10 +308,12 @@ export default class GitHub implements Implementation {
// Fetches a single entry. // Fetches a single entry.
getEntry(path: string) { getEntry(path: string) {
const repoURL = this.api!.originRepoURL; const repoURL = this.api!.originRepoURL;
return this.api!.readFile(path, null, { repoURL }).then(data => ({ return this.api!.readFile(path, null, { repoURL })
file: { path, id: null }, .then(data => ({
data: data as string, file: { path, id: null },
})); data: data as string,
}))
.catch(() => ({ file: { path, id: null }, data: '' }));
} }
getMedia(mediaFolder = this.mediaFolder) { getMedia(mediaFolder = this.mediaFolder) {
@ -412,7 +415,7 @@ export default class GitHub implements Implementation {
unpublishedEntries() { unpublishedEntries() {
const listEntriesKeys = () => const listEntriesKeys = () =>
this.api!.listUnpublishedBranches().then(branches => this.api!.listUnpublishedBranches().then(branches =>
branches.map(({ ref }) => this.api!.contentKeyFromRef(ref)), branches.map(branch => contentKeyFromBranch(branch)),
); );
const readUnpublishedBranchFile = (contentKey: string) => const readUnpublishedBranchFile = (contentKey: string) =>
@ -431,10 +434,10 @@ export default class GitHub implements Implementation {
) { ) {
const contentKey = this.api!.generateContentKey(collection, slug); const contentKey = this.api!.generateContentKey(collection, slug);
const data = await this.api!.readUnpublishedBranchFile(contentKey); const data = await this.api!.readUnpublishedBranchFile(contentKey);
const files = data.metaData.objects.files || []; const files = data.metaData.objects.entry.mediaFiles || [];
const mediaFiles = await loadEntryMediaFiles( const mediaFiles = await loadEntryMediaFiles(
data.metaData.branch, data.metaData.branch,
files.map(({ sha: id, path }) => ({ id, path })), files.map(({ id, path }) => ({ id, path })),
); );
return { return {
slug, slug,
@ -446,28 +449,18 @@ export default class GitHub implements Implementation {
}; };
} }
/** async getDeployPreview(collection: string, slug: string) {
* Uses GitHub's Statuses API to retrieve statuses, infers which is for a try {
* deploy preview via `getPreviewStatus`. Returns the url provided by the const statuses = await this.api!.getStatuses(collection, slug);
* status, as well as the status state, which should be one of 'success', const deployStatus = getPreviewStatus(statuses, this.previewContext);
* 'pending', and 'failure'.
*/
async getDeployPreview(collectionName: string, slug: string) {
const contentKey = this.api!.generateContentKey(collectionName, slug);
const data = await this.api!.retrieveMetadata(contentKey);
if (!data || !data.pr) { if (deployStatus) {
return null; const { target_url: url, state } = deployStatus;
} return { url, status: state };
} else {
const headSHA = typeof data.pr.head === 'string' ? data.pr.head : data.pr.head.sha; return null;
const statuses = await this.api!.getStatuses(headSHA); }
const deployStatus = getPreviewStatus(statuses, this.previewContext); } catch (e) {
if (deployStatus) {
const { target_url: url, state } = deployStatus;
return { url, status: state };
} else {
return null; return null;
} }
} }

View File

@ -38,37 +38,6 @@ export const blob = gql`
${fragments.blobWithText} ${fragments.blobWithText}
`; `;
export const unpublishedBranchFile = gql`
query unpublishedBranchFile(
$headOwner: String!
$headRepoName: String!
$headExpression: String!
$baseOwner: String!
$baseRepoName: String!
$baseExpression: String!
) {
head: repository(owner: $headOwner, name: $headRepoName) {
...RepositoryParts
object(expression: $headExpression) {
... on Blob {
...BlobWithTextParts
}
}
}
base: repository(owner: $baseOwner, name: $baseRepoName) {
...RepositoryParts
object(expression: $baseExpression) {
... on Blob {
id
oid
}
}
}
}
${fragments.repository}
${fragments.blobWithText}
`;
export const statues = gql` export const statues = gql`
query statues($owner: String!, $name: String!, $sha: GitObjectID!) { query statues($owner: String!, $name: String!, $sha: GitObjectID!) {
repository(owner: $owner, name: $name) { repository(owner: $owner, name: $name) {
@ -140,30 +109,6 @@ export const files = (depth: number) => gql`
${fragments.fileEntry} ${fragments.fileEntry}
`; `;
export const unpublishedPrBranches = gql`
query unpublishedPrBranches($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
...RepositoryParts
refs(refPrefix: "refs/heads/cms/", last: 50) {
nodes {
id
associatedPullRequests(last: 50, states: OPEN) {
nodes {
id
headRef {
id
name
prefix
}
}
}
}
}
}
}
${fragments.repository}
`;
const branchQueryPart = ` const branchQueryPart = `
branch: ref(qualifiedName: $qualifiedName) { branch: ref(qualifiedName: $qualifiedName) {
...BranchParts ...BranchParts
@ -181,6 +126,21 @@ export const branch = gql`
${fragments.branch} ${fragments.branch}
`; `;
export const cmsBranches = gql`
query cmsBranches($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
...RepositoryParts
refs(refPrefix: "refs/heads/cms/", last: 100) {
nodes {
...BranchParts
}
}
}
}
${fragments.repository}
${fragments.branch}
`;
export const repository = gql` export const repository = gql`
query repository($owner: String!, $name: String!) { query repository($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) { repository(owner: $owner, name: $name) {
@ -206,13 +166,27 @@ export const pullRequest = gql`
${fragments.pullRequest} ${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` export const pullRequestAndBranch = gql`
query pullRequestAndBranch($owner: String!, $name: String!, $origin_owner: String!, $origin_name: String!, $qualifiedName: String!, $number: Int!) { query pullRequestAndBranch($owner: String!, $name: String!, $originRepoOwner: String!, $originRepoName: String!, $qualifiedName: String!, $number: Int!) {
repository(owner: $owner, name: $name) { repository(owner: $owner, name: $name) {
...RepositoryParts ...RepositoryParts
${branchQueryPart} ${branchQueryPart}
} }
origin: repository(owner: $origin_owner, name: $origin_name) { origin: repository(owner: $originRepoOwner, name: $originRepoName) {
...RepositoryParts ...RepositoryParts
${pullRequestQueryPart} ${pullRequestQueryPart}
} }
@ -222,47 +196,6 @@ export const pullRequestAndBranch = gql`
${fragments.pullRequest} ${fragments.pullRequest}
`; `;
export const commitTree = gql`
query commitTree($owner: String!, $name: String!, $sha: GitObjectID!) {
repository(owner: $owner, name: $name) {
...RepositoryParts
commit: object(oid: $sha) {
...ObjectParts
... on Commit {
tree {
...ObjectParts
entries {
...TreeEntryParts
}
}
}
}
}
}
${fragments.repository}
${fragments.object}
${fragments.treeEntry}
`;
export const tree = gql`
query tree($owner: String!, $name: String!, $sha: GitObjectID!) {
repository(owner: $owner, name: $name) {
...RepositoryParts
tree: object(oid: $sha) {
...ObjectParts
... on Tree {
entries {
...TreeEntryParts
}
}
}
}
}
${fragments.repository}
${fragments.object}
${fragments.treeEntry}
`;
export const fileSha = gql` export const fileSha = gql`
query fileSha($owner: String!, $name: String!, $expression: String!) { query fileSha($owner: String!, $name: String!, $expression: String!) {
repository(owner: $owner, name: $name) { repository(owner: $owner, name: $name) {

View File

@ -21,6 +21,7 @@ import {
responseParser, responseParser,
PreviewState, PreviewState,
parseContentKey, parseContentKey,
branchFromContentKey,
} from 'netlify-cms-lib-util'; } from 'netlify-cms-lib-util';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import { Map, Set } from 'immutable'; import { Map, Set } from 'immutable';
@ -457,18 +458,6 @@ export default class API {
])(`${this.repoURL}/repository/files/${encodeURIComponent(path)}`); ])(`${this.repoURL}/repository/files/${encodeURIComponent(path)}`);
}; };
generateContentKey(collectionName: string, slug: string) {
return generateContentKey(collectionName, slug);
}
contentKeyFromBranch(branch: string) {
return branch.substring(`${CMS_BRANCH_PREFIX}/`.length);
}
branchFromContentKey(contentKey: string) {
return `${CMS_BRANCH_PREFIX}/${contentKey}`;
}
async getMergeRequests(sourceBranch?: string) { async getMergeRequests(sourceBranch?: string) {
const mergeRequests: GitLabMergeRequest[] = await this.requestJSON({ const mergeRequests: GitLabMergeRequest[] = await this.requestJSON({
url: `${this.repoURL}/merge_requests`, url: `${this.repoURL}/merge_requests`,
@ -555,7 +544,7 @@ export default class API {
async retrieveMetadata(contentKey: string) { async retrieveMetadata(contentKey: string) {
const { collection, slug } = parseContentKey(contentKey); const { collection, slug } = parseContentKey(contentKey);
const branch = this.branchFromContentKey(contentKey); const branch = branchFromContentKey(contentKey);
const mergeRequest = await this.getBranchMergeRequest(branch); const mergeRequest = await this.getBranchMergeRequest(branch);
const diff = await this.getDifferences(mergeRequest.sha); const diff = await this.getDifferences(mergeRequest.sha);
const { old_path: path, new_file: newFile } = diff.find(d => !d.binary) as { const { old_path: path, new_file: newFile } = diff.find(d => !d.binary) as {
@ -646,8 +635,8 @@ export default class API {
} }
async editorialWorkflowGit(files: (Entry | AssetProxy)[], entry: Entry, options: PersistOptions) { async editorialWorkflowGit(files: (Entry | AssetProxy)[], entry: Entry, options: PersistOptions) {
const contentKey = this.generateContentKey(options.collectionName as string, entry.slug); const contentKey = generateContentKey(options.collectionName as string, entry.slug);
const branch = this.branchFromContentKey(contentKey); const branch = branchFromContentKey(contentKey);
const unpublished = options.unpublished || false; const unpublished = options.unpublished || false;
if (!unpublished) { if (!unpublished) {
const items = await this.getCommitItems(files, this.branch); const items = await this.getCommitItems(files, this.branch);
@ -694,8 +683,8 @@ export default class API {
} }
async updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) { async updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
const contentKey = this.generateContentKey(collection, slug); const contentKey = generateContentKey(collection, slug);
const branch = this.branchFromContentKey(contentKey); const branch = branchFromContentKey(contentKey);
const mergeRequest = await this.getBranchMergeRequest(branch); const mergeRequest = await this.getBranchMergeRequest(branch);
const labels = [ const labels = [
@ -722,8 +711,8 @@ export default class API {
} }
async publishUnpublishedEntry(collectionName: string, slug: string) { async publishUnpublishedEntry(collectionName: string, slug: string) {
const contentKey = this.generateContentKey(collectionName, slug); const contentKey = generateContentKey(collectionName, slug);
const branch = this.branchFromContentKey(contentKey); const branch = branchFromContentKey(contentKey);
const mergeRequest = await this.getBranchMergeRequest(branch); const mergeRequest = await this.getBranchMergeRequest(branch);
await this.mergeMergeRequest(mergeRequest); await this.mergeMergeRequest(mergeRequest);
} }
@ -747,8 +736,8 @@ export default class API {
} }
async deleteUnpublishedEntry(collectionName: string, slug: string) { async deleteUnpublishedEntry(collectionName: string, slug: string) {
const contentKey = this.generateContentKey(collectionName, slug); const contentKey = generateContentKey(collectionName, slug);
const branch = this.branchFromContentKey(contentKey); const branch = branchFromContentKey(contentKey);
const mergeRequest = await this.getBranchMergeRequest(branch); const mergeRequest = await this.getBranchMergeRequest(branch);
await this.closeMergeRequest(mergeRequest); await this.closeMergeRequest(mergeRequest);
await this.deleteBranch(branch); await this.deleteBranch(branch);
@ -765,8 +754,8 @@ export default class API {
} }
async getStatuses(collectionName: string, slug: string) { async getStatuses(collectionName: string, slug: string) {
const contentKey = this.generateContentKey(collectionName, slug); const contentKey = generateContentKey(collectionName, slug);
const branch = this.branchFromContentKey(contentKey); const branch = branchFromContentKey(contentKey);
const mergeRequest = await this.getBranchMergeRequest(branch); const mergeRequest = await this.getBranchMergeRequest(branch);
const statuses: GitLabCommitStatus[] = await this.getMergeRequestStatues(mergeRequest, branch); const statuses: GitLabCommitStatus[] = await this.getMergeRequestStatues(mergeRequest, branch);
// eslint-disable-next-line @typescript-eslint/camelcase // eslint-disable-next-line @typescript-eslint/camelcase

View File

@ -27,6 +27,8 @@ import {
runWithLock, runWithLock,
getBlobSHA, getBlobSHA,
blobToFileObj, blobToFileObj,
contentKeyFromBranch,
generateContentKey,
} from 'netlify-cms-lib-util'; } from 'netlify-cms-lib-util';
import AuthenticationPage from './AuthenticationPage'; import AuthenticationPage from './AuthenticationPage';
import API, { API_NAME } from './API'; import API, { API_NAME } from './API';
@ -297,7 +299,7 @@ export default class GitLab implements Implementation {
async unpublishedEntries() { async unpublishedEntries() {
const listEntriesKeys = () => const listEntriesKeys = () =>
this.api!.listUnpublishedBranches().then(branches => this.api!.listUnpublishedBranches().then(branches =>
branches.map(branch => this.api!.contentKeyFromBranch(branch)), branches.map(branch => contentKeyFromBranch(branch)),
); );
const readUnpublishedBranchFile = (contentKey: string) => const readUnpublishedBranchFile = (contentKey: string) =>
@ -314,7 +316,7 @@ export default class GitLab implements Implementation {
this.loadEntryMediaFiles(branch, files), this.loadEntryMediaFiles(branch, files),
} = {}, } = {},
) { ) {
const contentKey = this.api!.generateContentKey(collection, slug); const contentKey = generateContentKey(collection, slug);
const data = await this.api!.readUnpublishedBranchFile(contentKey); const data = await this.api!.readUnpublishedBranchFile(contentKey);
const mediaFiles = await loadEntryMediaFiles( const mediaFiles = await loadEntryMediaFiles(
data.metaData.branch, data.metaData.branch,