From bcd80577d4de98dfbd9c6a591f5fc723045031e0 Mon Sep 17 00:00:00 2001 From: shmck Date: Sat, 30 May 2020 18:19:38 -0700 Subject: [PATCH 1/5] use version Signed-off-by: shmck --- package-lock.json | 2 +- package.json | 2 +- src/cli.ts | 94 ++++++++++++++++------------------------------- 3 files changed, 34 insertions(+), 64 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6b65974..360a192 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@coderoad/cli", - "version": "0.0.3", + "version": "0.0.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index dfb904d..055a00a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coderoad/cli", - "version": "0.0.3", + "version": "0.0.4", "description": "A CLI to build the configuration file for Coderoad Tutorials", "main": "src/main.js", "bin": { diff --git a/src/cli.ts b/src/cli.ts index de9766e..86f609a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -18,6 +18,7 @@ type ParsedArgs = { setupBranch: string; output?: string; help: string; + version?: string; }; type Options = { @@ -31,37 +32,6 @@ type Options = { const localGit = "Local directory"; const remoteGit = "Git remote address"; -function parseArgumentsIntoOptions(rawArgs: string[]): ParsedArgs { - const args = arg( - { - "--git": String, - "--dir": String, - "--code": String, - "--setup": String, - "--output": String, - "--help": Boolean, - "-g": "--git", - "-d": "--dir", - "-c": "--code", - "-s": "--setup", - "-o": "--output", - "-h": "--help", - }, - { - argv: rawArgs.slice(2), - } - ); - return { - command: args["_"][0], - git: args["--git"], - dir: args["--dir"], - codeBranch: args["--code"], - setupBranch: args["--setup"], - output: args["--output"] || "./config.json", - help: args["--help"] || false, - }; -} - export async function promptForMissingOptions( options: ParsedArgs ): Promise { @@ -152,36 +122,36 @@ export async function promptForMissingOptions( } export async function cli(args: string[]): Promise { - let parsedArgs: ParsedArgs = parseArgumentsIntoOptions(args); - - // If help called just print the help text and exit - if (parsedArgs.help) { - console.log( - "Docs can be found at github: https://github.com/coderoad/coderoad-cli/" - ); - } else if (!parsedArgs.command) { - console.log( - `The command is missing. Choose either 'create' or 'build' and its options.` - ); - } else { - switch (parsedArgs.command) { - case "build": - // Otherwise, continue with the other options - const options: BuildOptions = await promptForMissingOptions(parsedArgs); - const tutorial: T.Tutorial = await build(options); - - if (tutorial) { - if (options.output) { - fs.writeFileSync(options.output, JSON.stringify(tutorial), "utf8"); - } else { - console.log(JSON.stringify(tutorial, null, 2)); - } - } - break; - - case "create": - create(process.cwd()); - break; - } + const command: string = args[2]; + + switch (command) { + case "--version": + case "-v": + const version = require("../package.json").version; + console.log(`v${version}`); + return; + case "build": + // Otherwise, continue with the other options + // const options: BuildOptions = await promptForMissingOptions(parsedArgs); + // const tutorial: T.Tutorial = await build(options); + + // if (tutorial) { + // if (options.output) { + // fs.writeFileSync(options.output, JSON.stringify(tutorial), "utf8"); + // } else { + // console.log(JSON.stringify(tutorial, null, 2)); + // } + // } + break; + + case "create": + create(process.cwd()); + break; + case "--help": + case "-h": + default: + console.log( + "Docs can be found at github: https://github.com/coderoad/coderoad-cli/" + ); } } From 1b47da5651ecff88778ff0a37a1952d78a334e9b Mon Sep 17 00:00:00 2001 From: shmck Date: Sat, 30 May 2020 19:29:16 -0700 Subject: [PATCH 2/5] git commit check changes in progress Signed-off-by: shmck --- src/build.ts | 191 +++++++++++++++---------------------------- src/cli.ts | 141 ++------------------------------ src/utils/args.ts | 32 ++++++++ src/utils/commits.ts | 116 ++++++++++++++++++++++++++ src/utils/parse.ts | 1 - src/utils/prompt.ts | 116 ++++++++++++++++++++++++++ tsconfig.json | 3 +- typings/lib.d.ts | 4 - 8 files changed, 341 insertions(+), 263 deletions(-) create mode 100644 src/utils/args.ts create mode 100644 src/utils/commits.ts create mode 100644 src/utils/prompt.ts diff --git a/src/build.ts b/src/build.ts index ee16bc8..d306ca6 100644 --- a/src/build.ts +++ b/src/build.ts @@ -2,12 +2,16 @@ import * as yamlParser from "js-yaml"; import * as path from "path"; import * as _ from "lodash"; import * as fs from "fs"; -import * as T from "../typings/tutorial"; +import * as util from "util"; import { parse } from "./utils/parse"; -// import validate from './validator'; +import { getArg } from "./utils/args"; +import { getCommits } from "./utils/commits"; +import * as T from "../typings/tutorial"; + +const write = util.promisify(fs.writeFile); +const read = util.promisify(fs.readFile); // import not working -const simpleGit = require("simple-git/promise"); const workingDir = "tmp"; @@ -56,143 +60,84 @@ async function cleanupFiles(workingDir: string) { } } -export type BuildOptions = { - repo: string; // Git url to the repo. It should finish with .git - codeBranch: string; // The branch containing the tutorial code - setupBranch: string; // The branch containing the tutorialuration files - isLocal: boolean; // define if the repo is local or remote - output: string; +export type BuildConfigOptions = { + text: string; // text document from markdown + config: T.Tutorial; // yaml config file converted to json + commits: { [key: string]: string[] }; }; -async function build({ repo, codeBranch, setupBranch, isLocal }: BuildOptions) { - let git: any; - let isSubModule = false; - let localPath: string; - - if (isLocal) { - git = simpleGit(repo); - localPath = repo; - } else { - const gitTest = simpleGit(process.cwd()); - const isRepo = await gitTest.checkIsRepo(); - localPath = path.join(process.cwd(), workingDir); +async function generateConfig({ text, config, commits }: BuildConfigOptions) { + const tutorial = parse(text, config); - if (isRepo) { - await gitTest.submoduleAdd(repo, workingDir); - - isSubModule = true; - } else { - await gitTest.clone(repo, localPath); - } - - git = simpleGit(localPath); - } - - await git.fetch(); + // const isValid = validate(tutorial); - // checkout the branch to load tutorialuration and content branch - await git.checkout(setupBranch); + // if (!isValid) { + // console.log(JSON.stringify(validate.errors, null, 2)); + // return; + // } - // Load files - const _content = fs.readFileSync(path.join(localPath, "TUTORIAL.md"), "utf8"); - let _config = fs.readFileSync(path.join(localPath, "coderoad.yaml"), "utf8"); + return tutorial; +} - const tutorial = parse(_content, _config); +type BuildArgs = { + dir: string; + markdown: string; + yaml: string; + output: string; +}; - // Checkout the code branches - await git.checkout(codeBranch); +const parseArgs = (args: string[]): BuildArgs => { + // default . + const dir = args[0] || "."; + // -o --output - default coderoad.json + const output = + getArg(args, { name: "output", alias: "o" }) || "coderoad.json"; + // -m --markdown - default TUTORIAL.md + const markdown = + getArg(args, { name: "markdown", alias: "m" }) || "TUTORIAL.md"; + // -y --yaml - default coderoad-config.yml + const yaml = + getArg(args, { name: "coderoad-config.yml", alias: "y" }) || + "coderoad-config.yml"; + + return { + dir, + output, + markdown, + yaml, + }; +}; - // Load all logs - const logs = await git.log(); +async function build(args: string[]) { + const options = parseArgs(args); - // Filter relevant logs - const parts = new Set(); + // path to run build from + const localPath = path.join(process.cwd(), options.dir); - for (const commit of logs.all) { - const matches = commit.message.match( - /^(?(?L\d+)S\d+)(?[QA])?/ - ); + // load files + const [_markdown, _yaml] = await Promise.all([ + read(path.join(localPath, options.markdown), "utf8"), + read(path.join(localPath, options.yaml), "utf8"), + ]); - if (matches && !parts.has(matches[0])) { - // Uses a set to make sure only the latest commit is proccessed - parts.add(matches[0]); + const config = yamlParser.load(_yaml); - // Add the content and git hash to the tutorial - if (matches.groups.stepId) { - // If it's a step: add the content and the setup/solution hashes depending on the type - const level: T.Level | null = - tutorial.levels.find( - (level: T.Level) => level.id === matches.groups.levelId - ) || null; - if (!level) { - console.log(`Level ${matches.groups.levelId} not found`); - } else { - const theStep: T.Step | null = - level.steps.find( - (step: T.Step) => step.id === matches.groups.stepId - ) || null; - - if (!theStep) { - console.log(`Step ${matches.groups.stepId} not found`); - } else { - if (matches.groups.stepType === "Q") { - theStep.setup.commits.push(commit.hash.substr(0, 7)); - } else if ( - matches.groups.stepType === "A" && - theStep.solution && - theStep.solution.commits - ) { - theStep.solution.commits.push(commit.hash.substr(0, 7)); - } - } - } - } else { - // If it's level: add the commit hash (if the level has the commit key) and the content to the tutorial - const theLevel: T.Level | null = - tutorial.levels.find( - (level: T.Level) => level.id === matches.groups.levelId - ) || null; - - if (!theLevel) { - console.log(`Level ${matches.groups.levelId} not found`); - } else { - if (_.has(theLevel, "tutorial.commits")) { - if (theLevel.setup) { - theLevel.setup.commits.push(commit.hash.substr(0, 7)); - } - } - } - } - } - } + const commits = getCommits(config.config.repo.branch); - // cleanup the submodules - if (!isLocal) { - let cleanupErr; + // Otherwise, continue with the other options + const tutorial: T.Tutorial = await generateConfig({ + text: _markdown, + config, + commits, + }); - if (isSubModule) { - cleanupErr = await cleanupFiles(workingDir); + if (tutorial) { + if (options.output) { + await write(options.output, JSON.stringify(tutorial), "utf8"); } else { - cleanupErr = rmDir(path.join(process.cwd(), workingDir)); - } - - if (cleanupErr) { - console.log( - `Error when deleting temporary files on ${ - isSubModule ? "module" : "folder" - } ${workingDir}.` - ); + console.log(JSON.stringify(tutorial, null, 2)); } } - - // const isValid = validate(tutorial); - - // if (!isValid) { - // console.log(JSON.stringify(validate.errors, null, 2)); - // return; - // } - - return tutorial; } export default build; diff --git a/src/cli.ts b/src/cli.ts index 86f609a..e7d0749 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,128 +1,9 @@ -import * as inquirer from "inquirer"; -import * as fs from "fs"; -import * as T from "../typings/tutorial"; -import build, { BuildOptions } from "./build"; +import build from "./build"; import create from "./create"; -// import not working -const arg = require("arg"); -const simpleGit = require("simple-git/promise"); - -type Q = inquirer.Question & { choices?: string[] }; - -type ParsedArgs = { - command: string; - git?: string; - dir?: string; - codeBranch: string; - setupBranch: string; - output?: string; - help: string; - version?: string; -}; - -type Options = { - repo: string; - setupBranch: string; - codeBranch: string; - output: string; - isLocal: boolean; -}; - -const localGit = "Local directory"; -const remoteGit = "Git remote address"; - -export async function promptForMissingOptions( - options: ParsedArgs -): Promise { - const questions: Q[] = []; - - // if no git remote addres is provided, assume current folder - if (!options.git && !options.dir) { - // check if the current dir is a valid repo - const git = simpleGit(process.cwd()); - const isRepo = await git.checkIsRepo(); - - if (!isRepo) { - questions.push({ - type: "list", - name: "source", - message: `The current directory (${process.cwd()}) is not a valid git repo. Would you like to provide a...`, - choices: [localGit, remoteGit], - default: localGit, - }); - - questions.push({ - type: "input", - name: "localGit", - message: - "Please, provide a local directory of the valid git repository: ", - when: (input: any) => input.source === localGit, - }); - - questions.push({ - type: "input", - name: "remoteGit", - message: "Please, provide the address of a remote git repository: ", - when: (input: any) => input.source === remoteGit, - }); - } - } - // if both local dir and remote repos are provided - else if (options.git && options.dir) { - questions.push({ - type: "list", - name: "source", - message: - "A local git directory and a remote address were both provided. Please, choose either one or those to parse: ", - choices: [localGit, remoteGit], - default: localGit, - }); - } - - // if the branch containing the code is not provided - if (!options.codeBranch) { - questions.push({ - type: "input", - name: "codeBranch", - message: "Please, provide the branch with the code commits: ", - }); - } - - // if the branch containing the setup files is not provided - if (!options.setupBranch) { - questions.push({ - type: "input", - name: "setupBranch", - message: - "Please, provide the branch with the setup files (coderoad.yaml and tutorial.md): ", - }); - } - - const answers: any = await inquirer.prompt(questions); - - let repo: string; - let isLocal: boolean; - - if (answers.source) { - repo = (answers.source === localGit ? options.dir : options.git) || ""; - isLocal = answers.source === localGit; - } else { - repo = options.dir || options.git || process.cwd(); - isLocal = options.git ? false : true; - } - - return { - repo, - setupBranch: options.setupBranch || answers.setupBranch, - codeBranch: options.codeBranch || answers.codeBranch, - output: options.output || ".", - isLocal, - }; -} - -export async function cli(args: string[]): Promise { - const command: string = args[2]; +export async function cli(rawArgs: string[]): Promise { + const command: string = rawArgs[2]; + const args = rawArgs.slice(3); switch (command) { case "--version": @@ -130,23 +11,15 @@ export async function cli(args: string[]): Promise { const version = require("../package.json").version; console.log(`v${version}`); return; - case "build": - // Otherwise, continue with the other options - // const options: BuildOptions = await promptForMissingOptions(parsedArgs); - // const tutorial: T.Tutorial = await build(options); - // if (tutorial) { - // if (options.output) { - // fs.writeFileSync(options.output, JSON.stringify(tutorial), "utf8"); - // } else { - // console.log(JSON.stringify(tutorial, null, 2)); - // } - // } + case "build": + build(args); break; case "create": create(process.cwd()); break; + case "--help": case "-h": default: diff --git a/src/utils/args.ts b/src/utils/args.ts new file mode 100644 index 0000000..e9688c9 --- /dev/null +++ b/src/utils/args.ts @@ -0,0 +1,32 @@ +type ArgValueParams = { name: string; alias?: string; param?: boolean }; + +const checkValue = ( + args: string[], + string: string, + options: ArgValueParams +) => { + const nameIndex = args.indexOf(string); + if (nameIndex > -1) { + if (options.param) { + const value = args[nameIndex + 1]; + if (!value) { + throw new Error(`Argument ${string} is missing a parameter value`); + } + return value; + } + } + return null; +}; + +export function getArg(args: string[], options: ArgValueParams): string | null { + let value: null | string = null; + + const aliasString = `-${options.alias}`; + value = checkValue(args, aliasString, options); + if (!value) { + const nameString = `--${options.name}`; + value = checkValue(args, nameString, options); + } + + return value; +} diff --git a/src/utils/commits.ts b/src/utils/commits.ts new file mode 100644 index 0000000..a7189e6 --- /dev/null +++ b/src/utils/commits.ts @@ -0,0 +1,116 @@ +import * as path from "path"; +import gitP, { SimpleGit, StatusResult } from "simple-git/promise"; + +export async function getCommits() { + const git: SimpleGit = gitP(process.cwd()); + + const isRepo = await git.checkIsRepo(); + + const tmpDirectory = "tmp"; + const localPath = path.join(process.cwd(), tmpDirectory); + + if (!isRepo) { + throw new Error("No git repo provided"); + } + + // if (isRepo) { + // await gitTest.submoduleAdd(repo, workingDir); + + // isSubModule = true; + // } else { + // await gitTest.clone(repo, localPath); + // } + + // await git.fetch(); + + // // checkout the branch to load tutorialuration and content branch + // await git.checkout(setupBranch); + + // // Checkout the code branches + // await git.checkout(codeBranch); + + // // Load all logs + // const logs = await git.log(); + + // // Filter relevant logs + // const parts = new Set(); + + // for (const commit of logs.all) { + // const matches = commit.message.match( + // /^(?(?L\d+)S\d+)(?[QA])?/ + // ); + + // if (matches && !parts.has(matches[0])) { + // // Uses a set to make sure only the latest commit is proccessed + // parts.add(matches[0]); + + // // Add the content and git hash to the tutorial + // if (matches.groups.stepId) { + // // If it's a step: add the content and the setup/solution hashes depending on the type + // const level: T.Level | null = + // tutorial.levels.find( + // (level: T.Level) => level.id === matches.groups.levelId + // ) || null; + // if (!level) { + // console.log(`Level ${matches.groups.levelId} not found`); + // } else { + // const theStep: T.Step | null = + // level.steps.find( + // (step: T.Step) => step.id === matches.groups.stepId + // ) || null; + + // if (!theStep) { + // console.log(`Step ${matches.groups.stepId} not found`); + // } else { + // if (matches.groups.stepType === "Q") { + // theStep.setup.commits.push(commit.hash.substr(0, 7)); + // } else if ( + // matches.groups.stepType === "A" && + // theStep.solution && + // theStep.solution.commits + // ) { + // theStep.solution.commits.push(commit.hash.substr(0, 7)); + // } + // } + // } + // } else { + // // If it's level: add the commit hash (if the level has the commit key) and the content to the tutorial + // const theLevel: T.Level | null = + // tutorial.levels.find( + // (level: T.Level) => level.id === matches.groups.levelId + // ) || null; + + // if (!theLevel) { + // console.log(`Level ${matches.groups.levelId} not found`); + // } else { + // if (_.has(theLevel, "tutorial.commits")) { + // if (theLevel.setup) { + // theLevel.setup.commits.push(commit.hash.substr(0, 7)); + // } + // } + // } + // } + // } + // } + + // // cleanup the submodules + // if (!isLocal) { + // let cleanupErr; + + // if (isSubModule) { + // cleanupErr = await cleanupFiles(workingDir); + // } else { + // cleanupErr = rmDir(path.join(process.cwd(), workingDir)); + // } + + // if (cleanupErr) { + // console.log( + // `Error when deleting temporary files on ${ + // isSubModule ? "module" : "folder" + // } ${workingDir}.` + // ); + // } + // } +} + +getCommits(); diff --git a/src/utils/parse.ts b/src/utils/parse.ts index d00da9e..5bc5570 100644 --- a/src/utils/parse.ts +++ b/src/utils/parse.ts @@ -1,4 +1,3 @@ -import * as yamlParser from "js-yaml"; import * as _ from "lodash"; import * as T from "../../typings/tutorial"; diff --git a/src/utils/prompt.ts b/src/utils/prompt.ts new file mode 100644 index 0000000..cb21ee0 --- /dev/null +++ b/src/utils/prompt.ts @@ -0,0 +1,116 @@ +// import not working +// const arg = require("arg"); +// const simpleGit = require("simple-git/promise"); + +// type Q = inquirer.Question & { choices?: string[] }; + +// type ParsedArgs = { +// command: string; +// git?: string; +// dir?: string; +// codeBranch: string; +// setupBranch: string; +// output?: string; +// help: string; +// version?: string; +// }; + +// type Options = { +// repo: string; +// setupBranch: string; +// codeBranch: string; +// output: string; +// isLocal: boolean; +// }; + +// const localGit = "Local directory"; +// const remoteGit = "Git remote address"; + +// export async function promptForMissingOptions( +// options: ParsedArgs +// ): Promise { +// const questions: Q[] = []; + +// // if no git remote addres is provided, assume current folder +// if (!options.git && !options.dir) { +// // check if the current dir is a valid repo +// const git = simpleGit(process.cwd()); +// const isRepo = await git.checkIsRepo(); + +// if (!isRepo) { +// questions.push({ +// type: "list", +// name: "source", +// message: `The current directory (${process.cwd()}) is not a valid git repo. Would you like to provide a...`, +// choices: [localGit, remoteGit], +// default: localGit, +// }); + +// questions.push({ +// type: "input", +// name: "localGit", +// message: +// "Please, provide a local directory of the valid git repository: ", +// when: (input: any) => input.source === localGit, +// }); + +// questions.push({ +// type: "input", +// name: "remoteGit", +// message: "Please, provide the address of a remote git repository: ", +// when: (input: any) => input.source === remoteGit, +// }); +// } +// } +// // if both local dir and remote repos are provided +// else if (options.git && options.dir) { +// questions.push({ +// type: "list", +// name: "source", +// message: +// "A local git directory and a remote address were both provided. Please, choose either one or those to parse: ", +// choices: [localGit, remoteGit], +// default: localGit, +// }); +// } + +// // if the branch containing the code is not provided +// if (!options.codeBranch) { +// questions.push({ +// type: "input", +// name: "codeBranch", +// message: "Please, provide the branch with the code commits: ", +// }); +// } + +// // if the branch containing the setup files is not provided +// if (!options.setupBranch) { +// questions.push({ +// type: "input", +// name: "setupBranch", +// message: +// "Please, provide the branch with the setup files (coderoad.yaml and tutorial.md): ", +// }); +// } + +// const answers: any = await inquirer.prompt(questions); + +// let repo: string; +// let isLocal: boolean; + +// if (answers.source) { +// repo = (answers.source === localGit ? options.dir : options.git) || ""; +// isLocal = answers.source === localGit; +// } else { +// repo = options.dir || options.git || process.cwd(); +// isLocal = options.git ? false : true; +// } + +// return { +// repo, +// setupBranch: options.setupBranch || answers.setupBranch, +// codeBranch: options.codeBranch || answers.codeBranch, +// output: options.output || ".", +// isLocal, +// }; +// } diff --git a/tsconfig.json b/tsconfig.json index 04d2d49..d3d6cfc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,8 @@ "emitDecoratorMetadata": true, "allowJs": true, "removeComments": true, - "skipLibCheck": true + "skipLibCheck": true, + "esModuleInterop": true }, "exclude": ["node_modules", ".vscode", "bin", "build", "tests"] } diff --git a/typings/lib.d.ts b/typings/lib.d.ts index 3b2928e..a9b884c 100644 --- a/typings/lib.d.ts +++ b/typings/lib.d.ts @@ -2,7 +2,3 @@ declare module "arg" { export default (args: { [key: string]: any }, options: { argv: string[] }) => any; } - -declare module "simple-git/promise" { - export default any; -} From 740c1cf81c68615adbf2e5e503d0b4ce58b8273a Mon Sep 17 00:00:00 2001 From: shmck Date: Sat, 30 May 2020 20:48:23 -0700 Subject: [PATCH 3/5] build/parse refactor Signed-off-by: shmck --- src/build.ts | 70 ++----------------- src/utils/commits.ts | 163 +++++++++++++++---------------------------- src/utils/parse.ts | 77 +++++++++++++++++--- 3 files changed, 130 insertions(+), 180 deletions(-) diff --git a/src/build.ts b/src/build.ts index d306ca6..7b83aa9 100644 --- a/src/build.ts +++ b/src/build.ts @@ -5,80 +5,18 @@ import * as fs from "fs"; import * as util from "util"; import { parse } from "./utils/parse"; import { getArg } from "./utils/args"; -import { getCommits } from "./utils/commits"; +import { getCommits, CommitLogObject } from "./utils/commits"; import * as T from "../typings/tutorial"; const write = util.promisify(fs.writeFile); const read = util.promisify(fs.readFile); -// import not working - -const workingDir = "tmp"; - -function rmDir(dir: string, rmSelf = false) { - try { - let files; - rmSelf = rmSelf === undefined ? true : rmSelf; - - try { - files = fs.readdirSync(dir); - } catch (e) { - console.log(`Sorry, directory '${dir}' doesn't exist.`); - return; - } - - if (files.length > 0) { - files.forEach(function (filePath: string) { - if (fs.statSync(path.join(dir, filePath)).isDirectory()) { - rmDir(path.join(dir, filePath)); - } else { - fs.unlinkSync(path.join(dir, filePath)); - } - }); - } - - if (rmSelf) { - // check if user want to delete the directory ir just the files in this directory - fs.rmdirSync(dir); - } - } catch (error) { - return error; - } -} - -async function cleanupFiles(workingDir: string) { - try { - const gitModule = simpleGit(process.cwd()); - - await gitModule.subModule(["deinit", "-f", workingDir]); - await gitModule.rm(workingDir); - await gitModule.reset(["HEAD"]); - rmDir(path.join(process.cwd(), ".git", "modules", workingDir)); - rmDir(workingDir); - } catch (error) { - return error; - } -} - export type BuildConfigOptions = { text: string; // text document from markdown config: T.Tutorial; // yaml config file converted to json - commits: { [key: string]: string[] }; + commits: CommitLogObject; // an object of tutorial positions with a list of commit hashes }; -async function generateConfig({ text, config, commits }: BuildConfigOptions) { - const tutorial = parse(text, config); - - // const isValid = validate(tutorial); - - // if (!isValid) { - // console.log(JSON.stringify(validate.errors, null, 2)); - // return; - // } - - return tutorial; -} - type BuildArgs = { dir: string; markdown: string; @@ -122,10 +60,10 @@ async function build(args: string[]) { const config = yamlParser.load(_yaml); - const commits = getCommits(config.config.repo.branch); + const commits: CommitLogObject = await getCommits(config.config.repo.branch); // Otherwise, continue with the other options - const tutorial: T.Tutorial = await generateConfig({ + const tutorial: T.Tutorial = await parse({ text: _markdown, config, commits, diff --git a/src/utils/commits.ts b/src/utils/commits.ts index a7189e6..afbc57b 100644 --- a/src/utils/commits.ts +++ b/src/utils/commits.ts @@ -1,116 +1,69 @@ +import * as fs from "fs"; +import util from "util"; import * as path from "path"; -import gitP, { SimpleGit, StatusResult } from "simple-git/promise"; +import gitP, { SimpleGit } from "simple-git/promise"; +import * as T from "../../typings/tutorial"; -export async function getCommits() { - const git: SimpleGit = gitP(process.cwd()); +const mkdir = util.promisify(fs.mkdir); +const exists = util.promisify(fs.exists); +const rmdir = util.promisify(fs.rmdir); - const isRepo = await git.checkIsRepo(); +type GetCommitOptions = { + localDir: string; + codeBranch: string; +}; + +export type CommitLogObject = { [position: string]: string[] }; - const tmpDirectory = "tmp"; - const localPath = path.join(process.cwd(), tmpDirectory); +export async function getCommits({ + localDir, + codeBranch, +}: GetCommitOptions): Promise { + const git: SimpleGit = gitP(localDir); + + const isRepo = await git.checkIsRepo(); if (!isRepo) { throw new Error("No git repo provided"); } - // if (isRepo) { - // await gitTest.submoduleAdd(repo, workingDir); - - // isSubModule = true; - // } else { - // await gitTest.clone(repo, localPath); - // } - - // await git.fetch(); - - // // checkout the branch to load tutorialuration and content branch - // await git.checkout(setupBranch); - - // // Checkout the code branches - // await git.checkout(codeBranch); - - // // Load all logs - // const logs = await git.log(); - - // // Filter relevant logs - // const parts = new Set(); + const tmpDir = path.join(localDir, ".tmp"); - // for (const commit of logs.all) { - // const matches = commit.message.match( - // /^(?(?L\d+)S\d+)(?[QA])?/ - // ); - - // if (matches && !parts.has(matches[0])) { - // // Uses a set to make sure only the latest commit is proccessed - // parts.add(matches[0]); - - // // Add the content and git hash to the tutorial - // if (matches.groups.stepId) { - // // If it's a step: add the content and the setup/solution hashes depending on the type - // const level: T.Level | null = - // tutorial.levels.find( - // (level: T.Level) => level.id === matches.groups.levelId - // ) || null; - // if (!level) { - // console.log(`Level ${matches.groups.levelId} not found`); - // } else { - // const theStep: T.Step | null = - // level.steps.find( - // (step: T.Step) => step.id === matches.groups.stepId - // ) || null; - - // if (!theStep) { - // console.log(`Step ${matches.groups.stepId} not found`); - // } else { - // if (matches.groups.stepType === "Q") { - // theStep.setup.commits.push(commit.hash.substr(0, 7)); - // } else if ( - // matches.groups.stepType === "A" && - // theStep.solution && - // theStep.solution.commits - // ) { - // theStep.solution.commits.push(commit.hash.substr(0, 7)); - // } - // } - // } - // } else { - // // If it's level: add the commit hash (if the level has the commit key) and the content to the tutorial - // const theLevel: T.Level | null = - // tutorial.levels.find( - // (level: T.Level) => level.id === matches.groups.levelId - // ) || null; - - // if (!theLevel) { - // console.log(`Level ${matches.groups.levelId} not found`); - // } else { - // if (_.has(theLevel, "tutorial.commits")) { - // if (theLevel.setup) { - // theLevel.setup.commits.push(commit.hash.substr(0, 7)); - // } - // } - // } - // } - // } - // } - - // // cleanup the submodules - // if (!isLocal) { - // let cleanupErr; - - // if (isSubModule) { - // cleanupErr = await cleanupFiles(workingDir); - // } else { - // cleanupErr = rmDir(path.join(process.cwd(), workingDir)); - // } - - // if (cleanupErr) { - // console.log( - // `Error when deleting temporary files on ${ - // isSubModule ? "module" : "folder" - // } ${workingDir}.` - // ); - // } - // } + const tmpDirExists = await exists(tmpDir); + if (tmpDirExists) { + await rmdir(tmpDir, { recursive: true }); + } + await mkdir(tmpDir); + const tempGit = gitP(tmpDir); + await tempGit.clone(localDir, tmpDir); + + // Checkout the code branches + await git.checkout(codeBranch); + + // Load all logs + const logs = await git.log(); + + // Filter relevant logs + const commits: CommitLogObject = {}; + + for (const commit of logs.all) { + const matches = commit.message.match( + /^(?(?L\d+)(S\d+))(?[QA])?/ + ); + + if (matches && matches.length) { + // Use an object of commit arrays to collect all commits + const position = matches[0]; + if (!commits[position]) { + // does not exist, create the list + commits[position] = [commit.hash]; + } else { + // add to the list + commits[position].push(commit.hash); + } + } + } + // cleanup the tmp directory + await rmdir(tmpDir, { recursive: true }); + return commits; } - -getCommits(); diff --git a/src/utils/parse.ts b/src/utils/parse.ts index 5bc5570..f9ccca3 100644 --- a/src/utils/parse.ts +++ b/src/utils/parse.ts @@ -1,4 +1,5 @@ import * as _ from "lodash"; +import { CommitLogObject } from "./commits"; import * as T from "../../typings/tutorial"; type TutorialFrame = { @@ -91,17 +92,23 @@ export function parseMdContent(md: string): TutorialFrame | never { return sections; } -export function parse(_content: string, _config: string): T.Tutorial { - const mdContent: TutorialFrame = parseMdContent(_content); - // Parse tutorial to JSON - const tutorial: T.Tutorial = yamlParser.load(_config); +type ParseParams = { + text: string; + config: T.Tutorial; + commits: CommitLogObject; +}; + +export function parse(params: ParseParams): T.Tutorial { + const parsed = { ...params.config }; + + const mdContent: TutorialFrame = parseMdContent(params.text); // Add the summary to the tutorial file - tutorial["summary"] = mdContent.summary; + parsed["summary"] = mdContent.summary; // merge content and tutorial - if (tutorial.levels) { - tutorial.levels.forEach((level: T.Level) => { + if (parsed.levels) { + parsed.levels.forEach((level: T.Level, levelIndex: number) => { const levelContent = mdContent[level.id]; if (!levelContent) { console.log(`Markdown content not found for ${level.id}`); @@ -110,12 +117,64 @@ export function parse(_content: string, _config: string): T.Tutorial { const { steps, ...content } = levelContent; if (steps) { - steps.forEach((step: T.Step) => _.merge(step, steps[step.id])); + steps.forEach((step: T.Step, stepIndex: number) => { + return _.merge(step, steps[step.id]); + }); } _.merge(level, content); }); } - return tutorial; + return parsed; } + +/* +// Add the content and git hash to the tutorial + if (matches.groups.stepId) { + // If it's a step: add the content and the setup/solution hashes depending on the type + const level: T.Level | null = + tutorial.levels.find( + (level: T.Level) => level.id === matches.groups.levelId + ) || null; + if (!level) { + console.log(`Level ${matches.groups.levelId} not found`); + } else { + const theStep: T.Step | null = + level.steps.find( + (step: T.Step) => step.id === matches.groups.stepId + ) || null; + + if (!theStep) { + console.log(`Step ${matches.groups.stepId} not found`); + } else { + if (matches.groups.stepType === "Q") { + theStep.setup.commits.push(commit.hash.substr(0, 7)); + } else if ( + matches.groups.stepType === "A" && + theStep.solution && + theStep.solution.commits + ) { + theStep.solution.commits.push(commit.hash.substr(0, 7)); + } + } + } + } else { + // If it's level: add the commit hash (if the level has the commit key) and the content to the tutorial + const theLevel: T.Level | null = + tutorial.levels.find( + (level: T.Level) => level.id === matches.groups.levelId + ) || null; + + if (!theLevel) { + console.log(`Level ${matches.groups.levelId} not found`); + } else { + if (_.has(theLevel, "tutorial.commits")) { + if (theLevel.setup) { + theLevel.setup.commits.push(commit.hash.substr(0, 7)); + } + } + } + } + } +*/ From f5f882c86dfa78d2a86c74aa8164e358e5a03f47 Mon Sep 17 00:00:00 2001 From: shmck Date: Sat, 30 May 2020 21:16:32 -0700 Subject: [PATCH 4/5] parser test progress Signed-off-by: shmck --- src/utils/parse.ts | 96 +++++++++++++++--------------------- tests/parse.test.ts | 116 +++++++++++++++++++++++++++++++------------- 2 files changed, 121 insertions(+), 91 deletions(-) diff --git a/src/utils/parse.ts b/src/utils/parse.ts index f9ccca3..0f6c08d 100644 --- a/src/utils/parse.ts +++ b/src/utils/parse.ts @@ -94,11 +94,11 @@ export function parseMdContent(md: string): TutorialFrame | never { type ParseParams = { text: string; - config: T.Tutorial; + config: Partial; commits: CommitLogObject; }; -export function parse(params: ParseParams): T.Tutorial { +export function parse(params: ParseParams): any { const parsed = { ...params.config }; const mdContent: TutorialFrame = parseMdContent(params.text); @@ -110,71 +110,53 @@ export function parse(params: ParseParams): T.Tutorial { if (parsed.levels) { parsed.levels.forEach((level: T.Level, levelIndex: number) => { const levelContent = mdContent[level.id]; + console.log(levelContent); if (!levelContent) { console.log(`Markdown content not found for ${level.id}`); return; } - const { steps, ...content } = levelContent; - if (steps) { - steps.forEach((step: T.Step, stepIndex: number) => { - return _.merge(step, steps[step.id]); + // add level setup commits + const levelSetupKey = `L${levelIndex + 1}S`; + if (params.commits[levelSetupKey]) { + if (!level.setup) { + level.setup = { + commits: [], + }; + } + level.setup.commits = params.commits[levelSetupKey]; + } + + // add level step commits + if (levelContent.steps) { + levelContent.steps.forEach((step: T.Step, stepIndex: number) => { + const stepSetupKey = `${levelSetupKey}S${stepIndex + `1`}Q`; + if (params.commits[stepSetupKey]) { + if (!step.setup) { + step.setup = { + commits: [], + }; + } + step.setup.commits = params.commits[stepSetupKey]; + } + + const stepSolutionKey = `${levelSetupKey}S${stepIndex + `1`}A`; + if (params.commits[stepSolutionKey]) { + if (!step.solution) { + step.solution = { + commits: [], + }; + } + step.solution.commits = params.commits[stepSolutionKey]; + } + + return _.merge(step, levelContent.steps[step.id]); }); } - _.merge(level, content); + _.merge(level); }); } return parsed; } - -/* -// Add the content and git hash to the tutorial - if (matches.groups.stepId) { - // If it's a step: add the content and the setup/solution hashes depending on the type - const level: T.Level | null = - tutorial.levels.find( - (level: T.Level) => level.id === matches.groups.levelId - ) || null; - if (!level) { - console.log(`Level ${matches.groups.levelId} not found`); - } else { - const theStep: T.Step | null = - level.steps.find( - (step: T.Step) => step.id === matches.groups.stepId - ) || null; - - if (!theStep) { - console.log(`Step ${matches.groups.stepId} not found`); - } else { - if (matches.groups.stepType === "Q") { - theStep.setup.commits.push(commit.hash.substr(0, 7)); - } else if ( - matches.groups.stepType === "A" && - theStep.solution && - theStep.solution.commits - ) { - theStep.solution.commits.push(commit.hash.substr(0, 7)); - } - } - } - } else { - // If it's level: add the commit hash (if the level has the commit key) and the content to the tutorial - const theLevel: T.Level | null = - tutorial.levels.find( - (level: T.Level) => level.id === matches.groups.levelId - ) || null; - - if (!theLevel) { - console.log(`Level ${matches.groups.levelId} not found`); - } else { - if (_.has(theLevel, "tutorial.commits")) { - if (theLevel.setup) { - theLevel.setup.commits.push(commit.hash.substr(0, 7)); - } - } - } - } - } -*/ diff --git a/tests/parse.test.ts b/tests/parse.test.ts index 9544e70..fc86cf3 100644 --- a/tests/parse.test.ts +++ b/tests/parse.test.ts @@ -8,8 +8,12 @@ describe("parse", () => { `; - const yaml = `version: "0.1.0"`; - const result = parse(md, yaml); + const config = { version: "0.1.0" }; + const result = parse({ + text: md, + config, + commits: {}, + }); const expected = { summary: { description: "Short description to be shown as a tutorial's subtitle.", @@ -31,11 +35,15 @@ Description. Some text `; - const yaml = `version: "0.1.0" -levels: -- id: L1 -`; - const result = parse(md, yaml); + const config = { + levels: [{ id: "L1" }], + }; + + const result = parse({ + text: md, + config, + commits: {}, + }); const expected = { levels: [ { @@ -62,17 +70,20 @@ Description. Some text `; - const yaml = `version: "0.1.0" -levels: -- id: L1 - setup: - files: [] - commits: [] - solution: - files: [] - commits: [] -`; - const result = parse(md, yaml); + const config = { + levels: [ + { + id: "L1", + setup: { files: [], commits: [] }, + solution: { files: [], commits: [] }, + }, + ], + }; + const result = parse({ + text: md, + config, + commits: {}, + }); const expected = { levels: [ { @@ -99,11 +110,12 @@ Description. Some text that becomes the summary `; - const yaml = `version: "0.1.0" -levels: -- id: L1 -`; - const result = parse(md, yaml); + const config = { levels: [{ id: 1 }] }; + const result = parse({ + text: md, + config, + commits: {}, + }); const expected = { levels: [ { @@ -127,11 +139,12 @@ Description. Some text that becomes the summary and goes beyond the maximum length of 80 so that it gets truncated at the end `; - const yaml = `version: "0.1.0" -levels: -- id: L1 -`; - const result = parse(md, yaml); + const config = { levels: [{ id: "L1" }] }; + const result = parse({ + text: md, + config, + commits: {}, + }); const expected = { levels: [ { @@ -161,11 +174,12 @@ Second line Third line `; - const yaml = `version: "0.1.0" -levels: -- id: L1 -`; - const result = parse(md, yaml); + const config = { levels: [{ id: "L1" }] }; + const result = parse({ + text: md, + config, + commits: {}, + }); const expected = { summary: { description: "Description.\n\nSecond description line", @@ -208,7 +222,41 @@ config: - name: node version: '>=10' `; - const result = parse(md, yaml); + + const config = { + config: { + testRunner: { + command: "./node_modules/.bin/mocha", + args: { + filter: "--grep", + tap: "--reporter=mocha-tap-reporter", + }, + directory: "coderoad", + setup: { + commits: ["abcdefg1"], + commands: [], + }, + }, + appVersions: { + vscode: ">=0.7.0", + }, + repo: { + uri: "https://path.to/repo", + branch: "aBranch", + }, + dependencies: [ + { + name: "node", + version: ">=10", + }, + ], + }, + }; + const result = parse({ + text: md, + config, + commits: {}, + }); const expected = { summary: { description: "Description.\n\nSecond description line", From a7d615b942b920cfc442304c3ba1a3390fb32ed3 Mon Sep 17 00:00:00 2001 From: shmck Date: Sat, 30 May 2020 21:24:52 -0700 Subject: [PATCH 5/5] cleanup parser tests Signed-off-by: shmck --- src/utils/parse.ts | 15 +++++++++------ tests/parse.test.ts | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/utils/parse.ts b/src/utils/parse.ts index 0f6c08d..6c66063 100644 --- a/src/utils/parse.ts +++ b/src/utils/parse.ts @@ -59,6 +59,7 @@ export function parseMdContent(md: string): TutorialFrame | never { levelSummary, levelContent, } = levelMatch.groups; + const level = { [levelId]: { id: levelId, @@ -110,14 +111,14 @@ export function parse(params: ParseParams): any { if (parsed.levels) { parsed.levels.forEach((level: T.Level, levelIndex: number) => { const levelContent = mdContent[level.id]; - console.log(levelContent); + if (!levelContent) { console.log(`Markdown content not found for ${level.id}`); return; } // add level setup commits - const levelSetupKey = `L${levelIndex + 1}S`; + const levelSetupKey = `L${levelIndex + 1}`; if (params.commits[levelSetupKey]) { if (!level.setup) { level.setup = { @@ -127,9 +128,11 @@ export function parse(params: ParseParams): any { level.setup.commits = params.commits[levelSetupKey]; } + const { steps, ...content } = levelContent; + // add level step commits - if (levelContent.steps) { - levelContent.steps.forEach((step: T.Step, stepIndex: number) => { + if (steps) { + steps.forEach((step: T.Step, stepIndex: number) => { const stepSetupKey = `${levelSetupKey}S${stepIndex + `1`}Q`; if (params.commits[stepSetupKey]) { if (!step.setup) { @@ -150,11 +153,11 @@ export function parse(params: ParseParams): any { step.solution.commits = params.commits[stepSolutionKey]; } - return _.merge(step, levelContent.steps[step.id]); + return _.merge(step, steps[step.id]); }); } - _.merge(level); + _.merge(level, content); }); } diff --git a/tests/parse.test.ts b/tests/parse.test.ts index fc86cf3..13ce2d2 100644 --- a/tests/parse.test.ts +++ b/tests/parse.test.ts @@ -110,7 +110,7 @@ Description. Some text that becomes the summary `; - const config = { levels: [{ id: 1 }] }; + const config = { levels: [{ id: "L1" }] }; const result = parse({ text: md, config,