Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions __mocks__/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,7 @@ export const Uri = {
file: vi.fn((path: string) => ({ fsPath: path })),
parse: vi.fn((value: string) => ({ toString: () => value })),
};

export const env = {
openExternal: vi.fn(() => Promise.resolve(true)),
};
28 changes: 28 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@
"title": "Redeliver",
"category": "GitHub Webhooks",
"icon": "$(history)"
},
{
"command": "githubWebhooks.openWebhookSettings",
"title": "Open Webhook Settings on GitHub",
"category": "GitHub Webhooks",
"icon": "$(gear)"
},
{
"command": "githubWebhooks.openRepoWebhooks",
"title": "Open Repository Webhooks Page on GitHub",
"category": "GitHub Webhooks",
"icon": "$(github-inverted)"
}
],
"menus": {
Expand All @@ -70,6 +82,11 @@
"command": "githubWebhooks.refresh",
"when": "view == githubWebhooks.deliveries",
"group": "navigation@1"
},
{
"command": "githubWebhooks.openRepoWebhooks",
"when": "view == githubWebhooks.deliveries",
"group": "navigation@2"
}
],
"view/item/context": [
Expand All @@ -82,6 +99,17 @@
"command": "githubWebhooks.showDelivery",
"when": "view == githubWebhooks.deliveries && viewItem == delivery",
"group": "1_actions"
},
{
"command": "githubWebhooks.openWebhookSettings",
"when": "view == githubWebhooks.deliveries && viewItem == webhook",
"group": "inline"
}
],
"commandPalette": [
{
"command": "githubWebhooks.openWebhookSettings",
"when": "false"
}
]
}
Expand Down
29 changes: 26 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as vscode from 'vscode';
import { getOctokit } from './auth';
import { resolveRepo, type RepoRef } from './github/repoResolver';
import { resolveRepo, webhookSettingsUrl, type RepoRef } from './github/repoResolver';
import { WebhookClient } from './github/webhookClient';
import { WebhookTreeProvider } from './tree/webhookTreeProvider';
import { DeliveryNode } from './tree/nodes';
import { DeliveryNode, WebhookNode } from './tree/nodes';
import { DeliveryPanel } from './panel/deliveryPanel';
import { toMessage } from './errors';

Expand Down Expand Up @@ -81,14 +81,37 @@ export function activate(context: vscode.ExtensionContext): void {
}
}

async function openWebhookSettings(node: WebhookNode): Promise<void> {
if (!currentRepo || !node?.webhook) {
return;
}
await vscode.env.openExternal(
vscode.Uri.parse(webhookSettingsUrl(currentRepo, node.webhook.id))
);
}

async function openRepoWebhooks(): Promise<void> {
if (!currentRepo) {
void vscode.window.showWarningMessage(
'GitHub Webhook Manager: No GitHub repository detected in this workspace.'
);
return;
}
await vscode.env.openExternal(vscode.Uri.parse(webhookSettingsUrl(currentRepo)));
}

context.subscriptions.push(
vscode.commands.registerCommand('githubWebhooks.refresh', () => provider.refresh()),
vscode.commands.registerCommand('githubWebhooks.showDelivery', (node: DeliveryNode) =>
showDelivery(node)
),
vscode.commands.registerCommand('githubWebhooks.redeliver', (node: DeliveryNode) =>
redeliver(node)
)
),
vscode.commands.registerCommand('githubWebhooks.openWebhookSettings', (node: WebhookNode) =>
openWebhookSettings(node)
),
vscode.commands.registerCommand('githubWebhooks.openRepoWebhooks', () => openRepoWebhooks())
);

