mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-19 17:26:34 +00:00
commit
58de7375a2
@ -289,7 +289,7 @@ rules:
|
|||||||
- error
|
- error
|
||||||
- anonymous: always
|
- anonymous: always
|
||||||
named: always
|
named: always
|
||||||
asyncArrow: never
|
asyncArrow: always
|
||||||
template-tag-spacing:
|
template-tag-spacing:
|
||||||
- error
|
- error
|
||||||
- always
|
- always
|
||||||
|
@ -119,7 +119,8 @@ exports.setProgressState = (state) => {
|
|||||||
const data = _.assign({}, state, {
|
const data = _.assign({}, state, {
|
||||||
percentage: _.isFinite(state.percentage)
|
percentage: _.isFinite(state.percentage)
|
||||||
? Math.floor(state.percentage)
|
? Math.floor(state.percentage)
|
||||||
: state.percentage,
|
// eslint-disable-next-line no-undefined
|
||||||
|
: undefined,
|
||||||
|
|
||||||
speed: _.attempt(() => {
|
speed: _.attempt(() => {
|
||||||
if (_.isFinite(state.speed)) {
|
if (_.isFinite(state.speed)) {
|
||||||
|
122
lib/gui/app/os/windows-network-drives.js
Normal file
122
lib/gui/app/os/windows-network-drives.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 resin.io
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const cp = require('child_process')
|
||||||
|
const _ = require('lodash')
|
||||||
|
const os = require('os')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Promisified child_process.execFile
|
||||||
|
* @function
|
||||||
|
*
|
||||||
|
* @param {String} file - command
|
||||||
|
* @param {String[]} args - arguments
|
||||||
|
* @param {Object} options - child_process.execFile options
|
||||||
|
*
|
||||||
|
* @returns {Promise<Object>} - { stdout, stderr }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* execFileAsync('ls', [ '.' ])
|
||||||
|
* .then(console.log);
|
||||||
|
*/
|
||||||
|
const execFileAsync = async (file, args, options) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
cp.execFile(
|
||||||
|
file,
|
||||||
|
args,
|
||||||
|
options,
|
||||||
|
(error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error)
|
||||||
|
} else {
|
||||||
|
resolve({ stdout, stderr })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary returns a Map of drive letter -> network locations on Windows
|
||||||
|
* @function
|
||||||
|
*
|
||||||
|
* @returns {Promise<Map<String, String>>} - 'Z:' -> '\\\\192.168.0.1\\Public'
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* getWindowsNetworkDrives()
|
||||||
|
* .then(console.log);
|
||||||
|
*/
|
||||||
|
const getWindowsNetworkDrives = async () => {
|
||||||
|
const result = await execFileAsync(
|
||||||
|
'wmic',
|
||||||
|
[ 'path', 'Win32_LogicalDisk', 'Where', 'DriveType="4"', 'get', 'DeviceID,ProviderName' ],
|
||||||
|
{ windowsHide: true, windowsVerbatimArguments: true }
|
||||||
|
)
|
||||||
|
const couples = _.chain(result.stdout)
|
||||||
|
.split('\n')
|
||||||
|
|
||||||
|
// Remove header line
|
||||||
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
.slice(1)
|
||||||
|
|
||||||
|
// Remove extra spaces / tabs / carriage returns
|
||||||
|
.invokeMap(String.prototype.trim)
|
||||||
|
|
||||||
|
// Filter out empty lines
|
||||||
|
.compact()
|
||||||
|
.map((str) => {
|
||||||
|
const colonPosition = str.indexOf(':')
|
||||||
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
if (colonPosition === -1) {
|
||||||
|
throw new Error(`Can't parse wmic output: ${result.stdout}`)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
return [ str.slice(0, colonPosition + 1), _.trim(str.slice(colonPosition + 1)) ]
|
||||||
|
})
|
||||||
|
.value()
|
||||||
|
return new Map(couples)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Replaces network drive letter with network drive location in the provided filePath on Windows
|
||||||
|
* @function
|
||||||
|
*
|
||||||
|
* @param {String} filePath - file path
|
||||||
|
*
|
||||||
|
* @returns {String} - updated file path
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* replaceWindowsNetworkDriveLetter('Z:\\some-file')
|
||||||
|
* .then(console.log);
|
||||||
|
*/
|
||||||
|
exports.replaceWindowsNetworkDriveLetter = async (filePath) => {
|
||||||
|
let result = filePath
|
||||||
|
if (os.platform() === 'win32') {
|
||||||
|
const matches = /^([A-Z]+:)\\(.*)$/.exec(filePath)
|
||||||
|
if (matches !== null) {
|
||||||
|
const [ , drive, relativePath ] = matches
|
||||||
|
const drives = await getWindowsNetworkDrives()
|
||||||
|
const location = drives.get(drive)
|
||||||
|
// eslint-disable-next-line no-undefined
|
||||||
|
if (location !== undefined) {
|
||||||
|
result = `${location}\\${relativePath}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
@ -31,7 +31,6 @@ const availableDrives = require('../../../models/available-drives')
|
|||||||
const selection = require('../../../models/selection-state')
|
const selection = require('../../../models/selection-state')
|
||||||
|
|
||||||
module.exports = function (
|
module.exports = function (
|
||||||
$q,
|
|
||||||
$state,
|
$state,
|
||||||
$timeout,
|
$timeout,
|
||||||
FlashErrorModalService,
|
FlashErrorModalService,
|
||||||
@ -67,39 +66,34 @@ module.exports = function (
|
|||||||
*
|
*
|
||||||
* @param {Array<Object>} drives - list of drive objects
|
* @param {Array<Object>} drives - list of drive objects
|
||||||
* @param {Object} image - image object
|
* @param {Object} image - image object
|
||||||
* @returns {Promise}
|
* @returns {Promise<Boolean>}
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* displayTailoredWarning(drives, image).then(() => {
|
* displayTailoredWarning(drives, image).then((ok) => {
|
||||||
* console.log('Continue pressed')
|
* if (ok) {
|
||||||
* }).catch(() => {
|
* console.log('No warning was shown or continue was pressed')
|
||||||
* console.log('Change pressed')
|
* } else {
|
||||||
|
* console.log('Change was pressed')
|
||||||
|
* }
|
||||||
* })
|
* })
|
||||||
*/
|
*/
|
||||||
const displayTailoredWarning = (drives, image) => {
|
const displayTailoredWarning = async (drives, image) => {
|
||||||
const warningMessages = _.reduce(drives, (accumMessages, drive) => {
|
const warningMessages = []
|
||||||
|
for (const drive of drives) {
|
||||||
if (constraints.isDriveSizeLarge(drive)) {
|
if (constraints.isDriveSizeLarge(drive)) {
|
||||||
return accumMessages.concat(messages.warning.largeDriveSize(drive))
|
warningMessages.push(messages.warning.largeDriveSize(drive))
|
||||||
} else if (!constraints.isDriveSizeRecommended(drive, image)) {
|
} else if (!constraints.isDriveSizeRecommended(drive, image)) {
|
||||||
return accumMessages.concat(messages.warning.unrecommendedDriveSize(image, drive))
|
warningMessages.push(messages.warning.unrecommendedDriveSize(image, drive))
|
||||||
}
|
}
|
||||||
|
|
||||||
return accumMessages
|
// TODO(Shou): we should consider adding the same warning dialog for system drives and remove unsafe mode
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
if (!warningMessages.length) {
|
if (!warningMessages.length) {
|
||||||
// TODO(Shou): we should consider adding the same warning dialog for system drives and remove unsafe mode
|
return true
|
||||||
return $q.resolve()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return confirmationWarningModal(warningMessages).then((value) => {
|
return confirmationWarningModal(warningMessages)
|
||||||
if (!value) {
|
|
||||||
DriveSelectorService.open()
|
|
||||||
return $q.reject()
|
|
||||||
}
|
|
||||||
|
|
||||||
return $q.resolve()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -118,35 +112,41 @@ module.exports = function (
|
|||||||
* '/dev/disk5'
|
* '/dev/disk5'
|
||||||
* ])
|
* ])
|
||||||
*/
|
*/
|
||||||
this.flashImageToDrive = () => {
|
this.flashImageToDrive = async () => {
|
||||||
const image = selection.getImage()
|
|
||||||
const devices = selection.getSelectedDevices()
|
const devices = selection.getSelectedDevices()
|
||||||
|
const image = selection.getImage()
|
||||||
|
const drives = _.filter(availableDrives.getDrives(), (drive) => {
|
||||||
|
return _.includes(devices, drive.device)
|
||||||
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
if (drives.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus(drives, image)
|
||||||
|
if (hasDangerStatus) {
|
||||||
|
if (!(await displayTailoredWarning(drives, image))) {
|
||||||
|
DriveSelectorService.open()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (flashState.isFlashing()) {
|
if (flashState.isFlashing()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const drives = _.filter(availableDrives.getDrives(), (drive) => {
|
|
||||||
return _.includes(devices, drive.device)
|
|
||||||
})
|
|
||||||
|
|
||||||
const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus(drives, image)
|
|
||||||
|
|
||||||
const userConfirm = hasDangerStatus ? _.partial(displayTailoredWarning, drives, image) : $q.resolve
|
|
||||||
|
|
||||||
// Trigger Angular digests along with store updates, as the flash state
|
// Trigger Angular digests along with store updates, as the flash state
|
||||||
// updates. Without this there is essentially no progress to watch.
|
// updates. Without this there is essentially no progress to watch.
|
||||||
const unsubscribe = store.observe($timeout)
|
const unsubscribe = store.observe($timeout)
|
||||||
|
|
||||||
const iconPath = '../../../assets/icon.png'
|
|
||||||
|
|
||||||
userConfirm().then(() => {
|
|
||||||
// Stop scanning drives when flashing
|
// Stop scanning drives when flashing
|
||||||
// otherwise Windows throws EPERM
|
// otherwise Windows throws EPERM
|
||||||
driveScanner.stop()
|
driveScanner.stop()
|
||||||
|
|
||||||
return imageWriter.flash(image.path, drives)
|
const iconPath = '../../../assets/icon.png'
|
||||||
}).then(() => {
|
try {
|
||||||
|
await imageWriter.flash(image.path, drives)
|
||||||
if (!flashState.wasLastFlashCancelled()) {
|
if (!flashState.wasLastFlashCancelled()) {
|
||||||
const flashResults = flashState.getFlashResults()
|
const flashResults = flashState.getFlashResults()
|
||||||
notification.send('Flash complete!', {
|
notification.send('Flash complete!', {
|
||||||
@ -155,7 +155,7 @@ module.exports = function (
|
|||||||
})
|
})
|
||||||
$state.go('success')
|
$state.go('success')
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
} catch (error) {
|
||||||
// When flashing is cancelled before starting above there is no error
|
// When flashing is cancelled before starting above there is no error
|
||||||
if (!error) {
|
if (!error) {
|
||||||
return
|
return
|
||||||
@ -183,11 +183,11 @@ module.exports = function (
|
|||||||
FlashErrorModalService.show(messages.error.genericFlashError())
|
FlashErrorModalService.show(messages.error.genericFlashError())
|
||||||
exceptionReporter.report(error)
|
exceptionReporter.report(error)
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
} finally {
|
||||||
availableDrives.setDrives([])
|
availableDrives.setDrives([])
|
||||||
driveScanner.start()
|
driveScanner.start()
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,6 +29,7 @@ const analytics = require('../../../modules/analytics')
|
|||||||
const settings = require('../../../models/settings')
|
const settings = require('../../../models/settings')
|
||||||
const selectionState = require('../../../models/selection-state')
|
const selectionState = require('../../../models/selection-state')
|
||||||
const osDialog = require('../../../os/dialog')
|
const osDialog = require('../../../os/dialog')
|
||||||
|
const { replaceWindowsNetworkDriveLetter } = require('../../../os/windows-network-drives')
|
||||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
const exceptionReporter = require('../../../modules/exception-reporter')
|
||||||
|
|
||||||
module.exports = function (
|
module.exports = function (
|
||||||
@ -145,7 +146,13 @@ module.exports = function (
|
|||||||
* @example
|
* @example
|
||||||
* ImageSelectionController.selectImageByPath('path/to/image.img');
|
* ImageSelectionController.selectImageByPath('path/to/image.img');
|
||||||
*/
|
*/
|
||||||
this.selectImageByPath = (imagePath) => {
|
this.selectImageByPath = async (imagePath) => {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
imagePath = await replaceWindowsNetworkDriveLetter(imagePath)
|
||||||
|
} catch (error) {
|
||||||
|
analytics.logException(error)
|
||||||
|
}
|
||||||
if (!supportedFormats.isSupportedImage(imagePath)) {
|
if (!supportedFormats.isSupportedImage(imagePath)) {
|
||||||
const invalidImageError = errors.createUserError({
|
const invalidImageError = errors.createUserError({
|
||||||
title: 'Invalid image',
|
title: 'Invalid image',
|
||||||
@ -158,12 +165,10 @@ module.exports = function (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const source = new sdk.sourceDestination.File(imagePath, sdk.sourceDestination.File.OpenFlags.Read)
|
const source = new sdk.sourceDestination.File(imagePath, sdk.sourceDestination.File.OpenFlags.Read)
|
||||||
source.getInnerSource()
|
try {
|
||||||
.then((innerSource) => {
|
const innerSource = await source.getInnerSource()
|
||||||
return innerSource.getMetadata()
|
const metadata = await innerSource.getMetadata()
|
||||||
.then((metadata) => {
|
const partitionTable = await innerSource.getPartitionTable()
|
||||||
return innerSource.getPartitionTable()
|
|
||||||
.then((partitionTable) => {
|
|
||||||
if (partitionTable) {
|
if (partitionTable) {
|
||||||
metadata.hasMBR = true
|
metadata.hasMBR = true
|
||||||
metadata.partitions = partitionTable.partitions
|
metadata.partitions = partitionTable.partitions
|
||||||
@ -173,20 +178,20 @@ module.exports = function (
|
|||||||
metadata.extension = path.extname(imagePath).slice(1)
|
metadata.extension = path.extname(imagePath).slice(1)
|
||||||
this.selectImage(metadata)
|
this.selectImage(metadata)
|
||||||
$timeout()
|
$timeout()
|
||||||
})
|
} catch (error) {
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
const imageError = errors.createUserError({
|
const imageError = errors.createUserError({
|
||||||
title: 'Error opening image',
|
title: 'Error opening image',
|
||||||
description: messages.error.openImage(path.basename(imagePath), error.message)
|
description: messages.error.openImage(path.basename(imagePath), error.message)
|
||||||
})
|
})
|
||||||
osDialog.showError(imageError)
|
osDialog.showError(imageError)
|
||||||
analytics.logException(error)
|
analytics.logException(error)
|
||||||
})
|
} finally {
|
||||||
.finally(() => {
|
try {
|
||||||
return source.close().catch(_.noop)
|
await source.close()
|
||||||
})
|
} catch (error) {
|
||||||
|
// Noop
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
BIN
tests/data/wmic-output.txt
Executable file
BIN
tests/data/wmic-output.txt
Executable file
Binary file not shown.
@ -126,23 +126,6 @@ describe('Model: flashState', function () {
|
|||||||
}).to.not.throw('Missing flash fields: percentage')
|
}).to.not.throw('Missing flash fields: percentage')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw if percentage is not a number', function () {
|
|
||||||
flashState.setFlashingFlag()
|
|
||||||
m.chai.expect(function () {
|
|
||||||
flashState.setProgressState({
|
|
||||||
flashing: 2,
|
|
||||||
verifying: 0,
|
|
||||||
successful: 0,
|
|
||||||
failed: 0,
|
|
||||||
type: 'write',
|
|
||||||
percentage: '50',
|
|
||||||
eta: 15,
|
|
||||||
speed: 100000000000,
|
|
||||||
totalSpeed: 200000000000
|
|
||||||
})
|
|
||||||
}).to.throw('Invalid state percentage: 50')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw if percentage is outside maximum bound', function () {
|
it('should throw if percentage is outside maximum bound', function () {
|
||||||
flashState.setFlashingFlag()
|
flashState.setFlashingFlag()
|
||||||
m.chai.expect(function () {
|
m.chai.expect(function () {
|
||||||
|
47
tests/gui/os/windows-network-drives.spec.js
Normal file
47
tests/gui/os/windows-network-drives.spec.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 resin.io
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const { readFile } = require('fs')
|
||||||
|
const os = require('os')
|
||||||
|
const cp = require('child_process')
|
||||||
|
const m = require('mochainon')
|
||||||
|
const { promisify } = require('util')
|
||||||
|
|
||||||
|
const { replaceWindowsNetworkDriveLetter } = require('../../../lib/gui/app/os/windows-network-drives')
|
||||||
|
|
||||||
|
const readFileAsync = promisify(readFile)
|
||||||
|
|
||||||
|
describe('Network drives on Windows', () => {
|
||||||
|
before(async () => {
|
||||||
|
this.osPlatformStub = m.sinon.stub(os, 'platform')
|
||||||
|
this.osPlatformStub.returns('win32')
|
||||||
|
const wmicOutput = await readFileAsync('tests/data/wmic-output.txt', { encoding: 'ucs2' })
|
||||||
|
this.execFileStub = m.sinon.stub(cp, 'execFile')
|
||||||
|
this.execFileStub.callsArgWith(3, null, wmicOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse network drive mapping on Windows', async () => {
|
||||||
|
m.chai.expect(await replaceWindowsNetworkDriveLetter('Z:\\some-folder\\some-file'))
|
||||||
|
.to.equal('\\\\192.168.1.1\\Public\\some-folder\\some-file')
|
||||||
|
})
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
this.osPlatformStub.restore()
|
||||||
|
this.execFileStub.restore()
|
||||||
|
})
|
||||||
|
})
|
@ -1,97 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
|
|
||||||
subjectParser: 'angular',
|
|
||||||
|
|
||||||
getGitReferenceFromVersion: 'v-prefix',
|
|
||||||
|
|
||||||
editChangelog: true,
|
|
||||||
|
|
||||||
editVersion: true,
|
|
||||||
|
|
||||||
addEntryToChangelog: {
|
|
||||||
preset: 'prepend',
|
|
||||||
fromLine: 5
|
|
||||||
},
|
|
||||||
|
|
||||||
includeCommitWhen: 'has-changelog-entry',
|
|
||||||
|
|
||||||
getIncrementLevelFromCommit: (commit) => {
|
|
||||||
if (/none/i.test(commit.footer['Change-type'])) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return commit.footer['Change-type'] &&
|
|
||||||
commit.footer['Change-type'].toLowerCase()
|
|
||||||
},
|
|
||||||
|
|
||||||
transformTemplateData: (data) => {
|
|
||||||
if (data.commits.length === 0) {
|
|
||||||
throw new Error('No commits annotated with Changelog-entry')
|
|
||||||
}
|
|
||||||
data.features = data.commits.filter((commit) => {
|
|
||||||
return commit.subject.type === 'feat'
|
|
||||||
})
|
|
||||||
|
|
||||||
data.fixes = data.commits.filter((commit) => {
|
|
||||||
return commit.subject.type === 'fix'
|
|
||||||
})
|
|
||||||
|
|
||||||
data.misc = data.commits.filter((commit) => {
|
|
||||||
return !([ 'fix', 'feat' ].includes(commit.subject.type))
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
|
||||||
},
|
|
||||||
|
|
||||||
template: [
|
|
||||||
'## v{{version}} - {{moment date "Y-MM-DD"}}',
|
|
||||||
'{{#if features.length}}',
|
|
||||||
'',
|
|
||||||
'### Features',
|
|
||||||
'',
|
|
||||||
'{{#each features}}',
|
|
||||||
'{{#with footer}}',
|
|
||||||
'- {{capitalize Changelog-entry}}',
|
|
||||||
'{{/with}}',
|
|
||||||
'{{/each}}',
|
|
||||||
'{{/if}}',
|
|
||||||
'{{#if fixes.length}}',
|
|
||||||
'',
|
|
||||||
'### Fixes',
|
|
||||||
'',
|
|
||||||
'{{#each fixes}}',
|
|
||||||
'{{#with footer}}',
|
|
||||||
'- {{capitalize Changelog-entry}}',
|
|
||||||
'{{/with}}',
|
|
||||||
'{{/each}}',
|
|
||||||
'{{/if}}',
|
|
||||||
'{{#if misc.length}}',
|
|
||||||
'',
|
|
||||||
'### Misc',
|
|
||||||
'',
|
|
||||||
'{{#each misc}}',
|
|
||||||
'{{#with footer}}',
|
|
||||||
'- {{capitalize Changelog-entry}}',
|
|
||||||
'{{/with}}',
|
|
||||||
'{{/each}}',
|
|
||||||
'{{/if}}'
|
|
||||||
].join('\n')
|
|
||||||
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user