feat: enable specifying custom open authoring commit message (#2810)

This commit is contained in:
Erez Rokah 2019-10-28 21:29:47 +02:00 committed by Shawn Erquhart
parent f206e7e5a1
commit 2841ff9ffe
8 changed files with 294 additions and 45 deletions

View File

@ -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);

View File

@ -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() {

View File

@ -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() {

View File

@ -66,7 +66,7 @@ export default class GitLab {
}
// Authorized user
return { ...user, token: state.token };
return { ...user, login: user.username, token: state.token };
}
logout() {

View File

@ -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, {
collection,
slug: entryObj.slug,
path: entryObj.path,
});
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);
}

View File

@ -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.',
);
});
});

View 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;
};

View File

@ -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)