void load();
Expand Down
10 changes: 10 additions & 0 deletions src/github/repoResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ export interface RepoRef {

export const GITHUB_COM = 'github.com';

/**
* Build the browser URL for a repository's webhook settings page.
* With a hookId, points at that single webhook; without it, the hooks list.
* Works for github.com and GitHub Enterprise Server (the host is taken from repo).
*/
export function webhookSettingsUrl(repo: RepoRef, hookId?: number): string {
const base = `https://${repo.host}/${repo.owner}/${repo.repo}/settings/hooks`;
return hookId === undefined ? base : `${base}/${hookId}`;
}

/**
* Parse a git remote URL (https, ssh://, or scp-style) into a RepoRef.
* The host is extracted from the URL and included in the result, so both
Expand Down
51 changes: 49 additions & 2 deletions tests/extension.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import * as vscode from 'vscode';
import { activate, deactivate } from '../src/extension';
import { DeliveryNode, MessageNode } from '../src/tree/nodes';
import type { DeliverySummary } from '../src/github/webhookClient';
import { DeliveryNode, MessageNode, WebhookNode } from '../src/tree/nodes';
import type { DeliverySummary, Webhook } from '../src/github/webhookClient';

const repos = vi.hoisted(() => ({
listWebhooks: vi.fn(),
Expand All @@ -27,6 +27,8 @@ const COMMANDS = [
'githubWebhooks.refresh',
'githubWebhooks.showDelivery',
'githubWebhooks.redeliver',
'githubWebhooks.openWebhookSettings',
'githubWebhooks.openRepoWebhooks',
];

const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
Expand Down Expand Up @@ -67,6 +69,14 @@ const deliveryNode = new DeliveryNode(7, {
duration: 1,
} as unknown as DeliverySummary);

const webhookNode = new WebhookNode({
id: 42,
name: 'web',
active: true,
events: ['push'],
config: {},
} as unknown as Webhook);

describe('activate', () => {
beforeEach(() => {
vi.clearAllMocks();
Expand Down Expand Up @@ -137,6 +147,43 @@ describe('activate', () => {
expect(roots[0]).toBeInstanceOf(MessageNode);
});

it('opens the repository webhooks page in the browser', async () => {
activate(makeContext());
await flush();

await handlerFor('githubWebhooks.openRepoWebhooks')();

expect(vscode.Uri.parse).toHaveBeenCalledWith(
'https://github.com/seiya-koji/demo/settings/hooks'
);
expect(vscode.env.openExternal).toHaveBeenCalled();
});

it('opens a single webhook settings page in the browser', async () => {
activate(makeContext());
await flush();

await handlerFor('githubWebhooks.openWebhookSettings')(webhookNode);

expect(vscode.Uri.parse).toHaveBeenCalledWith(
'https://github.com/seiya-koji/demo/settings/hooks/42'
);
expect(vscode.env.openExternal).toHaveBeenCalled();
});

it('warns instead of opening when no repository is detected', async () => {
execFile.mockImplementation((_c: string, _a: string[], _o: unknown, cb: ExecCb) =>
cb(new Error('not a git repository'), '')
);
activate(makeContext());
await flush();

await handlerFor('githubWebhooks.openRepoWebhooks')();

expect(vscode.env.openExternal).not.toHaveBeenCalled();
expect(vscode.window.showWarningMessage).toHaveBeenCalled();
});

it('deactivate runs without throwing', () => {
expect(() => deactivate()).not.toThrow();
});
Expand Down
20 changes: 19 additions & 1 deletion tests/github/repoResolver.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import * as vscode from 'vscode';
import { parseGitHubRemote, resolveRepo } from '../../src/github/repoResolver';
import { parseGitHubRemote, resolveRepo, webhookSettingsUrl } from '../../src/github/repoResolver';

const execFile = vi.hoisted(() => vi.fn());
vi.mock('node:child_process', () => ({ execFile }));
Expand Down Expand Up @@ -133,3 +133,21 @@ describe('resolveRepo', () => {
expect(execFile).not.toHaveBeenCalled();
});
});

describe('webhookSettingsUrl', () => {
const repo = { host: 'github.com', owner: 'owner', repo: 'repo' };

it('builds the single-webhook settings URL when a hookId is given', () => {
expect(webhookSettingsUrl(repo, 123)).toBe('https://github.com/owner/repo/settings/hooks/123');
});

it('builds the hooks-list URL when no hookId is given', () => {
expect(webhookSettingsUrl(repo)).toBe('https://github.com/owner/repo/settings/hooks');
});

it('uses the host from the repo for GitHub Enterprise Server', () => {
expect(webhookSettingsUrl({ host: 'github.example.com', owner: 'org', repo: 'app' }, 7)).toBe(
'https://github.example.com/org/app/settings/hooks/7'
);
});
});
Loading