diff --git a/src/parser-includes.ts b/src/parser-includes.ts index 9919267c6..8062812de 100644 --- a/src/parser-includes.ts +++ b/src/parser-includes.ts @@ -377,15 +377,15 @@ export class ParserIncludes { await fs.mkdirp(path.dirname(`${cwd}/${target}/${normalizedFile}`)); tmpDir = `${cwd}/${target}.${ext}`; - const gitCloneBranch = (ref === "HEAD") ? "" : `--branch ${ref}`; + const gitCloneBranch = (ref === "HEAD") ? "" : `--branch ${Utils.safeBashString(ref)}`; await Utils.bashMulti([ - `cd ${cwd}/${stateDir}`, - `git clone ${gitCloneBranch} -n --depth=1 --filter=tree:0 ${remote.schema}://${remote.host}:${remote.port}/${project}.git ${tmpDir}`, - `cd ${tmpDir}`, - `git sparse-checkout set --no-cone ${normalizedFile}`, + `cd ${Utils.safeBashString(`${cwd}/${stateDir}`)}`, + `git clone ${gitCloneBranch} -n --depth=1 --filter=tree:0 ${remote.schema}://${remote.host}:${remote.port}/${project}.git ${Utils.safeBashString(tmpDir)}`, + `cd ${Utils.safeBashString(tmpDir)}`, + `git sparse-checkout set --no-cone ${Utils.safeBashString(normalizedFile)}`, "git checkout", - `cd ${cwd}/${stateDir}`, - `cp ${tmpDir}/${normalizedFile} ${cwd}/${target}/${normalizedFile}`, + `cd ${Utils.safeBashString(`${cwd}/${stateDir}`)}`, + `cp ${Utils.safeBashString(`${tmpDir}/${normalizedFile}`)} ${Utils.safeBashString(`${cwd}/${target}/${normalizedFile}`)}`, ], cwd); } else { await fs.mkdirp(`${cwd}/${target}`); @@ -418,16 +418,16 @@ export class ParserIncludes { await fs.mkdirp(path.dirname(`${cwd}/${target}/templates`)); tmpDir = `${cwd}/${target}.${ext}`; - const gitCloneBranch = (ref === "HEAD") ? "" : `--branch ${ref}`; + const gitCloneBranch = (ref === "HEAD") ? "" : `--branch ${Utils.safeBashString(ref)}`; await Utils.bashMulti([ - `cd ${cwd}/${stateDir}`, - `git clone ${gitCloneBranch} -n --depth=1 --filter=tree:0 ${remote.schema}://${remote.host}:${remote.port}/${project}.git ${tmpDir}`, - `cd ${tmpDir}`, - `git sparse-checkout set --no-cone ${files[0]} ${files[1]}`, + `cd ${Utils.safeBashString(`${cwd}/${stateDir}`)}`, + `git clone ${gitCloneBranch} -n --depth=1 --filter=tree:0 ${remote.schema}://${remote.host}:${remote.port}/${project}.git ${Utils.safeBashString(tmpDir)}`, + `cd ${Utils.safeBashString(tmpDir)}`, + `git sparse-checkout set --no-cone ${Utils.safeBashString(files[0])} ${Utils.safeBashString(files[1])}`, "git checkout", - `cd ${cwd}/${stateDir}`, - `mkdir -p ${tmpDir}/templates`, // create templates subdir (if it doesn't exist), as the check out may not create it - `cp -r ${tmpDir}/templates ${cwd}/${target}`, + `cd ${Utils.safeBashString(`${cwd}/${stateDir}`)}`, + `mkdir -p ${Utils.safeBashString(`${tmpDir}/templates`)}`, // create templates subdir (if it doesn't exist), as the check out may not create it + `cp -r ${Utils.safeBashString(`${tmpDir}/templates`)} ${Utils.safeBashString(`${cwd}/${target}`)}`, ], cwd); } else { // git archive fails if the paths do not exist, to work around this we use a wildcard "templates/component*.yml" diff --git a/tests/parser-includes.test.ts b/tests/parser-includes.test.ts index e2a5f1977..c927b0c63 100644 --- a/tests/parser-includes.test.ts +++ b/tests/parser-includes.test.ts @@ -1,4 +1,8 @@ -import {resolveSemanticVersionRange} from "../src/parser-includes.js"; +import fs from "fs-extra"; +import {vi} from "vitest"; +import {ParserIncludes, resolveSemanticVersionRange} from "../src/parser-includes.js"; +import {Utils} from "../src/utils.js"; +import {WriteStreamsMock} from "../src/write-streams.js"; const tests = [ { @@ -45,3 +49,47 @@ describe("resolveSemanticVersionRange", () => { }); }); }); + +describe("downloadIncludeComponent", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("quotes generated bash paths with spaces", async () => { + const cwd = "C:\\Users\\My Name\\IdeaProjects\\project"; + const stateDir = ".gitlab-ci-local"; + const target = `${stateDir}/includes/gitlab.com/arc/ci-cd-components/1.3.0`; + const tmpDir = `${cwd}/${target}.tmp-0.42`; + const bashMultiSpy = vi.spyOn(Utils, "bashMulti").mockResolvedValue({stdout: "", stderr: "", exitCode: 0}); + + vi.spyOn(Math, "random").mockReturnValue(0.42); + vi.spyOn(fs, "pathExists").mockResolvedValue(undefined); + vi.spyOn(fs, "mkdirp").mockResolvedValue(undefined); + vi.spyOn(fs, "rm").mockResolvedValue(undefined); + + await ParserIncludes.downloadIncludeComponent({ + cwd, + stateDir, + fetchIncludes: false, + writeStreams: new WriteStreamsMock(), + gitData: { + remote: { + schema: "https", + host: "gitlab.com", + port: "443", + }, + }, + } as any, "arc/ci-cd-components", "1.3.0", "templates/foo"); + + expect(bashMultiSpy).toHaveBeenCalledWith([ + `cd '${cwd}/${stateDir}'`, + `git clone --branch '1.3.0' -n --depth=1 --filter=tree:0 https://gitlab.com:443/arc/ci-cd-components.git '${tmpDir}'`, + `cd '${tmpDir}'`, + "git sparse-checkout set --no-cone 'templates/foo.yml' 'templates/foo/template.yml'", + "git checkout", + `cd '${cwd}/${stateDir}'`, + `mkdir -p '${tmpDir}/templates'`, + `cp -r '${tmpDir}/templates' '${cwd}/${target}'`, + ], cwd); + }); +});