Skip to content

Feature/markdown as master #48

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jun 24, 2020
24 changes: 10 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ The configuration file is created by matching the `level` and `step` ids between

Tutorial description.

## L1 This is a level with id = 1
## 1. This is a level with id = 1

This level has two steps...

### L1S1 First step
### 1.1 First step

The first step with id L1S1. The Step id should start with the level id.

Expand All @@ -62,7 +62,7 @@ The first step with id L1S1. The Step id should start with the level id.
- The second hint that will show
- The third and final hint, as it is last in order

### L1S2 The second step
### 1.2 The second step

The second step...
```
Expand All @@ -72,10 +72,10 @@ The second step...
```yaml
---
levels:
- id: L1
- id: "1"
config: {}
steps:
- id: L1S1
- id: "1.1"
setup:
files:
- package.json
Expand All @@ -86,7 +86,7 @@ levels:
- package.json
commands:
- npm install
- id: L1S2
- id: "1.2"
setup:
files:
- src/server.js
Expand All @@ -104,23 +104,19 @@ commit 8e0e3a42ae565050181fdb68298114df21467a74 (HEAD -> v2, origin/v2)
Author: creator <[email protected]>
Date: Sun May 3 16:16:01 2020 -0700

L1S1Q setup step 1 for level 1
1.1 setup for level 1, step 1

commit 9499611fc9b311040dcabaf2d98439fc0c356cc9
Author: creator <[email protected]>
Date: Sun May 3 16:13:37 2020 -0700

L1S2A checkout solution for level 1, step 2
1.1S solution for level 1, step 1

commit c5c62041282579b495d3589b2eb1fdda2bcd7155
Author: creator <[email protected]>
Date: Sun May 3 16:11:42 2020 -0700

