feat(backend-github): GitHub GraphQL API support (#2456)
* add GitHub GraphQL api initial support * support mutiple backends for e2e tests - initial commit * add github backend e2e test (currently skipped), fix bugs per tests * refactor e2e tests, add fork workflow tests, support fork workflow in GraphQL api * remove log message that might contain authentication token * return empty error when commit is not found when using GraphQL (align with base class) * disable github backend tests * fix bugs introduced after rebase of GraphQL and OpenAuthoring features * test: update tests per openAuthoring changes, split tests into multiple files * fix: pass in headers for pagination requests, avoid async iterator as it requires a polyfill on old browsers * test(e2e): disable github backend tests
This commit is contained in:
committed by
Shawn Erquhart
parent
083a336ba4
commit
ece136c92e
@ -17,10 +17,17 @@
|
||||
"scripts": {
|
||||
"develop": "yarn build:esm --watch",
|
||||
"build": "cross-env NODE_ENV=production webpack",
|
||||
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore **/__tests__ --root-mode upward"
|
||||
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore **/__tests__ --root-mode upward",
|
||||
"createFragmentTypes": "node scripts/createFragmentTypes.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"apollo-cache-inmemory": "^1.6.2",
|
||||
"apollo-client": "^2.6.3",
|
||||
"apollo-link-context": "^1.0.18",
|
||||
"apollo-link-http": "^1.5.15",
|
||||
"common-tags": "^1.8.0",
|
||||
"graphql": "^14.4.2",
|
||||
"graphql-tag": "^2.10.1",
|
||||
"js-base64": "^2.5.1",
|
||||
"semaphore": "^1.1.0"
|
||||
},
|
||||
|
@ -0,0 +1,48 @@
|
||||
const fetch = require('node-fetch');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const API_HOST = process.env.GITHUB_HOST || 'https://api.github.com';
|
||||
const API_TOKEN = process.env.GITHUB_API_TOKEN;
|
||||
|
||||
if (!API_TOKEN) {
|
||||
throw new Error('Missing environment variable GITHUB_API_TOKEN');
|
||||
}
|
||||
|
||||
fetch(`${API_HOST}/graphql`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `bearer ${API_TOKEN}` },
|
||||
body: JSON.stringify({
|
||||
variables: {},
|
||||
query: `
|
||||
{
|
||||
__schema {
|
||||
types {
|
||||
kind
|
||||
name
|
||||
possibleTypes {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
})
|
||||
.then(result => result.json())
|
||||
.then(result => {
|
||||
// here we're filtering out any type information unrelated to unions or interfaces
|
||||
const filteredData = result.data.__schema.types.filter(type => type.possibleTypes !== null);
|
||||
result.data.__schema.types = filteredData;
|
||||
fs.writeFile(
|
||||
path.join(__dirname, '..', 'src', 'fragmentTypes.js'),
|
||||
`module.exports = ${JSON.stringify(result.data)}`,
|
||||
err => {
|
||||
if (err) {
|
||||
console.error('Error writing fragmentTypes file', err);
|
||||
} else {
|
||||
console.log('Fragment types successfully extracted!');
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
@ -3,7 +3,7 @@ import semaphore from 'semaphore';
|
||||
import { find, flow, get, hasIn, initial, last, partial, result, uniq } from 'lodash';
|
||||
import { map } from 'lodash/fp';
|
||||
import {
|
||||
getPaginatedRequestIterator,
|
||||
getAllResponses,
|
||||
APIError,
|
||||
EditorialWorkflowError,
|
||||
filterPromisesWith,
|
||||
@ -21,15 +21,19 @@ export default class API {
|
||||
this.api_root = config.api_root || 'https://api.github.com';
|
||||
this.token = config.token || false;
|
||||
this.branch = config.branch || 'master';
|
||||
this.originRepo = config.originRepo;
|
||||
this.useOpenAuthoring = config.useOpenAuthoring;
|
||||
this.repo = config.repo || '';
|
||||
this.originRepo = config.originRepo || this.repo;
|
||||
this.repoURL = `/repos/${this.repo}`;
|
||||
this.originRepoURL = this.originRepo && `/repos/${this.originRepo}`;
|
||||
// when not in 'useOpenAuthoring' mode originRepoURL === repoURL
|
||||
this.originRepoURL = `/repos/${this.originRepo}`;
|
||||
this.merge_method = config.squash_merges ? 'squash' : 'merge';
|
||||
this.initialWorkflowStatus = config.initialWorkflowStatus;
|
||||
}
|
||||
|
||||
static DEFAULT_COMMIT_MESSAGE = 'Automatically generated by Netlify CMS';
|
||||
static DEFAULT_PR_BODY = 'Automatically generated by Netlify CMS';
|
||||
|
||||
user() {
|
||||
if (!this._userPromise) {
|
||||
this._userPromise = this.request('/user');
|
||||
@ -113,13 +117,10 @@ export default class API {
|
||||
}
|
||||
|
||||
async requestAllPages(url, options = {}) {
|
||||
const headers = this.requestHeaders(options.headers || {});
|
||||
const processedURL = this.urlFor(url, options);
|
||||
const pagesIterator = getPaginatedRequestIterator(processedURL, options);
|
||||
const pagesToParse = [];
|
||||
for await (const page of pagesIterator) {
|
||||
pagesToParse.push(this.parseResponse(page));
|
||||
}
|
||||
const pages = await Promise.all(pagesToParse);
|
||||
const allResponses = await getAllResponses(processedURL, { ...options, headers });
|
||||
const pages = await Promise.all(allResponses.map(res => this.parseResponse(res)));
|
||||
return [].concat(...pages);
|
||||
}
|
||||
|
||||
@ -224,34 +225,46 @@ export default class API {
|
||||
cache: 'no-store',
|
||||
};
|
||||
|
||||
const errorHandler = err => {
|
||||
if (err.message === 'Not Found') {
|
||||
console.log(
|
||||
'%c %s does not have metadata',
|
||||
'line-height: 30px;text-align: center;font-weight: bold',
|
||||
key,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
};
|
||||
|
||||
if (!this.useOpenAuthoring) {
|
||||
return this.request(`${this.repoURL}/contents/${key}.json`, metadataRequestOptions)
|
||||
.then(response => JSON.parse(response))
|
||||
.catch(err => {
|
||||
if (err.message === 'Not Found') {
|
||||
console.log(
|
||||
'%c %s does not have metadata',
|
||||
'line-height: 30px;text-align: center;font-weight: bold',
|
||||
key,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
.catch(errorHandler);
|
||||
}
|
||||
|
||||
const [user, repo] = key.split('/');
|
||||
return this.request(`/repos/${user}/${repo}/contents/${key}.json`, metadataRequestOptions)
|
||||
.then(response => JSON.parse(response))
|
||||
.catch(err => {
|
||||
if (err.message === 'Not Found') {
|
||||
console.log(
|
||||
'%c %s does not have metadata',
|
||||
'line-height: 30px;text-align: center;font-weight: bold',
|
||||
key,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
.catch(errorHandler);
|
||||
});
|
||||
}
|
||||
|
||||
retrieveContent(path, branch, repoURL) {
|
||||
return this.request(`${repoURL}/contents/${path}`, {
|
||||
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
||||
params: { ref: branch },
|
||||
cache: 'no-store',
|
||||
}).catch(error => {
|
||||
if (hasIn(error, 'message.errors') && find(error.message.errors, { code: 'too_large' })) {
|
||||
const dir = path
|
||||
.split('/')
|
||||
.slice(0, -1)
|
||||
.join('/');
|
||||
return this.listFiles(dir, { repoURL, branch })
|
||||
.then(files => files.find(file => file.path === path))
|
||||
.then(file => this.getBlob(file.sha, { repoURL }));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
@ -259,34 +272,23 @@ export default class API {
|
||||
if (sha) {
|
||||
return this.getBlob(sha);
|
||||
} else {
|
||||
return this.request(`${repoURL}/contents/${path}`, {
|
||||
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
||||
params: { ref: branch },
|
||||
cache: 'no-store',
|
||||
}).catch(error => {
|
||||
if (hasIn(error, 'message.errors') && find(error.message.errors, { code: 'too_large' })) {
|
||||
const dir = path
|
||||
.split('/')
|
||||
.slice(0, -1)
|
||||
.join('/');
|
||||
return this.listFiles(dir, { repoURL, branch })
|
||||
.then(files => files.find(file => file.path === path))
|
||||
.then(file => this.getBlob(file.sha, { repoURL }));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
return this.retrieveContent(path, branch, repoURL);
|
||||
}
|
||||
}
|
||||
|
||||
retrieveBlob(sha, repoURL) {
|
||||
return this.request(`${repoURL}/git/blobs/${sha}`, {
|
||||
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
||||
});
|
||||
}
|
||||
|
||||
getBlob(sha, { repoURL = this.repoURL } = {}) {
|
||||
return localForage.getItem(`gh.${sha}`).then(cached => {
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
return this.request(`${repoURL}/git/blobs/${sha}`, {
|
||||
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
||||
}).then(result => {
|
||||
return this.retrieveBlob(sha, repoURL).then(result => {
|
||||
localForage.setItem(`gh.${sha}`, result);
|
||||
return result;
|
||||
});
|
||||
@ -335,7 +337,7 @@ export default class API {
|
||||
isUnpublishedEntryModification(path, branch) {
|
||||
return this.readFile(path, null, {
|
||||
branch,
|
||||
repoURL: this.useOpenAuthoring ? this.originRepoURL : this.repoURL,
|
||||
repoURL: this.originRepoURL,
|
||||
})
|
||||
.then(() => true)
|
||||
.catch(err => {
|
||||
@ -385,7 +387,7 @@ export default class API {
|
||||
// closed or not and update the status accordingly.
|
||||
if (prMetadata) {
|
||||
const { number: prNumber } = prMetadata;
|
||||
const originPRInfo = await this.request(`${this.originRepoURL}/pulls/${prNumber}`);
|
||||
const originPRInfo = await this.getPullRequest(prNumber);
|
||||
const { state: currentState, merged_at: mergedAt } = originPRInfo;
|
||||
if (currentState === 'closed' && mergedAt) {
|
||||
// The PR has been merged; delete the unpublished entry
|
||||
@ -453,9 +455,8 @@ export default class API {
|
||||
* concept of entry "status". Useful for things like deploy preview links.
|
||||
*/
|
||||
async getStatuses(sha) {
|
||||
const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
|
||||
try {
|
||||
const resp = await this.request(`${repoURL}/commits/${sha}/status`);
|
||||
const resp = await this.request(`${this.originRepoURL}/commits/${sha}/status`);
|
||||
return resp.statuses;
|
||||
} catch (err) {
|
||||
if (err && err.message && err.message === 'Ref not found') {
|
||||
@ -517,25 +518,33 @@ export default class API {
|
||||
});
|
||||
}
|
||||
|
||||
deleteFile(path, message, options = {}) {
|
||||
if (this.useOpenAuthoring) {
|
||||
return Promise.reject('Cannot delete published entries as an Open Authoring user!');
|
||||
}
|
||||
const branch = options.branch || this.branch;
|
||||
const pathArray = path.split('/');
|
||||
const filename = last(pathArray);
|
||||
const directory = initial(pathArray).join('/');
|
||||
const fileDataPath = encodeURIComponent(directory);
|
||||
const fileDataURL = `${this.repoURL}/git/trees/${branch}:${fileDataPath}`;
|
||||
const fileURL = `${this.repoURL}/contents/${path}`;
|
||||
|
||||
getFileSha(path, branch) {
|
||||
/**
|
||||
* We need to request the tree first to get the SHA. We use extended SHA-1
|
||||
* syntax (<rev>:<path>) to get a blob from a tree without having to recurse
|
||||
* through the tree.
|
||||
*/
|
||||
|
||||
const pathArray = path.split('/');
|
||||
const filename = last(pathArray);
|
||||
const directory = initial(pathArray).join('/');
|
||||
const fileDataPath = encodeURIComponent(directory);
|
||||
const fileDataURL = `${this.repoURL}/git/trees/${branch}:${fileDataPath}`;
|
||||
|
||||
return this.request(fileDataURL, { cache: 'no-store' }).then(resp => {
|
||||
const { sha } = resp.tree.find(file => file.path === filename);
|
||||
return sha;
|
||||
});
|
||||
}
|
||||
|
||||
deleteFile(path, message, options = {}) {
|
||||
if (this.useOpenAuthoring) {
|
||||
return Promise.reject('Cannot delete published entries as an Open Authoring user!');
|
||||
}
|
||||
|
||||
const branch = options.branch || this.branch;
|
||||
|
||||
return this.getFileSha(path, branch).then(sha => {
|
||||
const opts = { method: 'DELETE', params: { sha, message, branch } };
|
||||
if (this.commitAuthor) {
|
||||
opts.params.author = {
|
||||
@ -543,10 +552,16 @@ export default class API {
|
||||
date: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
const fileURL = `${this.repoURL}/contents/${path}`;
|
||||
return this.request(fileURL, opts);
|
||||
});
|
||||
}
|
||||
|
||||
async createBranchAndPullRequest(branchName, sha, commitMessage) {
|
||||
await this.createBranch(branchName, sha);
|
||||
return this.createPR(commitMessage, branchName);
|
||||
}
|
||||
|
||||
async editorialWorkflowGit(fileTree, entry, filesList, options) {
|
||||
const contentKey = this.generateContentKey(options.collectionName, entry.slug);
|
||||
const branchName = this.generateBranchName(contentKey);
|
||||
@ -557,10 +572,18 @@ export default class API {
|
||||
const branchData = await this.getBranch();
|
||||
const changeTree = await this.updateTree(branchData.commit.sha, '/', fileTree);
|
||||
const commitResponse = await this.commit(options.commitMessage, changeTree);
|
||||
await this.createBranch(branchName, commitResponse.sha);
|
||||
const pr = this.useOpenAuthoring
|
||||
? undefined
|
||||
: await this.createPR(options.commitMessage, branchName);
|
||||
|
||||
let pr;
|
||||
if (this.useOpenAuthoring) {
|
||||
await this.createBranch(branchName, commitResponse.sha);
|
||||
} else {
|
||||
pr = await this.createBranchAndPullRequest(
|
||||
branchName,
|
||||
commitResponse.sha,
|
||||
options.commitMessage,
|
||||
);
|
||||
}
|
||||
|
||||
const user = await userPromise;
|
||||
return this.storeMetadata(contentKey, {
|
||||
type: 'PR',
|
||||
@ -610,6 +633,9 @@ export default class API {
|
||||
|
||||
if (pr) {
|
||||
return this.rebasePullRequest(pr.number, branchName, contentKey, metadata, commit);
|
||||
} else if (this.useOpenAuthoring) {
|
||||
// if a PR hasn't been created yet for the forked repo, just patch the branch
|
||||
await this.patchBranch(branchName, commit.sha, { force: true });
|
||||
}
|
||||
|
||||
return this.storeMetadata(contentKey, updatedMetadata);
|
||||
@ -629,8 +655,10 @@ export default class API {
|
||||
* Get the published branch and create new commits over it. If the pull
|
||||
* request is up to date, no rebase will occur.
|
||||
*/
|
||||
const baseBranch = await this.getBranch();
|
||||
const commits = await this.getPullRequestCommits(prNumber, head);
|
||||
const [baseBranch, commits] = await Promise.all([
|
||||
this.getBranch(),
|
||||
this.getPullRequestCommits(prNumber, head),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Sometimes the list of commits for a pull request isn't updated
|
||||
@ -731,16 +759,14 @@ export default class API {
|
||||
* Get a pull request by PR number.
|
||||
*/
|
||||
getPullRequest(prNumber) {
|
||||
const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
|
||||
return this.request(`${repoURL}/pulls/${prNumber} }`);
|
||||
return this.request(`${this.originRepoURL}/pulls/${prNumber} }`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of commits for a given pull request.
|
||||
*/
|
||||
getPullRequestCommits(prNumber) {
|
||||
const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
|
||||
return this.request(`${repoURL}/pulls/${prNumber}/commits`);
|
||||
return this.request(`${this.originRepoURL}/pulls/${prNumber}/commits`);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -779,7 +805,7 @@ export default class API {
|
||||
const { pr: prMetadata } = metadata;
|
||||
if (prMetadata) {
|
||||
const { number: prNumber } = prMetadata;
|
||||
const originPRInfo = await this.request(`${this.originRepoURL}/pulls/${prNumber}`);
|
||||
const originPRInfo = await this.getPullRequest(prNumber);
|
||||
const { state } = originPRInfo;
|
||||
if (state === 'open' && status === 'draft') {
|
||||
await this.closePR(prMetadata);
|
||||
@ -800,7 +826,7 @@ export default class API {
|
||||
|
||||
if (!prMetadata && status === 'pending_review') {
|
||||
const branchName = this.generateBranchName(contentKey);
|
||||
const commitMessage = metadata.commitMessage || 'Automatically generated by Netlify CMS';
|
||||
const commitMessage = metadata.commitMessage || API.DEFAULT_COMMIT_MESSAGE;
|
||||
const { number, head } = await this.createPR(commitMessage, branchName);
|
||||
return this.storeMetadata(contentKey, {
|
||||
...metadata,
|
||||
@ -883,21 +909,23 @@ export default class API {
|
||||
return this.deleteRef('heads', branchName);
|
||||
}
|
||||
|
||||
async createPR(title, head, base = this.branch) {
|
||||
const body = 'Automatically generated by Netlify CMS';
|
||||
const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
|
||||
async createPR(title, head) {
|
||||
const headReference = this.useOpenAuthoring ? `${(await this.user()).login}:${head}` : head;
|
||||
return this.request(`${repoURL}/pulls`, {
|
||||
return this.request(`${this.originRepoURL}/pulls`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ title, body, head: headReference, base }),
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
body: API.DEFAULT_PR_BODY,
|
||||
head: headReference,
|
||||
base: this.branch,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async openPR(pullRequest) {
|
||||
const { number } = pullRequest;
|
||||
const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
|
||||
console.log('%c Re-opening PR', 'line-height: 30px;text-align: center;font-weight: bold');
|
||||
return this.request(`${repoURL}/pulls/${number}`, {
|
||||
return this.request(`${this.originRepoURL}/pulls/${number}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
state: 'open',
|
||||
@ -905,11 +933,10 @@ export default class API {
|
||||
});
|
||||
}
|
||||
|
||||
closePR(pullrequest) {
|
||||
const prNumber = pullrequest.number;
|
||||
const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
|
||||
closePR(pullRequest) {
|
||||
const { number } = pullRequest;
|
||||
console.log('%c Deleting PR', 'line-height: 30px;text-align: center;font-weight: bold');
|
||||
return this.request(`${repoURL}/pulls/${prNumber}`, {
|
||||
return this.request(`${this.originRepoURL}/pulls/${number}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
state: 'closed',
|
||||
@ -918,11 +945,9 @@ export default class API {
|
||||
}
|
||||
|
||||
mergePR(pullrequest, objects) {
|
||||
const headSha = pullrequest.head;
|
||||
const prNumber = pullrequest.number;
|
||||
const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
|
||||
const { head: headSha, number } = pullrequest;
|
||||
console.log('%c Merging PR', 'line-height: 30px;text-align: center;font-weight: bold');
|
||||
return this.request(`${repoURL}/pulls/${prNumber}/merge`, {
|
||||
return this.request(`${this.originRepoURL}/pulls/${number}/merge`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
commit_message: 'Automatically generated. Merged on Netlify CMS.',
|
||||
|
627
packages/netlify-cms-backend-github/src/GraphQLAPI.js
Normal file
627
packages/netlify-cms-backend-github/src/GraphQLAPI.js
Normal file
@ -0,0 +1,627 @@
|
||||
import { ApolloClient } from 'apollo-client';
|
||||
import {
|
||||
InMemoryCache,
|
||||
defaultDataIdFromObject,
|
||||
IntrospectionFragmentMatcher,
|
||||
} from 'apollo-cache-inmemory';
|
||||
import { createHttpLink } from 'apollo-link-http';
|
||||
import { setContext } from 'apollo-link-context';
|
||||
import { APIError, EditorialWorkflowError } from 'netlify-cms-lib-util';
|
||||
import introspectionQueryResultData from './fragmentTypes';
|
||||
import API from './API';
|
||||
import * as queries from './queries';
|
||||
import * as mutations from './mutations';
|
||||
|
||||
const NO_CACHE = 'no-cache';
|
||||
const CACHE_FIRST = 'cache-first';
|
||||
|
||||
const TREE_ENTRY_TYPE_TO_MODE = {
|
||||
blob: '100644',
|
||||
tree: '040000',
|
||||
commit: '160000',
|
||||
};
|
||||
|
||||
const fragmentMatcher = new IntrospectionFragmentMatcher({
|
||||
introspectionQueryResultData,
|
||||
});
|
||||
|
||||
export default class GraphQLAPI extends API {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
|
||||
const [repoParts, originRepoParts] = [this.repo.split('/'), this.originRepo.split('/')];
|
||||
this.repo_owner = repoParts[0];
|
||||
this.repo_name = repoParts[1];
|
||||
|
||||
this.origin_repo_owner = originRepoParts[0];
|
||||
this.origin_repo_name = originRepoParts[1];
|
||||
|
||||
this.client = this.getApolloClient();
|
||||
}
|
||||
|
||||
getApolloClient() {
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: this.token ? `token ${this.token}` : '',
|
||||
},
|
||||
};
|
||||
});
|
||||
const httpLink = createHttpLink({ uri: `${this.api_root}/graphql` });
|
||||
return new ApolloClient({
|
||||
link: authLink.concat(httpLink),
|
||||
cache: new InMemoryCache({ fragmentMatcher }),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: NO_CACHE,
|
||||
errorPolicy: 'ignore',
|
||||
},
|
||||
query: {
|
||||
fetchPolicy: NO_CACHE,
|
||||
errorPolicy: 'all',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
return this.client.resetStore();
|
||||
}
|
||||
|
||||
async getRepository(owner, name) {
|
||||
const { data } = await this.query({
|
||||
query: queries.repository,
|
||||
variables: { owner, name },
|
||||
fetchPolicy: CACHE_FIRST, // repository id doesn't change
|
||||
});
|
||||
return data.repository;
|
||||
}
|
||||
|
||||
query(options = {}) {
|
||||
return this.client.query(options).catch(error => {
|
||||
throw new APIError(error.message, 500, 'GitHub');
|
||||
});
|
||||
}
|
||||
|
||||
mutate(options = {}) {
|
||||
return this.client.mutate(options).catch(error => {
|
||||
throw new APIError(error.message, 500, 'GitHub');
|
||||
});
|
||||
}
|
||||
|
||||
async hasWriteAccess() {
|
||||
const { repo_owner: owner, repo_name: name } = this;
|
||||
try {
|
||||
const { data } = await this.query({
|
||||
query: queries.repoPermission,
|
||||
variables: { owner, name },
|
||||
fetchPolicy: CACHE_FIRST, // we can assume permission doesn't change often
|
||||
});
|
||||
// https://developer.github.com/v4/enum/repositorypermission/
|
||||
const { viewerPermission } = data.repository;
|
||||
return ['ADMIN', 'MAINTAIN', 'WRITE'].includes(viewerPermission);
|
||||
} catch (error) {
|
||||
console.error('Problem fetching repo data from GitHub');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async user() {
|
||||
const { data } = await this.query({
|
||||
query: queries.user,
|
||||
fetchPolicy: CACHE_FIRST, // we can assume user details don't change often
|
||||
});
|
||||
return data.viewer;
|
||||
}
|
||||
|
||||
async retrieveBlobObject(owner, name, expression, options = {}) {
|
||||
const { data } = await this.query({
|
||||
query: queries.blob,
|
||||
variables: { owner, name, expression },
|
||||
...options,
|
||||
});
|
||||
// https://developer.github.com/v4/object/blob/
|
||||
if (data.repository.object) {
|
||||
const { is_binary, text } = data.repository.object;
|
||||
return { is_null: false, is_binary, text };
|
||||
} else {
|
||||
return { is_null: true };
|
||||
}
|
||||
}
|
||||
|
||||
getOwnerAndNameFromRepoUrl(repoURL) {
|
||||
let { repo_owner: owner, repo_name: name } = this;
|
||||
|
||||
if (repoURL === this.originRepoURL) {
|
||||
({ origin_repo_owner: owner, origin_repo_name: name } = this);
|
||||
}
|
||||
|
||||
return { owner, name };
|
||||
}
|
||||
|
||||
async retrieveContent(path, branch, repoURL) {
|
||||
const { owner, name } = this.getOwnerAndNameFromRepoUrl(repoURL);
|
||||
const { is_null, is_binary, text } = await this.retrieveBlobObject(
|
||||
owner,
|
||||
name,
|
||||
`${branch}:${path}`,
|
||||
);
|
||||
if (is_null) {
|
||||
throw new APIError('Not Found', 404, 'GitHub');
|
||||
} else if (!is_binary) {
|
||||
return text;
|
||||
} else {
|
||||
return super.retrieveContent(path, branch, repoURL);
|
||||
}
|
||||
}
|
||||
|
||||
async retrieveBlob(sha, repoURL) {
|
||||
const { owner, name } = this.getOwnerAndNameFromRepoUrl(repoURL);
|
||||
const { is_null, is_binary, text } = await this.retrieveBlobObject(
|
||||
owner,
|
||||
name,
|
||||
sha,
|
||||
{ fetchPolicy: CACHE_FIRST }, // blob sha is derived from file content
|
||||
);
|
||||
|
||||
if (is_null) {
|
||||
throw new APIError('Not Found', 404, 'GitHub');
|
||||
} else if (!is_binary) {
|
||||
return text;
|
||||
} else {
|
||||
return super.retrieveBlob(sha);
|
||||
}
|
||||
}
|
||||
|
||||
async getStatuses(sha) {
|
||||
const { origin_repo_owner: owner, origin_repo_name: name } = this;
|
||||
const { data } = await this.query({ query: queries.statues, variables: { owner, name, sha } });
|
||||
if (data.repository.object) {
|
||||
const { status } = data.repository.object;
|
||||
const { contexts } = status || { contexts: [] };
|
||||
return contexts;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async listFiles(path) {
|
||||
const { repo_owner: owner, repo_name: name } = this;
|
||||
const { data } = await this.query({
|
||||
query: queries.files,
|
||||
variables: { owner, name, expression: `${this.branch}:${path}` },
|
||||
});
|
||||
|
||||
if (data.repository.object) {
|
||||
const files = data.repository.object.entries.map(e => ({
|
||||
...e,
|
||||
path: `${path}/${e.name}`,
|
||||
download_url: `https://raw.githubusercontent.com/${this.repo}/${this.branch}/${path}/${e.name}`,
|
||||
size: e.blob && e.blob.size,
|
||||
}));
|
||||
return files;
|
||||
} else {
|
||||
throw new APIError('Not Found', 404, 'GitHub');
|
||||
}
|
||||
}
|
||||
|
||||
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 { repo_owner: owner, repo_name: name } = this;
|
||||
const { data } = await this.query({
|
||||
query: queries.unpublishedPrBranches,
|
||||
variables: { owner, name },
|
||||
});
|
||||
const { nodes } = data.repository.refs;
|
||||
if (nodes.length > 0) {
|
||||
const branches = [];
|
||||
nodes.forEach(({ associatedPullRequests }) => {
|
||||
associatedPullRequests.nodes.forEach(({ headRef }) => {
|
||||
branches.push({ ref: `${headRef.prefix}${headRef.name}` });
|
||||
});
|
||||
});
|
||||
return branches;
|
||||
} 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) {
|
||||
// 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 { repo_owner: headOwner, repo_name: headRepoName } = this;
|
||||
const { origin_repo_owner: baseOwner, origin_repo_name: 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,
|
||||
};
|
||||
return result;
|
||||
} else {
|
||||
throw new EditorialWorkflowError('content is not under editorial workflow', true);
|
||||
}
|
||||
}
|
||||
|
||||
getBranchQualifiedName(branch) {
|
||||
return `refs/heads/${branch}`;
|
||||
}
|
||||
|
||||
getBranchQuery(branch) {
|
||||
const { repo_owner: owner, repo_name: name } = this;
|
||||
|
||||
return {
|
||||
query: queries.branch,
|
||||
variables: {
|
||||
owner,
|
||||
name,
|
||||
qualifiedName: this.getBranchQualifiedName(branch),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getBranch(branch = this.branch) {
|
||||
// don't cache base branch to always get the latest data
|
||||
const fetchPolicy = branch === this.branch ? NO_CACHE : CACHE_FIRST;
|
||||
const { data } = await this.query({
|
||||
...this.getBranchQuery(branch),
|
||||
fetchPolicy,
|
||||
});
|
||||
return data.repository.branch;
|
||||
}
|
||||
|
||||
async patchRef(type, name, sha, opts = {}) {
|
||||
if (type !== 'heads') {
|
||||
return super.patchRef(type, name, sha, opts);
|
||||
}
|
||||
|
||||
const force = opts.force || false;
|
||||
|
||||
const branch = await this.getBranch(name);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.updateBranch,
|
||||
variables: {
|
||||
input: { oid: sha, refId: branch.id, force },
|
||||
},
|
||||
});
|
||||
return data.updateRef.branch;
|
||||
}
|
||||
|
||||
async deleteBranch(branchName) {
|
||||
const branch = await this.getBranch(branchName);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.deleteBranch,
|
||||
variables: {
|
||||
deleteRefInput: { refId: branch.id },
|
||||
},
|
||||
update: store => store.data.delete(defaultDataIdFromObject(branch)),
|
||||
});
|
||||
|
||||
return data.deleteRef;
|
||||
}
|
||||
|
||||
getPullRequestQuery(number) {
|
||||
const { origin_repo_owner: owner, origin_repo_name: name } = this;
|
||||
|
||||
return {
|
||||
query: queries.pullRequest,
|
||||
variables: { owner, name, number },
|
||||
};
|
||||
}
|
||||
|
||||
async getPullRequest(number) {
|
||||
const { data } = await this.query({
|
||||
...this.getPullRequestQuery(number),
|
||||
fetchPolicy: CACHE_FIRST,
|
||||
});
|
||||
|
||||
// https://developer.github.com/v4/enum/pullrequeststate/
|
||||
// GraphQL state: [CLOSED, MERGED, OPEN]
|
||||
// REST API state: [closed, open]
|
||||
const state = data.repository.pullRequest.state === 'OPEN' ? 'open' : 'closed';
|
||||
return {
|
||||
...data.repository.pullRequest,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
getPullRequestAndBranchQuery(branch, number) {
|
||||
const { repo_owner: owner, repo_name: name } = this;
|
||||
const { origin_repo_owner: origin_owner, origin_repo_name: origin_name } = this;
|
||||
|
||||
return {
|
||||
query: queries.pullRequestAndBranch,
|
||||
variables: {
|
||||
owner,
|
||||
name,
|
||||
origin_owner,
|
||||
origin_name,
|
||||
number,
|
||||
qualifiedName: this.getBranchQualifiedName(branch),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getPullRequestAndBranch(branch, number) {
|
||||
const { data } = await this.query({
|
||||
...this.getPullRequestAndBranchQuery(branch, number),
|
||||
fetchPolicy: CACHE_FIRST,
|
||||
});
|
||||
|
||||
const { repository, origin } = data;
|
||||
return { branch: repository.branch, pullRequest: origin.pullRequest };
|
||||
}
|
||||
|
||||
async openPR({ number }) {
|
||||
const pullRequest = await this.getPullRequest(number);
|
||||
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.reopenPullRequest,
|
||||
variables: {
|
||||
reopenPullRequestInput: { pullRequestId: pullRequest.id },
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { pullRequest } = mutationResult.reopenPullRequest;
|
||||
const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestQuery(pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return data.closePullRequest;
|
||||
}
|
||||
|
||||
async closePR({ number }) {
|
||||
const pullRequest = await this.getPullRequest(number);
|
||||
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.closePullRequest,
|
||||
variables: {
|
||||
closePullRequestInput: { pullRequestId: pullRequest.id },
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { pullRequest } = mutationResult.closePullRequest;
|
||||
const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestQuery(pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return data.closePullRequest;
|
||||
}
|
||||
|
||||
async deleteUnpublishedEntry(collectionName, slug) {
|
||||
try {
|
||||
const contentKey = this.generateContentKey(collectionName, slug);
|
||||
const branchName = this.generateBranchName(contentKey);
|
||||
|
||||
const metadata = await this.retrieveMetadata(contentKey);
|
||||
if (metadata && metadata.pr) {
|
||||
const { branch, pullRequest } = await this.getPullRequestAndBranch(
|
||||
branchName,
|
||||
metadata.pr.number,
|
||||
);
|
||||
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.closePullRequestAndDeleteBranch,
|
||||
variables: {
|
||||
deleteRefInput: { refId: branch.id },
|
||||
closePullRequestInput: { pullRequestId: pullRequest.id },
|
||||
},
|
||||
update: store => {
|
||||
store.data.delete(defaultDataIdFromObject(branch));
|
||||
store.data.delete(defaultDataIdFromObject(pullRequest));
|
||||
},
|
||||
});
|
||||
|
||||
return data.closePullRequest;
|
||||
} else {
|
||||
return await this.deleteBranch(branchName);
|
||||
}
|
||||
} catch (e) {
|
||||
const { graphQLErrors } = e;
|
||||
if (graphQLErrors && graphQLErrors.length > 0) {
|
||||
const branchNotFound = graphQLErrors.some(e => e.type === 'NOT_FOUND');
|
||||
if (branchNotFound) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async createPR(title, head) {
|
||||
const [repository, headReference] = await Promise.all([
|
||||
this.getRepository(this.origin_repo_owner, this.origin_repo_name),
|
||||
this.useOpenAuthoring ? `${(await this.user()).login}:${head}` : head,
|
||||
]);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.createPullRequest,
|
||||
variables: {
|
||||
createPullRequestInput: {
|
||||
baseRefName: this.branch,
|
||||
body: API.DEFAULT_PR_BODY,
|
||||
title,
|
||||
headRefName: headReference,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { pullRequest } = mutationResult.createPullRequest;
|
||||
const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestQuery(pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { pullRequest } = data.createPullRequest;
|
||||
return { ...pullRequest, head: { sha: pullRequest.headRefOid } };
|
||||
}
|
||||
|
||||
async createBranch(branchName, sha) {
|
||||
const repository = await this.getRepository(this.repo_owner, this.repo_name);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.createBranch,
|
||||
variables: {
|
||||
createRefInput: {
|
||||
name: this.getBranchQualifiedName(branchName),
|
||||
oid: sha,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { branch } = mutationResult.createRef;
|
||||
const branchData = { repository: { ...branch.repository, branch } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getBranchQuery(branchName),
|
||||
data: branchData,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { branch } = data.createRef;
|
||||
return branch;
|
||||
}
|
||||
|
||||
async createBranchAndPullRequest(branchName, sha, title) {
|
||||
const repository = await this.getRepository(this.origin_repo_owner, this.origin_repo_name);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.createBranchAndPullRequest,
|
||||
variables: {
|
||||
createRefInput: {
|
||||
name: this.getBranchQualifiedName(branchName),
|
||||
oid: sha,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
createPullRequestInput: {
|
||||
baseRefName: this.branch,
|
||||
body: API.DEFAULT_PR_BODY,
|
||||
title,
|
||||
headRefName: branchName,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { branch } = mutationResult.createRef;
|
||||
const { pullRequest } = mutationResult.createPullRequest;
|
||||
const branchData = { repository: { ...branch.repository, branch } };
|
||||
const pullRequestData = {
|
||||
repository: { ...pullRequest.repository, branch },
|
||||
origin: { ...pullRequest.repository, pullRequest },
|
||||
};
|
||||
|
||||
store.writeQuery({
|
||||
...this.getBranchQuery(branchName),
|
||||
data: branchData,
|
||||
});
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestAndBranchQuery(branchName, pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { pullRequest } = data.createPullRequest;
|
||||
return { ...pullRequest, head: { sha: pullRequest.headRefOid } };
|
||||
}
|
||||
|
||||
async getTree(sha) {
|
||||
if (!sha) {
|
||||
return Promise.resolve({ tree: [] });
|
||||
}
|
||||
|
||||
const { repo_owner: owner, repo_name: name } = this;
|
||||
const variables = {
|
||||
owner,
|
||||
name,
|
||||
sha,
|
||||
};
|
||||
|
||||
// sha can be either for a commit or a tree
|
||||
const [commitTree, tree] = await Promise.all([
|
||||
this.client.query({
|
||||
query: queries.commitTree,
|
||||
variables,
|
||||
fetchPolicy: CACHE_FIRST,
|
||||
}),
|
||||
this.client.query({
|
||||
query: queries.tree,
|
||||
variables,
|
||||
fetchPolicy: CACHE_FIRST,
|
||||
}),
|
||||
]);
|
||||
|
||||
let entries = null;
|
||||
|
||||
if (commitTree.data.repository.commit.tree) {
|
||||
entries = commitTree.data.repository.commit.tree.entries;
|
||||
}
|
||||
|
||||
if (tree.data.repository.tree.entries) {
|
||||
entries = tree.data.repository.tree.entries;
|
||||
}
|
||||
|
||||
if (entries) {
|
||||
return { tree: entries.map(e => ({ ...e, mode: TREE_ENTRY_TYPE_TO_MODE[e.type] })) };
|
||||
}
|
||||
|
||||
return Promise.reject('Could not get tree');
|
||||
}
|
||||
|
||||
async getPullRequestCommits(number) {
|
||||
const { origin_repo_owner: owner, origin_repo_name: name } = this;
|
||||
const { data } = await this.query({
|
||||
query: queries.pullRequestCommits,
|
||||
variables: { owner, name, number },
|
||||
});
|
||||
const { nodes } = data.repository.pullRequest.commits;
|
||||
const commits = nodes.map(n => ({ ...n.commit, parents: n.commit.parents.nodes }));
|
||||
|
||||
return commits;
|
||||
}
|
||||
|
||||
async getFileSha(path, branch) {
|
||||
const { repo_owner: owner, repo_name: name } = this;
|
||||
const { data } = await this.query({
|
||||
query: queries.fileSha,
|
||||
variables: { owner, name, expression: `${branch}:${path}` },
|
||||
});
|
||||
|
||||
return data.repository.file.sha;
|
||||
}
|
||||
}
|
1
packages/netlify-cms-backend-github/src/fragmentTypes.js
Normal file
1
packages/netlify-cms-backend-github/src/fragmentTypes.js
Normal file
File diff suppressed because one or more lines are too long
76
packages/netlify-cms-backend-github/src/fragments.js
Normal file
76
packages/netlify-cms-backend-github/src/fragments.js
Normal file
@ -0,0 +1,76 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const repository = gql`
|
||||
fragment RepositoryParts on Repository {
|
||||
id
|
||||
}
|
||||
`;
|
||||
|
||||
export const blobWithText = gql`
|
||||
fragment BlobWithTextParts on Blob {
|
||||
id
|
||||
text
|
||||
is_binary: isBinary
|
||||
}
|
||||
`;
|
||||
|
||||
export const object = gql`
|
||||
fragment ObjectParts on GitObject {
|
||||
id
|
||||
sha: oid
|
||||
}
|
||||
`;
|
||||
|
||||
export const branch = gql`
|
||||
fragment BranchParts on Ref {
|
||||
commit: target {
|
||||
...ObjectParts
|
||||
}
|
||||
id
|
||||
name
|
||||
repository {
|
||||
...RepositoryParts
|
||||
}
|
||||
}
|
||||
${object}
|
||||
${repository}
|
||||
`;
|
||||
|
||||
export const pullRequest = gql`
|
||||
fragment PullRequestParts on PullRequest {
|
||||
id
|
||||
baseRefName
|
||||
body
|
||||
headRefName
|
||||
headRefOid
|
||||
number
|
||||
state
|
||||
title
|
||||
merged_at: mergedAt
|
||||
repository {
|
||||
...RepositoryParts
|
||||
}
|
||||
}
|
||||
${repository}
|
||||
`;
|
||||
|
||||
export const treeEntry = gql`
|
||||
fragment TreeEntryParts on TreeEntry {
|
||||
path: name
|
||||
sha: oid
|
||||
type
|
||||
mode
|
||||
}
|
||||
`;
|
||||
|
||||
export const fileEntry = gql`
|
||||
fragment FileEntryParts on TreeEntry {
|
||||
name
|
||||
sha: oid
|
||||
blob: object {
|
||||
... on Blob {
|
||||
size: byteSize
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
@ -4,6 +4,7 @@ import semaphore from 'semaphore';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import API from './API';
|
||||
import GraphQLAPI from './GraphQLAPI';
|
||||
|
||||
const MAX_CONCURRENT_DOWNLOADS = 10;
|
||||
|
||||
@ -59,12 +60,13 @@ export default class GitHub {
|
||||
}
|
||||
this.originRepo = config.getIn(['backend', 'repo'], '');
|
||||
} else {
|
||||
this.repo = config.getIn(['backend', 'repo'], '');
|
||||
this.repo = this.originRepo = config.getIn(['backend', 'repo'], '');
|
||||
}
|
||||
this.branch = config.getIn(['backend', 'branch'], 'master').trim();
|
||||
this.api_root = config.getIn(['backend', 'api_root'], 'https://api.github.com');
|
||||
this.token = '';
|
||||
this.squash_merges = config.getIn(['backend', 'squash_merges']);
|
||||
this.use_graphql = config.getIn(['backend', 'use_graphql']);
|
||||
}
|
||||
|
||||
authComponent() {
|
||||
@ -155,11 +157,12 @@ export default class GitHub {
|
||||
|
||||
async authenticate(state) {
|
||||
this.token = state.token;
|
||||
this.api = new API({
|
||||
const apiCtor = this.use_graphql ? GraphQLAPI : API;
|
||||
this.api = new apiCtor({
|
||||
token: this.token,
|
||||
branch: this.branch,
|
||||
repo: this.repo,
|
||||
originRepo: this.useOpenAuthoring ? this.originRepo : undefined,
|
||||
originRepo: this.originRepo,
|
||||
api_root: this.api_root,
|
||||
squash_merges: this.squash_merges,
|
||||
useOpenAuthoring: this.useOpenAuthoring,
|
||||
@ -191,6 +194,9 @@ export default class GitHub {
|
||||
|
||||
logout() {
|
||||
this.token = null;
|
||||
if (typeof this.api.reset === 'function') {
|
||||
return this.api.reset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -243,7 +249,7 @@ export default class GitHub {
|
||||
|
||||
// Fetches a single entry.
|
||||
getEntry(collection, slug, path) {
|
||||
const repoURL = `/repos/${this.useOpenAuthoring ? this.originRepo : this.repo}`;
|
||||
const repoURL = `/repos/${this.originRepo}`;
|
||||
return this.api.readFile(path, null, { repoURL }).then(data => ({
|
||||
file: { path },
|
||||
data,
|
||||
|
109
packages/netlify-cms-backend-github/src/mutations.js
Normal file
109
packages/netlify-cms-backend-github/src/mutations.js
Normal file
@ -0,0 +1,109 @@
|
||||
import gql from 'graphql-tag';
|
||||
import * as fragments from './fragments';
|
||||
|
||||
// updateRef only works for branches at the moment
|
||||
export const updateBranch = gql`
|
||||
mutation updateRef($input: UpdateRefInput!) {
|
||||
updateRef(input: $input) {
|
||||
branch: ref {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
// deleteRef only works for branches at the moment
|
||||
const deleteRefMutationPart = `
|
||||
deleteRef(input: $deleteRefInput) {
|
||||
clientMutationId
|
||||
}
|
||||
`;
|
||||
export const deleteBranch = gql`
|
||||
mutation deleteRef($deleteRefInput: DeleteRefInput!) {
|
||||
${deleteRefMutationPart}
|
||||
}
|
||||
`;
|
||||
|
||||
const closePullRequestMutationPart = `
|
||||
closePullRequest(input: $closePullRequestInput) {
|
||||
clientMutationId
|
||||
pullRequest {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const closePullRequest = gql`
|
||||
mutation closePullRequestAndDeleteBranch($closePullRequestInput: ClosePullRequestInput!) {
|
||||
${closePullRequestMutationPart}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const closePullRequestAndDeleteBranch = gql`
|
||||
mutation closePullRequestAndDeleteBranch(
|
||||
$closePullRequestInput: ClosePullRequestInput!
|
||||
$deleteRefInput: DeleteRefInput!
|
||||
) {
|
||||
${closePullRequestMutationPart}
|
||||
${deleteRefMutationPart}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
const createPullRequestMutationPart = `
|
||||
createPullRequest(input: $createPullRequestInput) {
|
||||
clientMutationId
|
||||
pullRequest {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const createPullRequest = gql`
|
||||
mutation createPullRequest($createPullRequestInput: CreatePullRequestInput!) {
|
||||
${createPullRequestMutationPart}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const createBranch = gql`
|
||||
mutation createBranch($createRefInput: CreateRefInput!) {
|
||||
createRef(input: $createRefInput) {
|
||||
branch: ref {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
// createRef only works for branches at the moment
|
||||
export const createBranchAndPullRequest = gql`
|
||||
mutation createBranchAndPullRequest(
|
||||
$createRefInput: CreateRefInput!
|
||||
$createPullRequestInput: CreatePullRequestInput!
|
||||
) {
|
||||
createRef(input: $createRefInput) {
|
||||
branch: ref {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
${createPullRequestMutationPart}
|
||||
}
|
||||
${fragments.branch}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const reopenPullRequest = gql`
|
||||
mutation reopenPullRequest($reopenPullRequestInput: ReopenPullRequestInput!) {
|
||||
reopenPullRequest(input: $reopenPullRequestInput) {
|
||||
clientMutationId
|
||||
pullRequest {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
274
packages/netlify-cms-backend-github/src/queries.js
Normal file
274
packages/netlify-cms-backend-github/src/queries.js
Normal file
@ -0,0 +1,274 @@
|
||||
import gql from 'graphql-tag';
|
||||
import * as fragments from './fragments';
|
||||
|
||||
export const repoPermission = gql`
|
||||
query repoPermission($owner: String!, $name: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
viewerPermission
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
`;
|
||||
|
||||
export const user = gql`
|
||||
query {
|
||||
viewer {
|
||||
id
|
||||
avatar_url: avatarUrl
|
||||
name
|
||||
login
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const blob = gql`
|
||||
query blob($owner: String!, $name: String!, $expression: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
object(expression: $expression) {
|
||||
... on Blob {
|
||||
...BlobWithTextParts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.blobWithText}
|
||||
`;
|
||||
|
||||
export const 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`
|
||||
query statues($owner: String!, $name: String!, $sha: GitObjectID!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
object(oid: $sha) {
|
||||
...ObjectParts
|
||||
... on Commit {
|
||||
status {
|
||||
id
|
||||
contexts {
|
||||
id
|
||||
context
|
||||
state
|
||||
target_url: targetUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.object}
|
||||
`;
|
||||
|
||||
export const files = gql`
|
||||
query files($owner: String!, $name: String!, $expression: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
object(expression: $expression) {
|
||||
...ObjectParts
|
||||
... on Tree {
|
||||
entries {
|
||||
...FileEntryParts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.object}
|
||||
${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 = `
|
||||
branch: ref(qualifiedName: $qualifiedName) {
|
||||
...BranchParts
|
||||
}
|
||||
`;
|
||||
|
||||
export const branch = gql`
|
||||
query branch($owner: String!, $name: String!, $qualifiedName: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
${branchQueryPart}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
export const repository = gql`
|
||||
query repository($owner: String!, $name: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
`;
|
||||
|
||||
const pullRequestQueryPart = `
|
||||
pullRequest(number: $number) {
|
||||
...PullRequestParts
|
||||
}
|
||||
`;
|
||||
|
||||
export const pullRequest = gql`
|
||||
query pullRequest($owner: String!, $name: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
id
|
||||
${pullRequestQueryPart}
|
||||
}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const pullRequestAndBranch = gql`
|
||||
query pullRequestAndBranch($owner: String!, $name: String!, $origin_owner: String!, $origin_name: String!, $qualifiedName: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
${branchQueryPart}
|
||||
}
|
||||
origin: repository(owner: $origin_owner, name: $origin_name) {
|
||||
...RepositoryParts
|
||||
${pullRequestQueryPart}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.branch}
|
||||
${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 pullRequestCommits = gql`
|
||||
query pullRequestCommits($owner: String!, $name: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
pullRequest(number: $number) {
|
||||
id
|
||||
commits(last: 100) {
|
||||
nodes {
|
||||
id
|
||||
commit {
|
||||
...ObjectParts
|
||||
parents(last: 100) {
|
||||
nodes {
|
||||
...ObjectParts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.object}
|
||||
`;
|
||||
|
||||
export const fileSha = gql`
|
||||
query fileSha($owner: String!, $name: String!, $expression: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
file: object(expression: $expression) {
|
||||
...ObjectParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.object}
|
||||
`;
|
Reference in New Issue
Block a user