Skip to content

Fix/check git remote #236

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 9 commits into from
Apr 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions errors/FailedToConnectToGitRepo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### Failed to Connect to Git Repo

There are several possible causes:

- you may not be connected to the internet or have an unstable connection.
- you may not have access permission to the remote tutorial repo.
- the remote tutorial repo may not exist at the provided location
3 changes: 3 additions & 0 deletions errors/GitNotFound.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Git Not Found

Make sure you install Git. See the [Git docs](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) for help.
5 changes: 5 additions & 0 deletions errors/GitProjectAlreadyExists.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Git Project Already Exists

CodeRoad requires an empty Git project.

Open a new workspace to start a tutorial.
5 changes: 5 additions & 0 deletions errors/UnknownError.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Unknown Error

Sorry! An unknown error occurred.

Please help out by posting an issue at github.com/coderoad/coderoad-vscode/issues/new/choose!
5 changes: 5 additions & 0 deletions errors/WorkspaceNotEmpty.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Select An Empty VSCode Workspace

Start a project in an empty folder.

Once selected, the extension will close and need to be re-started.
55 changes: 36 additions & 19 deletions src/actions/tutorialConfig.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,54 @@
import * as T from 'typings'
import * as E from 'typings/error'
import * as TT from 'typings/tutorial'
import * as vscode from 'vscode'
import { COMMANDS } from '../editor/commands'
import * as git from '../services/git'
import onError from '../services/sentry/onError'

interface TutorialConfigParams {
config: TT.TutorialConfig
alreadyConfigured?: boolean
onComplete?(): void
}

