-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(backend-github): prepend collection name #2878
Changes from 15 commits
b1e8a58
0c04316
232de41
93b9b75
73e7ef5
19a650f
7f32f8e
6bc1071
32c5e99
8bbd5d6
c448609
24cd1d5
6b534e4
eca36df
7ae7e52
2a9a3bd
24ccfd9
3910cea
f7ce7b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,18 +12,19 @@ import { | |
differenceBy, | ||
trimStart, | ||
} from 'lodash'; | ||
import { map } from 'lodash/fp'; | ||
import { map, filter } from 'lodash/fp'; | ||
import { | ||
getAllResponses, | ||
APIError, | ||
EditorialWorkflowError, | ||
filterPromisesWith, | ||
flowAsync, | ||
localForage, | ||
onlySuccessfulPromises, | ||
resolvePromiseProperties, | ||
} from 'netlify-cms-lib-util'; | ||
|
||
const CMS_BRANCH_PREFIX = 'cms'; | ||
const CURRENT_METADATA_VERSION = '1'; | ||
|
||
const replace404WithEmptyArray = err => { | ||
if (err && err.status === 404) { | ||
|
@@ -148,9 +149,7 @@ export default class API { | |
|
||
generateContentKey(collectionName, slug) { | ||
if (!this.useOpenAuthoring) { | ||
// this doesn't use the collection, but we need to leave it that way for backwards | ||
// compatibility | ||
return slug; | ||
return `${collectionName}/${slug}`; | ||
} | ||
|
||
return `${this.repo}/${collectionName}/${slug}`; | ||
|
@@ -225,6 +224,29 @@ export default class API { | |
); | ||
} | ||
|
||
deleteMetadata(key) { | ||
if (!this._metadataSemaphore) { | ||
this._metadataSemaphore = semaphore(1); | ||
} | ||
return new Promise(resolve => | ||
this._metadataSemaphore.take(async () => { | ||
try { | ||
const branchData = await this.checkMetadataRef(); | ||
const file = { path: `${key}.json`, sha: null }; | ||
|
||
const changeTree = await this.updateTree(branchData.sha, [file]); | ||
const { sha } = await this.commit(`Deleting “${key}” metadata`, changeTree); | ||
await this.patchRef('meta', '_netlify_cms', sha); | ||
this._metadataSemaphore.leave(); | ||
resolve(); | ||
} catch (err) { | ||
this._metadataSemaphore.leave(); | ||
resolve(); | ||
} | ||
}), | ||
); | ||
} | ||
|
||
retrieveMetadata(key) { | ||
const cache = localForage.getItem(`gh.meta.${key}`); | ||
return cache.then(cached => { | ||
|
@@ -394,30 +416,19 @@ export default class API { | |
}); | ||
} | ||
|
||
getPRsForBranchName = ({ | ||
branchName, | ||
state, | ||
base = this.branch, | ||
repoURL = this.repoURL, | ||
usernameOfFork, | ||
} = {}) => { | ||
getPRsForBranchName = branchName => { | ||
// Get PRs with a `head` of `branchName`. Note that this is a | ||
// substring match, so we need to check that the `head.ref` of | ||
// at least one of the returned objects matches `branchName`. | ||
return this.requestAllPages(`${repoURL}/pulls`, { | ||
return this.requestAllPages(`${this.repoURL}/pulls`, { | ||
params: { | ||
head: usernameOfFork ? `${usernameOfFork}:${branchName}` : branchName, | ||
...(state ? { state } : {}), | ||
base, | ||
head: branchName, | ||
state: 'open', | ||
base: this.branch, | ||
}, | ||
}); | ||
}; | ||
|
||
branchHasPR = async ({ branchName, ...rest }) => { | ||
const prs = await this.getPRsForBranchName({ branchName, ...rest }); | ||
return prs.some(pr => pr.head.ref === branchName); | ||
}; | ||
|
||
getUpdatedOpenAuthoringMetadata = async (contentKey, { metadata: metadataArg } = {}) => { | ||
const metadata = metadataArg || (await this.retrieveMetadata(contentKey)) || {}; | ||
const { pr: prMetadata, status } = metadata; | ||
|
@@ -459,33 +470,82 @@ export default class API { | |
return metadata; | ||
}; | ||
|
||
async migrateToVersion1(branch, metaData) { | ||
// hard code key/branch generation logic to ignore future changes | ||
const oldContentKey = branch.ref.substring(`refs/heads/cms/`.length); | ||
const newContentKey = `${metaData.collection}/${oldContentKey}`; | ||
const newBranchName = `cms/${newContentKey}`; | ||
|
||
// create new branch and pull request in new format | ||
const newBranch = await this.createBranch(newBranchName, metaData.pr.head); | ||
const pr = await this.createPR(metaData.commitMessage, newBranchName); | ||
|
||
// store new metadata | ||
await this.storeMetadata(newContentKey, { | ||
...metaData, | ||
pr: { | ||
number: pr.number, | ||
head: pr.head.sha, | ||
}, | ||
branch: newBranchName, | ||
version: CURRENT_METADATA_VERSION, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be hard coded to '1' since we don't want it to change when someone updates e.g. let's say we have at some point
The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, I will remove it. |
||
}); | ||
|
||
// remove old data | ||
await this.closePR(metaData.pr); | ||
await this.deleteBranch(metaData.branch); | ||
await this.deleteMetadata(oldContentKey); | ||
|
||
return newBranch; | ||
} | ||
|
||
async migrateBranch(branch) { | ||
const metadata = await this.retrieveMetadata(this.contentKeyFromRef(branch.ref)); | ||
if (!metadata.version) { | ||
// migrate branch from cms/slug to cms/collection/slug | ||
branch = await this.migrateToVersion1(branch, metadata); | ||
} | ||
|
||
return branch; | ||
} | ||
|
||
async listUnpublishedBranches() { | ||
console.log( | ||
'%c Checking for Unpublished entries', | ||
'line-height: 30px;text-align: center;font-weight: bold', | ||
); | ||
const onlyBranchesWithOpenPRs = filterPromisesWith(({ ref }) => | ||
this.branchHasPR({ branchName: this.branchNameFromRef(ref), state: 'open' }), | ||
); | ||
const getUpdatedOpenAuthoringBranches = flow([ | ||
map(async branch => { | ||
const contentKey = this.contentKeyFromRef(branch.ref); | ||
const metadata = await this.getUpdatedOpenAuthoringMetadata(contentKey); | ||
// filter out removed entries | ||
if (!metadata) { | ||
return Promise.reject('Unpublished entry was removed'); | ||
} | ||
return branch; | ||
}), | ||
onlySuccessfulPromises, | ||
]); | ||
|
||
try { | ||
const branches = await this.request(`${this.repoURL}/git/refs/heads/cms`).catch( | ||
replace404WithEmptyArray, | ||
); | ||
const filterFunction = this.useOpenAuthoring | ||
? getUpdatedOpenAuthoringBranches | ||
: onlyBranchesWithOpenPRs; | ||
|
||
let filterFunction; | ||
if (this.useOpenAuthoring) { | ||
const getUpdatedOpenAuthoringBranches = flow([ | ||
map(async branch => { | ||
const contentKey = this.contentKeyFromRef(branch.ref); | ||
const metadata = await this.getUpdatedOpenAuthoringMetadata(contentKey); | ||
// filter out removed entries | ||
if (!metadata) { | ||
return Promise.reject('Unpublished entry was removed'); | ||
} | ||
return branch; | ||
}), | ||
onlySuccessfulPromises, | ||
]); | ||
filterFunction = getUpdatedOpenAuthoringBranches; | ||
} else { | ||
const prs = await this.getPRsForBranchName(CMS_BRANCH_PREFIX); | ||
const onlyBranchesWithOpenPRs = flowAsync([ | ||
filter(({ ref }) => prs.some(pr => pr.head.ref === this.branchNameFromRef(ref))), | ||
map(branch => this.migrateBranch(branch)), | ||
onlySuccessfulPromises, | ||
]); | ||
|
||
filterFunction = onlyBranchesWithOpenPRs; | ||
} | ||
|
||
return await filterFunction(branches); | ||
} catch (err) { | ||
console.log( | ||
|
@@ -621,6 +681,7 @@ export default class API { | |
files: mediaFilesList, | ||
}, | ||
timeStamp: new Date().toISOString(), | ||
version: CURRENT_METADATA_VERSION, | ||
}); | ||
} else { | ||
// Entry is already on editorial review workflow - just update metadata and commit to existing branch | ||
|
@@ -862,6 +923,7 @@ export default class API { | |
this.retrieveMetadata(contentKey) | ||
.then(metadata => (metadata && metadata.pr ? this.closePR(metadata.pr) : Promise.resolve())) | ||
.then(() => this.deleteBranch(branchName)) | ||
.then(() => this.deleteMetadata(contentKey)) | ||
// If the PR doesn't exist, then this has already been deleted - | ||
// deletion should be idempotent, so we can consider this a | ||
// success. | ||
|
@@ -881,6 +943,7 @@ export default class API { | |
const metadata = await this.retrieveMetadata(contentKey); | ||
await this.mergePR(metadata.pr, metadata.objects); | ||
await this.deleteBranch(branchName); | ||
await this.deleteMetadata(contentKey); | ||
|
||
return metadata; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,7 @@ export const branch = gql` | |
} | ||
id | ||
name | ||
prefix | ||
repository { | ||
...RepositoryParts | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import uuid from 'uuid/v4'; | ||
import { get } from 'lodash'; | ||
import { actions as notifActions } from 'redux-notifications'; | ||
import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; | ||
import { serializeValues } from 'Lib/serializeEntryValues'; | ||
|
@@ -242,6 +243,12 @@ export function loadUnpublishedEntry(collection, slug) { | |
return async (dispatch, getState) => { | ||
const state = getState(); | ||
const backend = currentBackend(state.config); | ||
const entriesLoaded = get(state.editorialWorkflow.toJS(), 'pages.ids', false); | ||
//run possible unpublishedEntries migration | ||
if (!entriesLoaded) { | ||
const response = await backend.unpublishedEntries(state.collections).catch(() => false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For published entry that have a draft entry, let's run the migration code first before loading the draft. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can happen when opening an entry in edit mode directly without going through the workflow tab first? Am I correct? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that's correct. |
||
response && dispatch(unpublishedEntriesLoaded(response.entries, response.pagination)); | ||
} | ||
|
||
dispatch(unpublishedEntryLoading(collection, slug)); | ||
|
||
|
@@ -294,8 +301,10 @@ export function loadUnpublishedEntry(collection, slug) { | |
export function loadUnpublishedEntries(collections) { | ||
return (dispatch, getState) => { | ||
const state = getState(); | ||
if (state.config.get('publish_mode') !== EDITORIAL_WORKFLOW) return; | ||
const backend = currentBackend(state.config); | ||
const entriesLoaded = get(state.editorialWorkflow.toJS(), 'pages.ids', false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The unpublished entries(workflow page) should now load only once. |
||
if (state.config.get('publish_mode') !== EDITORIAL_WORKFLOW || entriesLoaded) return; | ||
|
||
dispatch(unpublishedEntriesLoading()); | ||
backend | ||
.unpublishedEntries(collections) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!