L1S2Q setup level 1, step 2
1.2 setup for level 1, step 2
```

Note that the step `L1S2` has two commits, one with the suffix `Q` and another one with `A`. The suffixes mean `Question` and `Answer`, respectively, and refer to the unit tests and the commit that makes them pass.

Steps defined as questions are **required** as they are meant to set the task to be executed by the student. The answer is optional and should be used when a commit must be loaded to verify the student's solution.

If there are multiple commits for a level or step, they are captured in order.
Note that the step `1.1` has two commits, one with the suffix `S`. The first commit refers to the required tests and setup, while the second optional commit contains the solution. If there are multiple commits for a level or step, they are captured in order.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coderoad/cli",
"version": "0.3.1",
"version": "0.4.0",
"description": "A CLI to build the configuration file for Coderoad Tutorials",
"keywords": [
"coderoad",
Expand Down
2 changes: 1 addition & 1 deletion src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ async function build(args: string[]) {
try {
const valid = validateSchema(skeletonSchema, skeleton);
if (!valid) {
console.error("Tutorial validation failed. See above to see what to fix");
console.error("Skeleton validation failed. See above to see what to fix");
return;
}
} catch (e) {
Expand Down
35 changes: 35 additions & 0 deletions src/schema/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,40 @@ export default {
},
additionalProperties: false,
},
setup_action_without_commits: {
type: "object",
description:
"A collection of files/commands that run when a level/step or solution is loaded",
properties: {
files: {
$ref: "#/definitions/file_array",
},
commands: {
$ref: "#/definitions/command_array",
},
watchers: {
type: "array",
items: {
$ref: "#/definitions/file_path",
// uniqueItems: true,
},
description:
"An array file paths that, when updated, will trigger the test runner to run",
},
filter: {
type: "string",
description:
"A regex pattern that will be passed to the test runner to limit the number of tests running",
examples: ["^TestSuiteName"],
},
subtasks: {
type: "boolean",
description:
'A feature that shows subtasks: all active test names and the status of the tests (pass/fail). Use together with "filter"',
examples: [true],
},
},
additionalProperties: false,
},
},
};
16 changes: 8 additions & 8 deletions src/schema/skeleton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ export default {
examples: ["coderoad"],
},
setup: {
$ref: "#/definitions/setup_action",
$ref: "#/definitions/setup_action_without_commits",
description:
"Setup commits or commands used for setting up the test runner on tutorial launch",
"Setup actions or commands used for setting up the test runner on tutorial launch",
},
},
required: ["command", "args"],
Expand Down Expand Up @@ -135,9 +135,9 @@ export default {
examples: ["L1", "L11"],
},
setup: {
$ref: "#/definitions/setup_action",
$ref: "#/definitions/setup_action_without_commits",
description:
"An optional point for loading commits, running commands or opening files",
"An optional point for running actions, commands or opening files",
},
steps: {
type: "array",
Expand All @@ -152,18 +152,18 @@ export default {
setup: {
allOf: [
{
$ref: "#/definitions/setup_action",
$ref: "#/definitions/setup_action_without_commits",
description:
"A point for loading commits. It can also run commands and/or open files",
"A point for running actions, commands and/or opening files",
},
],
},
solution: {
allOf: [
{
$ref: "#/definitions/setup_action",
$ref: "#/definitions/setup_action_without_commits",
description:
"The solution commits that can be loaded if the user gets stuck. It can also run commands and/or open files",
"The solution can be loaded if the user gets stuck. It can run actions, commands and/or open files",
},
{
required: [],
Expand Down
2 changes: 1 addition & 1 deletion src/schema/tutorial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export default {
examples: ["Have you tried doing X?"],
},
},
required: ["content", "setup", "solution"],
required: ["content", "setup"],
},
},
},
Expand Down
91 changes: 53 additions & 38 deletions src/utils/commits.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as fs from "fs";
import util from "util";
import * as path from "path";
import { ListLogSummary } from "simple-git/typings/response";
import gitP, { SimpleGit } from "simple-git/promise";
import { validateCommitOrder } from "./validateCommits";

Expand All @@ -15,6 +16,54 @@ type GetCommitOptions = {

export type CommitLogObject = { [position: string]: string[] };



export function parseCommits(logs: ListLogSummary<any>): { [hash: string]: string[]} {
// Filter relevant logs
const commits: CommitLogObject = {};
const positions: string[] = [];

for (const commit of logs.all) {
const matches = commit.message.match(
/^(?<init>INIT)|(L?(?<levelId>\d+)[S|\.]?(?<stepId>\d+)?(?<stepType>[Q|A|T|S])?)/
);

if (matches && matches.length) {
// Use an object of commit arrays to collect all commits
const { groups } = matches
let position
if (groups.init) {
position = 'INIT'
} else if (groups.levelId && groups.stepId) {
let stepType
// @deprecated Q
if (!groups.stepType || ['Q', 'T'].includes(groups.stepType)) {
stepType = 'T' // test
// @deprecated A
} else if (!groups.stepType || ['A', 'S'].includes(groups.stepType)) {
stepType = 'S' // solution
}
position = `${groups.levelId}.${groups.stepId}:${stepType}`
} else if (groups.levelId) {
position = groups.levelId
} else {
console.warn(`No matcher for commit "${commit.message}"`)
}
commits[position] = [...(commits[position] || []), commit.hash]
positions.unshift(position);
} else {
const initMatches = commit.message.match(/^INIT/);
if (initMatches && initMatches.length) {
commits.INIT = [...(commits.INIT || []), commit.hash]
positions.unshift("INIT");
}
}
}
// validate order
validateCommitOrder(positions);
return commits;
}

export async function getCommits({
localDir,
codeBranch,
Expand Down Expand Up @@ -49,48 +98,16 @@ export async function getCommits({
// track the original branch in case of failure
const originalBranch = branches.current;

// Filter relevant logs
const commits: CommitLogObject = {};

try {
// Checkout the code branches
await git.checkout(codeBranch);

// Load all logs
const logs = await git.log();
const positions: string[] = [];

for (const commit of logs.all) {
const matches = commit.message.match(
/^(?<stepId>(?<levelId>L\d+)(S\d+))(?<stepType>[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);
}
positions.unshift(position);
} else {
const initMatches = commit.message.match(/^INIT/);
if (initMatches && initMatches.length) {
if (!commits.INIT) {
// does not exist, create the list
commits.INIT = [commit.hash];
} else {
// add to the list
commits.INIT.push(commit.hash);
}
positions.unshift("INIT");
}
}
}
validateCommitOrder(positions);

const commits = parseCommits(logs);

return commits;
} catch (e) {
console.error("Error with checkout or commit matching");
throw new Error(e.message);
Expand All @@ -100,6 +117,4 @@ export async function getCommits({
// cleanup the tmp directory
await rmdir(tmpDir, { recursive: true });
}

return commits;
}
Loading