const tutorialConfig = async (
{ config, alreadyConfigured }: TutorialConfigParams,
handleError: (msg: T.ErrorMessage) => void,
) => {
const tutorialConfig = async ({ config, alreadyConfigured }: TutorialConfigParams): Promise<E.ErrorMessage | void> => {
if (!alreadyConfigured) {
// setup git, add remote
await git.initIfNotExists().catch((error) => {
onError(new Error('Git not found'))
// failed to setup git
handleError({
title: error.message,
description:
'Make sure you install Git. See the docs for help https://git-scm.com/book/en/v2/Getting-Started-Installing-Git',
})
})
const initError: E.ErrorMessage | void = await git.initIfNotExists().catch(
(error: Error): E.ErrorMessage => ({
type: 'GitNotFound',
message: error.message,
actions: [{ label: 'Retry', transition: '' }],
}),
)

if (initError) {
return initError
}

// verify that internet is connected, remote exists and branch exists
const remoteConnectError: E.ErrorMessage | void = await git.checkRemoteConnects(config.repo).catch(
(error: Error): E.ErrorMessage => ({
type: 'FailedToConnectToGitRepo',
message: error.message,
actions: [{ label: 'Retry', transition: '' }],
}),
)

if (remoteConnectError) {
return remoteConnectError
}

// TODO if remote not already set
await git.setupRemote(config.repo.uri).catch((error) => {
onError(error)
handleError({ title: error.message, description: 'Remove your current Git project and reload the editor' })
})
const coderoadRemoteError: E.ErrorMessage | void = await git.setupCodeRoadRemote(config.repo.uri).catch(
(error: Error): E.ErrorMessage => ({
type: 'GitRemoteAlreadyExists',
message: error.message,
}),
)

if (coderoadRemoteError) {
return coderoadRemoteError
}
}

await vscode.commands.executeCommand(COMMANDS.CONFIG_TEST_RUNNER, config.testRunner)
Expand Down
84 changes: 70 additions & 14 deletions src/channel/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as T from 'typings'
import * as TT from 'typings/tutorial'
import * as E from 'typings/error'
import * as vscode from 'vscode'
import saveCommit from '../actions/saveCommit'
import setupActions from '../actions/setupActions'
Expand All @@ -10,6 +11,11 @@ import logger from '../services/logger'
import Context from './context'
import { version as gitVersion } from '../services/git'
import { openWorkspace, checkWorkspaceEmpty } from '../services/workspace'
import { readFile } from 'fs'
import { join } from 'path'
import { promisify } from 'util'

const readFileAsync = promisify(readFile)

interface Channel {
receive(action: T.Action): Promise<void>
Expand Down Expand Up @@ -39,7 +45,9 @@ class Channel implements Channel {
public receive = async (action: T.Action) => {
// action may be an object.type or plain string
const actionType: string = typeof action === 'string' ? action : action.type
const onError = (error: T.ErrorMessage) => this.send({ type: 'ERROR', payload: { error } })
// const onError = (error: T.ErrorMessage) => this.send({ type: 'ERROR', payload: { error } })

// console.log(`ACTION: ${actionType}`)

switch (actionType) {
case 'EDITOR_ENV_GET':
Expand Down Expand Up @@ -86,7 +94,16 @@ class Channel implements Channel {
// setup tutorial config (save watcher, test runner, etc)
await this.context.setTutorial(this.workspaceState, data)

await tutorialConfig({ config: data.config }, onError)
const error: E.ErrorMessage | void = await tutorialConfig({ config: data.config }).catch((error: Error) => ({
type: 'UnknownError',
message: `Location: tutorial config.\n\n${error.message}`,
}))

// has error
if (error && error.type) {
this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } })
return
}

// report back to the webview that setup is complete
this.send({ type: 'TUTORIAL_CONFIGURED' })
Expand All @@ -97,34 +114,54 @@ class Channel implements Channel {
throw new Error('Invalid tutorial to continue')
}
const continueConfig: TT.TutorialConfig = tutorialContinue.config
await tutorialConfig(
{
config: continueConfig,
alreadyConfigured: true,
},
onError,
)
await tutorialConfig({
config: continueConfig,
alreadyConfigured: true,
})
// update the current stepId on startup
vscode.commands.executeCommand(COMMANDS.SET_CURRENT_STEP, action.payload)
return
case 'EDITOR_VALIDATE_SETUP':
// 1. check workspace is selected
const isEmptyWorkspace = await checkWorkspaceEmpty(this.workspaceRoot.uri.path)
if (!isEmptyWorkspace) {
this.send({ type: 'NOT_EMPTY_WORKSPACE' })
const error: E.ErrorMessage = {
type: 'WorkspaceNotEmpty',
message: '',
actions: [
{
label: 'Open Workspace',
transition: 'REQUEST_WORKSPACE',
},
{
label: 'Check Again',
transition: 'RETRY',
},
],
}
this.send({ type: 'VALIDATE_SETUP_FAILED', payload: { error } })
return
}
// 2. check Git is installed.
// Should wait for workspace before running otherwise requires access to root folder
const isGitInstalled = await gitVersion()
if (!isGitInstalled) {
this.send({ type: 'GIT_NOT_INSTALLED' })
const error: E.ErrorMessage = {
type: 'GitNotFound',
message: '',
actions: [
{
label: 'Check Again',
transition: 'RETRY',
},
],
}
this.send({ type: 'VALIDATE_SETUP_FAILED', payload: { error } })
return
}
this.send({ type: 'SETUP_VALIDATED' })
return
case 'EDITOR_REQUEST_WORKSPACE':
console.log('request workspace')
openWorkspace()
return
// load step actions (git commits, commands, open files)
Expand All @@ -146,6 +183,24 @@ class Channel implements Channel {
}
// send to webview
public send = async (action: T.Action) => {
// Error middleware
if (action?.payload?.error?.type) {
// load error markdown message
const error = action.payload.error
const errorMarkdownFile = join(__dirname, '..', '..', 'errors', `${action.payload.error.type}.md`)
const errorMarkdown = await readFileAsync(errorMarkdownFile).catch(() => {
// onError(new Error(`Error Markdown file not found for ${action.type}`))
})

// log error to console for safe keeping
console.log(`ERROR:\n ${errorMarkdown}`)

if (errorMarkdown) {
// add a clearer error message for the user
error.message = `${errorMarkdown}\n${error.message}`
}
}

// action may be an object.type or plain string
const actionType: string = typeof action === 'string' ? action : action.type
switch (actionType) {
Expand All @@ -160,8 +215,9 @@ class Channel implements Channel {
saveCommit()
}

const success = await this.postMessage(action)
if (!success) {
// send message
const sentToClient = await this.postMessage(action)
if (!sentToClient) {
throw new Error(`Message post failure: ${JSON.stringify(action)}`)
}
}
Expand Down
48 changes: 31 additions & 17 deletions src/services/git/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as TT from 'typings/tutorial'
import node from '../node'
import logger from '../logger'
import onError from '../sentry/onError'

const gitOrigin = 'coderoad'

const stashAllFiles = async () => {
const stashAllFiles = async (): Promise<never | void> => {
// stash files including untracked (eg. newly created file)
const { stdout, stderr } = await node.exec(`git stash --include-untracked`)
if (stderr) {
Expand All @@ -13,7 +13,7 @@ const stashAllFiles = async () => {
}
}

const cherryPickCommit = async (commit: string, count = 0): Promise<void> => {
const cherryPickCommit = async (commit: string, count = 0): Promise<never | void> => {
if (count > 1) {
console.warn('cherry-pick failed')
return
Expand All @@ -37,7 +37,7 @@ const cherryPickCommit = async (commit: string, count = 0): Promise<void> => {
SINGLE git cherry-pick %COMMIT%
if fails, will stash all and retry
*/
export function loadCommit(commit: string): Promise<void> {
export function loadCommit(commit: string): Promise<never | void> {
return cherryPickCommit(commit)
}

Expand All @@ -46,7 +46,7 @@ export function loadCommit(commit: string): Promise<void> {
git commit -am '${level}/${step} complete'
*/

export async function saveCommit(message: string): Promise<void> {
export async function saveCommit(message: string): Promise<never | void> {
const { stdout, stderr } = await node.exec(`git commit -am '${message}'`)
if (stderr) {
console.error(stderr)
Expand All @@ -55,7 +55,7 @@ export async function saveCommit(message: string): Promise<void> {
logger(['save with commit & continue stdout', stdout])
}

export async function clear(): Promise<void> {
export async function clear(): Promise<Error | void> {
try {
// commit progress to git
const { stderr } = await node.exec('git reset HEAD --hard && git clean -fd')
Expand All @@ -82,23 +82,38 @@ export async function version(): Promise<string | null> {
return null
}

async function init(): Promise<void> {
async function init(): Promise<Error | void> {
const { stderr } = await node.exec('git init')
if (stderr) {
const error = new Error('Error initializing Git')
onError(error)
throw error
throw new Error('Error initializing Git')
}
}

export async function initIfNotExists(): Promise<void> {
export async function initIfNotExists(): Promise<never | void> {
const hasGitInit = node.exists('.git')
if (!hasGitInit) {
await init()
}
}

export async function addRemote(repo: string): Promise<void> {
export async function checkRemoteConnects(repo: TT.TutorialRepo): Promise<never | void> {
// check for git repo
const externalRepoExists = await node.exec(`git ls-remote --exit-code --heads ${repo.uri}`)
if (externalRepoExists.stderr) {
// no repo found or no internet connection
throw new Error(externalRepoExists.stderr)
}
// check for git repo branch
const { stderr, stdout } = await node.exec(`git ls-remote --exit-code --heads ${repo.uri} ${repo.branch}`)
if (stderr) {
throw new Error(stderr)
}
if (!stdout || !stdout.length) {
throw new Error('Tutorial branch does not exist')
}
}

export async function addRemote(repo: string): Promise<never | void> {
const { stderr } = await node.exec(`git remote add ${gitOrigin} ${repo} && git fetch ${gitOrigin}`)
if (stderr) {
const alreadyExists = stderr.match(`${gitOrigin} already exists.`)
Expand Down Expand Up @@ -126,14 +141,13 @@ export async function checkRemoteExists(): Promise<boolean> {
}
}

export async function setupRemote(repo: string): Promise<void> {
export async function setupCodeRoadRemote(repo: string): Promise<never | void> {
// check coderoad remote not taken
const hasRemote = await checkRemoteExists()
// git remote add coderoad tutorial
// git fetch coderoad
if (!hasRemote) {
await addRemote(repo)
} else {
throw new Error('A Remote is already configured')
if (hasRemote) {
throw new Error('A CodeRoad remote is already configured')
}
await addRemote(repo)
}
5 changes: 4 additions & 1 deletion src/services/workspace/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as vscode from 'vscode'
import * as fs from 'fs'
import { promisify } from 'util'

const readDir = promisify(fs.readdir)

export const openWorkspace = () => {
const openInNewWindow = false
Expand All @@ -9,7 +12,7 @@ export const openWorkspace = () => {
export const checkWorkspaceEmpty = async (dirname: string) => {
let files
try {
files = await fs.promises.readdir(dirname)
files = await readDir(dirname)
} catch (error) {
throw new Error('Failed to check workspace')
}
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"emitDecoratorMetadata": true,
"paths": {
"typings": ["../typings/index.d.ts"],
"typings/tutorial": ["../typings/tutorial.d.ts"]
"typings/tutorial": ["../typings/tutorial.d.ts"],
"typings/error": ["../typings/error.d.ts"]
},
"allowJs": true,
"removeComments": true
Expand Down
Loading