Skip to content

Commit

Permalink
Closes #4137 adds experimental gk cli integration
Browse files Browse the repository at this point in the history
  • Loading branch information
eamodio committed Mar 8, 2025
1 parent 0d14a77 commit b1a9590
Show file tree
Hide file tree
Showing 11 changed files with 405 additions and 0 deletions.
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3990,6 +3990,16 @@
"scope": "window",
"order": 20
},
"gitlens.gitKraken.cli.integration.enabled": {
"type": "boolean",
"default": false,
"markdownDescription": "Specifies whether to enable experimental integration with the GitKraken CLI",
"scope": "window",
"order": 30,
"tags": [
"experimental"
]
},
"gitlens.terminal.overrideGitEditor": {
"type": "boolean",
"default": true,
Expand Down
7 changes: 7 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,13 @@ interface GitCommandsConfig {

interface GitKrakenConfig {
readonly activeOrganizationId: string | null;
readonly cli: GitKrakenCliConfig;
}

interface GitKrakenCliConfig {
readonly integration: {
readonly enabled: boolean;
};
}

export interface GraphConfig {
Expand Down
6 changes: 6 additions & 0 deletions src/container.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ConfigurationChangeEvent, Disposable, Event, ExtensionContext } from 'vscode';
import { EventEmitter, ExtensionMode } from 'vscode';
import {
getGkCliIntegrationProvider,
getSharedGKStorageLocationProvider,
getSupportedGitProviders,
getSupportedRepositoryLocationProvider,
Expand Down Expand Up @@ -256,6 +257,11 @@ export class Container {
this._disposables.push((this._terminalLinks = new GitTerminalLinkProvider(this)));
}

const cliIntegration = getGkCliIntegrationProvider(this);
if (cliIntegration != null) {
this._disposables.push(cliIntegration);
}

this._disposables.push(
configuration.onDidChange(e => {
if (configuration.changed(e, 'terminalLinks.enabled')) {
Expand Down
4 changes: 4 additions & 0 deletions src/env/browser/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@ export function getSupportedWorkspacesStorageProvider(
): GkWorkspacesSharedStorageProvider | undefined {
return undefined;
}

export function getGkCliIntegrationProvider(_container: Container): undefined {
return undefined;
}
33 changes: 33 additions & 0 deletions src/env/node/git/sub-providers/refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import type { GitCache } from '../../../../git/cache';
import { GitErrorHandling } from '../../../../git/commandOptions';
import type { GitRefsSubProvider } from '../../../../git/gitProvider';
import type { GitBranch } from '../../../../git/models/branch';
import type { GitReference } from '../../../../git/models/reference';
import { deletedOrMissing } from '../../../../git/models/revision';
import type { GitTag } from '../../../../git/models/tag';
import { createReference } from '../../../../git/utils/reference.utils';
import { isSha, isShaLike, isUncommitted, isUncommittedParent } from '../../../../git/utils/revision.utils';
import { TimedCancellationSource } from '../../../../system/-webview/cancellation';
import { log } from '../../../../system/decorators/log';
Expand All @@ -21,6 +23,37 @@ export class RefsGitSubProvider implements GitRefsSubProvider {
private readonly provider: LocalGitProvider,
) {}

@log()
async getReference(repoPath: string, ref: string): Promise<GitReference | undefined> {
if (!ref || ref === deletedOrMissing) return undefined;

if (!(await this.validateReference(repoPath, ref))) return undefined;

if (ref !== 'HEAD' && !isShaLike(ref)) {
const branch = await this.provider.branches.getBranch(repoPath, ref);
if (branch != null) {
return createReference(branch.ref, repoPath, {
id: branch.id,
refType: 'branch',
name: branch.name,
remote: branch.remote,
upstream: branch.upstream,
});
}

const tag = await this.provider.tags.getTag(repoPath, ref);
if (tag != null) {
return createReference(tag.ref, repoPath, {
id: tag.id,
refType: 'tag',
name: tag.name,
});
}
}

return createReference(ref, repoPath, { refType: 'revision' });
}

@log({ args: { 1: false } })
async hasBranchOrTag(
repoPath: string | undefined,
Expand Down
150 changes: 150 additions & 0 deletions src/env/node/gk/cli/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import type { Disposable } from 'vscode';
import type { CompareWithCommandArgs } from '../../../../commands/compareWith';
import type { Container } from '../../../../container';
import { cherryPick, merge, rebase } from '../../../../git/actions/repository';
import type { Repository } from '../../../../git/models/repository';
import { executeCommand } from '../../../../system/-webview/command';
import type { CliCommandRequest, CliCommandResponse, CliIpcServer } from './integration';

interface CliCommand {
command: string;
handler: (request: CliCommandRequest, repo?: Repository | undefined) => Promise<CliCommandResponse>;
}

const commandHandlers: CliCommand[] = [];
function command(command: string) {
return function (
target: unknown,
contextOrKey?: string | ClassMethodDecoratorContext,
descriptor?: PropertyDescriptor,
) {
// ES Decorator
if (contextOrKey && typeof contextOrKey === 'object' && 'kind' in contextOrKey) {
if (contextOrKey.kind !== 'method') {
throw new Error('The command decorator can only be applied to methods');
}

commandHandlers.push({ command: command, handler: target as CliCommand['handler'] });
return;
}

// TypeScript experimental decorator
if (descriptor) {
commandHandlers.push({ command: command, handler: descriptor.value as CliCommand['handler'] });
return descriptor;
}

throw new Error('Invalid decorator usage');
};
}

export class CliCommandHandlers implements Disposable {
constructor(
private readonly container: Container,
private readonly server: CliIpcServer,
) {
for (const { command, handler } of commandHandlers) {
this.server.registerHandler(command, rq => this.wrapHandler(rq, handler));
}
}

dispose(): void {}

private wrapHandler(
request: CliCommandRequest,
handler: (request: CliCommandRequest, repo?: Repository | undefined) => Promise<CliCommandResponse>,
) {
let repo: Repository | undefined;
if (request?.cwd) {
repo = this.container.git.getRepository(request.cwd);
}

return handler(request, repo);
}

@command('cherry-pick')
async handleCherryPickCommand(
_request: CliCommandRequest,
repo?: Repository | undefined,
): Promise<CliCommandResponse> {
return cherryPick(repo);
}

@command('compare')
async handleCompareCommand(
_request: CliCommandRequest,
repo?: Repository | undefined,
): Promise<CliCommandResponse> {
if (!repo || !_request.args?.length) {
await executeCommand('gitlens.compareWith');
return;
}

const [ref1, ref2] = _request.args;
if (!ref1 || !ref2) {
await executeCommand('gitlens.compareWith');
return;
}

if (ref1) {
if (!(await repo.git.refs().validateReference(ref1))) {
// TODO: Send an error back to the CLI?
await executeCommand('gitlens.compareWith');
return;
}
}

if (ref2) {
if (!(await repo.git.refs().validateReference(ref2))) {
// TODO: Send an error back to the CLI?
await executeCommand<CompareWithCommandArgs>('gitlens.compareWith', { ref1: ref1 });
return;
}
}

await executeCommand<CompareWithCommandArgs>('gitlens.compareWith', { ref1: ref1, ref2: ref2 });
}

@command('graph')
async handleGraphCommand(request: CliCommandRequest, repo?: Repository | undefined): Promise<CliCommandResponse> {
if (!repo || !request.args?.length) {
await executeCommand('gitlens.showGraphView');
return;
}

const [ref] = request.args;
const reference = await repo.git.refs().getReference(ref);
if (ref && !reference) {
// TODO: Send an error back to the CLI?
await executeCommand('gitlens.showInCommitGraph', repo);
return;
}

await executeCommand('gitlens.showInCommitGraph', { ref: reference });
}

@command('merge')
async handleMergeCommand(request: CliCommandRequest, repo?: Repository | undefined): Promise<CliCommandResponse> {
if (!repo || !request.args?.length) return merge(repo);

const [ref] = request.args;
const reference = await repo.git.refs().getReference(ref);
if (ref && !reference) {
// TODO: Send an error back to the CLI?
}
return merge(repo, reference);
}

@command('rebase')
async handleRebaseCommand(request: CliCommandRequest, repo?: Repository | undefined): Promise<CliCommandResponse> {
if (!repo || !request.args?.length) return rebase(repo);

const [ref] = request.args;
const reference = await repo.git.refs().getReference(ref);
if (ref && !reference) {
// TODO: Send an error back to the CLI?
}

return rebase(repo, reference);
}
}
59 changes: 59 additions & 0 deletions src/env/node/gk/cli/integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { ConfigurationChangeEvent } from 'vscode';
import { Disposable } from 'vscode';
import type { Container } from '../../../../container';
import { configuration } from '../../../../system/-webview/configuration';
import { CliCommandHandlers } from './commands';
import type { IpcServer } from './server';
import { createIpcServer } from './server';

export interface CliCommandRequest {
cwd?: string;
args?: string[];
}
export type CliCommandResponse = string | void;
export type CliIpcServer = IpcServer<CliCommandRequest, CliCommandResponse>;

export class GkCliIntegrationProvider implements Disposable {
private readonly _disposable: Disposable;
private _runningDisposable: Disposable | undefined;

constructor(private readonly container: Container) {
this._disposable = configuration.onDidChange(e => this.onConfigurationChanged(e));

this.onConfigurationChanged();
}

dispose(): void {
this.stop();
this._disposable?.dispose();
}

private onConfigurationChanged(e?: ConfigurationChangeEvent): void {
if (e == null || configuration.changed(e, 'gitKraken.cli.integration.enabled')) {
if (!configuration.get('gitKraken.cli.integration.enabled')) {
this.stop();
} else {
void this.start();
}
}
}

private async start() {
const server = await createIpcServer<CliCommandRequest, CliCommandResponse>();

const { environmentVariableCollection: envVars } = this.container.context;

envVars.clear();
envVars.persistent = false;
envVars.replace('GK_GL_ADDR', server.ipcAddress);
envVars.description = 'Enables GK CLI integration';

this._runningDisposable = Disposable.from(new CliCommandHandlers(this.container, server), server);
}

private stop() {
this.container.context.environmentVariableCollection.clear();
this._runningDisposable?.dispose();
this._runningDisposable = undefined;
}
}
Loading

0 comments on commit b1a9590

Please sign in to comment.