Skip to content

Commit

Permalink
⚗️ Group similar commits together (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
fabienjuif authored Nov 3, 2018
1 parent 6a3b1b6 commit 06c8f3c
Show file tree
Hide file tree
Showing 14 changed files with 1,037 additions and 20 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,4 @@ typings/
.next

CHANGELOG.json
.vscode
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"devDependencies": {
"eslint": "^5.4.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-plugin-import": "^2.14.0",
"jest": "^23.5.0",
"lerna": "^2.11.0"
},
Expand Down
6 changes: 6 additions & 0 deletions packages/gitmoji-changelog-cli/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ async function main(options = {}) {
}
} catch (e) { /* ignore error */ }

if (options.groupSimilarCommits) {
logger.warn('⚗️ You are using a beta feature - may not working as expected')
logger.warn('Feel free to open issues or PR into gitmoji-changelog')
logger.warn('\t> https://github.com/frinyvonnick/gitmoji-changelog')
}

try {
const changelog = await generateChangelog(options)

Expand Down
1 change: 1 addition & 0 deletions packages/gitmoji-changelog-cli/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ yargs

.option('format', { default: 'markdown', desc: 'changelog format (markdown, json)' })
.option('output', { desc: 'output changelog file' })
.option('group-similar-commits', { desc: '[⚗️ - beta] try to group similar commits', default: false })
.option('author', { default: false, desc: 'add the author in changelog lines' })

.help('help')
Expand Down
1 change: 1 addition & 0 deletions packages/gitmoji-changelog-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"homepage": "https://github.com/frinyvonnick/gitmoji-changelog#readme",
"dependencies": {
"concat-stream": "^1.6.2",
"fast-levenshtein": "^2.0.6",
"get-pkg-repo": "^2.0.0",
"git-raw-commits": "^2.0.0",
"git-remote-origin-url": "^2.0.0",
Expand Down
31 changes: 24 additions & 7 deletions packages/gitmoji-changelog-core/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const { parseCommit } = require('./parser')
const { getPackageInfo, getRepoInfo } = require('./metaInfo')
const groupMapping = require('./groupMapping')
const logger = require('./logger')
const { groupSentencesByDistance } = require('./utils')

const gitSemverTagsAsync = promisify(gitSemverTags)

Expand Down Expand Up @@ -62,24 +63,39 @@ function filterCommits(commits) {
.filter(commit => commit.group !== 'useless')
}

async function generateVersion({ from, to, version }) {
const commits = filterCommits(await getCommits(from, to))
const lastCommitDate = getLastCommitDate(commits)
async function generateVersion(options) {
const {
from,
to,
version,
groupSimilarCommits,
} = options
let commits = filterCommits(await getCommits(from, to))

if (groupSimilarCommits) {
commits = groupSentencesByDistance(commits.map(commit => commit.message))
.map(indexes => indexes.map(index => commits[index]))
.map(([first, ...siblings]) => ({
...first,
siblings,
}))
}

return {
version,
date: version !== 'next' ? lastCommitDate : undefined,
date: version !== 'next' ? getLastCommitDate(commits) : undefined,
groups: makeGroups(commits),
}
}

async function generateVersions(tags) {
async function generateVersions({ tags, groupSimilarCommits }) {
let nextTag = ''

return Promise.all(
[...tags, '']
.map(tag => {
const params = {
groupSimilarCommits,
from: tag,
to: nextTag,
version: nextTag ? sanitizeVersion(nextTag) : 'next',
Expand All @@ -97,7 +113,7 @@ async function generateVersions(tags) {
}

async function generateChangelog(options = {}) {
const { mode, release } = options
const { mode, release, groupSimilarCommits } = options

const packageInfo = await getPackageInfo()

Expand All @@ -116,9 +132,10 @@ async function generateChangelog(options = {}) {
const lastTag = head(tags)

if (mode === 'init') {
changes = await generateVersions(tags)
changes = await generateVersions({ tags, groupSimilarCommits })
} else {
const lastChanges = await generateVersion({
groupSimilarCommits,
from: lastTag,
version,
})
Expand Down
37 changes: 32 additions & 5 deletions packages/gitmoji-changelog-core/src/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,26 @@ const sparklesCommit = {
hash: 'c40ee8669ba7ea5151adc2942fa8a7fc98d9e23a',
author: 'John Doe',
date: '2018-08-28T10:06:00+02:00',
subject: ':sparkles: Upgrade brand new feature',
subject: ':sparkles: Add a brand new feature',
body: 'Waouh this is awesome 2',
emoji: '✨',
emojiCode: 'sparkles',
group: 'added',
message: 'Upgrade brand new feature',
message: 'Add a brand new feature',
siblings: [],
}

const recycleCommit = {
hash: 'c40ee8669ba7ea5151adc2942fa8a7fc98d9e23c',
author: 'John Doe',
date: '2018-08-01T10:07:00+02:00',
subject: ':recycle: Upgrade brand new feature',
subject: ':recycle: Make some reworking on code',
body: 'Waouh this is awesome 3',
emoji: '♻️',
emojiCode: 'recycle',
group: 'changed',
message: 'Upgrade brand new feature',
message: 'Make some reworking on code',
siblings: [],
}

const secondRecycleCommit = {
Expand All @@ -49,6 +51,7 @@ const secondRecycleCommit = {
emojiCode: 'recycle',
group: 'changed',
message: 'Upgrade another brand new feature',
siblings: [],
}

const lipstickCommit = {
Expand All @@ -61,6 +64,7 @@ const lipstickCommit = {
emojiCode: 'lipstick',
group: 'changed',
message: 'Change graphics for a feature',
siblings: [],
}

const secondLipstickCommit = {
Expand All @@ -73,6 +77,7 @@ const secondLipstickCommit = {
emojiCode: 'lipstick',
group: 'changed',
message: 'Change more graphics for a feature',
siblings: [],
}

describe('changelog', () => {
Expand Down Expand Up @@ -111,7 +116,12 @@ describe('changelog', () => {
{
group: 'changed',
label: 'Changed',
commits: [lipstickCommit, secondLipstickCommit, recycleCommit, secondRecycleCommit],
commits: [
lipstickCommit,
secondLipstickCommit,
recycleCommit,
secondRecycleCommit,
],
},
],
},
Expand All @@ -129,6 +139,23 @@ describe('changelog', () => {
])
})

it('should group similar commits', async () => {
mockGroups()

gitSemverTags.mockImplementation(cb => cb(null, ['v1.0.0']))

const { changes } = await generateChangelog({ mode: 'init', groupSimilarCommits: true })

expect(changes[0].groups[0].commits).toEqual([
{
...lipstickCommit,
siblings: [secondLipstickCommit],
},
recycleCommit,
secondRecycleCommit,
])
})

it('should filter some commits out', async () => {
gitRawCommits.mockReset()
mockGroup([uselessCommit, lipstickCommit])
Expand Down
1 change: 1 addition & 0 deletions packages/gitmoji-changelog-core/src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function parseCommit(commit) {
emoji,
message,
group,
siblings: [],
body: body.join('\n'),
}
}
Expand Down
68 changes: 68 additions & 0 deletions packages/gitmoji-changelog-core/src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const { deburr } = require('lodash')
const levenshtein = require('fast-levenshtein')

// this is a magic number, this comes from various testing
// feel free to tweak it
const MAX_DISTANCE_PERCENT = 0.30

function groupSentencesByDistance(texts = []) {
const textsWithSortedWords = texts
.map(text => (
// to basic latin characters
deburr(text)
// replace specials characters by a filler
.replace(/[^\w\s]/gi, '▩')
// split words
.split(' ')
// little words are replaces by fillers
// this way -> we remove useless word like (a, of, etc)
// we keep the string length for the algorithm
.map(word => word.length < 4 ? Array.from({ length: word.length }).join('▩') : word)
// we sort words
.sort()
// we make them a sentence
.join('')
))

const alreadyProcessedWords = new Set()
const keyGroups = []

for (
let indexesFromStart = 0;
indexesFromStart < textsWithSortedWords.length;
indexesFromStart += 1
) {
if (!alreadyProcessedWords.has(indexesFromStart)) {
alreadyProcessedWords.add(indexesFromStart)
const group = [indexesFromStart]
keyGroups.push(group)

for (
let indexesFromNext = indexesFromStart + 1;
indexesFromNext < textsWithSortedWords.length;
indexesFromNext += 1
) {
const textA = textsWithSortedWords[indexesFromStart]
const textB = textsWithSortedWords[indexesFromNext]
const distance = levenshtein.get(textA, textB)
const textAverageLength = (textA.length + textB.length) / 2

if (
// close distance
(textAverageLength * MAX_DISTANCE_PERCENT) >= distance
// not already in a group
&& !alreadyProcessedWords.has(indexesFromNext)
) {
group.push(indexesFromNext)
alreadyProcessedWords.add(indexesFromNext)
}
}
}
}

return keyGroups
}

module.exports = {
groupSentencesByDistance,
}
18 changes: 18 additions & 0 deletions packages/gitmoji-changelog-core/src/utils.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const { groupSentencesByDistance } = require('./utils')

describe('utils', () => {
describe('groupSentencesByDistance', () => {
it('should group values together', () => {
const messages = [
'add levenshtein', // 0 - group1
'fix a bug about failures graph', // 1 - group2
'levenshtein', // 2 - group1
'fix levenshtein', // 3 - group1
'nothing to group with me',
'fix a graph of failures bug', // 5 - group2
]

expect(groupSentencesByDistance(messages)).toEqual([[0, 2, 3], [1, 5], [4]])
})
})
})
17 changes: 11 additions & 6 deletions packages/gitmoji-changelog-markdown/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,24 @@ function buildMarkdownFile(changelog = {}, options = {}) {
return markdownIncremental(changelog, options)
}

function toMarkdown({ meta, changes }, { author }) {
const template = fs.readFileSync(MARKDOWN_TEMPLATE, 'utf-8')

const compileTemplate = handlebars.compile(template)
function mapCommit(meta, options) {
const { author } = options

const changelog = update(changes, '[:].groups[:].commits[:]', commit => ({
return commit => ({
...commit,
hash: getShortHash(commit.hash, meta.repository),
subject: autolink(commit.subject, meta.repository),
message: autolink(commit.message, meta.repository),
body: autolink(commit.body, meta.repository),
author: author ? commit.author : null,
}))
siblings: commit.siblings.map(mapCommit(meta, options)),
})
}

function toMarkdown({ meta, changes }, options) {
const template = fs.readFileSync(MARKDOWN_TEMPLATE, 'utf-8')
const compileTemplate = handlebars.compile(template)
const changelog = update(changes, '[:].groups[:].commits[:]', mapCommit(meta, options))

return compileTemplate({ changelog })
}
Expand Down
4 changes: 4 additions & 0 deletions packages/gitmoji-changelog-markdown/src/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe('Markdown converter', () => {
emoji: '♻️',
message: 'Upgrade brand new feature',
body: 'Waouh this is awesome 3',
siblings: [],
},
],
},
Expand All @@ -61,6 +62,7 @@ describe('Markdown converter', () => {
emoji: '✨',
message: 'Upgrade brand new feature',
body: 'Waouh this is awesome 2',
siblings: [],
},
],
},
Expand Down Expand Up @@ -124,6 +126,7 @@ describe('Markdown converter', () => {
emoji: '♻️',
message: 'Upgrade brand new feature',
body: 'Waouh this is awesome 3',
siblings: [],
},
],
},
Expand Down Expand Up @@ -208,6 +211,7 @@ I am the last version
emoji: '♻️',
message: 'Upgrade brand new feature',
body: 'Waouh this is awesome 3',
siblings: [],
},
],
},
Expand Down
3 changes: 3 additions & 0 deletions packages/gitmoji-changelog-markdown/src/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

{{#each commits}}
- {{emoji}} {{message}} [{{hash}}]{{#if author}} (by {{author}}){{/if}}
{{#each siblings}}
* {{emoji}} {{message}} ({{hash}})
{{/each}}
{{/each}}

{{/each}}
Expand Down
Loading

0 comments on commit 06c8f3c

Please sign in to comment.