Fix: handle branch names conflicts (#3879)
This commit is contained in:
parent
0bdddfd43b
commit
da7fbe0638
@ -26,6 +26,7 @@ import {
|
|||||||
branchFromContentKey,
|
branchFromContentKey,
|
||||||
requestWithBackoff,
|
requestWithBackoff,
|
||||||
readFileMetadata,
|
readFileMetadata,
|
||||||
|
throwOnConflictingBranches,
|
||||||
} 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';
|
||||||
@ -250,10 +251,18 @@ export default class API {
|
|||||||
return response.ok;
|
return response.ok;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getBranch = async (branchName: string) => {
|
||||||
|
const branch: BitBucketBranch = await this.requestJSON(
|
||||||
|
`${this.repoURL}/refs/branches/${branchName}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return branch;
|
||||||
|
};
|
||||||
|
|
||||||
branchCommitSha = async (branch: string) => {
|
branchCommitSha = async (branch: string) => {
|
||||||
const {
|
const {
|
||||||
target: { hash: branchSha },
|
target: { hash: branchSha },
|
||||||
}: BitBucketBranch = await this.requestJSON(`${this.repoURL}/refs/branches/${branch}`);
|
}: BitBucketBranch = await this.getBranch(branch);
|
||||||
|
|
||||||
return branchSha;
|
return branchSha;
|
||||||
};
|
};
|
||||||
@ -442,11 +451,20 @@ export default class API {
|
|||||||
formData.append('parents', parentSha);
|
formData.append('parents', parentSha);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.request({
|
try {
|
||||||
url: `${this.repoURL}/src`,
|
await this.requestText({
|
||||||
method: 'POST',
|
url: `${this.repoURL}/src`,
|
||||||
body: formData,
|
method: 'POST',
|
||||||
});
|
body: formData,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const message = error.message || '';
|
||||||
|
// very descriptive message from Bitbucket
|
||||||
|
if (parentSha && message.includes('Something went wrong')) {
|
||||||
|
await throwOnConflictingBranches(branch, name => this.getBranch(name), API_NAME);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
requestWithBackoff,
|
requestWithBackoff,
|
||||||
unsentRequest,
|
unsentRequest,
|
||||||
ApiRequest,
|
ApiRequest,
|
||||||
|
throwOnConflictingBranches,
|
||||||
} from 'netlify-cms-lib-util';
|
} from 'netlify-cms-lib-util';
|
||||||
import { Octokit } from '@octokit/rest';
|
import { Octokit } from '@octokit/rest';
|
||||||
|
|
||||||
@ -1253,8 +1254,45 @@ export default class API {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
createBranch(branchName: string, sha: string) {
|
async backupBranch(branchName: string) {
|
||||||
return this.createRef('heads', branchName, sha);
|
try {
|
||||||
|
const existingBranch = await this.getBranch(branchName);
|
||||||
|
await this.createBranch(
|
||||||
|
existingBranch.name.replace(
|
||||||
|
new RegExp(`${CMS_BRANCH_PREFIX}/`),
|
||||||
|
`${CMS_BRANCH_PREFIX}_${Date.now()}/`,
|
||||||
|
),
|
||||||
|
existingBranch.commit.sha,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createBranch(branchName: string, sha: string) {
|
||||||
|
try {
|
||||||
|
const result = await this.createRef('heads', branchName, sha);
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
const message = String(e.message || '');
|
||||||
|
if (message === 'Reference update failed') {
|
||||||
|
await throwOnConflictingBranches(branchName, name => this.getBranch(name), API_NAME);
|
||||||
|
} else if (
|
||||||
|
message === 'Reference already exists' &&
|
||||||
|
branchName.startsWith(`${CMS_BRANCH_PREFIX}/`)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// this can happen if the branch wasn't deleted when the PR was merged
|
||||||
|
// we backup the existing branch just in case and patch it with the new sha
|
||||||
|
await this.backupBranch(branchName);
|
||||||
|
const result = await this.patchBranch(branchName, sha, { force: true });
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assertCmsBranch(branchName: string) {
|
assertCmsBranch(branchName: string) {
|
||||||
|
@ -14,8 +14,9 @@ import {
|
|||||||
DEFAULT_PR_BODY,
|
DEFAULT_PR_BODY,
|
||||||
branchFromContentKey,
|
branchFromContentKey,
|
||||||
CMS_BRANCH_PREFIX,
|
CMS_BRANCH_PREFIX,
|
||||||
|
throwOnConflictingBranches,
|
||||||
} from 'netlify-cms-lib-util';
|
} from 'netlify-cms-lib-util';
|
||||||
import { trim } from 'lodash';
|
import { trim, trimStart } from 'lodash';
|
||||||
import introspectionQueryResultData from './fragmentTypes';
|
import introspectionQueryResultData from './fragmentTypes';
|
||||||
import API, { Config, BlobArgs, API_NAME, PullRequestState, MOCK_PULL_REQUEST } from './API';
|
import API, { Config, BlobArgs, API_NAME, PullRequestState, MOCK_PULL_REQUEST } from './API';
|
||||||
import * as queries from './queries';
|
import * as queries from './queries';
|
||||||
@ -134,10 +135,44 @@ export default class GraphQLAPI extends API {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mutate(options: MutationOptions<OperationVariables>) {
|
async mutate(options: MutationOptions<OperationVariables>) {
|
||||||
return this.client.mutate(options).catch(error => {
|
try {
|
||||||
|
const result = await this.client.mutate(options);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const errors = error.graphQLErrors;
|
||||||
|
if (Array.isArray(errors) && errors.some(e => e.message === 'Ref cannot be created.')) {
|
||||||
|
const refName = options?.variables?.createRefInput?.name || '';
|
||||||
|
const branchName = trimStart(refName, 'refs/heads/');
|
||||||
|
if (branchName) {
|
||||||
|
await throwOnConflictingBranches(branchName, name => this.getBranch(name), API_NAME);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
Array.isArray(errors) &&
|
||||||
|
errors.some(e =>
|
||||||
|
new RegExp(
|
||||||
|
`A ref named "refs/heads/${CMS_BRANCH_PREFIX}/.+?" already exists in the repository.`,
|
||||||
|
).test(e.message),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const refName = options?.variables?.createRefInput?.name || '';
|
||||||
|
const sha = options?.variables?.createRefInput?.oid || '';
|
||||||
|
const branchName = trimStart(refName, 'refs/heads/');
|
||||||
|
if (branchName && branchName.startsWith(`${CMS_BRANCH_PREFIX}/`) && sha) {
|
||||||
|
try {
|
||||||
|
// this can happen if the branch wasn't deleted when the PR was merged
|
||||||
|
// we backup the existing branch just in case an re-run the mutation
|
||||||
|
await this.backupBranch(branchName);
|
||||||
|
await this.deleteBranch(branchName);
|
||||||
|
const result = await this.client.mutate(options);
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
throw new APIError(error.message, 500, 'GitHub');
|
throw new APIError(error.message, 500, 'GitHub');
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasWriteAccess() {
|
async hasWriteAccess() {
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
requestWithBackoff,
|
requestWithBackoff,
|
||||||
readFileMetadata,
|
readFileMetadata,
|
||||||
FetchError,
|
FetchError,
|
||||||
|
throwOnConflictingBranches,
|
||||||
} from 'netlify-cms-lib-util';
|
} from 'netlify-cms-lib-util';
|
||||||
import { Base64 } from 'js-base64';
|
import { Base64 } from 'js-base64';
|
||||||
import { Map } from 'immutable';
|
import { Map } from 'immutable';
|
||||||
@ -407,7 +408,14 @@ export default class API {
|
|||||||
toBase64 = (str: string) => Promise.resolve(Base64.encode(str));
|
toBase64 = (str: string) => Promise.resolve(Base64.encode(str));
|
||||||
fromBase64 = (str: string) => Base64.decode(str);
|
fromBase64 = (str: string) => Base64.decode(str);
|
||||||
|
|
||||||
uploadAndCommit(
|
async getBranch(branchName: string) {
|
||||||
|
const branch: GitLabBranch = await this.requestJSON(
|
||||||
|
`${this.repoURL}/repository/branches/${encodeURIComponent(branchName)}`,
|
||||||
|
);
|
||||||
|
return branch;
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadAndCommit(
|
||||||
items: CommitItem[],
|
items: CommitItem[],
|
||||||
{ commitMessage = '', branch = this.branch, newBranch = false },
|
{ commitMessage = '', branch = this.branch, newBranch = false },
|
||||||
) {
|
) {
|
||||||
@ -434,12 +442,21 @@ export default class API {
|
|||||||
commitParams.author_email = email;
|
commitParams.author_email = email;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.requestJSON({
|
try {
|
||||||
url: `${this.repoURL}/repository/commits`,
|
const result = await this.requestJSON({
|
||||||
method: 'POST',
|
url: `${this.repoURL}/repository/commits`,
|
||||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
method: 'POST',
|
||||||
body: JSON.stringify(commitParams),
|
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||||
});
|
body: JSON.stringify(commitParams),
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error.message || '';
|
||||||
|
if (newBranch && message.includes(`Could not update ${branch}`)) {
|
||||||
|
await throwOnConflictingBranches(branch, name => this.getBranch(name), API_NAME);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCommitItems(files: (Entry | AssetProxy)[], branch: string) {
|
async getCommitItems(files: (Entry | AssetProxy)[], branch: string) {
|
||||||
@ -781,9 +798,7 @@ export default class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getDefaultBranch() {
|
async getDefaultBranch() {
|
||||||
const branch: GitLabBranch = await this.requestJSON(
|
const branch: GitLabBranch = await this.getBranch(this.branch);
|
||||||
`${this.repoURL}/repository/branches/${encodeURIComponent(this.branch)}`,
|
|
||||||
);
|
|
||||||
return branch;
|
return branch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { asyncLock, AsyncLock } from './asyncLock';
|
import { asyncLock, AsyncLock } from './asyncLock';
|
||||||
import unsentRequest from './unsentRequest';
|
import unsentRequest from './unsentRequest';
|
||||||
|
import APIError from './APIError';
|
||||||
|
|
||||||
export interface FetchError extends Error {
|
export interface FetchError extends Error {
|
||||||
status: number;
|
status: number;
|
||||||
@ -174,3 +175,41 @@ export const getPreviewStatus = (
|
|||||||
return isPreviewContext(context, previewContext);
|
return isPreviewContext(context, previewContext);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getConflictingBranches = (branchName: string) => {
|
||||||
|
// for cms/posts/post-1, conflicting branches are cms/posts, cms
|
||||||
|
const parts = branchName.split('/');
|
||||||
|
parts.pop();
|
||||||
|
|
||||||
|
const conflictingBranches = parts.reduce((acc, _, index) => {
|
||||||
|
acc = [...acc, parts.slice(0, index + 1).join('/')];
|
||||||
|
return acc;
|
||||||
|
}, [] as string[]);
|
||||||
|
|
||||||
|
return conflictingBranches;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const throwOnConflictingBranches = async (
|
||||||
|
branchName: string,
|
||||||
|
getBranch: (name: string) => Promise<{ name: string }>,
|
||||||
|
apiName: string,
|
||||||
|
) => {
|
||||||
|
const possibleConflictingBranches = getConflictingBranches(branchName);
|
||||||
|
|
||||||
|
const conflictingBranches = await Promise.all(
|
||||||
|
possibleConflictingBranches.map(b =>
|
||||||
|
getBranch(b)
|
||||||
|
.then(b => b.name)
|
||||||
|
.catch(() => ''),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const conflictingBranch = conflictingBranches.filter(Boolean)[0];
|
||||||
|
if (conflictingBranch) {
|
||||||
|
throw new APIError(
|
||||||
|
`Failed creating branch '${branchName}' since there is already a branch named '${conflictingBranch}'. Please delete the '${conflictingBranch}' branch and try again`,
|
||||||
|
500,
|
||||||
|
apiName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -49,6 +49,7 @@ import {
|
|||||||
FetchError as FE,
|
FetchError as FE,
|
||||||
ApiRequest as AR,
|
ApiRequest as AR,
|
||||||
requestWithBackoff,
|
requestWithBackoff,
|
||||||
|
throwOnConflictingBranches,
|
||||||
} from './API';
|
} from './API';
|
||||||
import {
|
import {
|
||||||
CMS_BRANCH_PREFIX,
|
CMS_BRANCH_PREFIX,
|
||||||
@ -140,6 +141,7 @@ export const NetlifyCmsLibUtil = {
|
|||||||
requestWithBackoff,
|
requestWithBackoff,
|
||||||
allEntriesByFolder,
|
allEntriesByFolder,
|
||||||
AccessTokenError,
|
AccessTokenError,
|
||||||
|
throwOnConflictingBranches,
|
||||||
};
|
};
|
||||||
export {
|
export {
|
||||||
APIError,
|
APIError,
|
||||||
@ -195,4 +197,5 @@ export {
|
|||||||
requestWithBackoff,
|
requestWithBackoff,
|
||||||
allEntriesByFolder,
|
allEntriesByFolder,
|
||||||
AccessTokenError,
|
AccessTokenError,
|
||||||
|
throwOnConflictingBranches,
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user