Skip to content

Commit 6b081dd

Browse files
committed
[dashboard] Allow creating new Projects from templates
1 parent 236c1bb commit 6b081dd

File tree

10 files changed

+103
-5
lines changed

10 files changed

+103
-5
lines changed

components/dashboard/src/projects/Projects.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { toRemoteURL } from "./render-utils";
1919
import ContextMenu from "../components/ContextMenu";
2020
import ConfirmationModal from "../components/ConfirmationModal"
2121
import { prebuildStatusIcon } from "./Prebuilds";
22+
import { UserContext } from "../user-context";
2223

2324
export default function () {
2425
const location = useLocation();
@@ -225,5 +226,34 @@ export default function () {
225226
</div>
226227
</div>
227228
)}
229+
<NewRepositoryFromTemplate />
228230
</>;
229231
}
232+
233+
function NewRepositoryFromTemplate() {
234+
const { user } = useContext(UserContext);
235+
const [ templateUrl, setTemplateUrl ] = useState<string>('');
236+
const [ repoOwner, setRepoOwner ] = useState<string>(user?.name || '');
237+
const [ repoName, setRepoName ] = useState<string>('');
238+
239+
const create = async () => {
240+
try {
241+
const repository = await getGitpodService().server.createRepositoryFromTemplate({
242+
owner: repoOwner,
243+
repo: repoName,
244+
templateUrl,
245+
});
246+
console.log('created repository:', repository);
247+
} catch (error) {
248+
console.error('could not create repository:', error);
249+
}
250+
}
251+
252+
return <div className="app-container">
253+
<h1>Create Git Repository</h1>
254+
<label>Template URL: <input type="text" value={templateUrl} onChange={e => setTemplateUrl(e.target.value)} /></label>
255+
<label>Git scope: <input type="text" value={repoOwner} onChange={e => setRepoOwner(e.target.value)} /></label>
256+
<label>Repository name: <input type="text" value={repoName} onChange={e => setRepoName(e.target.value)} /></label>
257+
<button onClick={create}>Create</button>
258+
</div>;
259+
}

