From cb798547499f84b968b38639c2536b57f859c1b5 Mon Sep 17 00:00:00 2001 From: shmck Date: Sun, 12 Jul 2020 12:26:55 -0700 Subject: [PATCH 1/5] send reset from client Signed-off-by: shmck --- src/channel/index.ts | 12 ++++++------ web-app/src/containers/Tutorial/index.tsx | 6 +++--- web-app/src/services/state/actions/editor.ts | 4 ++-- web-app/src/services/state/machine.ts | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/channel/index.ts b/src/channel/index.ts index 29649369..f5907ca3 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -320,17 +320,17 @@ class Channel implements Channel { case 'EDITOR_RUN_TEST': vscode.commands.executeCommand(COMMANDS.RUN_TEST, action?.payload) return - case 'EDITOR_RUN_RESET_SCRIPT': - const tutorial: TT.Tutorial | null = this.context.tutorial.get() + case 'EDITOR_RUN_RESET': + // reset to timeline + // 1. get last pass commit + // 2. load timeline until last pass commit + // if tutorial.config.reset.command, run it + const tutorial: TT.Tutorial | null = this.context.tutorial.get() if (tutorial?.config?.reset?.command) { await exec({ command: tutorial.config.reset.command }) } return - case 'EDITOR_RUN_RESET_TO_LAST_PASS': - return - case 'EDITOR_RUN_RESET_TO_TIMELINE': - return default: logger(`No match for action type: ${actionType}`) return diff --git a/web-app/src/containers/Tutorial/index.tsx b/web-app/src/containers/Tutorial/index.tsx index cb2d7f10..79d0a10b 100644 --- a/web-app/src/containers/Tutorial/index.tsx +++ b/web-app/src/containers/Tutorial/index.tsx @@ -12,7 +12,7 @@ import StepProgress from './components/StepProgress' import { DISPLAY_RUN_TEST_BUTTON } from '../../environment' import formatLevels from './formatLevels' // import SettingsPage from './containers/Settings' -// import Reset from './components/Reset' +import Reset from './components/Reset' const styles = { header: { @@ -95,7 +95,7 @@ const TutorialPage = (props: PageProps) => { } const onReset = (): void => { - // TODO + props.send({ type: 'RUN_RESET' }) } const [menuVisible, setMenuVisible] = React.useState(false) @@ -148,7 +148,7 @@ const TutorialPage = (props: PageProps) => { {/* Center */}
- {/* 0} /> */} + 0} />
{/* Right */} diff --git a/web-app/src/services/state/actions/editor.ts b/web-app/src/services/state/actions/editor.ts index befbac26..225e3e09 100644 --- a/web-app/src/services/state/actions/editor.ts +++ b/web-app/src/services/state/actions/editor.ts @@ -117,9 +117,9 @@ export default (editorSend: any) => ({ payload: { position: context.position }, }) }, - runResetScript() { + runReset() { editorSend({ - type: 'EDITOR_RUN_RESET_SCRIPT', + type: 'EDITOR_RUN_RESET', }) }, }) diff --git a/web-app/src/services/state/machine.ts b/web-app/src/services/state/machine.ts index 453e977a..76195e31 100644 --- a/web-app/src/services/state/machine.ts +++ b/web-app/src/services/state/machine.ts @@ -168,8 +168,8 @@ export const createMachine = (options: any) => { RUN_TEST: { actions: ['runTest'], }, - RESET_SCRIPT: { - actions: ['runResetScript'], + RUN_RESET: { + actions: ['runReset'], }, }, }, From f6c861f60ab110661e99d44313700d81a86f10b2 Mon Sep 17 00:00:00 2001 From: shmck Date: Sun, 12 Jul 2020 21:57:34 -0700 Subject: [PATCH 2/5] add reset script Signed-off-by: shmck --- src/services/git/index.ts | 3 +-- src/services/git/reset.ts | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/services/git/reset.ts diff --git a/src/services/git/index.ts b/src/services/git/index.ts index 3b32475d..3bb3aed2 100644 --- a/src/services/git/index.ts +++ b/src/services/git/index.ts @@ -1,9 +1,8 @@ import * as TT from 'typings/tutorial' import { exec, exists } from '../node' import logger from '../logger' -import { stringify } from 'querystring' -const gitOrigin = 'coderoad' +export const gitOrigin = 'coderoad' const stashAllFiles = async (): Promise => { // stash files including untracked (eg. newly created file) diff --git a/src/services/git/reset.ts b/src/services/git/reset.ts new file mode 100644 index 00000000..7da538ef --- /dev/null +++ b/src/services/git/reset.ts @@ -0,0 +1,49 @@ +import * as fs from 'fs' +import { exec, exists } from '../node' + +interface Input { + hash: string + branch: string +} + +// note: attempted to do this as a bash script +// but requires the bash script has git permissions +const reset = async ({ branch, hash }: Input): Promise => { + // TODO: capture branch + const localBranch = 'master' + + // switch to an empty branch + await exec({ + command: 'git checkout --orphan reset-orphan-branch', + }) + // stash any current work + await exec({ + command: 'git stash', + }) + // remove any other files + await exec({ + command: 'git rm -rf .', + }) + // TODO: delete .gitignore + + await exec({ + command: `git branch -D ${localBranch}`, + }) + await exec({ + command: `git checkout -b ${localBranch}`, + }) + + // load git timeline + await exec({ + command: `git fetch coderoad ${branch}`, + }) + await exec({ + command: `git merge coderoad/${localBranch}`, + }) + // reset to target commit hash + await exec({ + command: `git reset --hard ${hash}`, + }) +} + +export default reset From b56993a304e6c520b58eb01e973c5399ccef6684 Mon Sep 17 00:00:00 2001 From: shmck Date: Sun, 12 Jul 2020 22:32:59 -0700 Subject: [PATCH 3/5] get last passing commit hash Signed-off-by: shmck --- src/channel/index.ts | 12 +++++-- src/services/git/lastHash.test.ts | 56 +++++++++++++++++++++++++++++++ src/services/git/lastHash.ts | 23 +++++++++++++ src/services/git/lastPass.ts | 12 ------- typings/tutorial.d.ts | 2 +- 5 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 src/services/git/lastHash.test.ts create mode 100644 src/services/git/lastHash.ts delete mode 100644 src/services/git/lastPass.ts diff --git a/src/channel/index.ts b/src/channel/index.ts index f5907ca3..f7a53533 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -18,6 +18,7 @@ import { openWorkspace, checkWorkspaceEmpty } from '../services/workspace' import { showOutput } from '../services/testRunner/output' import { exec } from '../services/node' import { WORKSPACE_ROOT, TUTORIAL_URL } from '../environment' +import getLastCommitHash from '../services/git/lastHash' const readFileAsync = promisify(readFile) @@ -322,11 +323,16 @@ class Channel implements Channel { return case 'EDITOR_RUN_RESET': // reset to timeline - // 1. get last pass commit - // 2. load timeline until last pass commit + const tutorial: TT.Tutorial | null = this.context.tutorial.get() + const position: T.Position = this.context.position.get() + + // get last pass commit + const hash = getLastCommitHash(position, tutorial?.levels || []) + + // load timeline until last pass commit + // TODO: run reset script // if tutorial.config.reset.command, run it - const tutorial: TT.Tutorial | null = this.context.tutorial.get() if (tutorial?.config?.reset?.command) { await exec({ command: tutorial.config.reset.command }) } diff --git a/src/services/git/lastHash.test.ts b/src/services/git/lastHash.test.ts new file mode 100644 index 00000000..445f1c9e --- /dev/null +++ b/src/services/git/lastHash.test.ts @@ -0,0 +1,56 @@ +import * as TT from '../../../typings/tutorial' +import * as T from '../../../typings' +import getLastCommitHash from './lastHash' + +describe('lastHash', () => { + it('should grab the last passing hash from a step', () => { + const position: T.Position = { levelId: '1', stepId: '1.2' } + const levels: TT.Level[] = [ + { + id: '1', + title: '', + summary: '', + content: '', + steps: [ + { + id: '1.1', + content: '', + setup: { commits: ['abcdef1'] }, + }, + { + id: '1.2', + content: '', + setup: { commits: ['abcdef2'] }, + }, + ], + }, + ] + const result = getLastCommitHash(position, levels) + expect(result).toBe('abcdef2') + }) + it('should grab the last passing hash from a step with several commits', () => { + const position: T.Position = { levelId: '1', stepId: '1.2' } + const levels: TT.Level[] = [ + { + id: '1', + title: '', + summary: '', + content: '', + steps: [ + { + id: '1.1', + content: '', + setup: { commits: ['abcdef1'] }, + }, + { + id: '1.2', + content: '', + setup: { commits: ['abcdef2', 'abcdef3'] }, + }, + ], + }, + ] + const result = getLastCommitHash(position, levels) + expect(result).toBe('abcdef3') + }) +}) diff --git a/src/services/git/lastHash.ts b/src/services/git/lastHash.ts new file mode 100644 index 00000000..afe0c1e7 --- /dev/null +++ b/src/services/git/lastHash.ts @@ -0,0 +1,23 @@ +import * as TT from '../../../typings/tutorial' +import * as T from '../../../typings' + +const getLastCommitHash = (position: T.Position, levels: TT.Level[]) => { + // get previous position + const { levelId, stepId } = position + + const level: TT.Level | undefined = levels.find((l) => levelId === l.id) + if (!level) { + throw new Error(`No level found matching ${levelId}`) + } + const step = level.steps.find((s) => stepId === s.id) + if (!step) { + throw new Error(`No step found matching ${stepId}`) + } + const commits = step.setup.commits + if (!commits.length) { + throw new Error(`No commits found on step ${stepId}`) + } + return commits[commits.length - 1] +} + +export default getLastCommitHash diff --git a/src/services/git/lastPass.ts b/src/services/git/lastPass.ts deleted file mode 100644 index 8a2324cf..00000000 --- a/src/services/git/lastPass.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as TT from '../../../typings/tutorial' -import * as T from '../../../typings' - -const getLastPassCommitHash = (position: T.Position, levels: TT.Level[]) => { - // get previous position - const { levelId, stepId } = position - - // get solution hash if it exists - // else get setup hash -} - -export default getLastPassCommitHash diff --git a/typings/tutorial.d.ts b/typings/tutorial.d.ts index 6dac2b9d..a75ca39d 100644 --- a/typings/tutorial.d.ts +++ b/typings/tutorial.d.ts @@ -34,7 +34,7 @@ export type Step = { id: string content: string setup: StepActions - solution: Maybe + solution?: Maybe hints?: string[] subtasks?: string[] } From 4641fdbd630c9342d265641c22fb3e9cf2258f0c Mon Sep 17 00:00:00 2001 From: shmck Date: Tue, 14 Jul 2020 21:02:25 -0700 Subject: [PATCH 4/5] add reset script Signed-off-by: shmck --- src/channel/index.ts | 3 +- src/services/git/reset.ts | 97 ++++++++++++++++++++++++--------------- 2 files changed, 63 insertions(+), 37 deletions(-) diff --git a/src/channel/index.ts b/src/channel/index.ts index f7a53533..25eff2f5 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -18,6 +18,7 @@ import { openWorkspace, checkWorkspaceEmpty } from '../services/workspace' import { showOutput } from '../services/testRunner/output' import { exec } from '../services/node' import { WORKSPACE_ROOT, TUTORIAL_URL } from '../environment' +import reset from '../services/git/reset' import getLastCommitHash from '../services/git/lastHash' const readFileAsync = promisify(readFile) @@ -330,7 +331,7 @@ class Channel implements Channel { const hash = getLastCommitHash(position, tutorial?.levels || []) // load timeline until last pass commit - // TODO: run reset script + reset({ branch: tutorial?.config.repo.branch, hash }) // if tutorial.config.reset.command, run it if (tutorial?.config?.reset?.command) { diff --git a/src/services/git/reset.ts b/src/services/git/reset.ts index 7da538ef..f761e953 100644 --- a/src/services/git/reset.ts +++ b/src/services/git/reset.ts @@ -1,49 +1,74 @@ import * as fs from 'fs' -import { exec, exists } from '../node' +import { promisify } from 'util' +import { exec } from '../node' + +const removeFile = promisify(fs.unlink) interface Input { hash: string branch: string } +const ignoreError = () => {} + // note: attempted to do this as a bash script // but requires the bash script has git permissions const reset = async ({ branch, hash }: Input): Promise => { - // TODO: capture branch - const localBranch = 'master' - - // switch to an empty branch - await exec({ - command: 'git checkout --orphan reset-orphan-branch', - }) - // stash any current work - await exec({ - command: 'git stash', - }) - // remove any other files - await exec({ - command: 'git rm -rf .', - }) - // TODO: delete .gitignore - - await exec({ - command: `git branch -D ${localBranch}`, - }) - await exec({ - command: `git checkout -b ${localBranch}`, - }) - - // load git timeline - await exec({ - command: `git fetch coderoad ${branch}`, - }) - await exec({ - command: `git merge coderoad/${localBranch}`, - }) - // reset to target commit hash - await exec({ - command: `git reset --hard ${hash}`, - }) + const remote = 'coderoad' + + try { + // if no git init, will initialize + // otherwise re-initializes git + await exec({ command: 'git init' }).catch(console.log) + + // capture current branch + const hasBranch = await exec({ command: 'git branch --show-current' }) + const localBranch = hasBranch.stdout + // check if coderoad remote exists + const hasRemote = await exec({ command: 'git remote -v' }).catch(console.warn) + if (!hasRemote || !hasRemote.stdout || !hasRemote.stdout.length) { + throw new Error('No remote found') + } else if (!hasRemote.stdout.match(new RegExp(remote))) { + throw new Error(`No "${remote}" remote found`) + } + + // switch to an empty branch + await exec({ + command: 'git checkout --orphan reset-orphan-branch', + }) + // stash any current work + await exec({ + command: 'git stash', + }).catch(ignoreError) + + // remove any other files + await exec({ + command: 'git rm -rf .', + }).catch(ignoreError) + await removeFile('.gitignore').catch(ignoreError) + + await exec({ + command: `git branch -D ${localBranch}`, + }) + await exec({ + command: `git checkout -b ${localBranch}`, + }) + + // load git timeline + await exec({ + command: `git fetch coderoad ${branch}`, + }) + await exec({ + command: `git merge coderoad/${branch}`, + }) + // reset to target commit hash + await exec({ + command: `git reset --hard ${hash}`, + }) + } catch (error) { + console.error('Error resetting') + console.error(error.message) + } } export default reset From 218375526e6f5caa8eec98e5448a4bcdf53ae7c3 Mon Sep 17 00:00:00 2001 From: shmck Date: Tue, 14 Jul 2020 21:13:59 -0700 Subject: [PATCH 5/5] target user folder with remove file Signed-off-by: shmck --- src/channel/index.ts | 13 ++++++++++--- src/services/node/index.ts | 5 +++++ src/services/{git/reset.ts => reset/index.ts} | 6 +----- src/services/{git => reset}/lastHash.test.ts | 0 src/services/{git => reset}/lastHash.ts | 0 5 files changed, 16 insertions(+), 8 deletions(-) rename src/services/{git/reset.ts => reset/index.ts} (93%) rename src/services/{git => reset}/lastHash.test.ts (100%) rename src/services/{git => reset}/lastHash.ts (100%) diff --git a/src/channel/index.ts b/src/channel/index.ts index 25eff2f5..f0bcb31d 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -18,8 +18,8 @@ import { openWorkspace, checkWorkspaceEmpty } from '../services/workspace' import { showOutput } from '../services/testRunner/output' import { exec } from '../services/node' import { WORKSPACE_ROOT, TUTORIAL_URL } from '../environment' -import reset from '../services/git/reset' -import getLastCommitHash from '../services/git/lastHash' +import reset from '../services/reset' +import getLastCommitHash from '../services/reset/lastHash' const readFileAsync = promisify(readFile) @@ -330,8 +330,15 @@ class Channel implements Channel { // get last pass commit const hash = getLastCommitHash(position, tutorial?.levels || []) + const branch = tutorial?.config.repo.branch + + if (!branch) { + console.error('No repo branch found for tutorial') + return + } + // load timeline until last pass commit - reset({ branch: tutorial?.config.repo.branch, hash }) + reset({ branch, hash }) // if tutorial.config.reset.command, run it if (tutorial?.config?.reset?.command) { diff --git a/src/services/node/index.ts b/src/services/node/index.ts index 1005d33c..7026c7a6 100644 --- a/src/services/node/index.ts +++ b/src/services/node/index.ts @@ -5,6 +5,7 @@ import { promisify } from 'util' import { WORKSPACE_ROOT } from '../../environment' const asyncExec = promisify(cpExec) +const asyncRemoveFile = promisify(fs.unlink) interface ExecParams { command: string @@ -19,3 +20,7 @@ export const exec = (params: ExecParams): Promise<{ stdout: string; stderr: stri export const exists = (...paths: string[]): boolean | never => { return fs.existsSync(join(WORKSPACE_ROOT, ...paths)) } + +export const removeFile = (...paths: string[]) => { + return asyncRemoveFile(join(WORKSPACE_ROOT, ...paths)) +} diff --git a/src/services/git/reset.ts b/src/services/reset/index.ts similarity index 93% rename from src/services/git/reset.ts rename to src/services/reset/index.ts index f761e953..4fb06898 100644 --- a/src/services/git/reset.ts +++ b/src/services/reset/index.ts @@ -1,8 +1,4 @@ -import * as fs from 'fs' -import { promisify } from 'util' -import { exec } from '../node' - -const removeFile = promisify(fs.unlink) +import { exec, removeFile } from '../node' interface Input { hash: string diff --git a/src/services/git/lastHash.test.ts b/src/services/reset/lastHash.test.ts similarity index 100% rename from src/services/git/lastHash.test.ts rename to src/services/reset/lastHash.test.ts diff --git a/src/services/git/lastHash.ts b/src/services/reset/lastHash.ts similarity index 100% rename from src/services/git/lastHash.ts rename to src/services/reset/lastHash.ts