Skip to content
This repository has been archived by the owner on Dec 15, 2022. It is now read-only.

Feature: Add commit template support #1713

Merged
merged 14 commits into from Oct 5, 2018
3 changes: 2 additions & 1 deletion lib/get-repo-pipeline-manager.js
Expand Up @@ -210,7 +210,8 @@ export default function({confirm, notificationManager, workspace}) {
commitPipeline.addMiddleware('failed-to-commit-error', async (next, repository) => {
try {
const result = await next();
repository.setCommitMessage('');
const message = await repository.getCommitMessageFromTemplate();
repository.setCommitMessage(message || '');
destroyFilePatchPaneItems({onlyStaged: true}, workspace);
return result;
} catch (error) {
Expand Down
27 changes: 27 additions & 0 deletions lib/git-shell-out-strategy.js
Expand Up @@ -415,6 +415,33 @@ export default class GitShellOutStrategy {
return this.exec(args, {writeOperation: true});
}

async getCommitMessageFromTemplate() {
let templatePath = await this.getConfig('commit.template');
if (!templatePath) {
return '';
}

/**
* Get absolute path from git path
* https://git-scm.com/docs/git-config#git-config-pathname
*/
const homeDir = os.homedir();
const regex = new RegExp('^~([^/]*)/');
templatePath = templatePath.trim().replace(regex, (_, user) => {
return `${user ? path.join(path.dirname(homeDir), user) : homeDir}/`;
});

if (!path.isAbsolute(templatePath)) {
templatePath = path.join(this.workingDir, templatePath);
}

if (!await fileExists(templatePath)) {
return '';
}
const message = await fs.readFile(templatePath, {encoding: 'utf8'});
return message.trim();
}

unstageFiles(paths, commit = 'HEAD') {
if (paths.length === 0) { return Promise.resolve(null); }
const args = ['reset', commit, '--'].concat(paths.map(toGitPathSep));
Expand Down
14 changes: 14 additions & 0 deletions lib/models/repository-states/present.js
Expand Up @@ -41,6 +41,7 @@ export default class Present extends State {
this.operationStates = new OperationStates({didUpdate: this.didUpdate.bind(this)});

this.commitMessage = '';
this.setCommitMessageFromTemplate();

if (history) {
this.discardHistory.updateHistory(history);
Expand All @@ -55,6 +56,19 @@ export default class Present extends State {
return this.commitMessage;
}

async setCommitMessageFromTemplate() {
const message = await this.getCommitMessageFromTemplate();
if (!message) {
return;
}
this.setCommitMessage(message);
this.didUpdate();
}

async getCommitMessageFromTemplate() {
return await this.git().getCommitMessageFromTemplate();
}

getOperationStates() {
return this.operationStates;
}
Expand Down
4 changes: 4 additions & 0 deletions lib/models/repository-states/state.js
Expand Up @@ -373,6 +373,10 @@ export default class State {
return '';
}

getCommitMessageFromTemplate() {
return unsupportedOperationPromise(this, 'getCommitMessageFromTemplate');
}

// Cache

getCache() {
Expand Down
1 change: 1 addition & 0 deletions lib/models/repository.js
Expand Up @@ -358,6 +358,7 @@ const delegates = [

'setCommitMessage',
'getCommitMessage',
'getCommitMessageFromTemplate',
'getCache',
];

Expand Down
33 changes: 33 additions & 0 deletions test/controllers/commit-controller.test.js
Expand Up @@ -60,6 +60,9 @@ describe('CommitController', function() {
const repository1 = await buildRepository(workdirPath1);
const workdirPath2 = await cloneRepository('three-files');
const repository2 = await buildRepository(workdirPath2);
const workdirPath3 = await cloneRepository('commit-template');
const repository3 = await buildRepository(workdirPath3);
const templateCommitMessage = await repository3.git.getCommitMessageFromTemplate();

app = React.cloneElement(app, {repository: repository1});
const wrapper = shallow(app, {disableLifecycleMethods: true});
Expand All @@ -74,8 +77,23 @@ describe('CommitController', function() {

wrapper.setProps({repository: repository1});
assert.equal(wrapper.instance().getCommitMessage(), 'message 1');
wrapper.setProps({repository: repository3});
await assert.async.strictEqual(wrapper.instance().getCommitMessage(), templateCommitMessage);
});


describe('when commit.template config is set', function() {
it('populates the commit message with the template', async function() {
const workdirPath = await cloneRepository('commit-template');
const repository = await buildRepository(workdirPath);
const templateCommitMessage = await repository.git.getCommitMessageFromTemplate();
app = React.cloneElement(app, {repository});
const wrapper = shallow(app, {disableLifecycleMethods: true});
await assert.async.strictEqual(wrapper.instance().getCommitMessage(), templateCommitMessage);
});
});


describe('the passed commit message', function() {
let repository;

Expand Down Expand Up @@ -140,6 +158,21 @@ describe('CommitController', function() {
assert.strictEqual(repository.getCommitMessage(), '');
});

it('reload the commit messages from commit template', async function() {
const repoPath = await cloneRepository('commit-template');
const repo = await buildRepositoryWithPipeline(repoPath, {confirm, notificationManager, workspace});
const templateCommitMessage = await repo.git.getCommitMessageFromTemplate();
const commitStub = sinon.stub().callsFake((...args) => repo.commit(...args));
const app2 = React.cloneElement(app, {repository: repo, commit: commitStub});

await fs.writeFile(path.join(repoPath, 'a.txt'), 'some changes', {encoding: 'utf8'});
await repo.git.exec(['add', '.']);

const wrapper = shallow(app2, {disableLifecycleMethods: true});
await wrapper.instance().commit('some message');
assert.strictEqual(repo.getCommitMessage(), templateCommitMessage);
});

it('sets the verbatim flag when committing from the mini editor', async function() {
await fs.writeFile(path.join(workdirPath, 'a.txt'), 'some changes', {encoding: 'utf8'});
await repository.git.exec(['add', '.']);
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/repo-commit-template/a.txt
@@ -0,0 +1 @@
foo
1 change: 1 addition & 0 deletions test/fixtures/repo-commit-template/b.txt
@@ -0,0 +1 @@
bar
1 change: 1 addition & 0 deletions test/fixtures/repo-commit-template/c.txt
@@ -0,0 +1 @@
baz
1 change: 1 addition & 0 deletions test/fixtures/repo-commit-template/dot-git/HEAD
@@ -0,0 +1 @@
ref: refs/heads/master
9 changes: 9 additions & 0 deletions test/fixtures/repo-commit-template/dot-git/config
@@ -0,0 +1,9 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[commit]
template = gitmessage.txt
1 change: 1 addition & 0 deletions test/fixtures/repo-commit-template/dot-git/description
@@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.
Binary file added test/fixtures/repo-commit-template/dot-git/index
Binary file not shown.
6 changes: 6 additions & 0 deletions test/fixtures/repo-commit-template/dot-git/info/exclude
@@ -0,0 +1,6 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
2 changes: 2 additions & 0 deletions test/fixtures/repo-commit-template/dot-git/logs/HEAD
@@ -0,0 +1,2 @@
0000000000000000000000000000000000000000 d4d73b42443436ee23eebd43174645bb4b9eaf31 Gaurav Chikhale <gauravchl@users.noreply.github.com> 1538479109 +0530 commit (initial): Initial commit
d4d73b42443436ee23eebd43174645bb4b9eaf31 3faf8f2e3b6247c9d7e86d3849612cba95e94af4 Gaurav Chikhale <gauravchl@users.noreply.github.com> 1538479155 +0530 commit: Add git message
@@ -0,0 +1,2 @@
0000000000000000000000000000000000000000 d4d73b42443436ee23eebd43174645bb4b9eaf31 Gaurav Chikhale <gauravchl@users.noreply.github.com> 1538479109 +0530 commit (initial): Initial commit
d4d73b42443436ee23eebd43174645bb4b9eaf31 3faf8f2e3b6247c9d7e86d3849612cba95e94af4 Gaurav Chikhale <gauravchl@users.noreply.github.com> 1538479155 +0530 commit: Add git message
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
x��A
�0@Q�9�셒�I;��x�i�6���4��E���/ߥe Ze%3�d�&�#vQ���^N=�vD'INF��F��Z|�p���W�"�i���n��fM����̡�:6.-gP�C=X%-����~Q�_��������ϝI�
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
3faf8f2e3b6247c9d7e86d3849612cba95e94af4
1 change: 1 addition & 0 deletions test/fixtures/repo-commit-template/gitmessage.txt
@@ -0,0 +1 @@
baz
1 change: 1 addition & 0 deletions test/fixtures/repo-commit-template/subdir-1/a.txt
@@ -0,0 +1 @@
foo
1 change: 1 addition & 0 deletions test/fixtures/repo-commit-template/subdir-1/b.txt
@@ -0,0 +1 @@
bar
1 change: 1 addition & 0 deletions test/fixtures/repo-commit-template/subdir-1/c.txt
@@ -0,0 +1 @@
baz
26 changes: 26 additions & 0 deletions test/git-strategies.test.js
@@ -1,6 +1,7 @@
import fs from 'fs-extra';
import path from 'path';
import http from 'http';
import os from 'os';

import mkdirp from 'mkdirp';
import dedent from 'dedent-js';
Expand Down Expand Up @@ -85,6 +86,31 @@ import * as reporterProxy from '../lib/reporter-proxy';
});
});

describe('getCommitMessageFromTemplate', function() {
it('supports repository root path', async function() {
const workingDirPath = await cloneRepository('commit-template');
const git = createTestStrategy(workingDirPath);
const absTemplatePath = path.join(workingDirPath, 'gitmessage.txt');
const commitTemplate = fs.readFileSync(absTemplatePath, 'utf8').trim();
const message = await git.getCommitMessageFromTemplate();
assert.equal(message, commitTemplate);
});

it('supports relative path', async function() {
const workingDirPath = await cloneRepository('commit-template');
const git = createTestStrategy(workingDirPath);
const homeDir = os.homedir();
const absTemplatePath = path.join(homeDir, '.gitMessageSample.txt');
await fs.writeFile(absTemplatePath, 'some commit message', {encoding: 'utf8'});
await git.exec(['config', '--local', 'commit.template', '~/.gitMessageSample.txt']);

const message = await git.getCommitMessageFromTemplate();
assert.equal(message, 'some commit message');
fs.removeSync(absTemplatePath);
});
});


if (process.platform === 'win32') {
describe('getStatusBundle()', function() {
it('normalizes the path separator on Windows', async function() {
Expand Down
10 changes: 9 additions & 1 deletion test/helpers.js
Expand Up @@ -42,9 +42,17 @@ export async function cloneRepository(repoName = 'three-files') {
if (!cachedClonedRepos[repoName]) {
const cachedPath = temp.mkdirSync('git-fixture-cache-');
const git = new GitShellOutStrategy(cachedPath);
await git.clone(path.join(__dirname, 'fixtures', `repo-${repoName}`, 'dot-git'), {noLocal: true});
const repoPath = path.join(__dirname, 'fixtures', `repo-${repoName}`, 'dot-git');

let templatePath = '';
try {
templatePath = await git.exec(['config', '--file', repoPath + '/config', 'commit.template']);
} catch (err) {}

await git.clone(repoPath, {noLocal: true});
await git.exec(['config', '--local', 'core.autocrlf', 'false']);
await git.exec(['config', '--local', 'commit.gpgsign', 'false']);
await git.exec(['config', '--local', 'commit.template', templatePath]);
await git.exec(['config', '--local', 'user.email', FAKE_USER.email]);
await git.exec(['config', '--local', 'user.name', FAKE_USER.name]);
await git.exec(['checkout', '--', '.']); // discard \r in working directory
Expand Down