diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8ae1b0c..853dd09 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. @@ -38,7 +38,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -49,7 +49,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -63,4 +63,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/LICENSE.md b/LICENSE.md index 29e3db9..e3fcb94 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,3 @@ - The MIT License (MIT) Copyright (c) 2019 Takeshi Iwana diff --git a/README.md b/README.md index f0475c0..5938f3a 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ console.log(await gitly('iwatakeshi/gitly', '/path/to/extracted/folder/')) ## Options -```typescript +````typescript interface GitlyOptions { /** * Use cache only (default: undefined) @@ -101,7 +101,7 @@ interface GitlyOptions { */ backend?: 'axios' | 'git' } -``` +```` ## Interfaces diff --git a/package-lock.json b/package-lock.json index b18f468..64b7579 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-prefer-arrow": "^1.2.3", "jest": "^29.7.0", + "prettier": "^3.2.5", "shelljs": "^0.8.5", "ts-jest": "^29.1.2", "tslib": "^2.6.2", @@ -4981,6 +4982,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -9694,6 +9710,12 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true + }, "pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", diff --git a/package.json b/package.json index 2d7d7fa..fa7e495 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-prefer-arrow": "^1.2.3", "jest": "^29.7.0", + "prettier": "^3.2.5", "shelljs": "^0.8.5", "ts-jest": "^29.1.2", "tslib": "^2.6.2", @@ -39,7 +40,8 @@ "build": "tsc", "test": "jest", "lint": "eslint -c .eslintrc.js --ext .ts src", - "lint:fix": "npm run lint --fix" + "lint:fix": "npm run lint --fix", + "format": "prettier --write \"src/**/*.ts\" \"*.json\" \"*.md\"" }, "types": "lib/main.d.ts", "keywords": [ diff --git a/src/interfaces/options.ts b/src/interfaces/options.ts index ac968cc..c13a1c1 100644 --- a/src/interfaces/options.ts +++ b/src/interfaces/options.ts @@ -51,4 +51,10 @@ export default interface GitlyOptions { * ``` */ backend?: 'axios' | 'git' + /** + * Set git options (default: undefined) + */ + git?: { + depth?: number + } } diff --git a/src/interfaces/url.ts b/src/interfaces/url.ts index a52e7b8..953d27b 100644 --- a/src/interfaces/url.ts +++ b/src/interfaces/url.ts @@ -4,7 +4,7 @@ export default interface URLInfo { hostname: string hash: string href: string - path: string, + path: string repository: string owner: string type: string diff --git a/src/main.ts b/src/main.ts index 3c58212..3ff571c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,4 +2,4 @@ export { default } from './utils/gitly' export { default as download } from './utils/download' export { default as extract } from './utils/extract' export { default as parse } from './utils/parse' -export { default as clone } from './utils/clone' \ No newline at end of file +export { default as clone } from './utils/clone' diff --git a/src/utils/__test__/archive.spec.ts b/src/utils/__test__/archive.spec.ts index 6e40627..cec2b2c 100644 --- a/src/utils/__test__/archive.spec.ts +++ b/src/utils/__test__/archive.spec.ts @@ -1,31 +1,31 @@ import parse from '../parse' -import { getUrl } from '../archive' +import { getArchiveUrl } from '../archive' import { getArchivePath } from '../archive' const isWin32 = process.platform === 'win32' describe('utils/archive', () => { describe('getUrl()', () => { it('should return a github url to the zipped file', () => { - expect(getUrl(parse('iwatakeshi/test'))).toEqual( + expect(getArchiveUrl(parse('iwatakeshi/test'))).toEqual( 'https://github.com/iwatakeshi/test/archive/master.tar.gz' ) }) it('should return a bitbucket url to the zipped file', () => { - expect(getUrl(parse('bitbucket:iwatakeshi/test'))).toEqual( + expect(getArchiveUrl(parse('bitbucket:iwatakeshi/test'))).toEqual( 'https://bitbucket.org/iwatakeshi/test/get/master.tar.gz' ) }) it('should return a gitlab url to the zipped file', () => { - expect(getUrl(parse('gitlab:iwatakeshi/test'))).toEqual( + expect(getArchiveUrl(parse('gitlab:iwatakeshi/test'))).toEqual( 'https://gitlab.com/iwatakeshi/test/-/archive/master/test-master.tar.gz' ) }) it('should return a custom url to the zipped file', () => { expect( - getUrl(parse('iwatakeshi/test'), { + getArchiveUrl(parse('iwatakeshi/test'), { url: { filter(info) { return `https://domain.com${info.path}/repo/archive.tar.gz?ref=${info.type}` @@ -41,8 +41,13 @@ describe('utils/archive', () => { describe('getArchivePath()', () => { it('should return a path to the zipped file', () => { expect(getArchivePath(parse('iwatakeshi/test'))).toEqual( - isWin32 ? expect.stringMatching(/\.gitly\\github\\iwatakeshi\\test\\master\.tar\.gz/) : - expect.stringMatching(/\.gitly\/github\/iwatakeshi\/test\/master\.tar\.gz/) + isWin32 + ? expect.stringMatching( + /\.gitly\\github\\iwatakeshi\\test\\master\.tar\.gz/ + ) + : expect.stringMatching( + /\.gitly\/github\/iwatakeshi\/test\/master\.tar\.gz/ + ) ) }) }) diff --git a/src/utils/__test__/clone.spec.ts b/src/utils/__test__/clone.spec.ts index b218d46..98d013c 100644 --- a/src/utils/__test__/clone.spec.ts +++ b/src/utils/__test__/clone.spec.ts @@ -1,27 +1,22 @@ -import exists from '../exists'; +import exists from '../exists' import clone from '../clone' import { rm } from 'shelljs' -import { join } from 'path'; - +import { join } from 'path' describe('utils/clone', () => { - const destination = join(__dirname, 'output', 'clone', 'example') const options = { - temp: join(__dirname, 'output', 'clone', '.gitcopy'), + temp: join(__dirname, 'output', 'clone'), + backend: 'git' as 'git' | 'axios', } - - beforeEach(async () => { - rm('-rf', join(__dirname, 'output', 'clone', '.gitcopy')) - }) - afterEach(async () => { - rm('-rf', destination) + beforeAll(() => { + rm('-rf', join(__dirname, 'output', 'clone')) }) - afterAll(async () => { - rm('-rf', join(__dirname, 'output', 'clone', '.gitcopy')) + afterAll(() => { + rm('-rf', join(__dirname, 'output', 'clone')) }) it('should clone the repository', async () => { const result = await clone('lukeed/gittar', options) expect(await exists(result)).toBe(true) }) -}) \ No newline at end of file +}) diff --git a/src/utils/__test__/fetch.spec.ts b/src/utils/__test__/download.spec.ts similarity index 70% rename from src/utils/__test__/fetch.spec.ts rename to src/utils/__test__/download.spec.ts index 1fdae91..5df95b8 100644 --- a/src/utils/__test__/fetch.spec.ts +++ b/src/utils/__test__/download.spec.ts @@ -5,39 +5,44 @@ import { rm } from 'shelljs' import download from '../download' import { GitlyDownloadError } from '../error' -describe('utils/fetch (no cache)', () => { +describe('utils/download (no cache)', () => { const options = { - temp: join(__dirname, 'output', 'fetch', '.gitcopy'), + temp: join(__dirname, 'output', 'download'), } - beforeEach(async () => { - rm('-rf', join(__dirname, 'output', 'fetch', '.gitcopy')) + + beforeAll(() => { + rm('-rf', join(__dirname, 'output', 'download')) }) - afterAll(async () => { - rm('-rf', join(__dirname, 'output', 'fetch', '.gitcopy')) + beforeEach(() => { + rm('-rf', join(__dirname, 'output', 'download')) + }) + + afterAll(() => { + rm('-rf', join(__dirname, 'output', 'download')) }) - it('should fetch "lukeed/gittar"', async () => { + it('should download "lukeed/gittar"', async () => { expect.assertions(2) const path = await download('lukeed/gittar', options) expect(path).toBeTruthy() expect(existsSync(path)).toBe(true) }) - it('should fetch "lukeed/gittar#v0.1.1"', async () => { + it('should download "lukeed/gittar#v0.1.1"', async () => { expect.assertions(2) const path = await download('lukeed/gittar#v0.1.1', options) expect(path).toBeTruthy() expect(existsSync(path)).toBe(true) }) - it('should fetch "https://github.com/lukeed/gittar"', async () => { + it('should download "https://github.com/lukeed/gittar"', async () => { expect.assertions(2) const path = await download('https://github.com/lukeed/gittar', options) expect(path).toBeTruthy() expect(existsSync(path)).toBe(true) }) - it('should fetch "https://github.com/lukeed/gittar#v0.1.1"', async () => { + it('should download "https://github.com/lukeed/gittar#v0.1.1"', async () => { expect.assertions(2) const path = await download( 'https://github.com/lukeed/gittar#v0.1.1', @@ -47,35 +52,35 @@ describe('utils/fetch (no cache)', () => { expect(existsSync(path)).toBe(true) }) - it('should fetch "github.com/lukeed/gittar"', async () => { + it('should download "github.com/lukeed/gittar"', async () => { expect.assertions(2) const path = await download('github.com/lukeed/gittar', options) expect(path).toBeTruthy() expect(existsSync(path)).toBe(true) }) - it('should fetch "github.com/lukeed/gittar#v0.1.1"', async () => { + it('should download "github.com/lukeed/gittar#v0.1.1"', async () => { expect.assertions(2) const path = await download('github.com/lukeed/gittar#v0.1.1', options) expect(path).toBeTruthy() expect(existsSync(path)).toBe(true) }) - it('should fetch "github:lukeed/gittar"', async () => { + it('should download "github:lukeed/gittar"', async () => { expect.assertions(2) const path = await download('github:lukeed/gittar', options) expect(path).toBeTruthy() expect(existsSync(path)).toBe(true) }) - it('should fetch "github:lukeed/gittar#v0.1.1"', async () => { + it('should download "github:lukeed/gittar#v0.1.1"', async () => { expect.assertions(2) const path = await download('github:lukeed/gittar#v0.1.1', options) expect(path).toBeTruthy() expect(existsSync(path)).toBe(true) }) - it('should fetch "gitlab:Rich-Harris/buble#v0.15.2"', async () => { + it('should download "gitlab:Rich-Harris/buble#v0.15.2"', async () => { expect.assertions(2) const path = await download('gitlab:Rich-Harris/buble#v0.15.2', options) expect(path).toBeTruthy() @@ -101,22 +106,22 @@ describe('utils/fetch (no cache)', () => { }) }) -describe('utils/fetch (cached)', () => { +describe('utils/download (cached)', () => { const options = { - temp: join(__dirname, 'output', 'fetch', 'cache'), + temp: join(__dirname, 'output', 'download', 'cache'), cache: true, } const isCached = (ms: number) => Date.now() - ms <= 15 beforeAll(async () => { - rm('-rf', join(__dirname, 'output', 'fetch', 'cache')) - // Prefetch + rm('-rf', join(__dirname, 'output', 'download', 'cache')) + // Predownload const path = await download('lukeed/gittar', { temp: options.temp }) expect(existsSync(path)).toBe(true) }) afterAll(async () => { - rm('-rf', join(__dirname, 'output', 'fetch', 'cache')) + rm('-rf', join(__dirname, 'output', 'download', 'cache')) }) it('should return a path to the cached zipped file', async () => { diff --git a/src/utils/__test__/error.ts b/src/utils/__test__/error.ts index 4b3294c..2faa141 100644 --- a/src/utils/__test__/error.ts +++ b/src/utils/__test__/error.ts @@ -1,4 +1,10 @@ -import { GitlyDownloadError, GitlyErrorType, GitlyExtractError, GitlyFetchError, GitlyUknownError } from '../error' +import { + GitlyDownloadError, + GitlyErrorType, + GitlyExtractError, + GitlyFetchError, + GitlyUknownError, +} from '../error' describe('utils/error', () => { describe('GitlyFetchError', () => { diff --git a/src/utils/__test__/exists.spec.ts b/src/utils/__test__/exists.spec.ts index 410d553..ab23a7d 100644 --- a/src/utils/__test__/exists.spec.ts +++ b/src/utils/__test__/exists.spec.ts @@ -4,25 +4,25 @@ import exists from '../exists' describe('utils/exists', () => { const options = { - temp: join(__dirname, 'fixtures') + temp: join(__dirname, 'fixtures'), } it('should return true when a path exists', async () => { - const result = (await exists(join(__dirname, 'fixtures', 'test.txt'))) + const result = await exists(join(__dirname, 'fixtures', 'test.txt')) expect(result).toBe(true) }) it('should return false when a path does not exist', async () => { - const result = (await exists(join(__dirname, 'fixtures', 'dummy'))) + const result = await exists(join(__dirname, 'fixtures', 'dummy')) expect(result).toBe(false) }) it('should return true when a non absolute path exists', async () => { - const result = (await exists('iwatakeshi/test', options)) + const result = await exists('iwatakeshi/test', options) expect(result).toBe(true) }) it('should return false when a non absolute path does not exists', async () => { - const result = (await exists('iwatakeshi/myrepo', options)) + const result = await exists('iwatakeshi/myrepo', options) expect(result).toBe(false) }) }) diff --git a/src/utils/__test__/extract.spec.ts b/src/utils/__test__/extract.spec.ts index 10ae1cf..41ae095 100644 --- a/src/utils/__test__/extract.spec.ts +++ b/src/utils/__test__/extract.spec.ts @@ -6,20 +6,20 @@ import download from '../download' import extract from '../extract' describe('utils/extract', () => { - const destination = join(__dirname, 'output', 'extract', 'example') + const destination = join(__dirname, 'output', 'extract') const options = { temp: join(__dirname, 'output', 'extract', '.gitcopy'), } - - beforeEach(async () => { + + beforeEach(() => { rm('-rf', join(__dirname, 'output', 'extract', '.gitcopy')) }) - afterEach(async () => { + afterEach(() => { rm('-rf', destination) }) - afterAll(async () => { - rm('-rf', join(__dirname, 'output', 'extract', '.gitcopy')) + afterAll(() => { + rm('-rf', join(__dirname, 'output', 'extract')) }) it('should extract "lukeed/gittar"', async () => { diff --git a/src/utils/__test__/gitly.spec.ts b/src/utils/__test__/gitly.spec.ts index 5a13f14..a89a704 100644 --- a/src/utils/__test__/gitly.spec.ts +++ b/src/utils/__test__/gitly.spec.ts @@ -1,29 +1,32 @@ -import { join } from "path"; -import gitly from "../gitly"; -import exists from "../exists"; -import { rm } from "shelljs"; +import { join } from 'path' +import gitly from '../gitly' +import exists from '../exists' +import { rm } from 'shelljs' describe('gitly', () => { - const destination = join(__dirname, 'output', 'gitly', 'example') + const destination = join(__dirname, 'output', 'gitly') const options = { - temp: join(__dirname, 'output', 'gitly', '.gitcopy'), + temp: join(__dirname, 'output', 'gitly'), } - beforeEach(async () => { - rm('-rf', join(__dirname, 'output', 'gitly', '.gitcopy')) + beforeEach(() => { + rm('-rf', join(__dirname, 'output', 'gitly')) }) - afterEach(async () => { + afterEach(() => { rm('-rf', destination) }) it('should clone the repository using axios', async () => { const result = await gitly('lukeed/gittar', destination, options) - expect(await exists(result[0])).toBeDefined() - expect(await exists(result[1])).toBeDefined() + expect(await exists(result[0])).toBe(true) + expect(await exists(result[1])).toBe(true) }) it('should clone the repository using git', async () => { - const result = await gitly('lukeed/gittar', destination, { ...options, backend: 'git' }) - expect(await exists(result[0])).toBeDefined() - expect(await exists(result[1])).toBeDefined() + const result = await gitly('lukeed/gittar', destination, { + ...options, + backend: 'git', + }) + expect(await exists(result[0])).toBe(true) + expect(await exists(result[1])).toBe(true) }) -}); \ No newline at end of file +}) diff --git a/src/utils/__test__/parse.spec.ts b/src/utils/__test__/parse.spec.ts index f99e972..ef9522b 100644 --- a/src/utils/__test__/parse.spec.ts +++ b/src/utils/__test__/parse.spec.ts @@ -12,7 +12,7 @@ describe('utils/parse', () => { path: '/owner/repo', repository: 'repo', owner: 'owner', - type: 'master' + type: 'master', } as URLInfo) }) @@ -26,7 +26,7 @@ describe('utils/parse', () => { path: '/owner/repo', repository: 'repo', owner: 'owner', - type: 'v1.0.0' + type: 'v1.0.0', } as URLInfo) }) @@ -40,7 +40,7 @@ describe('utils/parse', () => { path: '/owner/repo', repository: 'repo', owner: 'owner', - type: 'master' + type: 'master', } as URLInfo) }) @@ -54,7 +54,7 @@ describe('utils/parse', () => { path: '/owner/repo', repository: 'repo', owner: 'owner', - type: 'master' + type: 'master', } as URLInfo) }) @@ -68,7 +68,7 @@ describe('utils/parse', () => { path: '/owner/repo', repository: 'repo', owner: 'owner', - type: 'master' + type: 'master', } as URLInfo) }) @@ -82,7 +82,7 @@ describe('utils/parse', () => { path: '/owner/repo', repository: 'repo', owner: 'owner', - type: 'tag' + type: 'tag', } as URLInfo) }) @@ -96,7 +96,7 @@ describe('utils/parse', () => { path: '/owner/repo', repository: 'repo', owner: 'owner', - type: 'tag' + type: 'tag', } as URLInfo) }) @@ -110,7 +110,7 @@ describe('utils/parse', () => { path: '/owner/repo', repository: 'repo', owner: 'owner', - type: 'master' + type: 'master', } as URLInfo) }) it('should parse "host:owner/repo#tag"', () => { @@ -123,14 +123,16 @@ describe('utils/parse', () => { path: '/owner/repo', repository: 'repo', owner: 'owner', - type: 'tag' + type: 'tag', } as URLInfo) }) it('should parse "owner/repo#tag" with a different host', () => { - expect(parse('owner/repo#tag', { - host: 'blah.dev' - })).toStrictEqual({ + expect( + parse('owner/repo#tag', { + host: 'blah.dev', + }) + ).toStrictEqual({ protocol: 'https', host: 'blah.dev', hostname: 'blah', @@ -139,7 +141,7 @@ describe('utils/parse', () => { path: '/owner/repo', repository: 'repo', owner: 'owner', - type: 'tag' + type: 'tag', } as URLInfo) }) }) diff --git a/src/utils/archive.ts b/src/utils/archive.ts index 464cac6..1c97906 100644 --- a/src/utils/archive.ts +++ b/src/utils/archive.ts @@ -5,7 +5,10 @@ import tar = require('tar') import GitlyOptions from '../interfaces/options' import URLInfo from '../interfaces/url' -export function getUrl(info: URLInfo, options: GitlyOptions = {}): string { +export function getArchiveUrl( + info: URLInfo, + options: GitlyOptions = {} +): string { const { path: repo, type } = info if (options.url && options.url.filter) { @@ -24,7 +27,10 @@ export function getUrl(info: URLInfo, options: GitlyOptions = {}): string { } } -export function getArchivePath(info: URLInfo, options: GitlyOptions = {}): string { +export function getArchivePath( + info: URLInfo, + options: GitlyOptions = {} +): string { const { path, type, hostname: site } = info return join( diff --git a/src/utils/clone.ts b/src/utils/clone.ts index 1e2633d..3446f9c 100644 --- a/src/utils/clone.ts +++ b/src/utils/clone.ts @@ -1,3 +1,4 @@ +import { rm } from 'fs/promises' import GitlyOptions from '../interfaces/options' import { getArchivePath } from './archive' import { GitlyCloneError } from './error' @@ -6,33 +7,101 @@ import exists from './exists' import { isOffline } from './offline' import parse from './parse' import spawn from 'cross-spawn' - +import tar from 'tar' +import path from 'path' /** * Uses local git installation to clone a repository to the destination. + * @param repository The repository to clone + * @param options The options to use + * @returns The path to the cloned repository + * @throws {GitlyCloneError} When the repository fails to clone + * @note This method requires a local git installation + * @note This method caches the repository by default + * @example + * ```js + * // ... + * const path = await clone('iwatakeshi/git-copy') + * // ... + * ``` */ export default async function clone( repository: string, options: GitlyOptions = {} ): Promise { const info = parse(repository, options) - const path = getArchivePath(info, options) - + const archivePath = getArchivePath(info, options) + const directory = archivePath.replace(/\.tar\.gz$/, '') let order: (() => Promise)[] = [] - const local = async () => exists(path) - const remote = async () => new Promise((resolve, reject) => { - const child = spawn('git', ['clone', info.href, path]) + const local = async () => exists(archivePath + '.tar.gz') + const remote = async () => { + // If the repository is cached, remove the old cache + if (await exists(archivePath)) { + /* istanbul ignore next */ + await rm(archivePath) + } - child.on('close', (code) => { - if (code === 0) { - resolve(path) - } else { - reject(new GitlyCloneError('Failed to clone the repository')) - } - }) - }) + // Prevent second order command injection + + const depth = options?.git?.depth || 1 + if (repository.includes('--upload-pack') || directory.includes('--upload-pack')) { + throw new GitlyCloneError('Invalid argument') + } + + /* istanbul ignore if */ + if (typeof depth !== 'number') { + throw new GitlyCloneError('Invalid depth option') + } + /* istanbul ignore if */ + if (info.href.includes('--upload-pack')) { + throw new GitlyCloneError('Invalid argument') + } + + + const child = spawn('git', [ + 'clone', + '--depth', + depth.toString(), + info.href, + directory, + ]) + + await new Promise((resolve, reject) => { + child.on('error', (reason) => + reject(new GitlyCloneError(reason.message)) + ) + child.on('close', (code) => { + /* istanbul ignore next */ + if (code === 0) { + // Create the archive after cloning + tar + .create( + { + gzip: true, + file: archivePath, + // Go one level up to include the repository name in the archive + cwd: path.resolve(archivePath, '..'), + portable: true, + }, + [info.type] + ) + .then(() => + rm(path.resolve(directory), { + recursive: true, + }) + ) + .then(resolve) + .catch((error) => reject(new GitlyCloneError(error.message))) + } /* istanbul ignore next */ else { + reject(new GitlyCloneError('Failed to clone the repository')) + } + }) + }) + return archivePath + } + /* istanbul ignore next */ if ((await isOffline()) || options.cache) { order = [local] } else if (options.force || ['master', 'main'].includes(info.type)) { @@ -42,7 +111,7 @@ export default async function clone( try { const result = await execute(order) if (typeof result === 'boolean') { - return path + return archivePath } return result } catch (error) { diff --git a/src/utils/download.ts b/src/utils/download.ts index 38448be..0e2fb82 100644 --- a/src/utils/download.ts +++ b/src/utils/download.ts @@ -5,7 +5,8 @@ import exists from './exists' import fetch from './fetch' import { isOffline } from './offline' import parse from './parse' -import { getArchivePath, getUrl } from './archive' +import { getArchivePath, getArchiveUrl } from './archive' +import { rm } from 'shelljs' /** * Download the tar file from the repository @@ -24,10 +25,18 @@ export default async function download( options: GitlyOptions = {} ): Promise { const info = parse(repository, options) - const path = getArchivePath(info, options) - const url = getUrl(info, options) - const local = async () => exists(path) - const remote = async () => fetch(url, path, options) + const archivePath = getArchivePath(info, options) + const url = getArchiveUrl(info, options) + const local = async () => exists(archivePath) + const remote = async () => { + // If the repository is cached, remove the old cache + if (await exists(archivePath)) { + /* istanbul ignore next */ + rm(archivePath) + } + + return fetch(url, archivePath, options) + } let order = [local, remote] if ((await isOffline()) || options.cache) { order = [local] @@ -38,7 +47,7 @@ export default async function download( try { const result = await execute(order) if (typeof result === 'boolean') { - return path + return archivePath } return result } catch (error) { diff --git a/src/utils/error.ts b/src/utils/error.ts index c2d7d62..1b922ac 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -10,16 +10,19 @@ export default abstract class GitlyAbstractError extends Error { static type: GitlyErrorType type: GitlyErrorType rawMessage: string - constructor(readonly message: string, readonly code: number = -1) { + constructor( + readonly message: string, + readonly code: number = -1 + ) { super(message) this.rawMessage = message - const type = this.type = this.ctor.type + const type = (this.type = this.ctor.type) this.message = `[${type ? `gitly:${type}` : 'gitly'}]: ${message}` Object.setPrototypeOf(this, new.target.prototype) } get ctor(): typeof GitlyAbstractError { - return (this.constructor) as typeof GitlyAbstractError + return this.constructor as typeof GitlyAbstractError } } @@ -41,4 +44,4 @@ export const GitlyDownloadError = class extends GitlyAbstractError { export const GitlyCloneError = class extends GitlyAbstractError { static type = GitlyErrorType.Clone -} \ No newline at end of file +} diff --git a/src/utils/execute.ts b/src/utils/execute.ts index 2e29c4c..6c046cd 100644 --- a/src/utils/execute.ts +++ b/src/utils/execute.ts @@ -1,10 +1,12 @@ export type Task = () => Promise -export default async function execute(tasks: Task[]): Promise { +export default async function execute( + tasks: Task[] +): Promise { return new Promise((resolve, reject) => { const next = () => execute(tasks.slice(1)).then(resolve) return tasks[0]() - .then(t => t ? resolve(t) : next()) + .then((t) => (t ? resolve(t) : next())) .catch(reject) }) } diff --git a/src/utils/gitly.ts b/src/utils/gitly.ts index bf7d3ff..9de07a5 100644 --- a/src/utils/gitly.ts +++ b/src/utils/gitly.ts @@ -14,14 +14,14 @@ export default async function gitly( destination: string, options: GitlyOptions ): Promise<[string, string]> { - let source: string = ''; + let source: string = '' switch (options?.backend) { case 'git': source = await clone(repository, options) - break; + break default: source = await download(repository, options) - break; + break } return [source, await extract(source, destination, options)] diff --git a/src/utils/parse.ts b/src/utils/parse.ts index ab284bf..dbb3eb0 100644 --- a/src/utils/parse.ts +++ b/src/utils/parse.ts @@ -17,12 +17,15 @@ import URLInfo from '../interfaces/url' * 7. host:owner/repo#tag * ``` */ -export default function parse(url: string, options: GitlyOptions = {}): URLInfo { - const { url: normalized, host } = normalizeURL(url, options); - const result = new URL(normalized); - const paths = (result.pathname || '').split('/').filter(Boolean); - const owner = paths.shift() || ''; - const repository = paths.shift() || ''; +export default function parse( + url: string, + options: GitlyOptions = {} +): URLInfo { + const { url: normalized, host } = normalizeURL(url, options) + const result = new URL(normalized) + const paths = (result.pathname || '').split('/').filter(Boolean) + const owner = paths.shift() || '' + const repository = paths.shift() || '' return { protocol: (result.protocol || 'https').replace(/:/g, ''), host: result.host || host || 'github.com', @@ -33,34 +36,47 @@ export default function parse(url: string, options: GitlyOptions = {}): URLInfo repository, owner, type: (result.hash || '#master').substring(1), - }; + } } - function normalizeURL(url: string, options: GitlyOptions) { - const { host } = options; - const isNotProtocol = !/http(s)?:\/\//.test(url); - const hasHost = /([\S]+):.+/.test(url); - const hasTLD = /[\S]+\.([\D]+)/.test(url); + const { host } = options - let normalizedURL = url.replace("www.", "").replace(".git", ""); - let updatedHost = host || ""; + /* istanbul ignore if */ + if (url.includes('0') && Array.from(url.matchAll(/0/g)).length > 25) { + throw new Error('Invalid argument') + } + /* istanbul ignore if */ + if (host?.includes('0') && Array.from(host.matchAll(/0/g)).length > 25) { + throw new Error('Invalid argument') + } + + const isNotProtocol = !/http(s)?:\/\//.test(url) + const hasHost = /([\S]+):.+/.test(url) + const hasTLD = /[\S]+\.([\D]+)/.test(url) + + let normalizedURL = url.replace('www.', '').replace('.git', '') + let updatedHost = host || '' if (isNotProtocol && hasHost) { + // Matches host:owner/repo - const hostMatch = url.match(/([\S]+):.+/); - updatedHost = hostMatch ? hostMatch[1] : ""; - normalizedURL = `https://${updatedHost}.com/${normalizedURL.replace(`${updatedHost}:`, "")}`; + const hostMatch = url.match(/([\S]+):.+/) + updatedHost = hostMatch ? hostMatch[1] : '' + normalizedURL = `https://${updatedHost}.com/${normalizedURL.replace(`${updatedHost}:`, '')}` } else if (isNotProtocol && hasTLD) { // Matches host.com/... - normalizedURL = `https://${normalizedURL}`; + normalizedURL = `https://${normalizedURL}` } else if (isNotProtocol) { // Matches owner/repo - const tldMatch = (host || "").match(/[\S]+\.([\D]+)/); - const domain = (host || "github").replace(`.${tldMatch ? tldMatch[1] : "com"}`, ""); - const tld = tldMatch ? tldMatch[1] : "com"; - normalizedURL = `https://${domain}.${tld}/${normalizedURL}`; + const tldMatch = (host || '').match(/[\S]+\.([\D]+)/) + const domain = (host || 'github').replace( + `.${tldMatch ? tldMatch[1] : 'com'}`, + '' + ) + const tld = tldMatch ? tldMatch[1] : 'com' + normalizedURL = `https://${domain}.${tld}/${normalizedURL}` } - return { url: normalizedURL, host: updatedHost }; + return { url: normalizedURL, host: updatedHost } } diff --git a/tsconfig.json b/tsconfig.json index 4ef9206..4f271a4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2018", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2018" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ @@ -21,7 +21,7 @@ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ + "module": "commonjs" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ @@ -37,12 +37,12 @@ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ /* Emit */ - "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./lib", /* Specify an output folder for all emitted files. */ + "outDir": "./lib" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ @@ -63,11 +63,11 @@ /* Interop Constraints */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ + "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ @@ -90,7 +90,5 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": [ - "src/**/*.ts" - ], -} \ No newline at end of file + "include": ["src/**/*.ts"] +}