diff --git a/components/dashboard/src/components/MonacoEditor.tsx b/components/dashboard/src/components/MonacoEditor.tsx index 9e985e4575882b..62f49bcddba388 100644 --- a/components/dashboard/src/components/MonacoEditor.tsx +++ b/components/dashboard/src/components/MonacoEditor.tsx @@ -8,7 +8,46 @@ import { useContext, useEffect, useRef } from "react"; import * as monaco from "monaco-editor"; import { ThemeContext } from "../theme-context"; -export default function MonacoEditor(props: { classes: string, disabled?: boolean, language: string, value: string, onChange: (value: string) => void }) { +monaco.editor.defineTheme('gitpod', { + base: 'vs', + inherit: true, + rules: [], + colors: {}, +}); +monaco.editor.defineTheme('gitpod-disabled', { + base: 'vs', + inherit: true, + rules: [], + colors: { + 'editor.background': '#F5F5F4', // Tailwind's warmGray 100 https://tailwindcss.com/docs/customizing-colors + }, +}); +monaco.editor.defineTheme('gitpod-dark', { + base: 'vs-dark', + inherit: true, + rules: [], + colors: { + 'editor.background': '#292524', // Tailwind's warmGray 800 https://tailwindcss.com/docs/customizing-colors + }, +}); +monaco.editor.defineTheme('gitpod-dark-disabled', { + base: 'vs-dark', + inherit: true, + rules: [], + colors: { + 'editor.background': '#44403C', // Tailwind's warmGray 700 https://tailwindcss.com/docs/customizing-colors + }, +}); + +export interface MonacoEditorProps { + classes: string; + disabled?: boolean; + language: string; + value: string; + onChange: (value: string) => void; +} + +export default function MonacoEditor(props: MonacoEditorProps) { const containerRef = useRef(null); const editorRef = useRef(); const { isDark } = useContext(ThemeContext); @@ -22,10 +61,21 @@ export default function MonacoEditor(props: { classes: string, disabled?: boolea enabled: false, }, renderLineHighlight: 'none', + lineNumbers: 'off', + glyphMargin: false, + folding: false, }); editorRef.current.onDidChangeModelContent(() => { props.onChange(editorRef.current!.getValue()); }); + // 8px top margin: https://github.com/Microsoft/monaco-editor/issues/1333 + editorRef.current.changeViewZones(accessor => { + accessor.addZone({ + afterLineNumber: 0, + heightInPx: 8, + domNode: document.createElement('div'), + }); + }); } return () => editorRef.current?.dispose(); }, []); @@ -37,14 +87,13 @@ export default function MonacoEditor(props: { classes: string, disabled?: boolea }, [ props.value ]); useEffect(() => { - monaco.editor.setTheme(isDark ? 'vs-dark' : 'vs'); - }, [ isDark ]); - - useEffect(() => { + monaco.editor.setTheme(props.disabled + ? (isDark ? 'gitpod-dark-disabled' : 'gitpod-disabled') + : (isDark ? 'gitpod-dark' : 'gitpod')); if (editorRef.current) { editorRef.current.updateOptions({ readOnly: props.disabled }); } - }, [ props.disabled ]); + }, [ props.disabled, isDark ]); return
; } \ No newline at end of file diff --git a/components/dashboard/src/components/PrebuildLogs.tsx b/components/dashboard/src/components/PrebuildLogs.tsx index 55a00112f9fa1f..080684fb3bdb9e 100644 --- a/components/dashboard/src/components/PrebuildLogs.tsx +++ b/components/dashboard/src/components/PrebuildLogs.tsx @@ -11,7 +11,12 @@ import { getGitpodService } from "../service/service"; const WorkspaceLogs = React.lazy(() => import('./WorkspaceLogs')); -export default function PrebuildLogs(props: { workspaceId?: string }) { +export interface PrebuildLogsProps { + workspaceId?: string; + onInstanceUpdate?: (instance: WorkspaceInstance) => void; +} + +export default function PrebuildLogs(props: PrebuildLogsProps) { const [ workspace, setWorkspace ] = useState(); const [ workspaceInstance, setWorkspaceInstance ] = useState(); const [ error, setError ] = useState(); @@ -54,6 +59,9 @@ export default function PrebuildLogs(props: { workspaceId?: string }) { }, [ props.workspaceId ]); useEffect(() => { + if (props.onInstanceUpdate && workspaceInstance) { + props.onInstanceUpdate(workspaceInstance); + } switch (workspaceInstance?.status.phase) { // unknown indicates an issue within the system in that it cannot determine the actual phase of // a workspace. This phase is usually accompanied by an error. @@ -107,16 +115,9 @@ export default function PrebuildLogs(props: { workspaceId?: string }) { } }, [ workspaceInstance?.status.phase ]); - return <> - }> - - -
- {workspaceInstance?.status.phase === 'stopped' - ? - : } -
- ; + return }> + + ; } export function watchHeadlessLogs(instanceId: string, onLog: (chunk: string) => void, checkIsDone: () => Promise): DisposableCollection { diff --git a/components/dashboard/src/components/WorkspaceLogs.tsx b/components/dashboard/src/components/WorkspaceLogs.tsx index 56607596e87676..345ef266b576d2 100644 --- a/components/dashboard/src/components/WorkspaceLogs.tsx +++ b/components/dashboard/src/components/WorkspaceLogs.tsx @@ -5,11 +5,20 @@ */ import EventEmitter from 'events'; -import React from 'react'; +import { useContext, useEffect, useRef } from 'react'; import { Terminal, ITerminalOptions, ITheme } from 'xterm'; import { FitAddon } from 'xterm-addon-fit' import 'xterm/css/xterm.css'; -import { DisposableCollection } from '@gitpod/gitpod-protocol'; +import { ThemeContext } from '../theme-context'; + +const darkTheme: ITheme = { + background: '#292524', // Tailwind's warmGray 800 https://tailwindcss.com/docs/customizing-colors +}; +const lightTheme: ITheme = { + background: '#F5F5F4', // Tailwind's warmGray 100 https://tailwindcss.com/docs/customizing-colors + foreground: '#78716C', // Tailwind's warmGray 500 https://tailwindcss.com/docs/customizing-colors + cursor: '#78716C', // Tailwind's warmGray 500 https://tailwindcss.com/docs/customizing-colors +} export interface WorkspaceLogsProps { logsEmitter: EventEmitter; @@ -17,73 +26,66 @@ export interface WorkspaceLogsProps { classes?: string; } -export interface WorkspaceLogsState { -} - -export default class WorkspaceLogs extends React.Component { - protected xTermParentRef: React.RefObject; - protected terminal: Terminal | undefined; - protected fitAddon: FitAddon | undefined; - - constructor(props: WorkspaceLogsProps) { - super(props); - this.xTermParentRef = React.createRef(); - } +export default function WorkspaceLogs(props: WorkspaceLogsProps) { + const xTermParentRef = useRef(null); + const terminalRef = useRef(); + const fitAddon = new FitAddon(); + const { isDark } = useContext(ThemeContext); - private readonly toDispose = new DisposableCollection(); - componentDidMount() { - const element = this.xTermParentRef.current; - if (element === null) { + useEffect(() => { + if (!xTermParentRef.current) { return; } - const theme: ITheme = {}; const options: ITerminalOptions = { cursorBlink: false, disableStdin: true, fontSize: 14, - theme, + theme: darkTheme, scrollback: 9999999, }; - this.terminal = new Terminal(options); - this.fitAddon = new FitAddon(); - this.terminal.loadAddon(this.fitAddon); - this.terminal.open(element); - this.props.logsEmitter.on('logs', logs => { - if (this.fitAddon && this.terminal && logs) { - this.terminal.write(logs); + const terminal = new Terminal(options); + terminalRef.current = terminal; + terminal.loadAddon(fitAddon); + terminal.open(xTermParentRef.current); + props.logsEmitter.on('logs', logs => { + if (terminal && logs) { + terminal.write(logs); } }); - this.toDispose.push(this.terminal); - this.fitAddon.fit(); + fitAddon.fit(); + return function cleanUp() { + terminal.dispose(); + } + }); + useEffect(() => { // Fit terminal on window resize (debounced) let timeout: NodeJS.Timeout | undefined; const onWindowResize = () => { clearTimeout(timeout!); - timeout = setTimeout(() => this.fitAddon!.fit(), 20); + timeout = setTimeout(() => fitAddon.fit(), 20); }; window.addEventListener('resize', onWindowResize); - this.toDispose.push({ - dispose: () => { - clearTimeout(timeout!); - window.removeEventListener('resize', onWindowResize); - } - }); - } + return function cleanUp() { + clearTimeout(timeout!); + window.removeEventListener('resize', onWindowResize); + } + }); - componentDidUpdate() { - if (this.terminal && this.props.errorMessage) { - this.terminal.write(`\n\u001b[38;5;196m${this.props.errorMessage}\u001b[0m`); + useEffect(() => { + if (terminalRef.current && props.errorMessage) { + terminalRef.current.write(`\n\u001b[38;5;196m${props.errorMessage}\u001b[0m`); } - } + }, [ terminalRef.current, props.errorMessage ]); - componentWillUnmount() { - this.toDispose.dispose(); - } + useEffect(() => { + if (!terminalRef.current) { + return; + } + terminalRef.current.setOption('theme', isDark ? darkTheme : lightTheme); + }, [ terminalRef.current, isDark ]); - render() { - return
-
-
; - } + return
+
+
; } diff --git a/components/dashboard/src/icons/Spinner.svg b/components/dashboard/src/icons/Spinner.svg new file mode 100644 index 00000000000000..5ed476c64d224a --- /dev/null +++ b/components/dashboard/src/icons/Spinner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/dashboard/src/icons/SpinnerDark.svg b/components/dashboard/src/icons/SpinnerDark.svg new file mode 100644 index 00000000000000..3698f94217afc4 --- /dev/null +++ b/components/dashboard/src/icons/SpinnerDark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/dashboard/src/icons/StatusDone.svg b/components/dashboard/src/icons/StatusDone.svg new file mode 100644 index 00000000000000..64de51359476da --- /dev/null +++ b/components/dashboard/src/icons/StatusDone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/dashboard/src/icons/StatusFailed.svg b/components/dashboard/src/icons/StatusFailed.svg new file mode 100644 index 00000000000000..55236bd386bcc0 --- /dev/null +++ b/components/dashboard/src/icons/StatusFailed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/dashboard/src/icons/StatusPaused.svg b/components/dashboard/src/icons/StatusPaused.svg new file mode 100644 index 00000000000000..85bd6ad9c2cfe2 --- /dev/null +++ b/components/dashboard/src/icons/StatusPaused.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/dashboard/src/icons/StatusRunning.svg b/components/dashboard/src/icons/StatusRunning.svg new file mode 100644 index 00000000000000..7b2be94ddb78a2 --- /dev/null +++ b/components/dashboard/src/icons/StatusRunning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/dashboard/src/images/prebuild-logs-empty-dark.svg b/components/dashboard/src/images/prebuild-logs-empty-dark.svg new file mode 100644 index 00000000000000..b5daa400491de5 --- /dev/null +++ b/components/dashboard/src/images/prebuild-logs-empty-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/dashboard/src/images/prebuild-logs-empty.svg b/components/dashboard/src/images/prebuild-logs-empty.svg new file mode 100644 index 00000000000000..ce0d1fb3289e2d --- /dev/null +++ b/components/dashboard/src/images/prebuild-logs-empty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/dashboard/src/projects/ConfigureProject.tsx b/components/dashboard/src/projects/ConfigureProject.tsx index fb8ee827c27466..a36c1a7ab6e03a 100644 --- a/components/dashboard/src/projects/ConfigureProject.tsx +++ b/components/dashboard/src/projects/ConfigureProject.tsx @@ -6,12 +6,20 @@ import React, { Suspense, useContext, useEffect, useState } from "react"; import { useLocation, useRouteMatch } from "react-router"; -import { CreateWorkspaceMode, Project, WorkspaceCreationResult } from "@gitpod/gitpod-protocol"; +import { CreateWorkspaceMode, Project, WorkspaceCreationResult, WorkspaceInstance } from "@gitpod/gitpod-protocol"; import PrebuildLogs from "../components/PrebuildLogs"; import TabMenuItem from "../components/TabMenuItem"; import { getGitpodService } from "../service/service"; import { getCurrentTeam, TeamsContext } from "../teams/teams-context"; -import AlertBox from "../components/AlertBox"; +import Header from "../components/Header"; +import Spinner from "../icons/Spinner.svg"; +import SpinnerDark from "../icons/SpinnerDark.svg"; +import StatusDone from "../icons/StatusDone.svg"; +import StatusPaused from "../icons/StatusPaused.svg"; +import StatusRunning from "../icons/StatusRunning.svg"; +import PrebuildLogsEmpty from "../images/prebuild-logs-empty.svg"; +import PrebuildLogsEmptyDark from "../images/prebuild-logs-empty-dark.svg"; +import { ThemeContext } from "../theme-context"; const MonacoEditor = React.lazy(() => import('../components/MonacoEditor')); @@ -56,16 +64,23 @@ export default function () { const [ project, setProject ] = useState(); const [ gitpodYml, setGitpodYml ] = useState(''); const [ dockerfile, setDockerfile ] = useState(''); - const [ editorError, setEditorError ] = useState(null); + const [ editorMessage, setEditorMessage ] = useState(null); const [ selectedEditor, setSelectedEditor ] = useState<'.gitpod.yml'|'.gitpod.Dockerfile'>('.gitpod.yml'); const [ isEditorDisabled, setIsEditorDisabled ] = useState(true); + const [ isDetecting, setIsDetecting ] = useState(true); + const [ prebuildWasTriggered, setPrebuildWasTriggered ] = useState(false); const [ workspaceCreationResult, setWorkspaceCreationResult ] = useState(); + const [ prebuildPhase, setPrebuildPhase ] = useState(); + const { isDark } = useContext(ThemeContext); useEffect(() => { // Disable editing while loading, or when the config comes from Git. + setIsDetecting(true); setIsEditorDisabled(true); - setEditorError(null); + setEditorMessage(null); if (!teams) { + setIsDetecting(false); + setEditorMessage(); return; } (async () => { @@ -73,18 +88,35 @@ export default function () { ? await getGitpodService().server.getTeamProjects(team.id) : await getGitpodService().server.getUserProjects()); const project = projects.find(p => p.name === routeMatch?.params.projectSlug); - if (project) { - setProject(project); - const configString = await getGitpodService().server.fetchProjectRepositoryConfiguration(project.id); - if (configString) { - // TODO(janx): Link to .gitpod.yml directly instead of just the cloneUrl. - setEditorError(A Gitpod configuration already exists in the project's repository.
Please edit it in Gitpod instead.
); - setGitpodYml(configString); - } else { - setIsEditorDisabled(false); - setGitpodYml(project.config && project.config['.gitpod.yml'] || ''); - } + if (!project) { + setIsDetecting(false); + setEditorMessage(); + return; } + setProject(project); + const guessedConfigStringPromise = getGitpodService().server.guessProjectConfiguration(project.id); + const repoConfigString = await getGitpodService().server.fetchProjectRepositoryConfiguration(project.id); + if (repoConfigString) { + // TODO(janx): Link to .gitpod.yml directly instead of just the cloneUrl. + setIsDetecting(false); + setEditorMessage(); + setGitpodYml(repoConfigString); + return; + } + const guessedConfigString = await guessedConfigStringPromise; + setIsDetecting(false); + setIsEditorDisabled(false); + if (guessedConfigString) { + setEditorMessage(); + setGitpodYml(guessedConfigString); + return; + } + if (project.config && project.config['.gitpod.yml']) { + setGitpodYml(project.config['.gitpod.yml']); + return; + } + setEditorMessage(); + setGitpodYml(TASKS.Other); })(); }, [ teams, team ]); @@ -93,11 +125,12 @@ export default function () { return; } // (event.target as HTMLButtonElement).disabled = true; - setEditorError(null); + setEditorMessage(null); if (!!workspaceCreationResult) { setWorkspaceCreationResult(undefined); } try { + setPrebuildWasTriggered(true); await getGitpodService().server.setProjectConfiguration(project.id, gitpodYml); const result = await getGitpodService().server.createWorkspace({ contextUrl: `prebuild/${project.cloneUrl}`, @@ -105,40 +138,109 @@ export default function () { }); setWorkspaceCreationResult(result); } catch (error) { - setEditorError({String(error)}); + setPrebuildWasTriggered(false); + setEditorMessage(); } } + const onInstanceUpdate = (instance: WorkspaceInstance) => { + setPrebuildPhase(instance.status.phase); + } + useEffect(() => { document.title = 'Configure Project — Gitpod' }, []); - return
-

Configure Project

-

Fully-automate your project's dev setup. Learn more

-
-
- {editorError && {editorError}} - {!isEditorDisabled && } - {!!dockerfile &&
+ return <> +
+
+
+
setSelectedEditor('.gitpod.yml')} /> - setSelectedEditor('.gitpod.Dockerfile')} /> -
} + {!!dockerfile && setSelectedEditor('.gitpod.Dockerfile')} />} +
+ {editorMessage} }> {selectedEditor === '.gitpod.yml' && - } + } {selectedEditor === '.gitpod.Dockerfile' && - } + } -
- -
+ {isDetecting &&
+ + Detecting project type ... +
}
-
-

Output

- {!!workspaceCreationResult && } +
+
{workspaceCreationResult + ? + :
+ +

No Recent Prebuild

+

Edit the project configuration on the left to get started. Learn more

+
+ }
+
+ {prebuildWasTriggered && } +
+ + +
+ ; +} + +function EditorMessage(props: { heading: string, message: string, type: 'success' | 'warning' }) { + return
+ {props.heading} + {props.message} +
; +} + +function PrebuildStatus(props: { prebuildPhase?: string, isDark?: boolean }) { + let status = <>; + let details = <>; + switch (props.prebuildPhase) { + case undefined: // Fall through + case 'unknown': + status =
+ + PENDING +
; + details =
+ + Prebuild in progress ... +
; + break; + case 'preparing': // Fall through + case 'pending': // Fall through + case 'creating': // Fall through + case 'initializing': // Fall through + case 'running': // Fall through + case 'interrupted': + status =
+ + RUNNING +
; + details =
+ + Prebuild in progress ... +
; + break; + case 'stopping': // Fall through + case 'stopped': + status =
+ + READY +
; + // TODO(janx): Calculate actual duration from prebuild instance. + details =
+ + 00:34 +
; + break; + } + return
+
{status}
+
{details}
; } diff --git a/components/dashboard/src/projects/Prebuild.tsx b/components/dashboard/src/projects/Prebuild.tsx index 8b2d9d221c718c..72fa09739564ca 100644 --- a/components/dashboard/src/projects/Prebuild.tsx +++ b/components/dashboard/src/projects/Prebuild.tsx @@ -82,7 +82,11 @@ export default function () { return <>
-
- +
+
+ +
+
+ ; } \ No newline at end of file diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index 6eec038ce865a3..b3af655d55b8b0 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -132,6 +132,7 @@ export interface GitpodServer extends JsonRpcServer, AdminServer, triggerPrebuild(projectId: string, branch: string): Promise; setProjectConfiguration(projectId: string, configString: string): Promise; fetchProjectRepositoryConfiguration(projectId: string): Promise; + guessProjectConfiguration(projectId: string): Promise; // content service getContentBlobUploadUrl(name: string): Promise diff --git a/components/server/package.json b/components/server/package.json index 619bc6968d79c4..9fcf6bdcedb340 100644 --- a/components/server/package.json +++ b/components/server/package.json @@ -57,6 +57,7 @@ "express-mysql-session": "^2.1.0", "express-session": "^1.15.6", "fs-extra": "^10.0.0", + "gitpod-yml-inferrer": "^1.1.1", "google-protobuf": "3.15.8", "graphql": "^14.6.0", "graphql-tools": "^4.0.7", @@ -83,7 +84,6 @@ "ws": "^7.4.6" }, "devDependencies": { - "@types/cookie-parser": "^1.4.2", "@graphql-codegen/cli": "^1.19.4", "@graphql-codegen/introspection": "^1.18.1", "@graphql-codegen/typescript": "^1.19.0", @@ -95,6 +95,7 @@ "@types/body-parser": "^1.16.5", "@types/chai": "^4.1.2", "@types/chai-http": "^3.0.4", + "@types/cookie-parser": "^1.4.2", "@types/cors": "^2.8.3", "@types/deep-equal": "^1.0.1", "@types/dotenv": "^4.0.2", diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts index 044d1ef2012485..86113f974f3f0e 100644 --- a/components/server/src/auth/rate-limiter.ts +++ b/components/server/src/auth/rate-limiter.ts @@ -97,6 +97,7 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig { "triggerPrebuild": { group: "default", points: 1 }, "setProjectConfiguration": { group: "default", points: 1 }, "fetchProjectRepositoryConfiguration": { group: "default", points: 1 }, + "guessProjectConfiguration": { group: "default", points: 1 }, "getContentBlobUploadUrl": { group: "default", points: 1 }, "getContentBlobDownloadUrl": { group: "default", points: 1 }, "getGitpodTokens": { group: "default", points: 1 }, diff --git a/components/server/src/projects/projects-service.ts b/components/server/src/projects/projects-service.ts index 6eaa452b155f1f..65641224a1b6d4 100644 --- a/components/server/src/projects/projects-service.ts +++ b/components/server/src/projects/projects-service.ts @@ -6,10 +6,13 @@ import { inject, injectable } from "inversify"; import { DBWithTracing, ProjectDB, TeamDB, TracedWorkspaceDB, UserDB, WorkspaceDB } from "@gitpod/gitpod-db/lib"; -import { Branch, CommitInfo, CreateProjectParams, FindPrebuildsParams, PrebuildInfo, PrebuiltWorkspace, Project, ProjectConfig, User } from "@gitpod/gitpod-protocol"; +import { Branch, CommitContext, CommitInfo, CreateProjectParams, FindPrebuildsParams, PrebuildInfo, PrebuiltWorkspace, Project, ProjectConfig, User, WorkspaceConfig } from "@gitpod/gitpod-protocol"; +import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; import { HostContextProvider } from "../auth/host-context-provider"; -import { parseRepoUrl } from "../repohost"; +import { FileProvider, parseRepoUrl } from "../repohost"; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; +import { ContextParser } from "../workspace/context-parser-service"; +import { ConfigInferrer } from "gitpod-yml-inferrer"; @injectable() export class ProjectsService { @@ -19,6 +22,7 @@ export class ProjectsService { @inject(UserDB) protected readonly userDB: UserDB; @inject(TracedWorkspaceDB) protected readonly workspaceDb: DBWithTracing; @inject(HostContextProvider) protected readonly hostContextProvider: HostContextProvider; + @inject(ContextParser) protected contextParser: ContextParser; async getProject(projectId: string): Promise { return this.projectDB.findProjectById(projectId); @@ -204,8 +208,55 @@ export class ProjectsService { }; } - async setProjectConfiguration(projectId: string, config: ProjectConfig) { + async setProjectConfiguration(projectId: string, config: ProjectConfig): Promise { return this.projectDB.setProjectConfiguration(projectId, config); } + protected async getRepositoryFileProviderAndCommitContext(ctx: TraceContext, user: User, projectId: string): Promise<{fileProvider: FileProvider, commitContext: CommitContext}> { + const project = await this.getProject(projectId); + if (!project) { + throw new Error("Project not found"); + } + const normalizedContextUrl = this.contextParser.normalizeContextURL(project.cloneUrl); + const commitContext = (await this.contextParser.handle(ctx, user, normalizedContextUrl)) as CommitContext; + const { host } = commitContext.repository; + const hostContext = this.hostContextProvider.get(host); + if (!hostContext || !hostContext.services) { + throw new Error(`Cannot fetch repository configuration for host: ${host}`); + } + const fileProvider = hostContext.services.fileProvider; + return { fileProvider, commitContext }; + } + + async fetchProjectRepositoryConfiguration(ctx: TraceContext, user: User, projectId: string): Promise { + const { fileProvider, commitContext } = await this.getRepositoryFileProviderAndCommitContext(ctx, user, projectId); + const configString = await fileProvider.getGitpodFileContent(commitContext, user); + return configString; + } + + async guessProjectConfiguration(ctx: TraceContext, user: User, projectId: string): Promise { + const { fileProvider, commitContext } = await this.getRepositoryFileProviderAndCommitContext(ctx, user, projectId); + const cache: { [path: string]: string } = {}; + const readFile = async (path: string) => { + if (path in cache) { + return cache[path]; + } + const content = await fileProvider.getFileContent(commitContext, user, path); + if (content) { + cache[path] = content; + } + return content; + } + const config: WorkspaceConfig = await new ConfigInferrer().getConfig({ + config: {}, + read: readFile, + exists: async (path: string) => !!(await readFile(path)), + }); + if (!config.tasks) { + return; + } + const configString = `tasks:\n - ${config.tasks.map(task => Object.entries(task).map(([phase, command]) => `${phase}: ${command}`).join('\n ')).join('\n - ')}`; + return configString; + } + } diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index d5d9e40b634f5c..89375408787a15 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -1543,22 +1543,17 @@ export class GitpodServerImpl { + const user = this.checkUser("guessProjectConfiguration"); + const span = opentracing.globalTracer().startSpan("guessProjectConfiguration"); + span.setTag("projectId", projectId); + + await this.guardProjectOperation(user, projectId, "get"); + return this.projectsService.guessProjectConfiguration({ span }, user, projectId); } public async getContentBlobUploadUrl(name: string): Promise { diff --git a/yarn.lock b/yarn.lock index 1394f3cea89a83..4135f7be2e7be1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11096,6 +11096,11 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" +gitpod-yml-inferrer@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/gitpod-yml-inferrer/-/gitpod-yml-inferrer-1.1.1.tgz#79392c981abb5ef5f33ba9f7ac46d832b87ac0c9" + integrity sha512-ccQ4o3iEV7SX07HPSzmqRW5S82xyGiDXP2JfJ9bQVjWKa5JKe0w+jBsmPZImCkOfP9fM/LmgctIHBIcZ3KrPKA== + glob-parent@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"