Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: copy empty file from git #547

Merged
merged 11 commits into from
Mar 31, 2023
11 changes: 10 additions & 1 deletion __mocks__/child_process.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,16 @@ childProcess.spawn.mockImplementation(() => {
this.push(output.pop().join(EOL))
}
this.push(null)
mock.emit(error ? 'error' : 'close')
mock.emit('close')
},
})
mock.stderr = new Readable({
read() {
if (error) {
this.push('error')
}
this.push(null)
mock.emit('close')
},
})
return mock
Expand Down
16 changes: 11 additions & 5 deletions __tests__/integration/delta.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@ const fs = require('fs')
const child_process = require('child_process')
const app = require('../../src/main')
const { COMMIT_REF_TYPE, GIT_FOLDER } = require('../../src/utils/gitConstants')
const { outputFile } = require('fs-extra')
const { scanExtension } = require('../../src/utils/fsHelper')
jest.mock('fs')
jest.mock('fs-extra')
jest.mock('child_process')
jest.mock('../../src/utils/fsHelper')
scanExtension.mockImplementation(() => ({
[Symbol.asyncIterator]: () => ({
next: () => ({
value: '',
done: () => true,
}),
}),
}))

const lines = [
'D force-app/main/default/objects/Account/fields/deleted.field-meta.xml',
Expand Down Expand Up @@ -51,7 +60,7 @@ describe(`test if the appli`, () => {
expect(
await app({
output: 'output',
repo: '.',
repo: './',
source: '',
to: 'test',
from: 'main',
Expand All @@ -61,9 +70,6 @@ describe(`test if the appli`, () => {
})

test('catch internal warnings', async () => {
outputFile.mockImplementationOnce(() =>
Promise.reject(new Error('Not writable'))
)
child_process.__setOutput([
lines,
[],
Expand Down
10 changes: 10 additions & 0 deletions __tests__/integration/services.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,16 @@ const testContext = [
],
],
],
[
InFolderHandler,
[
[
'dashboards',
'force-app/main/default/dashboards/folder/file.dashboard-meta.xml',
new Set(['folder/file']),
],
],
],
[
InBundleHandler,
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('FlowTranslationProcessor', () => {
package: new Map(),
destructiveChanges: new Map(),
},
config: { source: '.', output: 'output', generateDelta: true },
config: { source: './', output: 'output', generateDelta: true },
}
sut = new FlowTranslationProcessor(work)
flap = trueAfter(1)
Expand Down
18 changes: 16 additions & 2 deletions __tests__/unit/lib/utils/childProcessUtils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ describe('childProcessUtils', () => {
stream.emit('close')
},
})
stream.stderr = new Readable({
read() {
this.push(null)
stream.emit('close')
},
})

// Act
const result = await getStreamContent(stream)
Expand All @@ -42,7 +48,15 @@ describe('childProcessUtils', () => {
stream.stdout = new Readable({
read() {
this.push(null)
stream.emit('error')
stream.emit('close')
},
})

stream.stderr = new Readable({
read() {
this.push('error')
this.push(null)
stream.emit('close')
},
})

Expand All @@ -52,7 +66,7 @@ describe('childProcessUtils', () => {

// Assert
} catch (error) {
expect(error).toBeDefined()
expect(error.message).toEqual('error')
}
})
})
Expand Down
55 changes: 31 additions & 24 deletions __tests__/unit/lib/utils/fsHelper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ let work
beforeEach(() => {
work = {
config: {
output: '',
output: '.',
source: '',
repo: '',
generateDelta: false,
Expand Down Expand Up @@ -105,11 +105,11 @@ describe('readPathFromGit', () => {
describe('copyFile', () => {
describe('when file is already copied', () => {
it('should not copy file', async () => {
await copyFiles(work.config, 'source/file', 'output/file')
await copyFiles(work.config, 'source/file')
jest.resetAllMocks()

// Act
await copyFiles(work.config, 'source/file', 'output/file')
await copyFiles(work.config, 'source/file')

// Assert
expect(spawn).not.toBeCalled()
Expand All @@ -119,14 +119,23 @@ describe('copyFile', () => {
})

describe('when source location is empty', () => {
it('should not copy file', async () => {
it('should copy file', async () => {
// Arrange
treatPathSep.mockImplementationOnce(() => 'output/source/copyFile')
getStreamContent.mockImplementation(() =>
Promise.resolve(Buffer.from(''))
)

// Act
await copyFiles(work.config, 'source/doNotCopy', 'output/doNotCopy')
await copyFiles(work.config, 'source/doNotCopy')

// Assert
expect(spawn).toBeCalled()
expect(getStreamContent).toBeCalled()
expect(outputFile).not.toBeCalled()
expect(outputFile).toBeCalledWith(
'output/source/copyFile',
Buffer.from('')
)
})
})

Expand All @@ -143,7 +152,7 @@ describe('copyFile', () => {
)

// Act
await copyFiles(work.config, 'source/copyDir', 'output/copyDir')
await copyFiles(work.config, 'source/copyDir')

// Assert
expect(spawn).toBeCalledTimes(2)
Expand All @@ -157,14 +166,15 @@ describe('copyFile', () => {
})
})
describe('when content is not a git location', () => {
it('should ignore the path', async () => {
it('should ignore this path', async () => {
// Arrange
const sourcePath = 'source/warning'
getStreamContent.mockImplementation(() =>
Promise.resolve(Buffer.from(''))
Promise.reject(`fatal: path '${sourcePath}' does not exist in 'HEAD'`)
)

// Act
await copyFiles(work.config, 'source/warning', 'output/warning')
await copyFiles(work.config, sourcePath)

// Assert
expect(spawn).toBeCalled()
Expand All @@ -178,18 +188,18 @@ describe('copyFile', () => {
getStreamContent.mockImplementation(() =>
Promise.resolve(Buffer.from('content'))
)
treatPathSep.mockImplementationOnce(() => 'output/copyFile')
treatPathSep.mockImplementationOnce(() => 'output/source/copyFile')
})
it('should copy the file', async () => {
// Act
await copyFiles(work.config, 'source/copyfile', 'output/copyfile')
await copyFiles(work.config, 'source/copyfile')

// Assert
expect(spawn).toBeCalled()
expect(getStreamContent).toBeCalled()
expect(outputFile).toBeCalledTimes(1)
expect(outputFile).toHaveBeenCalledWith(
'output/copyFile',
'output/source/copyFile',
Buffer.from('content')
)
expect(treatPathSep).toBeCalledTimes(1)
Expand Down Expand Up @@ -279,13 +289,12 @@ describe('scan', () => {
Promise.reject(new Error('mock'))
)
})
it('should throw', async () => {
it('should not throw', async () => {
// Arrange
expect.assertions(1)
const g = scan('dir', work)
const res = await scan('dir', work)

// Assert
expect(g.next()).rejects.toEqual(new Error('mock'))
expect(res).toMatchObject({})
})
})
describe('when getStreamContent returns nothing', () => {
Expand Down Expand Up @@ -498,20 +507,18 @@ describe('isSubDir', () => {
// Assert
expect(result).toBe(false)
})
it('throws when spawn throws', async () => {
it('do not throws when getStreamContent throws', async () => {
expect.assertions(1)
// Arrange
getStreamContent.mockImplementationOnce(() =>
Promise.reject(new Error('spawn issue'))
)

// Act
try {
await pathExists('path', work.config)
// Assert
} catch (error) {
expect(error.message).toBe('spawn issue')
}
const exist = await pathExists('path', work.config)

// Assert
expect(exist).toBe(false)
})
})

Expand Down
8 changes: 4 additions & 4 deletions __tests__/unit/lib/utils/repoSetup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const child_process = require('child_process')
describe(`test if repoSetup`, () => {
describe('repoConfiguration', () => {
test('can set core.quotepath to off', async () => {
const config = { repo: '.', from: 'HEAD~1' }
const config = { repo: './', from: 'HEAD~1' }
child_process.__setOutput([['']])
const repoSetup = new RepoSetup(config)
await repoSetup.repoConfiguration()
Expand All @@ -17,7 +17,7 @@ describe(`test if repoSetup`, () => {
describe('getCommitRefType', () => {
test('returns "commit" when commitRef is a commit', async () => {
const shaRef = 'HEAD'
const config = { repo: '.', to: shaRef }
const config = { repo: './', to: shaRef }
child_process.__setOutput([['commit']])
const repoSetup = new RepoSetup(config)
const commitRef = await repoSetup.getCommitRefType(shaRef)
Expand All @@ -27,7 +27,7 @@ describe(`test if repoSetup`, () => {

test('returns "tag" when commitRef is a tag', async () => {
const shaRef = 'tag'
const config = { repo: '.', to: shaRef }
const config = { repo: './', to: shaRef }
child_process.__setOutput([['tag']])
const repoSetup = new RepoSetup(config)
const commitRef = await repoSetup.getCommitRefType(shaRef)
Expand All @@ -37,7 +37,7 @@ describe(`test if repoSetup`, () => {

test('return empty string when commitRef is a not a git sha', async () => {
const shaRef = 'wrong sha'
const config = { repo: '.', to: shaRef }
const config = { repo: './', to: shaRef }
child_process.__setOutput([['']])
const repoSetup = new RepoSetup(config)
const commitRef = await repoSetup.getCommitRefType(shaRef)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
"shx": "^0.3.4",
"sinon": "^15.0.3",
"ts-node": "^10.9.1",
"typescript": "^5.0.2",
"typescript": "^5.0.3",
"yarn-upgrade-all": "^0.7.2"
},
"oclif": {
Expand Down
7 changes: 7 additions & 0 deletions src/utils/childProcessUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ const getStreamContent = async stream => {
for await (const chunk of stream.stdout) {
content.push(chunk)
}
const error = []
for await (const chunk of stream.stderr) {
error.push(chunk)
}
if (error.length > 0) {
throw new Error(error.join(''))
}
return Buffer.concat(content)
}

Expand Down
5 changes: 2 additions & 3 deletions src/utils/cliHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,7 @@ class CLIHelper {

static TO_DEFAULT_VALUE = 'HEAD'
static OUTPUT_DEFAULT_VALUE = './output'
static SOURCE_DEFAULT_VALUE = '.'
static REPO_DEFAULT_VALUE = '.'
static IGNORE_DEFAULT_VALUE = '.'
static SOURCE_DEFAULT_VALUE = './'
static REPO_DEFAULT_VALUE = './'
}
module.exports = CLIHelper
46 changes: 26 additions & 20 deletions src/utils/fsHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,26 @@ const copyFiles = async (config, src) => {
if (copiedFiles.has(src)) return
copiedFiles.add(src)

const bufferData = await readPathFromGitAsBuffer(src, config)
const utf8Data = bufferData?.toString(UTF8_ENCODING)
if (!utf8Data) {
return
}

if (utf8Data.startsWith(FOLDER)) {
const [header, , ...files] = utf8Data.split(EOLRegex)
const folder = header.split(':')[1]
for (const file of files) {
const fileSrc = join(folder, file)

await copyFiles(config, fileSrc)
try {
const bufferData = await readPathFromGitAsBuffer(src, config)
const utf8Data = bufferData?.toString(UTF8_ENCODING)

if (utf8Data.startsWith(FOLDER)) {
const [header, , ...files] = utf8Data.split(EOLRegex)
const folder = header.split(':')[1]
for (const file of files) {
const fileSrc = join(folder, file)

await copyFiles(config, fileSrc)
}
} else {
const dst = join(config.output, treatPathSep(src))
// Use Buffer to output the file content
// Let fs implementation detect the encoding ("utf8" or "binary")
await outputFile(dst, bufferData)
}
} else {
const dst = join(config.output, treatPathSep(src))
// Use Buffer to output the file content
// Let fs implementation detect the encoding ("utf8" or "binary")
await outputFile(dst, bufferData)
} catch {
/* empty */
}
}

Expand All @@ -54,8 +55,13 @@ const readPathFromGitAsBuffer = async (path, { repo, to }) => {
}

const readPathFromGit = async (path, config) => {
const bufferData = await readPathFromGitAsBuffer(path, config)
const utf8Data = bufferData.toString(UTF8_ENCODING)
let utf8Data = ''
try {
const bufferData = await readPathFromGitAsBuffer(path, config)
utf8Data = bufferData.toString(UTF8_ENCODING)
} catch {
/* empty */
}
return utf8Data
}

Expand Down
Loading