components/gitpod-protocol/src/gitpod-service.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
WhitelistedRepository, WorkspaceImageBuild, AuthProviderInfo, Branding, CreateWorkspaceMode,
1010
Token, UserEnvVarValue, ResolvePluginsParams, PreparePluginUploadParams, Terms,
1111
ResolvedPlugins, Configuration, InstallPluginsParams, UninstallPluginParams, UserInfo, GitpodTokenType,
12-
GitpodToken, AuthProviderEntry, GuessGitTokenScopesParams, GuessedGitTokenScopes
12+
GitpodToken, AuthProviderEntry, GuessGitTokenScopesParams, GuessedGitTokenScopes, Repository
1313
} from './protocol';
1414
import {
1515
Team, TeamMemberInfo,
@@ -128,6 +128,7 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
128128
// Projects
129129
getProviderRepositoriesForUser(params: GetProviderRepositoriesParams): Promise<ProviderRepository[]>;
130130
createProject(params: CreateProjectParams): Promise<Project>;
131+
createRepositoryFromTemplate(params: CreateRepositoryFromTemplateParams): Promise<Repository>;
131132
deleteProject(projectId: string): Promise<void>;
132133
getTeamProjects(teamId: string): Promise<Project[]>;
133134
getUserProjects(): Promise<Project[]>;
@@ -245,6 +246,11 @@ export interface CreateProjectParams {
245246
userId?: string;
246247
appInstallationId: string;
247248
}
249+
export interface CreateRepositoryFromTemplateParams {
250+
owner: string;
251+
repo: string;
252+
templateUrl: string;
253+
}
248254
export interface FindPrebuildsParams {
249255
projectId: string;
250256
branch?: string;

components/server/src/auth/rate-limiter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig {
9191
"deleteTeam": { group: "default", points: 1 },
9292
"getProviderRepositoriesForUser": { group: "default", points: 1 },
9393
"createProject": { group: "default", points: 1 },
94+
"createRepositoryFromTemplate": { group: "default", points: 1 },
9495
"getTeamProjects": { group: "default", points: 1 },
9596
"getUserProjects": { group: "default", points: 1 },
9697
"deleteProject": { group: "default", points: 1 },

components/server/src/bitbucket/bitbucket-repository-provider.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ export class BitbucketRepositoryProvider implements RepositoryProvider {
2626
return { host, owner, name, cloneUrl, description, avatarUrl, webUrl };
2727
}
2828

29+
async createRepositoryFromTemplate(user: User, owner: string, repo: string, templateRepo: string): Promise<Repository> {
30+
// todo
31+
throw new Error("not implemented yet");
32+
}
33+
2934
async getBranch(user: User, owner: string, repo: string, branch: string): Promise<Branch> {
3035
// todo
3136
throw new Error("not implemented");

components/server/src/github/api.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,11 +240,17 @@ export class GitHubRestApi {
240240
}
241241

242242
public async getRepository(user: User, params: RestEndpointMethodTypes["repos"]["get"]["parameters"]): Promise<Repository> {
243-
const key = `getRepository:${params.owner}/${params.owner}:${user.id}`;
243+
const key = `getRepository:${params.owner}/${params.repo}:${user.id}`;
244244
const response = await this.runWithCache(key, user, (api) => api.repos.get(params));
245245
return response.data;
246246
}
247247

248+
public async createRepositoryFromTemplate(user: User, params: RestEndpointMethodTypes["repos"]["createUsingTemplate"]["parameters"]): Promise<Repository> {
249+
const key = `createRepositoryFromTemplate:${params.template_owner}/${params.template_repo}:${params.owner}/${params.name}:${user.id}`;
250+
const response = await this.runWithCache(key, user, (api) => api.repos.createUsingTemplate(params));
251+
return response.data;
252+
}
253+
248254
public async getBranch(user: User, params: RestEndpointMethodTypes["repos"]["getBranch"]["parameters"]): Promise<Branch> {
249255
const key = `getBranch:${params.owner}/${params.owner}/${params.branch}:${user.id}`;
250256
const getBranchResponse = (await this.runWithCache(key, user, (api) => api.repos.getBranch(params))) as RestEndpointMethodTypes["repos"]["getBranch"]["response"];

components/server/src/github/github-repository-provider.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ export class GithubRepositoryProvider implements RepositoryProvider {
2828
return { host, owner, name: repo, cloneUrl, description, avatarUrl, webUrl, defaultBranch };
2929
}
3030

31+
async createRepositoryFromTemplate(user: User, owner: string, repo: string, templateUrl: string): Promise<Repository> {
32+
const { owner: template_owner, repo: template_repo } = RepoURL.parseRepoUrl(templateUrl)!;
33+
await this.github.createRepositoryFromTemplate(user, {
34+
owner,
35+
name: repo,
36+
template_owner,
37+
template_repo,
38+
});
39+
return this.getRepo(user, owner, repo);
40+
}
41+
3142
async getBranch(user: User, owner: string, repo: string, branch: string): Promise<Branch> {
3243
const result = await this.github.getBranch(user, { repo, owner, branch });
3344
return result;

components/server/src/gitlab/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class GitLabApi {
5454
// "description": "404 Commit Not Found"
5555
// }
5656

57-
return new GitLab.ApiError(`GitLab Request Error: ${error?.description}`, error);
57+
return new GitLab.ApiError(`GitLab Request Error: ${JSON.stringify(error.description)}`, error);
5858
}
5959
log.error(`GitLab request error`, error);
6060
throw error;

components/server/src/gitlab/gitlab-repository-provider.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,29 @@ export class GitlabRepositoryProvider implements RepositoryProvider {
3333
return { host, owner, name, cloneUrl, description, avatarUrl, webUrl, defaultBranch };
3434
}
3535

36+
async createRepositoryFromTemplate(user: User, owner: string, repo: string, templateUrl: string): Promise<Repository> {
37+
const { owner: template_owner, repo: template_repo } = RepoURL.parseRepoUrl(templateUrl)!;
38+
const template = await this.gitlab.run<GitLab.Project>(user, async g => {
39+
return g.Projects.show(`${template_owner}/${template_repo}`);
40+
});
41+
if (GitLab.ApiError.is(template)) {
42+
throw template;
43+
}
44+
const project = await this.gitlab.run<GitLab.Project>(user, async g => {
45+
return g.Projects.create({
46+
name: repo,
47+
template_name: template.name,
48+
// use_custom_template: true,
49+
// template_project_id: template.id,
50+
// group_with_project_templates_id: template.namespace.id,
51+
});
52+
});
53+
if (GitLab.ApiError.is(project)) {
54+
throw project;
55+
}
56+
return this.getRepo(user, owner, project.path);
57+
}
58+
3659
async getBranch(user: User, owner: string, repo: string, branch: string): Promise<Branch> {
3760
const response = await this.gitlab.run<GitLab.Branch>(user, async g => {
3861
return g.Branches.show(`${owner}/${repo}`, branch);

components/server/src/repohost/repository-provider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { Branch, CommitInfo, Repository, User } from "@gitpod/gitpod-protocol"
1010
export const RepositoryProvider = Symbol('RepositoryProvider');
1111
export interface RepositoryProvider {
1212
getRepo(user: User, owner: string, repo: string): Promise<Repository>;
13+
createRepositoryFromTemplate(user: User, owner: string, repo: string, templateUrl: string): Promise<Repository>;
1314
getBranch(user: User, owner: string, repo: string, branch: string): Promise<Branch>;
1415
getBranches(user: User, owner: string, repo: string): Promise<Branch[]>;
15-
getCommitInfo(user: User, owner: string, repo: string, ref: string): Promise<CommitInfo | undefined>
16+
getCommitInfo(user: User, owner: string, repo: string, ref: string): Promise<CommitInfo | undefined>;
1617
}

components/server/src/workspace/gitpod-server-impl.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { BlobServiceClient } from "@gitpod/content-service/lib/blobs_grpc_pb";
88
import { DownloadUrlRequest, DownloadUrlResponse, UploadUrlRequest, UploadUrlResponse } from '@gitpod/content-service/lib/blobs_pb';
99
import { AppInstallationDB, UserDB, UserMessageViewsDB, WorkspaceDB, DBWithTracing, TracedWorkspaceDB, DBGitpodToken, DBUser, UserStorageResourcesDB, TeamDB } from '@gitpod/gitpod-db/lib';
10-
import { AuthProviderEntry, AuthProviderInfo, Branding, CommitContext, Configuration, CreateWorkspaceMode, DisposableCollection, GetWorkspaceTimeoutResult, GitpodClient, GitpodServer, GitpodToken, GitpodTokenType, InstallPluginsParams, PermissionName, PortVisibility, PrebuiltWorkspace, PrebuiltWorkspaceContext, PreparePluginUploadParams, ResolvedPlugins, ResolvePluginsParams, SetWorkspaceTimeoutResult, StartPrebuildContext, StartWorkspaceResult, Terms, Token, UninstallPluginParams, User, UserEnvVar, UserEnvVarValue, UserInfo, WhitelistedRepository, Workspace, WorkspaceContext, WorkspaceCreationResult, WorkspaceImageBuild, WorkspaceInfo, WorkspaceInstance, WorkspaceInstancePort, WorkspaceInstanceUser, WorkspaceTimeoutDuration, GuessGitTokenScopesParams, GuessedGitTokenScopes, Team, TeamMemberInfo, TeamMembershipInvite, CreateProjectParams, Project, ProviderRepository, TeamMemberRole, WithDefaultConfig, FindPrebuildsParams, PrebuildWithStatus, StartPrebuildResult, ClientHeaderFields } from '@gitpod/gitpod-protocol';
10+
import { AuthProviderEntry, AuthProviderInfo, Branding, CommitContext, Configuration, CreateWorkspaceMode, DisposableCollection, GetWorkspaceTimeoutResult, GitpodClient, GitpodServer, GitpodToken, GitpodTokenType, InstallPluginsParams, PermissionName, PortVisibility, PrebuiltWorkspace, PrebuiltWorkspaceContext, PreparePluginUploadParams, ResolvedPlugins, ResolvePluginsParams, SetWorkspaceTimeoutResult, StartPrebuildContext, StartWorkspaceResult, Terms, Token, UninstallPluginParams, User, UserEnvVar, UserEnvVarValue, UserInfo, WhitelistedRepository, Workspace, WorkspaceContext, WorkspaceCreationResult, WorkspaceImageBuild, WorkspaceInfo, WorkspaceInstance, WorkspaceInstancePort, WorkspaceInstanceUser, WorkspaceTimeoutDuration, GuessGitTokenScopesParams, GuessedGitTokenScopes, Team, TeamMemberInfo, TeamMembershipInvite, CreateProjectParams, Project, ProviderRepository, TeamMemberRole, WithDefaultConfig, FindPrebuildsParams, PrebuildWithStatus, StartPrebuildResult, ClientHeaderFields, Repository, CreateRepositoryFromTemplateParams } from '@gitpod/gitpod-protocol';
1111
import { AccountStatement } from "@gitpod/gitpod-protocol/lib/accounting-protocol";
1212
import { AdminBlockUserRequest, AdminGetListRequest, AdminGetListResult, AdminGetWorkspacesRequest, AdminModifyPermanentWorkspaceFeatureFlagRequest, AdminModifyRoleOrPermissionRequest, WorkspaceAndInstance } from '@gitpod/gitpod-protocol/lib/admin-protocol';
1313
import { GetLicenseInfoResult, LicenseFeature, LicenseValidationResult } from '@gitpod/gitpod-protocol/lib/license-protocol';
@@ -1548,6 +1548,21 @@ export class GitpodServerImpl<Client extends GitpodClient, Server extends Gitpod
15481548
return project;
15491549
}
15501550

1551+
public async createRepositoryFromTemplate(params: CreateRepositoryFromTemplateParams): Promise<Repository> {
1552+
const user = this.checkUser("createRepositoryFromTemplate");
1553+
const templateRepoUrl = RepoURL.parseRepoUrl(params.templateUrl);
1554+
if (!templateRepoUrl) {
1555+
throw new Error(`Could not parse template repository URL: ${params.templateUrl}`);
1556+
}
1557+
const { host } = templateRepoUrl;
1558+
const hostContext = this.hostContextProvider.get(host);
1559+
if (!hostContext || !hostContext.services) {
1560+
throw new Error(`Unsupported template repository host: ${host}`);
1561+
}
1562+
const repoProvider = hostContext.services.repositoryProvider;
1563+
return await repoProvider.createRepositoryFromTemplate(user, params.owner, params.repo, params.templateUrl);
1564+
}
1565+
15511566
public async deleteProject(projectId: string): Promise<void> {
15521567
const user = this.checkUser("deleteProject");
15531568
await this.guardProjectOperation(user, projectId, "delete");

0 commit comments

Comments
 (0)