Skip to content

Commit

Permalink
feat: support git-lfs (#707)
Browse files Browse the repository at this point in the history

Co-authored-by: Russell Laboe <[email protected]>
  • Loading branch information
scolladon and Russman12 authored Oct 17, 2023
1 parent fd3cd8b commit b27abc8
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 110 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ module.exports = {
'no-compare-neg-zero': 'error',
'no-cond-assign': 'error',
'no-confusing-arrow': 'off',
'no-console': 'error',
'no-const-assign': 'error',
'no-constant-condition': 'error',
'no-control-regex': 'error',
Expand Down
8 changes: 3 additions & 5 deletions .lintstagedrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
{
"*.js": [
"eslint",
"eslint --fix",
"prettier --write"
]
"*.ts": ["eslint", "eslint --fix", "prettier --write"],
"*.js": ["eslint", "eslint --fix", "prettier --write"],
"*.md": ["prettier --write"]
}
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
- [`sfdx sgd:source:delta -f <string> [-t <string>] [-r <filepath>] [-i <filepath>] [-D <filepath>] [-s <filepath>] [-W] [-o <filepath>] [-a <number>] [-d] [-n <filepath>] [-N <filepath>] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-sgdsourcedelta--f-string--t-string--r-filepath--i-filepath--d-filepath--s-filepath--w--o-filepath--a-number--d--n-filepath--n-filepath---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal)
- [Windows users](#windows-users)
- [CI/CD specificity](#cicd-specificity)
- [Git LFS support](#git-lfs-support)
- [Use cases](#use-cases)
- [Walkthrough](#walkthrough)
- [Execute sgd](#execute-sgd)
Expand Down Expand Up @@ -232,6 +233,13 @@ Example with [github action](https://docs.github.com/en/actions/learn-github-act
run: echo y | sfdx plugins:install "sfdx-git-delta@${{ vars.SGD_VERSION }}"
```

### Git LFS support

The plugin is compatible with git LFS.
It will be able to read content from LFS locally.
It is the user responsibility to ensure LFS content is present when the plugin is executed.
/!\ The plugin **will not fetch** content from the LFS server /!\

### Use cases

Any git sha pointer is supported: commit sha, branch, tag, git expression (HEAD, etc.).
Expand Down
1 change: 1 addition & 0 deletions __tests__/perf/bench.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ suite
)
})
.on('cycle', event => {
// eslint-disable-next-line no-console
console.log(String(event.target))
})
.run()
36 changes: 34 additions & 2 deletions __tests__/unit/lib/utils/fsHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@ import {
getSpawnContent,
treatPathSep,
} from '../../../../src/utils/childProcessUtils'
import { Stats, outputFile, stat } from 'fs-extra'
import { readFile as fsReadFile, Stats, outputFile, stat } from 'fs-extra'
import {
isLFS,
getLFSObjectContentPath,
} from '../../../../src/utils/gitLfsHelper'
import { EOL } from 'os'
import { Work } from '../../../../src/types/work'
import { Config } from '../../../../src/types/config'

jest.mock('fs-extra')
jest.mock('../../../../src/utils/gitLfsHelper')
jest.mock('../../../../src/utils/childProcessUtils', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const actualModule: any = jest.requireActual(
Expand All @@ -41,6 +46,8 @@ jest.mock('../../../../src/utils/childProcessUtils', () => {
const mockedGetStreamContent = jest.mocked(getSpawnContent)
const mockedTreatPathSep = jest.mocked(treatPathSep)
const mockedStat = jest.mocked(stat)
const mockedIsLFS = jest.mocked(isLFS)
const mockedGetLFSObjectContentPath = jest.mocked(getLFSObjectContentPath)

let work: Work
beforeEach(() => {
Expand Down Expand Up @@ -101,6 +108,30 @@ describe('readPathFromGit', () => {
expect(content).toBe('')
})
})
describe('when file is LSF', () => {
const bufferContent =
Buffer.from(`version https://git-lfs.github.com/spec/v1
oid sha256:0a4ca7e5eca75024197fff96ef7e5de1b2ca35d6c058ce76e7e0d84bee1c8b14
size 72`)
beforeEach(async () => {
// Arrange
mockedGetStreamContent.mockResolvedValue(bufferContent)
mockedIsLFS.mockReturnValueOnce(true)
mockedGetLFSObjectContentPath.mockImplementationOnce(
() => 'lfs/objects/oid'
)
})
it('should copy the file', async () => {
// Act
await readPathFromGit('path/lfs/file', work.config)

// Assert
expect(getSpawnContent).toBeCalled()
expect(getLFSObjectContentPath).toBeCalledTimes(1)
expect(getLFSObjectContentPath).toHaveBeenCalledWith(bufferContent)
expect(fsReadFile).toBeCalledWith('lfs/objects/oid')
})
})
})

describe('copyFile', () => {
Expand Down Expand Up @@ -196,19 +227,20 @@ describe('copyFile', () => {
// Arrange
mockedGetStreamContent.mockResolvedValue(Buffer.from('content'))
mockedTreatPathSep.mockReturnValueOnce('source/copyFile')
mockedIsLFS.mockReturnValue(false)
})
it('should copy the file', async () => {
// Act
await copyFiles(work.config, 'source/copyfile')

// Assert
expect(getSpawnContent).toBeCalled()
expect(treatPathSep).toBeCalledTimes(1)
expect(outputFile).toBeCalledTimes(1)
expect(outputFile).toHaveBeenCalledWith(
'output/source/copyFile',
Buffer.from('content')
)
expect(treatPathSep).toBeCalledTimes(1)
})
})
})
Expand Down
64 changes: 64 additions & 0 deletions __tests__/unit/lib/utils/gitLfsHelper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use strict'
import { expect, describe, it } from '@jest/globals'
import {
isLFS,
getLFSObjectContentPath,
} from '../../../../src/utils/gitLfsHelper'

