Skip to content

Commit b1a9590

Browse files
committed
Closes #4137 adds experimental gk cli integration
1 parent 0d14a77 commit b1a9590

File tree

11 files changed

+405
-0
lines changed

11 files changed

+405
-0
lines changed

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3990,6 +3990,16 @@
39903990
"scope": "window",
39913991
"order": 20
39923992
},
3993+
"gitlens.gitKraken.cli.integration.enabled": {
3994+
"type": "boolean",
3995+
"default": false,
3996+
"markdownDescription": "Specifies whether to enable experimental integration with the GitKraken CLI",
3997+
"scope": "window",
3998+
"order": 30,
3999+
"tags": [
4000+
"experimental"
4001+
]
4002+
},
39934003
"gitlens.terminal.overrideGitEditor": {
39944004
"type": "boolean",
39954005
"default": true,

src/config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,13 @@ interface GitCommandsConfig {
352352

353353
interface GitKrakenConfig {
354354
readonly activeOrganizationId: string | null;
355+
readonly cli: GitKrakenCliConfig;
356+
}
357+
358+
interface GitKrakenCliConfig {
359+
readonly integration: {
360+
readonly enabled: boolean;
361+
};
355362
}
356363

357364
export interface GraphConfig {

src/container.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ConfigurationChangeEvent, Disposable, Event, ExtensionContext } from 'vscode';
22
import { EventEmitter, ExtensionMode } from 'vscode';
33
import {
4+
getGkCliIntegrationProvider,
45
getSharedGKStorageLocationProvider,
56
getSupportedGitProviders,
67
getSupportedRepositoryLocationProvider,
@@ -256,6 +257,11 @@ export class Container {
256257
this._disposables.push((this._terminalLinks = new GitTerminalLinkProvider(this)));
257258
}
258259

260+
const cliIntegration = getGkCliIntegrationProvider(this);
261+
if (cliIntegration != null) {
262+
this._disposables.push(cliIntegration);
263+
}
264+
259265
this._disposables.push(
260266
configuration.onDidChange(e => {
261267
if (configuration.changed(e, 'terminalLinks.enabled')) {

src/env/browser/providers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,7 @@ export function getSupportedWorkspacesStorageProvider(
4343
): GkWorkspacesSharedStorageProvider | undefined {
4444
return undefined;
4545
}
46+
47+
export function getGkCliIntegrationProvider(_container: Container): undefined {
48+
return undefined;
49+
}

src/env/node/git/sub-providers/refs.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import type { GitCache } from '../../../../git/cache';
44
import { GitErrorHandling } from '../../../../git/commandOptions';
55
import type { GitRefsSubProvider } from '../../../../git/gitProvider';
66
import type { GitBranch } from '../../../../git/models/branch';
7+
import type { GitReference } from '../../../../git/models/reference';
78
import { deletedOrMissing } from '../../../../git/models/revision';
89
import type { GitTag } from '../../../../git/models/tag';
10+
import { createReference } from '../../../../git/utils/reference.utils';
911
import { isSha, isShaLike, isUncommitted, isUncommittedParent } from '../../../../git/utils/revision.utils';
1012
import { TimedCancellationSource } from '../../../../system/-webview/cancellation';
1113
import { log } from '../../../../system/decorators/log';
@@ -21,6 +23,37 @@ export class RefsGitSubProvider implements GitRefsSubProvider {
2123
private readonly provider: LocalGitProvider,
2224
) {}
2325

26+
@log()
27+
async getReference(repoPath: string, ref: string): Promise<GitReference | undefined> {
28+
if (!ref || ref === deletedOrMissing) return undefined;
29+
30+
if (!(await this.validateReference(repoPath, ref))) return undefined;
31+
32+
if (ref !== 'HEAD' && !isShaLike(ref)) {
33+
const branch = await this.provider.branches.getBranch(repoPath, ref);
34+
if (branch != null) {
35+
return createReference(branch.ref, repoPath, {
36+
id: branch.id,
37+
refType: 'branch',
38+
name: branch.name,
39+
remote: branch.remote,
40+
upstream: branch.upstream,
41+
});
42+
}
43+
44+
const tag = await this.provider.tags.getTag(repoPath, ref);
45+
if (tag != null) {
46+
return createReference(tag.ref, repoPath, {
47+
id: tag.id,
48+
refType: 'tag',
49+
name: tag.name,
50+
});
51+
}
52+
}
53+
54+
return createReference(ref, repoPath, { refType: 'revision' });
55+
}
56+
2457
@log({ args: { 1: false } })
2558
async hasBranchOrTag(
2659
repoPath: string | undefined,

src/env/node/gk/cli/commands.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import type { Disposable } from 'vscode';
2+
import type { CompareWithCommandArgs } from '../../../../commands/compareWith';
3+
import type { Container } from '../../../../container';
4+
import { cherryPick, merge, rebase } from '../../../../git/actions/repository';
5+
import type { Repository } from '../../../../git/models/repository';
6+
import { executeCommand } from '../../../../system/-webview/command';
7+
import type { CliCommandRequest, CliCommandResponse, CliIpcServer } from './integration';
8+
9+
interface CliCommand {
10+
command: string;
11+
handler: (request: CliCommandRequest, repo?: Repository | undefined) => Promise<CliCommandResponse>;
12+
}
13+
14+
const commandHandlers: CliCommand[] = [];
15+
function command(command: string) {
16+
return function (
17+
target: unknown,
18+
contextOrKey?: string | ClassMethodDecoratorContext,
19+
descriptor?: PropertyDescriptor,
20+
) {
21+
// ES Decorator
22+
if (contextOrKey && typeof contextOrKey === 'object' && 'kind' in contextOrKey) {
23+
if (contextOrKey.kind !== 'method') {
24+
throw new Error('The command decorator can only be applied to methods');
25+
}
26+
27+
commandHandlers.push({ command: command, handler: target as CliCommand['handler'] });
28+
return;
29+
}
30+
31+
// TypeScript experimental decorator
32+
if (descriptor) {
33+
commandHandlers.push({ command: command, handler: descriptor.value as CliCommand['handler'] });
34+
return descriptor;
35+
}
36+
37+
throw new Error('Invalid decorator usage');
38+
};
39+
}
40+
41+
export class CliCommandHandlers implements Disposable {
42+
constructor(
43+
private readonly container: Container,
44+
private readonly server: CliIpcServer,
45+
) {
46+
for (const { command, handler } of commandHandlers) {
47+
this.server.registerHandler(command, rq => this.wrapHandler(rq, handler));
48+
}
49+
}
50+
51+
dispose(): void {}
52+
53+
private wrapHandler(
54+
request: CliCommandRequest,
55+
handler: (request: CliCommandRequest, repo?: Repository | undefined) => Promise<CliCommandResponse>,
56+
) {
57+
let repo: Repository | undefined;
58+
if (request?.cwd) {
59+
repo = this.container.git.getRepository(request.cwd);
60+
}
61+
62+
return handler(request, repo);
63+
}
64+
65+
@command('cherry-pick')
66+
async handleCherryPickCommand(
67+
_request: CliCommandRequest,
68+
repo?: Repository | undefined,
69+
): Promise<CliCommandResponse> {
70+
return cherryPick(repo);
71+
}
72+
73+
@command('compare')
74+
async handleCompareCommand(
75+
_request: CliCommandRequest,
76+
repo?: Repository | undefined,
77+
): Promise<CliCommandResponse> {
78+
if (!repo || !_request.args?.length) {
79+
await executeCommand('gitlens.compareWith');
80+
return;
81+
}
82+
83+
const [ref1, ref2] = _request.args;
84+
if (!ref1 || !ref2) {
85+
await executeCommand('gitlens.compareWith');
86+
return;
87+
}
88+
89+
if (ref1) {
90+
if (!(await repo.git.refs().validateReference(ref1))) {
91+
// TODO: Send an error back to the CLI?
92+
await executeCommand('gitlens.compareWith');
93+
return;
94+
}
95+
}
96+
97+
if (ref2) {
98+
if (!(await repo.git.refs().validateReference(ref2))) {
99+
// TODO: Send an error back to the CLI?
100+
await executeCommand<CompareWithCommandArgs>('gitlens.compareWith', { ref1: ref1 });
101+
return;
102+
}
103+
}
104+
105+
await executeCommand<CompareWithCommandArgs>('gitlens.compareWith', { ref1: ref1, ref2: ref2 });
106+
}
107+
108+
@command('graph')
109+
async handleGraphCommand(request: CliCommandRequest, repo?: Repository | undefined): Promise<CliCommandResponse> {
110+
if (!repo || !request.args?.length) {
111+
await executeCommand('gitlens.showGraphView');
112+
return;
113+
}
114+
115+
const [ref] = request.args;
116+
const reference = await repo.git.refs().getReference(ref);
117+
if (ref && !reference) {
118+
// TODO: Send an error back to the CLI?
119+
await executeCommand('gitlens.showInCommitGraph', repo);
120+
return;
121+
}
122+
123+
await executeCommand('gitlens.showInCommitGraph', { ref: reference });
124+
}
125+
126+
@command('merge')
127+
async handleMergeCommand(request: CliCommandRequest, repo?: Repository | undefined): Promise<CliCommandResponse> {
128+
if (!repo || !request.args?.length) return merge(repo);
129+
130+
const [ref] = request.args;
131+
const reference = await repo.git.refs().getReference(ref);
132+
if (ref && !reference) {
133+
// TODO: Send an error back to the CLI?
134+
}
135+
return merge(repo, reference);
136+
}
137+
138+
@command('rebase')
139+
async handleRebaseCommand(request: CliCommandRequest, repo?: Repository | undefined): Promise<CliCommandResponse> {
140+
if (!repo || !request.args?.length) return rebase(repo);
141+
142+
const [ref] = request.args;
143+
const reference = await repo.git.refs().getReference(ref);
144+
if (ref && !reference) {
145+
// TODO: Send an error back to the CLI?
146+
}
147+
148+
return rebase(repo, reference);
149+
}
150+
}

src/env/node/gk/cli/integration.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { ConfigurationChangeEvent } from 'vscode';
2+
import { Disposable } from 'vscode';
3+
import type { Container } from '../../../../container';
4+
import { configuration } from '../../../../system/-webview/configuration';
5+
import { CliCommandHandlers } from './commands';
6+
import type { IpcServer } from './server';
7+
import { createIpcServer } from './server';
8+
9+
export interface CliCommandRequest {
10+
cwd?: string;
11+
args?: string[];
12+
}
13+
export type CliCommandResponse = string | void;
14+
export type CliIpcServer = IpcServer<CliCommandRequest, CliCommandResponse>;
15+
16+
export class GkCliIntegrationProvider implements Disposable {
17+
private readonly _disposable: Disposable;
18+
private _runningDisposable: Disposable | undefined;
19+
20+
constructor(private readonly container: Container) {
21+
this._disposable = configuration.onDidChange(e => this.onConfigurationChanged(e));
22+
23+
this.onConfigurationChanged();
24+
}
25+
26+
dispose(): void {
27+
this.stop();
28+
this._disposable?.dispose();
29+
}
30+
31+
private onConfigurationChanged(e?: ConfigurationChangeEvent): void {
32+
if (e == null || configuration.changed(e, 'gitKraken.cli.integration.enabled')) {
33+
if (!configuration.get('gitKraken.cli.integration.enabled')) {
34+
this.stop();
35+
} else {
36+
void this.start();
37+
}
38+
}
39+
}
40+
41+
private async start() {
42+
const server = await createIpcServer<CliCommandRequest, CliCommandResponse>();
43+
44+
const { environmentVariableCollection: envVars } = this.container.context;
45+
46+
envVars.clear();
47+
envVars.persistent = false;
48+
envVars.replace('GK_GL_ADDR', server.ipcAddress);
49+
envVars.description = 'Enables GK CLI integration';
50+
51+
this._runningDisposable = Disposable.from(new CliCommandHandlers(this.container, server), server);
52+
}
53+
54+
private stop() {
55+
this.container.context.environmentVariableCollection.clear();
56+
this._runningDisposable?.dispose();
57+
this._runningDisposable = undefined;
58+
}
59+
}

0 commit comments

Comments
 (0)