Skip to content
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

Add initial Gitlab support #399

Merged
merged 21 commits into from
Nov 19, 2018
7 changes: 7 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ export {
export {
ProjectPersister,
} from "./lib/operations/generate/generatorUtils";
export {
GitlabRepoCreationParameters,
} from "./lib/operations/generate/GitlabRepoCreationParameters";
export {
RepoCreationParameters,
} from "./lib/operations/generate/RepoCreationParameters";
Expand Down Expand Up @@ -246,3 +249,7 @@ export * from "./lib/util/spawn";
export {
Maker,
} from "./lib/util/constructionUtils";
export * from "./lib/operations/common/gitlabRepoLoader";
export * from "./lib/operations/common/GitlabPrivateTokenCredentials";
export * from "./lib/operations/common/GitlabRepoRef";
export * from "./lib/operations/generate/GitlabRepoCreationParameters";
9 changes: 8 additions & 1 deletion lib/operations/common/AbstractRemoteRepoRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
*/

import { ActionResult } from "../../action/ActionResult";
import { isBasicAuthCredentials } from "../../operations/common/BasicAuthCredentials";
import { Configurable } from "../../project/git/Configurable";
import { isBasicAuthCredentials } from "./BasicAuthCredentials";
import { isGitlabPrivateTokenCredentials } from "./GitlabPrivateTokenCredentials";
import {
isTokenCredentials,
ProjectOperationCredentials,
Expand All @@ -41,6 +42,8 @@ export abstract class AbstractRemoteRepoRef implements RemoteRepoRef {

public readonly apiBase: string;

public readonly abstract kind: string;

/**
* Remote url not including scheme or trailing /
*/
Expand Down Expand Up @@ -87,6 +90,10 @@ export abstract class AbstractRemoteRepoRef implements RemoteRepoRef {
return `${this.scheme}${encodeURIComponent(creds.username)}:${encodeURIComponent(creds.password)}@` +
`${this.remoteBase}/${this.pathComponent}.git`;
}
if (isGitlabPrivateTokenCredentials(creds)) {
return `${this.scheme}gitlab-ci-token:${creds.privateToken}@` +
`${this.remoteBase}/${this.pathComponent}.git`;
}
if (!isTokenCredentials(creds)) {
throw new Error("Only token or basic auth supported");
}
Expand Down
2 changes: 2 additions & 0 deletions lib/operations/common/BitBucketRepoRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const BitBucketDotComBase = "https://bitbucket.org/api/2.0";

export class BitBucketRepoRef extends AbstractRemoteRepoRef {

public readonly kind = "bitbucket";

constructor(owner: string,
repo: string,
sha: string = "master",
Expand Down
2 changes: 2 additions & 0 deletions lib/operations/common/BitBucketServerRepoRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export class BitBucketServerRepoRef extends AbstractRemoteRepoRef {

public readonly ownerType: "projects" | "users";

public readonly kind = "bitbucketserver";

private httpStrategy = process.env.ATOMIST_CURL_FOR_BITBUCKET ? CurlHttpStrategy : AxiosHttpStrategy;

/**
Expand Down
13 changes: 13 additions & 0 deletions lib/operations/common/GitlabPrivateTokenCredentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ProjectOperationCredentials } from "./ProjectOperationCredentials";

/**
* Credentials that uses Gitlab private tokens
*/
export interface GitlabPrivateTokenCredentials extends ProjectOperationCredentials {
privateToken: string;
}

export function isGitlabPrivateTokenCredentials(poc: ProjectOperationCredentials): poc is GitlabPrivateTokenCredentials {
const q = poc as GitlabPrivateTokenCredentials;
return q.privateToken !== undefined;
}
155 changes: 155 additions & 0 deletions lib/operations/common/GitlabRepoRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import * as url from "url";
import {
ActionResult,
successOn,
} from "../../action/ActionResult";
import { automationClientInstance } from "../../globals";
import { Configurable } from "../../project/git/Configurable";
import { HttpMethod } from "../../spi/http/httpClient";
import { logger } from "../../util/logger";
import { AbstractRemoteRepoRef } from "./AbstractRemoteRepoRef";
import { GitlabPrivateTokenCredentials } from "./GitlabPrivateTokenCredentials";
import { GitShaRegExp } from "./params/validationPatterns";
import { ProjectOperationCredentials } from "./ProjectOperationCredentials";
import { ProviderType } from "./RepoId";

export const GitlabDotComApiBase = "https://gitlab.com/api/v4";
export const GitlabDotComRemoteUrl = "https://gitlab.com/";

/**
* Repository reference implementation for Gitlab
*/
export class GitlabRepoRef extends AbstractRemoteRepoRef {

public static from(params: {
owner: string,
repo: string,
sha?: string,
rawApiBase?: string,
path?: string,
gitlabRemoteUrl?: string,
branch?: string,
}): GitlabRepoRef {
if (params.sha && !params.sha.match(GitShaRegExp.pattern)) {
throw new Error("You provided an invalid SHA: " + params.sha);
}
const result = new GitlabRepoRef(params.owner, params.repo, params.sha, params.rawApiBase, params.gitlabRemoteUrl, params.path);
result.branch = params.branch;
return result;
}

lievendoclo marked this conversation as resolved.
Show resolved Hide resolved
private static concatUrl(base: string, segment: string): string {
if (base.endsWith("/")) {
if (segment.startsWith("/")) {
return base + segment.substr(1);
} else {
return base + segment;
}
} else {
if (segment.startsWith("/")) {
return base + segment;
} else {
return base + "/" + segment;
}
}
}

public readonly kind = "gitlab";

private constructor(owner: string,
repo: string,
sha: string,
public apiBase = GitlabDotComApiBase,
gitlabRemoteUrl: string = GitlabDotComRemoteUrl,
path?: string) {
super(apiBase === GitlabDotComApiBase ? ProviderType.gitlab_com : ProviderType.gitlab_enterprise,
gitlabRemoteUrl,
apiBase,
owner,
repo,
sha,
path);
}

public async createRemote(creds: ProjectOperationCredentials, description: string, visibility): Promise<ActionResult<this>> {
const gitlabUrl = GitlabRepoRef.concatUrl(this.apiBase, `projects`);
const httpClient = automationClientInstance().configuration.http.client.factory.create();
return httpClient.exchange(gitlabUrl, {
method: HttpMethod.Post,
body: {
name: `${this.repo}`,
visibility,
},
headers: {
"Private-Token": (creds as GitlabPrivateTokenCredentials).privateToken,
"Content-Type": "application/json",
},

}).then(response => {
return {
target: this,
success: true,
response,
};
}).catch(err => {
logger.error(`Error attempting to raise PR. ${url} ${err}`);
return Promise.reject(err);
});
}

public deleteRemote(creds: ProjectOperationCredentials): Promise<ActionResult<this>> {
const httpClient = automationClientInstance().configuration.http.client.factory.create();
const gitlabUrl = GitlabRepoRef.concatUrl(this.apiBase, `project/${this.owner}%2f${this.repo}`);
logger.debug(`Making request to '${url}' to delete repo`);
return httpClient.exchange(gitlabUrl, {
method: HttpMethod.Delete,
headers: {
"Private-Token": (creds as GitlabPrivateTokenCredentials).privateToken,
"Content-Type": "application/json",
},
}).then(response => {
return {
target: this,
success: true,
response,
};
}).catch(err => {
logger.error("Error attempting to delete repository: " + err);
return Promise.reject(err);
});
}

public setUserConfig(credentials: ProjectOperationCredentials, project: Configurable): Promise<ActionResult<any>> {
return Promise.resolve(successOn(this));
lievendoclo marked this conversation as resolved.
Show resolved Hide resolved
}

public raisePullRequest(credentials: ProjectOperationCredentials,
title: string, body: string, head: string, base: string): Promise<ActionResult<this>> {
const httpClient = automationClientInstance().configuration.http.client.factory.create();
const gitlabUrl = GitlabRepoRef.concatUrl(this.apiBase, `projects/${this.owner}%2f${this.repo}/merge_requests`);
logger.debug(`Making request to '${url}' to raise PR`);
return httpClient.exchange(gitlabUrl, {
method: HttpMethod.Post,
body: {
id: `${this.owner}%2f${this.repo}`,
title,
description: body,
source_branch: head,
target_branch: base,
},
headers: {
"Private-Token": (credentials as GitlabPrivateTokenCredentials).privateToken,
"Content-Type": "application/json",
},
}).then(response => {
return {
target: this,
success: true,
response,
};
}).catch(err => {
logger.error(`Error attempting to raise PR. ${url} ${err}`);
return Promise.reject(err);
});
}
}
3 changes: 3 additions & 0 deletions lib/operations/common/RepoId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export enum ProviderType {
github_com,
ghe,
bitbucket,
gitlab_com,
gitlab_enterprise,
}

/**
Expand All @@ -64,6 +66,7 @@ export enum ProviderType {
*/
export interface RemoteRepoRef extends RepoRef {

readonly kind: string;
/**
* Remote base
*/
Expand Down
33 changes: 33 additions & 0 deletions lib/operations/common/gitlabRepoLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
DefaultDirectoryManager,
GitCommandGitProject,
} from "../../project/git/GitCommandGitProject";
import { GitProject } from "../../project/git/GitProject";
import {
DefaultCloneOptions,
DirectoryManager,
} from "../../spi/clone/DirectoryManager";
import { GitlabRepoRef } from "./GitlabRepoRef";
import { ProjectOperationCredentials } from "./ProjectOperationCredentials";
import { isRemoteRepoRef } from "./RepoId";
import { RepoLoader } from "./repoLoader";

/**
* Materialize from gitlab
* @param credentials provider token
* @param directoryManager strategy for handling local storage
* @return function to materialize repos
*/
export function gitlabRepoLoader(credentials: ProjectOperationCredentials,
directoryManager: DirectoryManager = DefaultDirectoryManager): RepoLoader<GitProject> {
return repoId => {
// Default it if it isn't already a Gitlab repo ref
const gid = isRemoteRepoRef(repoId) ? repoId : GitlabRepoRef.from({
owner: repoId.owner,
repo: repoId.repo,
sha: repoId.sha,
branch: repoId.branch,
});
return GitCommandGitProject.cloned(credentials, gid, DefaultCloneOptions, directoryManager);
};
}
84 changes: 84 additions & 0 deletions lib/operations/common/params/GitlabTargetsParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
MappedParameter,
MappedParameters,
Parameter,
Parameters,
} from "../../../decorators";
import { ValidationResult } from "../../../SmartParameters";
import { GitlabPrivateTokenCredentials } from "../GitlabPrivateTokenCredentials";
import { GitlabRepoRef } from "../GitlabRepoRef";
import { ProjectOperationCredentials } from "../ProjectOperationCredentials";
import { TargetsParams } from "./TargetsParams";
import { GitBranchRegExp } from "./validationPatterns";

@Parameters()
export class GitlabTargetsParams extends TargetsParams {

@MappedParameter(MappedParameters.GitHubApiUrl, false)
public apiUrl: string;

@MappedParameter(MappedParameters.GitHubOwner, false)
public owner: string;

@MappedParameter(MappedParameters.GitHubRepository, false)
public repo: string;

@MappedParameter(MappedParameters.GitHubUrl, false)
public url: string;

@Parameter({
description: "Branch or ref. Defaults to 'master'",
...GitBranchRegExp,
required: false,
})
public sha: string = "master";

@Parameter({ description: "regex", required: false })
public repos: string = ".*";

@Parameter({ description: "Repository visibility. 'public' or 'private", required: false })
public visibility: "public" | "private";

get credentials(): ProjectOperationCredentials {
const creds: GitlabPrivateTokenCredentials = { privateToken: this.token };
return creds;
}

get description(): string {
return "Gitlab";
}

constructor(public token: string) {
super();
}

/**
* Return a single RepoRef or undefined if we're not identifying a single repo
* @return {RepoRef}
*/
get repoRef(): GitlabRepoRef {
return (!!this.owner && !!this.repo && !this.usesRegex) ?
GitlabRepoRef.from({
owner: this.owner,
repo: this.repo,
sha: this.sha,
rawApiBase: this.apiUrl,
gitlabRemoteUrl: this.url,
}) :
undefined;
}

public bindAndValidate(): ValidationResult {
if (!this.repo) {
if (!this.repos) {
return {
message:
"If not executing in a mapped channel, must identify a repo via: `targets.owner`" +
"and `targets.repo`, or a repo name regex via `targets.repos`",
};
}
this.repo = this.repos;
}
}

}
Loading