describe('isLFS', () => {
it('returns true when called with LFS file', async () => {
// Arrange
const lfsFileContent =
Buffer.from(`version https://git-lfs.github.com/spec/v1
oid sha256:0a4ca7e5eca75024197fff96ef7e5de1b2ca35d6c058ce76e7e0d84bee1c8b14
size 72`)

// Act
const result = isLFS(lfsFileContent)

// Assert
expect(result).toBe(true)
})
it('returns false when called with normal file', async () => {
// Arrange
const lfsFileContent = Buffer.from(`not lfs file`)

// Act
const result = isLFS(lfsFileContent)

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

describe('getLFSObjectContentPath', () => {
it('with LFS content, it creates LFS file path', async () => {
// Arrange
const lfsFileContent =
Buffer.from(`version https://git-lfs.github.com/spec/v1
oid sha256:0a4ca7e5eca75024197fff96ef7e5de1b2ca35d6c058ce76e7e0d84bee1c8b14
size 72`)

// Act
const lfsFilePath = await getLFSObjectContentPath(lfsFileContent)

// Assert
expect(lfsFilePath).toBe(
'.git/lfs/objects/0a/4c/0a4ca7e5eca75024197fff96ef7e5de1b2ca35d6c058ce76e7e0d84bee1c8b14'
)
})

it('without LFS content, it creates LFS file path', async () => {
// Arrange
expect.assertions(1)
const lfsFileContent = Buffer.from(`not lfs file`)

// Act
try {
await getLFSObjectContentPath(lfsFileContent)
} catch (e) {
// Assert
expect(e).toBeDefined()
}
})
})
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,23 @@
"@commitlint/config-conventional": "^17.7.0",
"@jest/globals": "^29.7.0",
"@oclif/dev-cli": "^1.26.10",
"@salesforce/cli-plugins-testkit": "^4.4.5",
"@salesforce/cli-plugins-testkit": "^4.4.7",
"@salesforce/dev-config": "^4.0.1",
"@salesforce/ts-sinon": "^1.4.17",
"@stryker-mutator/core": "^7.2.0",
"@stryker-mutator/jest-runner": "^7.2.0",
"@swc/core": "^1.3.81",
"@types/jest": "^29.5.5",
"@types/mocha": "^10.0.2",
"@types/node": "^20.8.2",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@types/node": "^20.8.4",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"benchmark": "^2.1.4",
"chai": "^4.3.10",
"depcheck": "^1.4.6",
"eslint": "^8.50.0",
"eslint": "^8.51.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-prettier": "^5.0.1",
"husky": "^8.0.3",
"jest": "^29.7.0",
"lint-staged": "^14.0.1",
Expand Down
7 changes: 6 additions & 1 deletion src/utils/fsHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
UTF8_ENCODING,
} from './gitConstants'
import { EOLRegex, getSpawnContent, treatPathSep } from './childProcessUtils'
import { isLFS, getLFSObjectContentPath } from './gitLfsHelper'
import { Config } from '../types/config'

const FOLDER = 'tree'
Expand Down Expand Up @@ -51,13 +52,17 @@ const readPathFromGitAsBuffer = async (
{ repo, to }: { repo: string; to: string }
) => {
const normalizedPath = gitPathSeparatorNormalizer(path)
const bufferData: Buffer = await getSpawnContent(
let bufferData: Buffer = await getSpawnContent(
GIT_COMMAND,
[...showCmd, `${to}:${normalizedPath}`],
{
cwd: repo,
}
)
if (isLFS(bufferData)) {
const lsfPath = getLFSObjectContentPath(bufferData)
bufferData = await fsReadFile(join(repo, lsfPath))
}

return bufferData
}
Expand Down
22 changes: 22 additions & 0 deletions src/utils/gitLfsHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict'
import { sep } from 'path'
import { GIT_FOLDER, UTF8_ENCODING } from './gitConstants'
import { EOLRegex } from './childProcessUtils'

const LFS_HEADER = Buffer.from('version https://git-lfs')

export const isLFS = (content: Buffer): boolean =>
content.subarray(0, LFS_HEADER.length).equals(LFS_HEADER)

export const getLFSObjectContentPath = (bufferContent: Buffer): string => {
const content = bufferContent.toString(UTF8_ENCODING)
const oid = content.split(EOLRegex)[1].split(':')[1]
return [
GIT_FOLDER,
'lfs',
'objects',
oid.slice(0, 2),
oid.slice(2, 4),
oid,
].join(sep)
}
Loading

0 comments on commit b27abc8

Please sign in to comment.