feat: enable specifying custom open authoring commit message (#2810)
This commit is contained in:
parent
f206e7e5a1
commit
2841ff9ffe
@ -45,7 +45,7 @@ export default class API {
|
||||
p => p.catch(err => Promise.reject(new APIError(err.message, null, 'BitBucket'))),
|
||||
])(req);
|
||||
|
||||
user = () => this.request('/user');
|
||||
user = () => this.requestJSON('/user');
|
||||
|
||||
hasWriteAccess = async () => {
|
||||
const response = await this.request(this.repoURL);
|
||||
|
@ -72,8 +72,7 @@ export default class BitbucketBackend {
|
||||
api_root: this.api_root,
|
||||
});
|
||||
|
||||
const user = await this.api.user();
|
||||
const isCollab = await this.api.hasWriteAccess(user).catch(error => {
|
||||
const isCollab = await this.api.hasWriteAccess().catch(error => {
|
||||
error.message = stripIndent`
|
||||
Repo "${this.repo}" not found.
|
||||
|
||||
@ -89,8 +88,16 @@ export default class BitbucketBackend {
|
||||
throw new Error('Your BitBucket user account does not have access to this repo.');
|
||||
}
|
||||
|
||||
const user = await this.api.user();
|
||||
|
||||
// Authorized user
|
||||
return { ...user, token: state.token, refresh_token: state.refresh_token };
|
||||
return {
|
||||
...user,
|
||||
name: user.display_name,
|
||||
login: user.username,
|
||||
token: state.token,
|
||||
refresh_token: state.refresh_token,
|
||||
};
|
||||
}
|
||||
|
||||
getRefreshedAccessToken() {
|
||||
|
@ -174,6 +174,7 @@ export default class GitGateway {
|
||||
if (!(await this.api.hasWriteAccess())) {
|
||||
throw new Error("You don't have sufficient permissions to access Netlify CMS");
|
||||
}
|
||||
return { name: userData.name, login: userData.email };
|
||||
});
|
||||
}
|
||||
restoreUser() {
|
||||
|
@ -66,7 +66,7 @@ export default class GitLab {
|
||||
}
|
||||
|
||||
// Authorized user
|
||||
return { ...user, token: state.token };
|
||||
return { ...user, login: user.username, token: state.token };
|
||||
}
|
||||
|
||||
logout() {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { attempt, flatten, isError, trimStart, trimEnd, flow, partialRight, uniq } from 'lodash';
|
||||
import { Map } from 'immutable';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import fuzzy from 'fuzzy';
|
||||
import { resolveFormat } from 'Formats/formats';
|
||||
@ -17,6 +16,7 @@ import {
|
||||
import { createEntry } from 'ValueObjects/Entry';
|
||||
import { sanitizeSlug } from 'Lib/urlHelper';
|
||||
import { getBackend } from 'Lib/registry';
|
||||
import { commitMessageFormatter } from 'Lib/backendHelper';
|
||||
import {
|
||||
localForage,
|
||||
Cursor,
|
||||
@ -100,34 +100,6 @@ function slugFormatter(collection, entryData, slugConfig) {
|
||||
return processSlug(template, new Date(), identifier, entryData);
|
||||
}
|
||||
|
||||
const commitMessageTemplates = Map({
|
||||
create: 'Create {{collection}} “{{slug}}”',
|
||||
update: 'Update {{collection}} “{{slug}}”',
|
||||
delete: 'Delete {{collection}} “{{slug}}”',
|
||||
uploadMedia: 'Upload “{{path}}”',
|
||||
deleteMedia: 'Delete “{{path}}”',
|
||||
});
|
||||
|
||||
const commitMessageFormatter = (type, config, { slug, path, collection }) => {
|
||||
const templates = commitMessageTemplates.merge(
|
||||
config.getIn(['backend', 'commit_messages'], Map()),
|
||||
);
|
||||
const messageTemplate = templates.get(type);
|
||||
return messageTemplate.replace(/\{\{([^}]+)\}\}/g, (_, variable) => {
|
||||
switch (variable) {
|
||||
case 'slug':
|
||||
return slug;
|
||||
case 'path':
|
||||
return path;
|
||||
case 'collection':
|
||||
return collection.get('label_singular') || collection.get('label');
|
||||
default:
|
||||
console.warn(`Ignoring unknown variable “${variable}” in commit message template.`);
|
||||
return '';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const extractSearchFields = searchFields => entry =>
|
||||
searchFields.reduce((acc, field) => {
|
||||
let nestedFields = field.split('.');
|
||||
@ -662,11 +634,19 @@ export class Backend {
|
||||
};
|
||||
}
|
||||
|
||||
const commitMessage = commitMessageFormatter(newEntry ? 'create' : 'update', config, {
|
||||
const user = await this.currentUser();
|
||||
const commitMessage = commitMessageFormatter(
|
||||
newEntry ? 'create' : 'update',
|
||||
config,
|
||||
{
|
||||
collection,
|
||||
slug: entryObj.slug,
|
||||
path: entryObj.path,
|
||||
});
|
||||
authorLogin: user.login,
|
||||
authorName: user.name,
|
||||
},
|
||||
user.useOpenAuthoring,
|
||||
);
|
||||
|
||||
const useWorkflow = config.getIn(['publish_mode']) === EDITORIAL_WORKFLOW;
|
||||
|
||||
@ -689,26 +669,58 @@ export class Backend {
|
||||
return this.implementation.persistEntry(entryObj, MediaFiles, opts).then(() => entryObj.slug);
|
||||
}
|
||||
|
||||
persistMedia(config, file) {
|
||||
async persistMedia(config, file) {
|
||||
const user = await this.currentUser();
|
||||
const options = {
|
||||
commitMessage: commitMessageFormatter('uploadMedia', config, { path: file.path }),
|
||||
commitMessage: commitMessageFormatter(
|
||||
'uploadMedia',
|
||||
config,
|
||||
{
|
||||
path: file.path,
|
||||
authorLogin: user.login,
|
||||
authorName: user.name,
|
||||
},
|
||||
user.useOpenAuthoring,
|
||||
),
|
||||
};
|
||||
return this.implementation.persistMedia(file, options);
|
||||
}
|
||||
|
||||
deleteEntry(config, collection, slug) {
|
||||
async deleteEntry(config, collection, slug) {
|
||||
const path = selectEntryPath(collection, slug);
|
||||
|
||||
if (!selectAllowDeletion(collection)) {
|
||||
throw new Error('Not allowed to delete entries in this collection');
|
||||
}
|
||||
|
||||
const commitMessage = commitMessageFormatter('delete', config, { collection, slug, path });
|
||||
const user = await this.currentUser();
|
||||
const commitMessage = commitMessageFormatter(
|
||||
'delete',
|
||||
config,
|
||||
{
|
||||
collection,
|
||||
slug,
|
||||
path,
|
||||
authorLogin: user.login,
|
||||
authorName: user.name,
|
||||
},
|
||||
user.useOpenAuthoring,
|
||||
);
|
||||
return this.implementation.deleteFile(path, commitMessage, { collection, slug });
|
||||
}
|
||||
|
||||
deleteMedia(config, path) {
|
||||
const commitMessage = commitMessageFormatter('deleteMedia', config, { path });
|
||||
async deleteMedia(config, path) {
|
||||
const user = await this.currentUser();
|
||||
const commitMessage = commitMessageFormatter(
|
||||
'deleteMedia',
|
||||
config,
|
||||
{
|
||||
path,
|
||||
authorLogin: user.login,
|
||||
authorName: user.name,
|
||||
},
|
||||
user.useOpenAuthoring,
|
||||
);
|
||||
return this.implementation.deleteFile(path, commitMessage);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,164 @@
|
||||
import { Map } from 'immutable';
|
||||
import { commitMessageFormatter } from '../backendHelper';
|
||||
|
||||
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
describe('commitMessageFormatter', () => {
|
||||
const config = {
|
||||
getIn: jest.fn(),
|
||||
};
|
||||
|
||||
const collection = {
|
||||
get: jest.fn().mockReturnValue('Collection'),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return default commit message on create', () => {
|
||||
expect(
|
||||
commitMessageFormatter('create', config, { slug: 'doc-slug', path: 'file-path', collection }),
|
||||
).toEqual('Create Collection “doc-slug”');
|
||||
});
|
||||
|
||||
it('should return default commit message on create', () => {
|
||||
collection.get.mockReturnValueOnce(undefined);
|
||||
collection.get.mockReturnValueOnce('Collections');
|
||||
|
||||
expect(
|
||||
commitMessageFormatter('update', config, { slug: 'doc-slug', path: 'file-path', collection }),
|
||||
).toEqual('Update Collections “doc-slug”');
|
||||
});
|
||||
|
||||
it('should return default commit message on delete', () => {
|
||||
expect(
|
||||
commitMessageFormatter('delete', config, { slug: 'doc-slug', path: 'file-path', collection }),
|
||||
).toEqual('Delete Collection “doc-slug”');
|
||||
});
|
||||
|
||||
it('should return default commit message on uploadMedia', () => {
|
||||
expect(
|
||||
commitMessageFormatter('uploadMedia', config, {
|
||||
slug: 'doc-slug',
|
||||
path: 'file-path',
|
||||
collection,
|
||||
}),
|
||||
).toEqual('Upload “file-path”');
|
||||
});
|
||||
|
||||
it('should return default commit message on deleteMedia', () => {
|
||||
expect(
|
||||
commitMessageFormatter('deleteMedia', config, {
|
||||
slug: 'doc-slug',
|
||||
path: 'file-path',
|
||||
collection,
|
||||
}),
|
||||
).toEqual('Delete “file-path”');
|
||||
});
|
||||
|
||||
it('should log warning on unknown variable', () => {
|
||||
config.getIn.mockReturnValueOnce(
|
||||
Map({
|
||||
create: 'Create {{collection}} “{{slug}}” with "{{unknown variable}}"',
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
commitMessageFormatter('create', config, {
|
||||
slug: 'doc-slug',
|
||||
path: 'file-path',
|
||||
collection,
|
||||
}),
|
||||
).toEqual('Create Collection “doc-slug” with ""');
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
'Ignoring unknown variable “unknown variable” in commit message template.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return custom commit message on update', () => {
|
||||
config.getIn.mockReturnValueOnce(
|
||||
Map({
|
||||
update: 'Custom commit message',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
commitMessageFormatter('update', config, {
|
||||
slug: 'doc-slug',
|
||||
path: 'file-path',
|
||||
collection,
|
||||
}),
|
||||
).toEqual('Custom commit message');
|
||||
});
|
||||
|
||||
it('should return custom open authoring message', () => {
|
||||
config.getIn.mockReturnValueOnce(
|
||||
Map({
|
||||
openAuthoring: '{{author-login}} - {{author-name}}: {{message}}',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
commitMessageFormatter(
|
||||
'create',
|
||||
config,
|
||||
{
|
||||
slug: 'doc-slug',
|
||||
path: 'file-path',
|
||||
collection,
|
||||
authorLogin: 'user-login',
|
||||
authorName: 'Test User',
|
||||
},
|
||||
true,
|
||||
),
|
||||
).toEqual('user-login - Test User: Create Collection “doc-slug”');
|
||||
});
|
||||
|
||||
it('should use empty values if "authorLogin" and "authorName" are missing in open authoring message', () => {
|
||||
config.getIn.mockReturnValueOnce(
|
||||
Map({
|
||||
openAuthoring: '{{author-login}} - {{author-name}}: {{message}}',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
commitMessageFormatter(
|
||||
'create',
|
||||
config,
|
||||
{
|
||||
slug: 'doc-slug',
|
||||
path: 'file-path',
|
||||
collection,
|
||||
},
|
||||
true,
|
||||
),
|
||||
).toEqual(' - : Create Collection “doc-slug”');
|
||||
});
|
||||
|
||||
it('should log warning on unknown variable in open authoring template', () => {
|
||||
config.getIn.mockReturnValueOnce(
|
||||
Map({
|
||||
openAuthoring: '{{author-email}}: {{message}}',
|
||||
}),
|
||||
);
|
||||
|
||||
commitMessageFormatter(
|
||||
'create',
|
||||
config,
|
||||
{
|
||||
slug: 'doc-slug',
|
||||
path: 'file-path',
|
||||
collection,
|
||||
authorLogin: 'user-login',
|
||||
authorName: 'Test User',
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
'Ignoring unknown variable “author-email” in open authoring message template.',
|
||||
);
|
||||
});
|
||||
});
|
57
packages/netlify-cms-core/src/lib/backendHelper.js
Normal file
57
packages/netlify-cms-core/src/lib/backendHelper.js
Normal file
@ -0,0 +1,57 @@
|
||||
import { Map } from 'immutable';
|
||||
|
||||
const commitMessageTemplates = Map({
|
||||
create: 'Create {{collection}} “{{slug}}”',
|
||||
update: 'Update {{collection}} “{{slug}}”',
|
||||
delete: 'Delete {{collection}} “{{slug}}”',
|
||||
uploadMedia: 'Upload “{{path}}”',
|
||||
deleteMedia: 'Delete “{{path}}”',
|
||||
openAuthoring: '{{message}}',
|
||||
});
|
||||
|
||||
const variableRegex = /\{\{([^}]+)\}\}/g;
|
||||
|
||||
export const commitMessageFormatter = (
|
||||
type,
|
||||
config,
|
||||
{ slug, path, collection, authorLogin, authorName },
|
||||
isOpenAuthoring,
|
||||
) => {
|
||||
const templates = commitMessageTemplates.merge(
|
||||
config.getIn(['backend', 'commit_messages'], Map()),
|
||||
);
|
||||
|
||||
const commitMessage = templates.get(type).replace(variableRegex, (_, variable) => {
|
||||
switch (variable) {
|
||||
case 'slug':
|
||||
return slug;
|
||||
case 'path':
|
||||
return path;
|
||||
case 'collection':
|
||||
return collection.get('label_singular') || collection.get('label');
|
||||
default:
|
||||
console.warn(`Ignoring unknown variable “${variable}” in commit message template.`);
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
if (!isOpenAuthoring) {
|
||||
return commitMessage;
|
||||
}
|
||||
|
||||
const message = templates.get('openAuthoring').replace(variableRegex, (_, variable) => {
|
||||
switch (variable) {
|
||||
case 'message':
|
||||
return commitMessage;
|
||||
case 'author-login':
|
||||
return authorLogin || '';
|
||||
case 'author-name':
|
||||
return authorName || '';
|
||||
default:
|
||||
console.warn(`Ignoring unknown variable “${variable}” in open authoring message template.`);
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
return message;
|
||||
};
|
@ -240,6 +240,7 @@ backend:
|
||||
delete: Delete {{collection}} “{{slug}}”
|
||||
uploadMedia: Upload “{{path}}”
|
||||
deleteMedia: Delete “{{path}}”
|
||||
openAuthoring: '{{message}}'
|
||||
```
|
||||
|
||||
Netlify CMS generates the following commit types:
|
||||
@ -251,6 +252,7 @@ Commit type | When is it triggered? | Available template tags
|
||||
`delete` | An exising entry is deleted | `slug`, `path`, `collection`
|
||||
`uploadMedia` | A media file is uploaded | `path`
|
||||
`deleteMedia` | A media file is deleted | `path`
|
||||
`openAuthoring` | A commit is made via a forked repository | `message`, `author-login`, `author-name`
|
||||
|
||||
Template tags produce the following output:
|
||||
|
||||
@ -259,3 +261,9 @@ Template tags produce the following output:
|
||||
- `{{collection}}`: the name of the collection containing the entry changed
|
||||
|
||||
- `{{path}}`: the full path to the file changed
|
||||
|
||||
- `{{message}}`: the relevant message based on the current change (e.g. the `create` message when an entry is created)
|
||||
|
||||
- `{{author-login}}`: the login/username of the author
|
||||
|
||||
- `{{author-name}}`: the full name of the author (might be empty based on the user's profile)
|
||||
|
Loading…
x
Reference in New Issue
Block a user