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'))),
|
p => p.catch(err => Promise.reject(new APIError(err.message, null, 'BitBucket'))),
|
||||||
])(req);
|
])(req);
|
||||||
|
|
||||||
user = () => this.request('/user');
|
user = () => this.requestJSON('/user');
|
||||||
|
|
||||||
hasWriteAccess = async () => {
|
hasWriteAccess = async () => {
|
||||||
const response = await this.request(this.repoURL);
|
const response = await this.request(this.repoURL);
|
||||||
|
@ -72,8 +72,7 @@ export default class BitbucketBackend {
|
|||||||
api_root: this.api_root,
|
api_root: this.api_root,
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = await this.api.user();
|
const isCollab = await this.api.hasWriteAccess().catch(error => {
|
||||||
const isCollab = await this.api.hasWriteAccess(user).catch(error => {
|
|
||||||
error.message = stripIndent`
|
error.message = stripIndent`
|
||||||
Repo "${this.repo}" not found.
|
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.');
|
throw new Error('Your BitBucket user account does not have access to this repo.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = await this.api.user();
|
||||||
|
|
||||||
// Authorized 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() {
|
getRefreshedAccessToken() {
|
||||||
|
@ -174,6 +174,7 @@ export default class GitGateway {
|
|||||||
if (!(await this.api.hasWriteAccess())) {
|
if (!(await this.api.hasWriteAccess())) {
|
||||||
throw new Error("You don't have sufficient permissions to access Netlify CMS");
|
throw new Error("You don't have sufficient permissions to access Netlify CMS");
|
||||||
}
|
}
|
||||||
|
return { name: userData.name, login: userData.email };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
restoreUser() {
|
restoreUser() {
|
||||||
|
@ -66,7 +66,7 @@ export default class GitLab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Authorized user
|
// Authorized user
|
||||||
return { ...user, token: state.token };
|
return { ...user, login: user.username, token: state.token };
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { attempt, flatten, isError, trimStart, trimEnd, flow, partialRight, uniq } from 'lodash';
|
import { attempt, flatten, isError, trimStart, trimEnd, flow, partialRight, uniq } from 'lodash';
|
||||||
import { Map } from 'immutable';
|
|
||||||
import { stripIndent } from 'common-tags';
|
import { stripIndent } from 'common-tags';
|
||||||
import fuzzy from 'fuzzy';
|
import fuzzy from 'fuzzy';
|
||||||
import { resolveFormat } from 'Formats/formats';
|
import { resolveFormat } from 'Formats/formats';
|
||||||
@ -17,6 +16,7 @@ import {
|
|||||||
import { createEntry } from 'ValueObjects/Entry';
|
import { createEntry } from 'ValueObjects/Entry';
|
||||||
import { sanitizeSlug } from 'Lib/urlHelper';
|
import { sanitizeSlug } from 'Lib/urlHelper';
|
||||||
import { getBackend } from 'Lib/registry';
|
import { getBackend } from 'Lib/registry';
|
||||||
|
import { commitMessageFormatter } from 'Lib/backendHelper';
|
||||||
import {
|
import {
|
||||||
localForage,
|
localForage,
|
||||||
Cursor,
|
Cursor,
|
||||||
@ -100,34 +100,6 @@ function slugFormatter(collection, entryData, slugConfig) {
|
|||||||
return processSlug(template, new Date(), identifier, entryData);
|
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 =>
|
const extractSearchFields = searchFields => entry =>
|
||||||
searchFields.reduce((acc, field) => {
|
searchFields.reduce((acc, field) => {
|
||||||
let nestedFields = field.split('.');
|
let nestedFields = field.split('.');
|
||||||
@ -662,11 +634,19 @@ export class Backend {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const commitMessage = commitMessageFormatter(newEntry ? 'create' : 'update', config, {
|
const user = await this.currentUser();
|
||||||
collection,
|
const commitMessage = commitMessageFormatter(
|
||||||
slug: entryObj.slug,
|
newEntry ? 'create' : 'update',
|
||||||
path: entryObj.path,
|
config,
|
||||||
});
|
{
|
||||||
|
collection,
|
||||||
|
slug: entryObj.slug,
|
||||||
|
path: entryObj.path,
|
||||||
|
authorLogin: user.login,
|
||||||
|
authorName: user.name,
|
||||||
|
},
|
||||||
|
user.useOpenAuthoring,
|
||||||
|
);
|
||||||
|
|
||||||
const useWorkflow = config.getIn(['publish_mode']) === EDITORIAL_WORKFLOW;
|
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);
|
return this.implementation.persistEntry(entryObj, MediaFiles, opts).then(() => entryObj.slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
persistMedia(config, file) {
|
async persistMedia(config, file) {
|
||||||
|
const user = await this.currentUser();
|
||||||
const options = {
|
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);
|
return this.implementation.persistMedia(file, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteEntry(config, collection, slug) {
|
async deleteEntry(config, collection, slug) {
|
||||||
const path = selectEntryPath(collection, slug);
|
const path = selectEntryPath(collection, slug);
|
||||||
|
|
||||||
if (!selectAllowDeletion(collection)) {
|
if (!selectAllowDeletion(collection)) {
|
||||||
throw new Error('Not allowed to delete entries in this 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 });
|
return this.implementation.deleteFile(path, commitMessage, { collection, slug });
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteMedia(config, path) {
|
async deleteMedia(config, path) {
|
||||||
const commitMessage = commitMessageFormatter('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);
|
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}}”
|
delete: Delete {{collection}} “{{slug}}”
|
||||||
uploadMedia: Upload “{{path}}”
|
uploadMedia: Upload “{{path}}”
|
||||||
deleteMedia: Delete “{{path}}”
|
deleteMedia: Delete “{{path}}”
|
||||||
|
openAuthoring: '{{message}}'
|
||||||
```
|
```
|
||||||
|
|
||||||
Netlify CMS generates the following commit types:
|
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`
|
`delete` | An exising entry is deleted | `slug`, `path`, `collection`
|
||||||
`uploadMedia` | A media file is uploaded | `path`
|
`uploadMedia` | A media file is uploaded | `path`
|
||||||
`deleteMedia` | A media file is deleted | `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:
|
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
|
- `{{collection}}`: the name of the collection containing the entry changed
|
||||||
|
|
||||||
- `{{path}}`: the full path to the file 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