Skip to content

Commit

Permalink
Adds CodeStream partnership
Browse files Browse the repository at this point in the history
  • Loading branch information
eamodio committed Jan 28, 2021
1 parent 24d872a commit d418897
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 1 deletion.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9111,6 +9111,7 @@
"dayjs": "1.10.4",
"iconv-lite": "0.6.2",
"lodash-es": "4.17.20",
"node-fetch": "3.0.0-beta.9",
"sortablejs": "1.13.0",
"vscode-codicons": "0.0.14",
"vsls": "1.0.3015"
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export enum BuiltInCommands {
ExecuteDocumentSymbolProvider = 'vscode.executeDocumentSymbolProvider',
ExecuteCodeLensProvider = 'vscode.executeCodeLensProvider',
FocusFilesExplorer = 'workbench.files.action.focusFilesExplorer',
InstallExtension = 'workbench.extensions.installExtension',
Open = 'vscode.open',
OpenFolder = 'vscode.openFolder',
OpenInTerminal = 'openInTerminal',
Expand Down
169 changes: 168 additions & 1 deletion src/partners.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,158 @@
'use strict';
import { ExtensionContext } from 'vscode';
import fetch from 'node-fetch';
import {
CancellationTokenSource,
commands,
Disposable,
env,
Extension,
ExtensionContext,
extensions,
Uri,
workspace,
} from 'vscode';
import { ActionRunnerType } from './api/actionRunners';
import { ActionContext, HoverCommandsActionContext } from './api/gitlens';
import { Commands, executeCommand, InviteToLiveShareCommandArgs } from './commands';
import { BuiltInCommands } from './constants';
import { Container } from './container';
import { Strings } from './system';

export async function installExtension<T>(
extensionId: string,
tokenSource: CancellationTokenSource,
timeout: number,
vsix?: Uri,
): Promise<Extension<T> | undefined> {
try {
let timer: any = 0;
const extension = new Promise<Extension<any> | undefined>(resolve => {
const disposable = extensions.onDidChange(() => {
const extension = extensions.getExtension(extensionId);
if (extension != null) {
clearTimeout(timer);
disposable.dispose();

resolve(extension);
}
});

tokenSource.token.onCancellationRequested(() => {
disposable.dispose();

resolve(undefined);
});
});

await commands.executeCommand(BuiltInCommands.InstallExtension, vsix ?? extensionId);
// Wait for extension activation until timeout expires
timer = setTimeout(() => tokenSource.cancel(), timeout);

return extension;
} catch {
tokenSource.cancel();
return undefined;
}
}

export function registerPartnerActionRunners(context: ExtensionContext): void {
registerCodeStream(context);
registerLiveShare(context);
}

function registerCodeStream(context: ExtensionContext): void {
if (extensions.getExtension('codestream.codestream') != null) return;

const subscriptions: Disposable[] = [];

const partnerId = 'codestream';

async function runner(ctx: ActionContext) {
const hashes = [];

for (const repo of await Container.git.getRepositories()) {
const user = await Container.git.getCurrentUser(repo.path);
if (user?.email != null) {
hashes.push(Strings.sha1(`gitlens:${user.email.trim().toLowerCase()}`, 'hex'));
}
}

const config = Container.config.partners?.[partnerId];

const url = (Container.insiders && config?.url) ?? 'https://api.codestream.com/no-auth/gitlens-user';
const body: { emailHashes: string[]; machineIdHash: string; installed?: boolean } = {
emailHashes: hashes,
machineIdHash: Strings.sha1(`gitlens:${env.machineId.trim().toLowerCase()}`, 'hex'),
};

void sendPartnerJsonRequest(url, JSON.stringify(body), 0);

const tokenSource = new CancellationTokenSource();

// Re-play action when/if we can find a matching newly installed runner
const { actionRunners } = Container;
const rerunDisposable = actionRunners.onDidChange(action => {
if (action != null && action !== ctx.type) return;

const runners = actionRunners.get(ctx.type);
if (runners == null || runners.length === 0) return;

const runner = runners.find(
r => r.type === ActionRunnerType.Partner && (r.partnerId === partnerId || r.name === 'CodeStream'),
);
if (runner != null) {
rerunDisposable.dispose();

void runner.run(ctx);
}
});
tokenSource.token.onCancellationRequested(() => rerunDisposable.dispose());

const extension = await installExtension(
'codestream.codestream',
tokenSource,
30000,
Container.insiders && config?.vsix != null ? Uri.file(config.vsix) : undefined,
);

if (extension == null) {
rerunDisposable.dispose();

return;
}

void workspace.fs.writeFile(Uri.joinPath(extension.extensionUri, '.gitlens'), new Uint8Array());

// Unregister the partner runners
Disposable.from(...subscriptions).dispose();

body.installed = true;
void sendPartnerJsonRequest(url, JSON.stringify(body), 0);

// Wait for 30s for new action runner registrations
setTimeout(() => tokenSource.cancel(), 30000);
}

subscriptions.push(
Container.actionRunners.registerBuiltInPartnerInstaller(partnerId, 'createPullRequest', {
name: 'CodeStream',
label: 'Create Pull Request in VS Code',
run: runner,
}),
Container.actionRunners.registerBuiltInPartnerInstaller(partnerId, 'openPullRequest', {
name: 'CodeStream',
label: 'Open Pull Request in VS Code',
run: runner,
}),
Container.actionRunners.registerBuiltInPartnerInstaller(partnerId, 'hover.commands', {
name: 'CodeStream',
label: '$(comment) Leave a Comment',
run: runner,
}),
);
context.subscriptions.push(...subscriptions);
}

function registerLiveShare(context: ExtensionContext) {
context.subscriptions.push(
Container.actionRunners.registerBuiltInPartner<HoverCommandsActionContext>('liveshare', 'hover.commands', {
Expand Down Expand Up @@ -40,3 +185,25 @@ function registerLiveShare(context: ExtensionContext) {
}),
);
}

async function sendPartnerJsonRequest(url: string, body: string, retryCount: number) {
try {
const response = await fetch(url, {
method: 'POST',
body: body,
headers: { 'Content-Type': 'application/json' },
});

if (response.ok) return;

throw new Error(response.statusText);
} catch (ex) {
retryCount++;
if (retryCount > 6) {
// Give up
return;
}

setTimeout(() => sendPartnerJsonRequest(url, body, retryCount), Math.pow(2, retryCount) * 2000);
}
}
18 changes: 18 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1337,6 +1337,11 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"

data-uri-to-buffer@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636"
integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==

dayjs@1.10.4:
version "1.10.4"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2"
Expand Down Expand Up @@ -2075,6 +2080,11 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"

fetch-blob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-2.1.1.tgz#a54ab0d5ed7ccdb0691db77b6674308b23fb2237"
integrity sha512-Uf+gxPCe1hTOFXwkxYyckn8iUSk6CFXGy5VENZKifovUTZC9eUODWSBhOBS7zICGrAetKzdwLMr85KhIcePMAQ==

figures@^1.3.5:
version "1.7.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
Expand Down Expand Up @@ -3503,6 +3513,14 @@ no-case@^3.0.4:
lower-case "^2.0.2"
tslib "^2.0.3"

node-fetch@3.0.0-beta.9:
version "3.0.0-beta.9"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.0.0-beta.9.tgz#0a7554cfb824380dd6812864389923c783c80d9b"
integrity sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg==
dependencies:
data-uri-to-buffer "^3.0.1"
fetch-blob "^2.1.1"

node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
Expand Down

0 comments on commit d418897

Please sign in to comment.