diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index 7183c2d03bbedd1b0ab7bc9f69b0b32e655eb2da..8ea4a24c37ba3ffa1cde738e0226bbd2bd5bffad 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -547,8 +547,8 @@ const options = [
     stage: 'package',
     type: 'json',
     default: {
-      branchName:
-        '{{{branchPrefix}}}{{{depNameSanitized}}}-{{{newVersionMajor}}}.{{{newVersionMinor}}}.x',
+      branchTopic:
+        '{{{depNameSanitized}}}-{{{newVersionMajor}}}.{{{newVersionMinor}}}.x',
     },
     cli: false,
     mergeable: true,
@@ -563,9 +563,9 @@ const options = [
       recreateClosed: true,
       rebaseStalePrs: true,
       groupName: 'Pin Dependencies',
+      commitMessageAction: 'Pin',
       group: {
-        commitMessage: 'Pin Dependencies',
-        prTitle: '{{{groupName}}}',
+        commitMessageTopic: 'dependencies',
       },
     },
     cli: false,
@@ -681,14 +681,29 @@ const options = [
     name: 'branchName',
     description: 'Branch name template',
     type: 'string',
-    default: template('branchName'),
+    default: '{{{branchPrefix}}}{{{managerBranchPrefix}}}{{{branchTopic}}}',
+    cli: false,
+  },
+  {
+    name: 'managerBranchPrefix',
+    description: 'Branch manager prefix',
+    type: 'string',
+    default: '',
+    cli: false,
+  },
+  {
+    name: 'branchTopic',
+    description: 'Branch topic',
+    type: 'string',
+    default: '{{{depNameSanitized}}}-{{{newVersionMajor}}}.x',
     cli: false,
   },
   {
     name: 'commitMessage',
-    description: 'Commit message template',
+    description: 'Message to use for commit messages and pull request titles',
     type: 'string',
-    default: template('commitMessage'),
+    default:
+      '{{commitMessagePrefix}} {{commitMessageAction}} {{commitMessageTopic}} {{commitMessageExtra}} {{commitMessageSuffix}}',
     cli: false,
   },
   {
@@ -698,11 +713,42 @@ const options = [
     type: 'string',
     cli: false,
   },
+  {
+    name: 'commitMessagePrefix',
+    description:
+      'Prefix to add to start of commit messages and PR titles. Uses a semantic prefix if semanticCommits enabled',
+    type: 'string',
+    cli: false,
+  },
+  {
+    name: 'commitMessageAction',
+    description: 'Action verb to use in commit messages and PR titles',
+    type: 'string',
+    default: 'Update',
+    cli: false,
+  },
+  {
+    name: 'commitMessageTopic',
+    description: 'The upgrade topic/noun used in commit messages and PR titles',
+    type: 'string',
+    default: 'dependency {{depName}}',
+    cli: false,
+  },
+  {
+    name: 'commitMessageExtra',
+    description:
+      'Extra description used after the commit message topic - typically the version',
+    type: 'string',
+    default:
+      'to {{#unless isRange}}v{{/unless}}{{#if isMajor}}{{newVersionMajor}}{{else}}{{newVersion}}{{/if}}',
+    cli: false,
+  },
   {
     name: 'prTitle',
-    description: 'Pull Request title template',
+    description:
+      'Pull Request title template (deprecated). Now uses commitMessage.',
     type: 'string',
-    default: template('prTitle'),
+    default: null,
     cli: false,
   },
   {
@@ -730,9 +776,9 @@ const options = [
       enabled: false,
       recreateClosed: true,
       rebaseStalePrs: true,
-      branchName: template('branchName', 'lock-file-maintenance'),
-      commitMessage: template('commitMessage', 'lock-file-maintenance'),
-      prTitle: template('prTitle', 'lock-file-maintenance'),
+      branchTopic: 'lock-file-maintenance',
+      commitMessageAction: 'Refresh',
+      commitMessageTopic: 'lock file versions',
       prBody: template('prBody', 'lock-file-maintenance'),
       schedule: ['before 5am on monday'],
       groupName: null,
@@ -767,9 +813,8 @@ const options = [
     description: 'Config if groupName is enabled',
     type: 'json',
     default: {
-      branchName: template('branchName', 'group'),
-      commitMessage: template('commitMessage', 'group'),
-      prTitle: template('prTitle', 'group'),
+      branchTopic: '{{groupSlug}}',
+      commitMessageTopic: '{{groupName}}',
       prBody: template('prBody', 'group'),
     },
     cli: false,
@@ -833,6 +878,9 @@ const options = [
     default: {
       groupName: 'Node.js',
       lazyGrouping: false,
+      group: {
+        commitMessageTopic: 'Node.js',
+      },
     },
     mergeable: true,
     cli: false,
@@ -861,29 +909,32 @@ const options = [
     stage: 'repository',
     type: 'json',
     default: {
-      branchName: template('branchName', 'docker'),
-      commitMessage: template('commitMessage', 'docker'),
-      prTitle: template('prTitle', 'docker'),
+      managerBranchPrefix: 'docker-',
+      commitMessageTopic: '{{depName}} Docker tag',
       prBody: template('prBody', 'docker'),
       major: { enabled: false },
       digest: {
-        branchName: template('branchName', 'docker-digest'),
-        commitMessage: template('commitMessage', 'docker-digest'),
+        branchTopic: '{{{depNameSanitized}}}-{{{currentTag}}}',
+        commitMessageExtra: 'to {{newDigestShort}}',
         prBody: template('prBody', 'docker-digest'),
-        prTitle: template('prTitle', 'docker-digest'),
+        commitMessageTopic: '{{depName}}:{{currentTag}} Docker digest',
+        group: {
+          prBody: template('prBody', 'docker-digest-group'),
+          commitMessageTopic: '{{groupName}}',
+        },
       },
       pin: {
-        branchName: template('branchName', 'docker-pin'),
-        prTitle: template('prTitle', 'docker-pin'),
+        commitMessageExtra: '',
         prBody: template('prBody', 'docker-pin'),
-        groupName: 'Pin Docker Digests',
+        groupName: 'Docker digests',
         group: {
-          prTitle: template('prTitle', 'docker-pin-group'),
           prBody: template('prBody', 'docker-pin-group'),
+          commitMessageTopic: '{{groupName}}',
+          branchTopic: 'digests-pin',
         },
       },
       group: {
-        prTitle: template('prTitle', 'docker-group'),
+        commitMessageTopic: '{{groupName}} Docker tags',
         prBody: template('prBody', 'docker-group'),
       },
     },
diff --git a/lib/config/templates/default/branch-name.hbs b/lib/config/templates/default/branch-name.hbs
deleted file mode 100644
index 438c0092208c1170bce39b2fcdd5d2215b65838e..0000000000000000000000000000000000000000
--- a/lib/config/templates/default/branch-name.hbs
+++ /dev/null
@@ -1 +0,0 @@
-{{{branchPrefix}}}{{{depNameSanitized}}}-{{{newVersionMajor}}}.x
diff --git a/lib/config/templates/default/pr-title.hbs b/lib/config/templates/default/pr-title.hbs
deleted file mode 100644
index 540afe2a3e4f65687d50a1ed386416a58c618a4e..0000000000000000000000000000000000000000
--- a/lib/config/templates/default/pr-title.hbs
+++ /dev/null
@@ -1 +0,0 @@
-{{#if isPin}}Pin{{else}}{{#if isRollback}}Roll back{{else}}Update{{/if}}{{/if}} dependency {{{depName}}} to {{#if isRange}}{{{newVersion}}}{{else}}{{#if isMajor}}v{{{newVersionMajor}}}{{else}}v{{{newVersion}}}{{/if}}{{/if}}
diff --git a/lib/config/templates/docker-digest-group/pr-body.hbs b/lib/config/templates/docker-digest-group/pr-body.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..7e2c8cdf6809bafe9e954a21cc2ecf338b57c2ac
--- /dev/null
+++ b/lib/config/templates/docker-digest-group/pr-body.hbs
@@ -0,0 +1,37 @@
+This Pull Request updates Dockerfiles to the latest image digests. For details on Renovate's Docker support, please visit https://renovateapp.com/docs/language-support/docker
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule ("{{{schedule}}}"{{#if timezone}} in timezone `{{{timezone}}}`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}`{{{depName}}}`{{/if}}: `{{upgrade.newDigest}}`
+{{/each}}
+
+**Important**: Renovate will wait until you have merged this Pin request before creating PRs for any *upgrades*. If you do not wish to pin anything, please update your config accordingly instead of leaving this PR open.
+
+{{#if hasErrors}}
+
+---
+
+# Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   `{{error.depName}}`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+# Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   `{{warning.depName}}`: {{warning.message}}
+{{/each}}
+{{/if}}
diff --git a/lib/config/templates/docker-digest/branch-name.hbs b/lib/config/templates/docker-digest/branch-name.hbs
deleted file mode 100644
index 06de4198bc0df91a9c4cf3d0089a42cab443930c..0000000000000000000000000000000000000000
--- a/lib/config/templates/docker-digest/branch-name.hbs
+++ /dev/null
@@ -1 +0,0 @@
-{{{branchPrefix}}}docker-{{{depNameSanitized}}}-{{{currentTag}}}
diff --git a/lib/config/templates/docker-digest/commit-message.hbs b/lib/config/templates/docker-digest/commit-message.hbs
deleted file mode 100644
index 27a48b56432afd3061b16871453e2c321291c057..0000000000000000000000000000000000000000
--- a/lib/config/templates/docker-digest/commit-message.hbs
+++ /dev/null
@@ -1 +0,0 @@
-Update {{{depName}}}:{{{currentTag}}} digest
diff --git a/lib/config/templates/docker-digest/pr-title.hbs b/lib/config/templates/docker-digest/pr-title.hbs
deleted file mode 100644
index a1f68f7e628e5c8cfd6af0cc50511c2537b1eb02..0000000000000000000000000000000000000000
--- a/lib/config/templates/docker-digest/pr-title.hbs
+++ /dev/null
@@ -1 +0,0 @@
-Update {{{depName}}} Docker image {{{currentTag}}} digest ({{{newDigestShort}}})
diff --git a/lib/config/templates/docker-group/pr-title.hbs b/lib/config/templates/docker-group/pr-title.hbs
deleted file mode 100644
index aaebe61c28ddafd97aafa8933423ccaacbbd80bb..0000000000000000000000000000000000000000
--- a/lib/config/templates/docker-group/pr-title.hbs
+++ /dev/null
@@ -1 +0,0 @@
-Update Docker {{{groupName}}} digests
diff --git a/lib/config/templates/docker-pin-group/pr-title.hbs b/lib/config/templates/docker-pin-group/pr-title.hbs
deleted file mode 100644
index 719292a0f94f5ec7b406ad8335aa2f6d1e3324bd..0000000000000000000000000000000000000000
--- a/lib/config/templates/docker-pin-group/pr-title.hbs
+++ /dev/null
@@ -1 +0,0 @@
-Pin Docker digests
diff --git a/lib/config/templates/docker-pin/branch-name.hbs b/lib/config/templates/docker-pin/branch-name.hbs
deleted file mode 100644
index 48b92818bb5cf52b5ed6eca1d0d68755b45c29f1..0000000000000000000000000000000000000000
--- a/lib/config/templates/docker-pin/branch-name.hbs
+++ /dev/null
@@ -1 +0,0 @@
-{{{branchPrefix}}}docker-pin-{{{depNameSanitized}}}-{{{currentTag}}}
diff --git a/lib/config/templates/docker-pin/pr-title.hbs b/lib/config/templates/docker-pin/pr-title.hbs
deleted file mode 100644
index c6edcdec1d30d431f09932cde617eabdfe9d99ed..0000000000000000000000000000000000000000
--- a/lib/config/templates/docker-pin/pr-title.hbs
+++ /dev/null
@@ -1 +0,0 @@
-Pin Docker {{{depName}}}:{{{currentTag}}} image digest
diff --git a/lib/config/templates/docker/branch-name.hbs b/lib/config/templates/docker/branch-name.hbs
deleted file mode 100644
index a0e81a09ba4586f0fb83c3917613394653e7c3dc..0000000000000000000000000000000000000000
--- a/lib/config/templates/docker/branch-name.hbs
+++ /dev/null
@@ -1 +0,0 @@
-{{{branchPrefix}}}docker-{{{depNameSanitized}}}-{{{newVersionMajor}}}.x
diff --git a/lib/config/templates/docker/commit-message.hbs b/lib/config/templates/docker/commit-message.hbs
deleted file mode 100644
index 01b064eaa1b44f16e6df32cf02c0af8dcf782a1d..0000000000000000000000000000000000000000
--- a/lib/config/templates/docker/commit-message.hbs
+++ /dev/null
@@ -1 +0,0 @@
-Update {{{depName}}} to tag {{{newTag}}}
diff --git a/lib/config/templates/docker/pr-title.hbs b/lib/config/templates/docker/pr-title.hbs
deleted file mode 100644
index addd9cef9aa7f2fc6cd80eab0f367f99ef7f67ff..0000000000000000000000000000000000000000
--- a/lib/config/templates/docker/pr-title.hbs
+++ /dev/null
@@ -1 +0,0 @@
-Update {{{depName}}} Docker tag to {{#if isMajor}}v{{{newVersionMajor}}}{{else}}v{{{newTag}}}{{/if}}
diff --git a/lib/config/templates/group/branch-name.hbs b/lib/config/templates/group/branch-name.hbs
deleted file mode 100644
index 30d001b693b9dfd57876e44db8b77862eb0d456c..0000000000000000000000000000000000000000
--- a/lib/config/templates/group/branch-name.hbs
+++ /dev/null
@@ -1 +0,0 @@
-{{{branchPrefix}}}{{groupSlug}}
diff --git a/lib/config/templates/group/commit-message.hbs b/lib/config/templates/group/commit-message.hbs
deleted file mode 100644
index 16e6b7d8b97e42a914aa1b963c51790b7969a52d..0000000000000000000000000000000000000000
--- a/lib/config/templates/group/commit-message.hbs
+++ /dev/null
@@ -1 +0,0 @@
-Update {{{groupName}}} packages
diff --git a/lib/config/templates/group/pr-title.hbs b/lib/config/templates/group/pr-title.hbs
deleted file mode 100644
index 30c85d545a74de627cb269f52f8f84294d0163d3..0000000000000000000000000000000000000000
--- a/lib/config/templates/group/pr-title.hbs
+++ /dev/null
@@ -1 +0,0 @@
-Update {{{groupName}}} packages{{#if singleVersion}} to {{#unless isRange}}v{{/unless}}{{{singleVersion}}}{{/if}}
diff --git a/lib/config/templates/lock-file-maintenance/branch-name.hbs b/lib/config/templates/lock-file-maintenance/branch-name.hbs
deleted file mode 100644
index 4f4502faebb87292e66fa111833419d5843eb425..0000000000000000000000000000000000000000
--- a/lib/config/templates/lock-file-maintenance/branch-name.hbs
+++ /dev/null
@@ -1 +0,0 @@
-{{{branchPrefix}}}lock-file-maintenance
diff --git a/lib/config/templates/lock-file-maintenance/commit-message.hbs b/lib/config/templates/lock-file-maintenance/commit-message.hbs
deleted file mode 100644
index 4637bf90cd7b3c25dbed1169f4ea7ba03bce5636..0000000000000000000000000000000000000000
--- a/lib/config/templates/lock-file-maintenance/commit-message.hbs
+++ /dev/null
@@ -1 +0,0 @@
-Update lock file
diff --git a/lib/config/templates/lock-file-maintenance/pr-title.hbs b/lib/config/templates/lock-file-maintenance/pr-title.hbs
deleted file mode 100644
index c5d18441be75eb7ac8e107b1fa2a2e994ca1ee8a..0000000000000000000000000000000000000000
--- a/lib/config/templates/lock-file-maintenance/pr-title.hbs
+++ /dev/null
@@ -1 +0,0 @@
-Lock file maintenance
diff --git a/lib/config/templates/node/branch-name.hbs b/lib/config/templates/node/branch-name.hbs
deleted file mode 100644
index cadb37ed119d168dbcf680636b69d10171bd34b4..0000000000000000000000000000000000000000
--- a/lib/config/templates/node/branch-name.hbs
+++ /dev/null
@@ -1 +0,0 @@
-{{{branchPrefix}}}nodejs
diff --git a/lib/config/validation.js b/lib/config/validation.js
index 456c23d91c75325179dfb3570fdb5d22a5331b9d..3b7d31db5dec109b5cbfba964a2345e0f6d9ad59 100644
--- a/lib/config/validation.js
+++ b/lib/config/validation.js
@@ -23,6 +23,15 @@ async function validateConfig(config, isPreset, parentPath) {
   let errors = [];
   let warnings = [];
 
+  function getDeprecationMessage(option) {
+    const deprecatedOptions = {
+      branchName: `Direct editing of branchName is now deprecated. Please edit branchPrefix, managerBranchPrefix, or branchTopic instead`,
+      commitMessage: `Direct editing of commitMessage is now deprecated. Please edit commitMessage's subcomponents instead.`,
+      prTitle: `Direct editing of prTitle is now deprecated. Please edit commitMessage subcomponents instead as they will be passed through to prTitle.`,
+    };
+    return deprecatedOptions[option];
+  }
+
   function isIgnored(key) {
     const ignoredNodes = [
       'prBanner',
@@ -54,6 +63,12 @@ async function validateConfig(config, isPreset, parentPath) {
       !isIgnored(key) && // We need to ignore some reserved keys
       !isAFunction(val) // Ignore all functions
     ) {
+      if (getDeprecationMessage(key)) {
+        warnings.push({
+          depName: 'Deprecation Warning',
+          message: getDeprecationMessage(key),
+        });
+      }
       if (!optionTypes[key]) {
         errors.push({
           depName: 'Configuration Error',
diff --git a/lib/manager/npm/versions.js b/lib/manager/npm/versions.js
index 36abd86d73a624eba5fd52d2117274148de719ae..a6c32a4b20bc60d3f12726a5b8fda33467ed6a85 100644
--- a/lib/manager/npm/versions.js
+++ b/lib/manager/npm/versions.js
@@ -118,6 +118,7 @@ function determineUpgrades(npmDep, config) {
         newVersion: rollbackVersion,
         newVersionMajor: getMajor(rollbackVersion),
         semanticCommitType: 'fix',
+        commitMessageAction: 'Roll back',
         branchName:
           '{{{branchPrefix}}}rollback-{{{depNameSanitized}}}-{{{newVersionMajor}}}.x',
       };
diff --git a/lib/workers/branch/commit.js b/lib/workers/branch/commit.js
index 135b5b6ba1731310838bbd60a7ff6a6b9e4b2569..f0dc4941b6f5256068c16f44e902e94c492c2b4e 100644
--- a/lib/workers/branch/commit.js
+++ b/lib/workers/branch/commit.js
@@ -1,5 +1,3 @@
-const handlebars = require('handlebars');
-
 module.exports = {
   commitFilesToBranch,
 };
@@ -10,28 +8,12 @@ async function commitFilesToBranch(config) {
   );
   if (updatedFiles && updatedFiles.length) {
     logger.debug(`${updatedFiles.length} file(s) to commit`);
-    let commitMessage = handlebars.compile(config.commitMessage)(config);
-    if (config.semanticCommits) {
-      const splitMessage = commitMessage.split('\n');
-      splitMessage[0] = splitMessage[0].toLowerCase();
-      let semanticPrefix = config.semanticCommitType;
-      if (config.semanticCommitScope) {
-        semanticPrefix += `(${handlebars.compile(config.semanticCommitScope)(
-          config
-        )})`;
-      }
-      commitMessage = `${semanticPrefix}: ${splitMessage.join('\n')}`;
-    }
-    if (config.commitBody) {
-      commitMessage = `${commitMessage}\n\n${handlebars.compile(
-        config.commitBody
-      )(config)}`;
-    }
+
     // API will know whether to create new branch or not
     await platform.commitFilesToBranch(
       config.branchName,
       updatedFiles,
-      commitMessage,
+      config.commitMessage,
       config.parentBranch || config.baseBranch || undefined,
       config.gitAuthor,
       config.gitPrivateKey
diff --git a/lib/workers/repository/updates/branchify.js b/lib/workers/repository/updates/branchify.js
index 07e03980f413fac2f0b8b9d8c7f22fe45577a119..cc1a5102ee631877ce2c8be99e26f250efde331e 100644
--- a/lib/workers/repository/updates/branchify.js
+++ b/lib/workers/repository/updates/branchify.js
@@ -40,13 +40,13 @@ function branchifyUpgrades(config) {
         logger.debug(
           `Dependency ${upgrade.depName} is part of group ${upgrade.groupName}`
         );
-
         upgrade.groupSlug = slugify(upgrade.groupSlug || upgrade.groupName, {
           lower: true,
         });
-        upgrade.branchName = handlebars.compile(upgrade.group.branchName)(
-          upgrade
-        );
+        upgrade.branchTopic = upgrade.group.branchTopic || upgrade.branchTopic;
+        upgrade.branchName = handlebars.compile(
+          upgrade.group.branchName || upgrade.branchName
+        )(upgrade);
       } else {
         upgrade.branchName = handlebars.compile(upgrade.branchName)(upgrade);
       }
diff --git a/lib/workers/repository/updates/generate.js b/lib/workers/repository/updates/generate.js
index 8f94aae503fea3c42e5db0cae6bf3d3cbc646899..1b1f14607ac6fa752ce5ff9e7d9c538483449b61 100644
--- a/lib/workers/repository/updates/generate.js
+++ b/lib/workers/repository/updates/generate.js
@@ -1,4 +1,6 @@
 const handlebars = require('handlebars');
+const semver = require('semver');
+const { mergeChildConfig } = require('../../../config');
 
 function generateBranchConfig(branchUpgrades) {
   logger.debug(`generateBranchConfig()`);
@@ -27,39 +29,76 @@ function generateBranchConfig(branchUpgrades) {
   const useGroupSettings = hasGroupName && groupEligible;
   logger.debug(`useGroupSettings: ${useGroupSettings}`);
   for (const branchUpgrade of branchUpgrades) {
-    const upgrade = { ...branchUpgrade };
+    let upgrade = { ...branchUpgrade };
     if (useGroupSettings) {
       // Now overwrite original config with group config
-      Object.assign(upgrade, upgrade.group);
+      upgrade = mergeChildConfig(upgrade, upgrade.group);
     } else {
       delete upgrade.groupName;
     }
     // Delete group config regardless of whether it was applied
     delete upgrade.group;
     delete upgrade.lazyGrouping;
-    if (newVersion.length === 1) {
-      [upgrade.singleVersion] = newVersion;
-    } else {
+    if (newVersion.length > 1) {
+      delete upgrade.commitMessageExtra;
       upgrade.recreateClosed = true;
+    } else if (semver.valid(newVersion[0])) {
+      upgrade.isRange = false;
     }
     // Use templates to generate strings
     logger.debug(
       { branchName: upgrade.branchName, prTitle: upgrade.prTitle },
       'Compiling prTitle'
     );
-    upgrade.prTitle +=
-      upgrade.baseBranches && upgrade.baseBranches.length > 1
-        ? ' ({{baseBranch}})'
-        : '';
-    upgrade.prTitle = handlebars.compile(upgrade.prTitle)(upgrade);
-    if (upgrade.semanticCommits) {
+    upgrade.branchName = handlebars.compile(upgrade.branchName)(upgrade);
+    if (upgrade.semanticCommits && !upgrade.commitMessagePrefix) {
       logger.debug('Upgrade has semantic commits enabled');
       let semanticPrefix = upgrade.semanticCommitType;
       if (upgrade.semanticCommitScope) {
-        semanticPrefix += `(${upgrade.semanticCommitScope})`;
+        semanticPrefix += `(${handlebars.compile(upgrade.semanticCommitScope)(
+          upgrade
+        )})`;
       }
-      upgrade.prTitle = `${semanticPrefix}: ${upgrade.prTitle.toLowerCase()}`;
+      upgrade.commitMessagePrefix = `${semanticPrefix}: `;
+      upgrade.toLowerCase = true;
+    }
+    // Compile a few times in case there are nested templates
+    upgrade.commitMessage = handlebars.compile(upgrade.commitMessage || '')(
+      upgrade
+    );
+    upgrade.commitMessage = handlebars.compile(upgrade.commitMessage)(upgrade);
+    upgrade.commitMessage = handlebars.compile(upgrade.commitMessage)(upgrade);
+    upgrade.commitMessage = upgrade.commitMessage.trim(); // Trim exterior whitespace
+    upgrade.commitMessage = upgrade.commitMessage.replace(/\s+/g, ' '); // Trim extra whitespace inside string
+    if (upgrade.toLowerCase) {
+      // We only need to lowercvase the first line
+      const splitMessage = upgrade.commitMessage.split('\n');
+      splitMessage[0] = splitMessage[0].toLowerCase();
+      upgrade.commitMessage = splitMessage.join('\n');
+    }
+    if (upgrade.commitBody) {
+      upgrade.commitMessage = `${upgrade.commitMessage}\n\n${handlebars.compile(
+        upgrade.commitBody
+      )(upgrade)}`;
     }
+    if (upgrade.prTitle) {
+      upgrade.prTitle = handlebars.compile(upgrade.prTitle)(upgrade);
+      upgrade.prTitle = handlebars.compile(upgrade.prTitle)(upgrade);
+      upgrade.prTitle = handlebars
+        .compile(upgrade.prTitle)(upgrade)
+        .trim()
+        .replace(/\s+/g, ' ');
+      if (upgrade.toLowerCase) {
+        upgrade.prTitle = upgrade.prTitle.toLowerCase();
+      }
+    } else {
+      [upgrade.prTitle] = upgrade.commitMessage.split('\n');
+    }
+    upgrade.prTitle +=
+      upgrade.baseBranches && upgrade.baseBranches.length > 1
+        ? ' ({{baseBranch}})'
+        : '';
+
     // Compile again to allow for nested handlebars templates
     upgrade.prTitle = handlebars.compile(upgrade.prTitle)(upgrade);
     logger.debug(`${upgrade.branchName}, ${upgrade.prTitle}`);
diff --git a/test/config/__snapshots__/index.spec.js.snap b/test/config/__snapshots__/index.spec.js.snap
index 0234866bd5d4c6934b4e920781ad1b43e39748de..ababe95b4bfde1413a58b836a7351cf89e70d446 100644
--- a/test/config/__snapshots__/index.spec.js.snap
+++ b/test/config/__snapshots__/index.spec.js.snap
@@ -16,12 +16,17 @@ Object {
   "automergeType": "pr",
   "baseBranches": Array [],
   "bazel": Object {},
-  "branchName": "{{{branchPrefix}}}{{{depNameSanitized}}}-{{{newVersionMajor}}}.x",
+  "branchName": "{{{branchPrefix}}}{{{managerBranchPrefix}}}{{{branchTopic}}}",
   "branchPrefix": "renovate/",
+  "branchTopic": "{{{depNameSanitized}}}-{{{newVersionMajor}}}.x",
   "bumpVersion": null,
   "circleci": Object {},
   "commitBody": null,
-  "commitMessage": "Update dependency {{{depName}}} to {{#unless isRange}}v{{/unless}}{{{newVersion}}}",
+  "commitMessage": "{{commitMessagePrefix}} {{commitMessageAction}} {{commitMessageTopic}} {{commitMessageExtra}} {{commitMessageSuffix}}",
+  "commitMessageAction": "Update",
+  "commitMessageExtra": "to {{#unless isRange}}v{{/unless}}{{#if isMajor}}{{newVersionMajor}}{{else}}{{newVersion}}{{/if}}",
+  "commitMessagePrefix": null,
+  "commitMessageTopic": "dependency {{depName}}",
   "copyLocalLibs": false,
   "depTypeList": Array [],
   "dependencies": Object {},
@@ -33,33 +38,36 @@ Object {
   "devDependencies": Object {},
   "digest": Object {},
   "docker": Object {
-    "branchName": "{{{branchPrefix}}}docker-{{{depNameSanitized}}}-{{{newVersionMajor}}}.x",
-    "commitMessage": "Update {{{depName}}} to tag {{{newTag}}}",
+    "commitMessageTopic": "{{depName}} Docker tag",
     "digest": Object {
-      "branchName": "{{{branchPrefix}}}docker-{{{depNameSanitized}}}-{{{currentTag}}}",
-      "commitMessage": "Update {{{depName}}}:{{{currentTag}}} digest",
+      "branchTopic": "{{{depNameSanitized}}}-{{{currentTag}}}",
+      "commitMessageExtra": "to {{newDigestShort}}",
+      "commitMessageTopic": "{{depName}}:{{currentTag}} Docker digest",
+      "group": Object {
+        "commitMessageTopic": "{{groupName}}",
+        "prBody": "This Pull Request updates Dockerfiles to the latest image digests. For details on Renovate's Docker support, please visit https://renovateapp.com/docs/language-support/docker\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#each upgrades as |upgrade|}}\\n-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{{depName}}}\`{{/if}}: \`{{upgrade.newDigest}}\`\\n{{/each}}\\n\\n**Important**: Renovate will wait until you have merged this Pin request before creating PRs for any *upgrades*. If you do not wish to pin anything, please update your config accordingly instead of leaving this PR open.\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
+      },
       "prBody": "This Pull Request updates Docker base image \`{{{depName}}}:{{{currentTag}}}\` to the latest digest (\`{{{newDigest}}}\`). For details on Renovate's Docker support, please visit https://renovateapp.com/docs/language-support/docker\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
-      "prTitle": "Update {{{depName}}} Docker image {{{currentTag}}} digest ({{{newDigestShort}}})",
     },
     "group": Object {
+      "commitMessageTopic": "{{groupName}} Docker tags",
       "prBody": "This Pull Request updates Dockerfiles to use image digests.\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#each upgrades as |upgrade|}}\\n-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{{depName}}}\`{{/if}}: \`{{upgrade.newDigest}}\`\\n{{/each}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
-      "prTitle": "Update Docker {{{groupName}}} digests",
     },
     "major": Object {
       "enabled": false,
     },
+    "managerBranchPrefix": "docker-",
     "pin": Object {
-      "branchName": "{{{branchPrefix}}}docker-pin-{{{depNameSanitized}}}-{{{currentTag}}}",
+      "commitMessageExtra": "",
       "group": Object {
+        "branchTopic": "digests-pin",
+        "commitMessageTopic": "{{groupName}}",
         "prBody": "This Pull Request pins Dockerfiles to use image digests. For details on Renovate's Docker support, please visit https://renovateapp.com/docs/language-support/docker\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#each upgrades as |upgrade|}}\\n-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{{depName}}}\`{{/if}}: \`{{upgrade.newDigest}}\`\\n{{/each}}\\n\\n**Important**: Renovate will wait until you have merged this Pin request before creating PRs for any *upgrades*. If you do not wish to pin anything, please update your config accordingly instead of leaving this PR open.\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
-        "prTitle": "Pin Docker digests",
       },
-      "groupName": "Pin Docker Digests",
+      "groupName": "Docker digests",
       "prBody": "This Pull Request pins Docker base image \`{{{depName}}}:{{{currentTag}}}\` to use a digest (\`{{{newDigest}}}\`).\\nThis digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry. For details on Renovate's Docker support, please visit https://renovateapp.com/docs/language-support/docker\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n**Important**: Renovate will wait until you have merged this Pin request before creating PRs for any *upgrades*. If you do not wish to pin anything, please update your config accordingly instead of leaving this PR open.\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
-      "prTitle": "Pin Docker {{{depName}}}:{{{currentTag}}} image digest",
     },
     "prBody": "This Pull Request updates Docker base image \`{{{depName}}}\` from tag \`{{{currentTag}}}\` to new tag \`{{{newTag}}}\`. For details on Renovate's Docker support, please visit https://renovateapp.com/docs/language-support/docker\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
-    "prTitle": "Update {{{depName}}} Docker tag to {{#if isMajor}}v{{{newVersionMajor}}}{{else}}v{{{newTag}}}{{/if}}",
   },
   "docker-compose": Object {},
   "enabled": true,
@@ -84,10 +92,9 @@ Object {
   "gitAuthor": null,
   "gitPrivateKey": null,
   "group": Object {
-    "branchName": "{{{branchPrefix}}}{{groupSlug}}",
-    "commitMessage": "Update {{{groupName}}} packages",
+    "branchTopic": "{{groupSlug}}",
+    "commitMessageTopic": "{{groupName}}",
     "prBody": "This Pull Request renovates the package group \\"{{{groupName}}}\\".\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#each upgrades as |upgrade|}}\\n-   {{#if repositoryUrl}}[{{{upgrade.depName}}}]({{upgrade.repositoryUrl}}){{else}}\`{{{depName}}}\`{{/if}} (\`{{{depType}}}\`): from \`{{{upgrade.currentVersion}}}\` to \`{{{upgrade.newVersion}}}\`\\n{{/each}}\\n\\n{{#if hasReleaseNotes}}\\n# Release Notes\\n{{#each upgrades as |upgrade|}}\\n{{#if upgrade.hasReleaseNotes}}\\n<details>\\n<summary>{{upgrade.githubName}}</summary>\\n\\n{{#each upgrade.releases as |release|}}\\n{{#if release.releaseNotes}}\\n### [\`v{{{release.version}}}\`]({{{release.releaseNotes.url}}})\\n\\n{{{release.releaseNotes.body}}}\\n\\n---\\n\\n{{/if}}\\n{{/each}}\\n\\n</details>\\n{{/if}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasCommits}}\\n# Commits\\n\\n{{#each upgrades as |upgrade|}}\\n{{#if upgrade.releases.length}}\\n<details>\\n<summary>{{upgrade.githubName}}</summary>\\n{{#each upgrade.releases as |release|}}\\n{{#if release.hasCommits}}\\n\\n#### v{{{release.version}}}\\n{{#each release.commits as |commit|}}\\n-   [\`{{commit.shortSha}}\`]({{commit.url}}){{commit.message}}\\n{{/each}}\\n{{/if}}\\n{{/each}}\\n\\n</details>\\n\\n{{/if}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if isPin}}\\n**Important**: Renovate will wait until you have merged this Pin request before creating PRs for any *upgrades*. If you do not wish to pin anything, please update your config accordingly instead of leaving this PR open.\\n{{/if}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
-    "prTitle": "Update {{{groupName}}} packages{{#if singleVersion}} to {{#unless isRange}}v{{/unless}}{{{singleVersion}}}{{/if}}",
   },
   "groupName": null,
   "groupSlug": null,
@@ -101,12 +108,12 @@ Object {
   "labels": Array [],
   "lazyGrouping": true,
   "lockFileMaintenance": Object {
-    "branchName": "{{{branchPrefix}}}lock-file-maintenance",
-    "commitMessage": "Update lock file",
+    "branchTopic": "lock-file-maintenance",
+    "commitMessageAction": "Refresh",
+    "commitMessageTopic": "lock file versions",
     "enabled": false,
     "groupName": null,
     "prBody": "This Pull Request updates \`package.json\` lock files to use the latest dependency versions.\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
-    "prTitle": "Lock file maintenance",
     "rebaseStalePrs": true,
     "recreateClosed": true,
     "schedule": Array [
@@ -115,11 +122,15 @@ Object {
   },
   "logLevel": "error",
   "major": Object {},
+  "managerBranchPrefix": "",
   "meteor": Object {},
   "minor": Object {},
   "mirrorMode": false,
   "multipleMajorPrs": false,
   "node": Object {
+    "group": Object {
+      "commitMessageTopic": "Node.js",
+    },
     "groupName": "Node.js",
     "lazyGrouping": false,
   },
@@ -136,7 +147,7 @@ Object {
   "packageRules": Array [],
   "patch": Object {
     "automerge": true,
-    "branchName": "{{{branchPrefix}}}{{{depNameSanitized}}}-{{{newVersionMajor}}}.{{{newVersionMinor}}}.x",
+    "branchTopic": "{{{depNameSanitized}}}-{{{newVersionMajor}}}.{{{newVersionMinor}}}.x",
   },
   "pathRules": Array [],
   "paths": Array [],
@@ -144,9 +155,9 @@ Object {
     "enabled": false,
   },
   "pin": Object {
+    "commitMessageAction": "Pin",
     "group": Object {
-      "commitMessage": "Pin Dependencies",
-      "prTitle": "{{{groupName}}}",
+      "commitMessageTopic": "dependencies",
     },
     "groupName": "Pin Dependencies",
     "rebaseStalePrs": true,
@@ -162,7 +173,7 @@ Object {
   "prFooter": "This PR has been generated by [Renovate Bot](https://renovateapp.com).",
   "prHourlyLimit": 10,
   "prNotPendingHours": 25,
-  "prTitle": "{{#if isPin}}Pin{{else}}{{#if isRollback}}Roll back{{else}}Update{{/if}}{{/if}} dependency {{{depName}}} to {{#if isRange}}{{{newVersion}}}{{else}}{{#if isMajor}}v{{{newVersionMajor}}}{{else}}v{{{newVersion}}}{{/if}}{{/if}}",
+  "prTitle": null,
   "privateKey": null,
   "rebaseStalePrs": null,
   "recreateClosed": false,
@@ -224,12 +235,12 @@ Array [
 
 exports[`config/index mergeChildConfig(parentConfig, childConfig) merges 1`] = `
 Object {
-  "branchName": "{{{branchPrefix}}}lock-file-maintenance",
-  "commitMessage": "Update lock file",
+  "branchTopic": "lock-file-maintenance",
+  "commitMessageAction": "Refresh",
+  "commitMessageTopic": "lock file versions",
   "enabled": false,
   "groupName": null,
   "prBody": "This Pull Request updates \`package.json\` lock files to use the latest dependency versions.\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
-  "prTitle": "Lock file maintenance",
   "rebaseStalePrs": true,
   "recreateClosed": true,
   "schedule": Array [
diff --git a/test/config/__snapshots__/validation.spec.js.snap b/test/config/__snapshots__/validation.spec.js.snap
index 7ac7a5b8f5f86ea9305e9bf1f7d3af35e5e492ce..ec7d7a889a0c65479bf7ff469752d4739b490902 100644
--- a/test/config/__snapshots__/validation.spec.js.snap
+++ b/test/config/__snapshots__/validation.spec.js.snap
@@ -59,6 +59,15 @@ Array [
 
 exports[`config/validation validateConfig(config) ignore packageRule nesting validation for presets 1`] = `Array []`;
 
+exports[`config/validation validateConfig(config) returns deprecation warnings 1`] = `
+Array [
+  Object {
+    "depName": "Deprecation Warning",
+    "message": "Direct editing of prTitle is now deprecated. Please edit commitMessage subcomponents instead as they will be passed through to prTitle.",
+  },
+]
+`;
+
 exports[`config/validation validateConfig(config) returns nested errors 1`] = `
 Array [
   Object {
diff --git a/test/config/validation.spec.js b/test/config/validation.spec.js
index 883bcdf618a37f6f2a5e49c9a0d0cd1291c53f11..136f1ed8d58bbeeb2be0a21ed2d6e46b35295fcc 100644
--- a/test/config/validation.spec.js
+++ b/test/config/validation.spec.js
@@ -2,6 +2,14 @@ const configValidation = require('../../lib/config/validation.js');
 
 describe('config/validation', () => {
   describe('validateConfig(config)', () => {
+    it('returns deprecation warnings', async () => {
+      const config = {
+        prTitle: 'something',
+      };
+      const { warnings } = await configValidation.validateConfig(config);
+      expect(warnings).toHaveLength(1);
+      expect(warnings).toMatchSnapshot();
+    });
     it('returns nested errors', async () => {
       const config = {
         foo: 1,
diff --git a/test/manager/npm/__snapshots__/versions.spec.js.snap b/test/manager/npm/__snapshots__/versions.spec.js.snap
index ef2d8ea89f544a0e457acdeb4a050e2231d1569b..1a36cb0652d153fe768285c7dc13097c2f18e7a4 100644
--- a/test/manager/npm/__snapshots__/versions.spec.js.snap
+++ b/test/manager/npm/__snapshots__/versions.spec.js.snap
@@ -385,6 +385,7 @@ Array [
 exports[`manager/npm/versions .determineUpgrades(npmDep, config) should downgrade from missing versions 1`] = `
 Object {
   "branchName": "{{{branchPrefix}}}rollback-{{{depNameSanitized}}}-{{{newVersionMajor}}}.x",
+  "commitMessageAction": "Roll back",
   "isRollback": true,
   "newVersion": "1.16.0",
   "newVersionMajor": 1,
diff --git a/test/workers/branch/__snapshots__/commit.spec.js.snap b/test/workers/branch/__snapshots__/commit.spec.js.snap
index cb12267eed42c8bf2114412c1c1b9ab9aefc3552..52f0f758a5f499584846bec6c6f2ccf4e7fcab44 100644
--- a/test/workers/branch/__snapshots__/commit.spec.js.snap
+++ b/test/workers/branch/__snapshots__/commit.spec.js.snap
@@ -1,43 +1,5 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`workers/branch/automerge commitFilesToBranch adds commit message body 1`] = `
-Array [
-  Array [
-    "renovate/some-branch",
-    Array [
-      Object {
-        "contents": "some contents",
-        "name": "package.json",
-      },
-    ],
-    "some commit message
-
-[skip-ci]",
-    undefined,
-    null,
-    null,
-  ],
-]
-`;
-
-exports[`workers/branch/automerge commitFilesToBranch applies semantic prefix 1`] = `
-Array [
-  Array [
-    "renovate/some-branch",
-    Array [
-      Object {
-        "contents": "some contents",
-        "name": "package.json",
-      },
-    ],
-    "a(b): some commit message",
-    undefined,
-    null,
-    null,
-  ],
-]
-`;
-
 exports[`workers/branch/automerge commitFilesToBranch commits files 1`] = `
 Array [
   Array [
diff --git a/test/workers/branch/commit.spec.js b/test/workers/branch/commit.spec.js
index 85791623752ad8a93dc7ae7f54f4656f153765a2..f1455a57ae37fd6243bec1c23b0f2ec82aaa923d 100644
--- a/test/workers/branch/commit.spec.js
+++ b/test/workers/branch/commit.spec.js
@@ -30,38 +30,5 @@ describe('workers/branch/automerge', () => {
       expect(platform.commitFilesToBranch.mock.calls.length).toBe(1);
       expect(platform.commitFilesToBranch.mock.calls).toMatchSnapshot();
     });
-    it('applies semantic prefix', async () => {
-      config.updatedPackageFiles.push({
-        name: 'package.json',
-        contents: 'some contents',
-      });
-      config.semanticCommits = true;
-      await commitFilesToBranch(config);
-      expect(platform.commitFilesToBranch.mock.calls.length).toBe(1);
-      expect(platform.commitFilesToBranch.mock.calls).toMatchSnapshot();
-    });
-    it('lowercases only the first line when applying semantic prefix', async () => {
-      config.updatedPackageFiles.push({
-        name: 'package.json',
-        contents: 'some contents',
-      });
-      config.commitMessage = 'Foo\n\nBar';
-      config.semanticCommits = true;
-      await commitFilesToBranch(config);
-      expect(platform.commitFilesToBranch.mock.calls.length).toBe(1);
-      expect(platform.commitFilesToBranch.mock.calls[0][2]).toEqual(
-        'a(b): foo\n\nBar'
-      );
-    });
-    it('adds commit message body', async () => {
-      config.updatedPackageFiles.push({
-        name: 'package.json',
-        contents: 'some contents',
-      });
-      config.commitBody = '[skip-ci]';
-      await commitFilesToBranch(config);
-      expect(platform.commitFilesToBranch.mock.calls.length).toBe(1);
-      expect(platform.commitFilesToBranch.mock.calls).toMatchSnapshot();
-    });
   });
 });
diff --git a/test/workers/repository/updates/__snapshots__/generate.spec.js.snap b/test/workers/repository/updates/__snapshots__/generate.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..f631d86e7f6c1701b65c53061dc15bf9bb1aeb94
--- /dev/null
+++ b/test/workers/repository/updates/__snapshots__/generate.spec.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`workers/repository/updates/generate generateBranchConfig() adds commit message body 1`] = `
+"Update dependency some-dep to v1.2.0
+
+[skip-ci]"
+`;
+
+exports[`workers/repository/updates/generate generateBranchConfig() supports manual prTitle 1`] = `"upgrade some-dep"`;
diff --git a/test/workers/repository/updates/generate.spec.js b/test/workers/repository/updates/generate.spec.js
index a4dd11bc64d8c2544d77147b5baeffac85c20a0e..bc778190941e270adf12f3cbe2ace89656c5532e 100644
--- a/test/workers/repository/updates/generate.spec.js
+++ b/test/workers/repository/updates/generate.spec.js
@@ -1,3 +1,5 @@
+const defaultConfig = require('../../../../lib/config/defaults').getConfig();
+
 beforeEach(() => {
   jest.resetAllMocks();
 });
@@ -102,7 +104,6 @@ describe('workers/repository/updates/generate', () => {
       ];
       const res = generateBranchConfig(branch);
       expect(res.foo).toBe(2);
-      expect(res.singleVersion).toBeDefined();
       expect(res.groupName).toBeDefined();
     });
     it('groups multiple upgrades different version', () => {
@@ -141,14 +142,13 @@ describe('workers/repository/updates/generate', () => {
     it('uses semantic commits', () => {
       const branch = [
         {
+          ...defaultConfig,
           depName: 'some-dep',
-          groupName: 'some-group',
-          branchName: 'some-branch',
-          prTitle: 'some-title',
           semanticCommits: true,
           semanticCommitType: 'chore',
           semanticCommitScope: 'package',
           lazyGrouping: true,
+          newVersion: '1.2.0',
           foo: 1,
           group: {
             foo: 2,
@@ -156,7 +156,34 @@ describe('workers/repository/updates/generate', () => {
         },
       ];
       const res = generateBranchConfig(branch);
-      expect(res.prTitle).toEqual('chore(package): some-title');
+      expect(res.prTitle).toEqual(
+        'chore(package): update dependency some-dep to v1.2.0'
+      );
+    });
+    it('adds commit message body', () => {
+      const branch = [
+        {
+          ...defaultConfig,
+          depName: 'some-dep',
+          commitBody: '[skip-ci]',
+          newVersion: '1.2.0',
+        },
+      ];
+      const res = generateBranchConfig(branch);
+      expect(res.commitMessage).toMatchSnapshot();
+      expect(res.commitMessage.includes('\n')).toBe(true);
+    });
+    it('supports manual prTitle', () => {
+      const branch = [
+        {
+          ...defaultConfig,
+          depName: 'some-dep',
+          prTitle: 'Upgrade {{depName}}',
+          toLowerCase: true,
+        },
+      ];
+      const res = generateBranchConfig(branch);
+      expect(res.prTitle).toMatchSnapshot();
     });
     it('handles @types specially', () => {
       const branch = [
diff --git a/website/docs/_posts/2017-10-05-configuration-options.md b/website/docs/_posts/2017-10-05-configuration-options.md
index 78c8e87051500ac4225e358cf7876d9c225cec6b..b355815162748ef1a45f7e3ab0b8f4bd96ed66df 100644
--- a/website/docs/_posts/2017-10-05-configuration-options.md
+++ b/website/docs/_posts/2017-10-05-configuration-options.md
@@ -138,6 +138,17 @@ Prefix to be used for all branch names
 
 You can modify this field if you want to change the prefix used. For example if you want branches to be like `deps/eslint-4.x` instead of `renovate/eslint-4.x` then you set `branchPrefix` = `deps/`. Or if you wish to avoid forward slashes in branch names then you could use `renovate_` instead, for example.
 
+## branchTopic
+
+The main name/text that Renovate should use when creating a branch on your repository.
+
+| name    | value                                                                |
+| ------- | -------------------------------------------------------------------- |
+| type    | string                                                               |
+| default | {{{depNameSanitized}}}-{{{newVersionMajor}}}.{{{newVersionMinor}}}.x |
+
+This field is combined with `branchPrefix` and `managerBranchPrefix` to form the full `branchName`. `branchName` uniqueness is important for dependency update grouping or non-grouping so be cautious about ever editing this field manually.
+
 ## bumpVersion
 
 Bump the version in the package.json being updated
@@ -178,15 +189,59 @@ For example, To add `[skip ci]` to every commit you could configure:
 
 Commit message template
 
-| name    | value                                                                                          |
-| ------- | ---------------------------------------------------------------------------------------------- |
-| type    | handlebars template                                                                            |
-| default | {% raw %}{{semanticPrefix}}Update dependency {{depName}} to version {{newVersion}}{% endraw %} |
+| name    | value                                                                                                                                      |
+| ------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
+| type    | handlebars template                                                                                                                        |
+| default | {% raw %}{{commitMessagePrefix}} {{commitMessageAction}} {{commitMessageTopic}} {{commitMessageExtra}} {{commitMessageSuffix}}{% endraw %} |
 
 The commit message is less important than branchName so you may override it if you wish.
 
 Example commit message: "chore(deps): Update dependency eslint to version 4.0.1"
 
+## commitMessageAction
+
+Action verb to use in commit messages and PR titles.
+
+| name    | value  |
+| ------- | ------ |
+| type    | string |
+| default | Update |
+
+This is used to alter `commitMessage` and `prTitle` without needing to copy/paste the whole string. Actions may be like 'Update', 'Pin', 'Roll back', 'Refresh', etc.
+
+## commitMessageExtra
+
+Extra description used after the commit message topic - typically the version.
+
+| name    | value                                                                                                                  |
+| ------- | ---------------------------------------------------------------------------------------------------------------------- |
+| type    | string                                                                                                                 |
+| default | {% raw %}to {{#unless isRange}}v{{/unless}}{{#if isMajor}}{{newVersionMajor}}{{else}}{{newVersion}}{{/if}}{% endraw %} |
+
+This is used to alter `commitMessage` and `prTitle` without needing to copy/paste the whole string. The "extra" is usually an identifier of the new version, e.g. "to v1.3.2" or "to tag 9.2".
+
+## commitMessagePrefix
+
+Prefix to add to start of commit messages and PR titles. Uses a semantic prefix if semanticCommits enabled.
+
+| name    | value  |
+| ------- | ------ |
+| type    | string |
+| default | ''     |
+
+This is used to alter `commitMessage` and `prTitle` without needing to copy/paste the whole string. The "prefix" is usually an automatically applied semantic commit prefix, however it can also be statically configured.
+
+## commitMessageTopic
+
+The upgrade topic/noun used in commit messages and PR titles.
+
+| name    | value                  |
+| ------- | ---------------------- |
+| type    | string                 |
+| default | dependency {{depName}} |
+
+This is used to alter `commitMessage` and `prTitle` without needing to copy/paste the whole string. The "topic" is usually refers to the dependency being updated, e.g. "dependency react".
+
 ## copyLocalLibs
 
 | name    | value   |
@@ -550,6 +605,17 @@ Configuration specific for major dependency updates.
 
 Add to this object if you wish to define rules that apply only to major updates.
 
+## managerBranchPrefix
+
+Prefix to be added after `branchPrefix` for distinguishing between branches for different branches.
+
+| name    | value  |
+| ------- | ------ |
+| type    | string |
+| default | ''     |
+
+This value defaults to empty string, as historically no prefix was necessary for when Renovate was JS-only. Now - for example - we use `docker-` for Docker branches, so they may look like `renovate/docker-ubuntu-16.x`.
+
 ## meteor
 
 Configuration specific for meteor updates.
diff --git a/website/docs/_posts/2018-04-09-configuration-templates.md b/website/docs/_posts/2018-04-09-configuration-templates.md
new file mode 100644
index 0000000000000000000000000000000000000000..fb5a18570bf78e2ce099b2b8d556c074f9905840
--- /dev/null
+++ b/website/docs/_posts/2018-04-09-configuration-templates.md
@@ -0,0 +1,51 @@
+---
+date: 2018-04-09
+title: Config Template Editing
+categories:
+  - deep-dives
+description: How to edit Renovate's config templates
+type: Document
+order: 30
+---
+
+Tnis document describes how you can edit the branch names, commit messages, or PR titles and content.
+
+## Branch Name
+
+The branch name is very important for Renovate because it helps determine "grouping" of updates, and also makes it efficient when an existing PR needs to be updated with a newer version. Also, if you change branchPrefix and have some upgrades "ignored" (closed without merging) then you may see duplicate PRs opened with your new branch name.
+
+`branchName` has a default value of `{{{branchPrefix}}}{{{managerBranchPrefix}}}{{{branchTopic}}}`.
+
+The most common type of branch name you will see looks like this: `renovate/react-16.x`. In this example, the `branchPrefix` is the default `renovate/`, `managerBranchPrefix` is empty, and `branchTopic` is `react-16.x`.
+
+Most people can leave `branchPrefix` as `renovate/` however if you prefer to have no forward slashes then you might pick `renovate-` instead. Please note that the onboarding PR is fixed to use `renovate/configure` however.
+
+`managerBranchPrefix` is optional and by default is empty for all JavaScript dependencies. We use `docker-` for all Docker updates, so you might see branches like `renovate/docker-ubuntu-16.x`.
+
+`branchTopic` depends on the package manager and upgrade type, so you will see a lot of variety. It is also the one you might be most likely to want to change, but be careful and consider posting your config to https://github.com/renovateapp/config-help first.
+
+## Commit Message
+
+Renovate is designed to have just a single commit per branch, for merging convenience. As such, that commitMessage should reflect the contents of the branch and usually be the same as the PR Title.
+
+`commitMessage` has a default value of `{{commitMessagePrefix}} {{commitMessageAction}} {{commitMessageTopic}} {{commitMessageExtra}} {{commitMessageSuffix}}`, with the intention that you only edit some of those subcomponents.
+
+`commitMessagePrefix` is usually not necessary to configure directly, and is used by Renovate if it needs to add a prefix due to Semantic Commit conventions. Do not touch it unless you know what you're doing.
+
+`commitMessageAction` is usually just 1 word, e.g. 'Update', 'Pin', 'Refresh', etc. It's usually also not necessary to edit, although maybe you prefer 'Upgrade' instead of 'Update'?
+
+`commitMessageTopic` is usually 2-3 words aimed to identify _what_ is being updated. e.g. it might be `dependency react` or `Docker image ubuntu`. You may want to edit this, but if you think your idea/requirement is a good one then maybe you can propose it to the project or publish it as a preset config for others with similar requirements.
+
+`commitMessageExtra` usually refers to the version being updated to. e.g. `to v16` for a major upgrade, or `to v16.0.3` for a patch update. It may also be empty in some cases, e.g. if the action/topic is `Pin Docker digests`.
+
+`commitMessageSuffix` defaults to empty and is there for flexibility and future use. Maybe for `major` updates you always want the PR to end with `(MAJOR)`, for instance.
+
+`commitBody` is used if you wish to add multi-line commit messages, such as for the `Signed-off-by` fields, or adding `[skip-ci]`, etc. It is appended to the generated `commitMessage`, separated by a newline.
+
+## PR Title
+
+Because commit messages should usually match with the PR title, the PR title template now defaults to `null` and inherits whatever is configured for `commitMessage`. If you have a requirement where `prTitle` should be different from `commitMessage`, then please raise a feature request for discussion.
+
+## PR Body
+
+The PR Body is currently a little difficult to edit because of its size, however it should soon be redesigned like the other templates to allow more easier editing without needing to copy/paste the whole template.