Просмотр исходного кода

Adds actions for PRs on the focus view

main
Keith Daulton 1 год назад
Родитель
Сommit
d78bf95048
10 измененных файлов: 246 добавлений и 52 удалений
  1. +1
    -1
      .vscode/queries.github-graphql-nb
  2. +1
    -1
      src/commands/ghpr/openOrCreateWorktree.ts
  3. +3
    -0
      src/git/models/pullRequest.ts
  4. +36
    -34
      src/plus/github/github.ts
  5. +4
    -0
      src/plus/github/models.ts
  6. +131
    -9
      src/plus/webviews/focus/focusWebview.ts
  7. +17
    -1
      src/plus/webviews/focus/protocol.ts
  8. +21
    -2
      src/webviews/apps/plus/focus/components/pull-request-row.ts
  9. +23
    -0
      src/webviews/apps/plus/focus/focus.ts
  10. +9
    -4
      src/webviews/apps/shared/components/code-icon.ts

+ 1
- 1
.vscode/queries.github-graphql-nb
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 1
- 1
src/commands/ghpr/openOrCreateWorktree.ts Просмотреть файл

@ -16,7 +16,7 @@ interface GHPRPullRequestNode {
readonly pullRequestModel: GHPRPullRequest;
}
interface GHPRPullRequest {
export interface GHPRPullRequest {
readonly base: {
readonly repositoryCloneUrl: {
readonly repositoryName: string;

+ 3
- 0
src/git/models/pullRequest.ts Просмотреть файл

@ -32,6 +32,7 @@ export interface PullRequestRef {
branch: string;
sha: string;
exists: boolean;
url: string;
}
export interface PullRequestRefs {
@ -102,6 +103,7 @@ export function serializePullRequest(value: PullRequest): PullRequestShape {
repo: value.refs.head.repo,
sha: value.refs.head.sha,
branch: value.refs.head.branch,
url: value.refs.head.url,
},
base: {
exists: value.refs.base.exists,
@ -109,6 +111,7 @@ export function serializePullRequest(value: PullRequest): PullRequestShape {
repo: value.refs.base.repo,
sha: value.refs.base.sha,
branch: value.refs.base.branch,
url: value.refs.base.url,
},
isCrossRepository: value.refs.isCrossRepository,
}

+ 36
- 34
src/plus/github/github.ts Просмотреть файл

@ -63,24 +63,25 @@ const emptyBlameResult: GitHubBlame = Object.freeze({ ranges: [] });
const prNodeProperties = `
assignees(first: 10) {
nodes {
nodes {
login
avatarUrl
url
}
}
author {
login
avatarUrl
url
}
}
author {
login
avatarUrl
url
}
baseRefName
baseRefOid
baseRepository {
name
owner {
login
}
name
owner {
login
}
url
}
checksUrl
isDraft
@ -89,10 +90,11 @@ isReadByViewer
headRefName
headRefOid
headRepository {
name
owner {
login
}
name
owner {
login
}
url
}
permalink
number
@ -105,33 +107,33 @@ closedAt
mergeable
mergedAt
mergedBy {
login
login
}
repository {
isFork
owner {
login
}
isFork
owner {
login
}
}
repository {
isFork
owner {
login
}
isFork
owner {
login
}
}
reviewDecision
reviewRequests(first: 10) {
nodes {
asCodeOwner
id
requestedReviewer {
... on User {
login
avatarUrl
url
}
nodes {
asCodeOwner
id
requestedReviewer {
... on User {
login
avatarUrl
url
}
}
}
}
}
totalCommentsCount
`;

+ 4
- 0
src/plus/github/models.ts Просмотреть файл

@ -127,6 +127,7 @@ export interface GitHubDetailedPullRequest extends GitHubPullRequest {
owner: {
login: string;
};
url: string;
};
headRefName: string;
headRefOid: string;
@ -135,6 +136,7 @@ export interface GitHubDetailedPullRequest extends GitHubPullRequest {
owner: {
login: string;
};
url: string;
};
reviewDecision: GitHubPullRequestReviewDecision;
isReadByViewer: boolean;
@ -272,6 +274,7 @@ export function fromGitHubPullRequestDetailed(
repo: pr.baseRepository?.name,
sha: pr.headRefOid,
branch: pr.headRefName,
url: pr.headRepository?.url,
},
base: {
exists: pr.baseRepository != null,
@ -279,6 +282,7 @@ export function fromGitHubPullRequestDetailed(
repo: pr.baseRepository?.name,
sha: pr.baseRefOid,
branch: pr.baseRefName,
url: pr.baseRepository?.url,
},
isCrossRepository: pr.isCrossRepository,
},

+ 131
- 9
src/plus/webviews/focus/focusWebview.ts Просмотреть файл

@ -1,28 +1,40 @@
import { Disposable } from 'vscode';
import { Disposable, Uri, window } from 'vscode';
import type { GHPRPullRequest } from '../../../commands';
import { Commands, ContextKeys } from '../../../constants';
import type { Container } from '../../../container';
import { setContext } from '../../../context';
import { PlusFeatures } from '../../../features';
import { add as addRemote } from '../../../git/actions/remote';
import * as RepoActions from '../../../git/actions/repository';
import type { SearchedIssue } from '../../../git/models/issue';
import { serializeIssue } from '../../../git/models/issue';
import type { SearchedPullRequest } from '../../../git/models/pullRequest';
import type { PullRequestShape, SearchedPullRequest } from '../../../git/models/pullRequest';
import {
PullRequestMergeableState,
PullRequestReviewDecision,
serializePullRequest,
} from '../../../git/models/pullRequest';
import { createReference, getReferenceFromBranch } from '../../../git/models/reference';
import type { GitRemote } from '../../../git/models/remote';
import type { Repository, RepositoryChangeEvent } from '../../../git/models/repository';
import { RepositoryChange, RepositoryChangeComparisonMode } from '../../../git/models/repository';
import { parseGitRemoteUrl } from '../../../git/parsers/remoteParser';
import type { RichRemoteProvider } from '../../../git/remotes/richRemoteProvider';
import type { Subscription } from '../../../subscription';
import { SubscriptionState } from '../../../subscription';
import { registerCommand } from '../../../system/command';
import { executeCommand, registerCommand } from '../../../system/command';
import type { IpcMessage } from '../../../webviews/protocol';
import { onIpc } from '../../../webviews/protocol';
import { WebviewBase } from '../../../webviews/webviewBase';
import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
import { ensurePlusFeaturesEnabled } from '../../subscription/utils';
import type { State } from './protocol';
import { DidChangeStateNotificationType, DidChangeSubscriptionNotificationType } from './protocol';
import type { OpenWorktreeParams, State, SwitchToBranchParams } from './protocol';
import {
DidChangeStateNotificationType,
DidChangeSubscriptionNotificationType,
OpenWorktreeCommandType,
SwitchToBranchCommandType,
} from './protocol';
interface RepoWithRichRemote {
repo: Repository;
@ -31,9 +43,13 @@ interface RepoWithRichRemote {
isGitHub: boolean;
}
interface SearchedPullRequestWithRemote extends SearchedPullRequest {
repoAndRemote: RepoWithRichRemote;
}
export class FocusWebview extends WebviewBase<State> {
private _bootstrapping = true;
private _pullRequests: SearchedPullRequest[] = [];
private _pullRequests: SearchedPullRequestWithRemote[] = [];
private _issues: SearchedIssue[] = [];
private _etagSubscription?: number;
private _repositoryEventsDisposable?: Disposable;
@ -70,6 +86,111 @@ export class FocusWebview extends WebviewBase {
void setContext(ContextKeys.FocusFocused, focused);
}
protected override onMessageReceived(e: IpcMessage) {
switch (e.method) {
case SwitchToBranchCommandType.method:
onIpc(SwitchToBranchCommandType, e, params => this.onSwitchBranch(params));
break;
case OpenWorktreeCommandType.method:
onIpc(OpenWorktreeCommandType, e, params => this.onOpenWorktree(params));
break;
}
}
private findSearchedPullRequest(pullRequest: PullRequestShape): SearchedPullRequestWithRemote | undefined {
return this._pullRequests?.find(r => r.pullRequest.id === pullRequest.id);
}
private async getRemoteBranch(searchedPullRequest: SearchedPullRequestWithRemote) {
const pullRequest = searchedPullRequest.pullRequest;
const repo = await searchedPullRequest.repoAndRemote.repo.getMainRepository();
const remoteUri = Uri.parse(pullRequest.refs!.head.url);
const remoteUrl = remoteUri.toString();
if (repo == null) {
void window.showWarningMessage(`Unable to find main repository(${remoteUrl}) for PR #${pullRequest.id}`);
return;
}
const [, remoteDomain, remotePath] = parseGitRemoteUrl(remoteUrl);
const remoteOwner = pullRequest.refs!.head.owner;
const ref = pullRequest.refs!.head.branch;
let remote: GitRemote | undefined;
[remote] = await repo.getRemotes({ filter: r => r.matches(remoteDomain, remotePath) });
if (remote != null) {
// Ensure we have the latest from the remote
await this.container.git.fetch(repo.path, { remote: remote.name });
} else {
const result = await window.showInformationMessage(
`Unable to find a remote for '${remoteUrl}'. Would you like to add a new remote?`,
{ modal: true },
{ title: 'Yes' },
{ title: 'No', isCloseAffordance: true },
);
if (result?.title !== 'Yes') return;
await addRemote(repo, remoteOwner, remoteUrl, {
confirm: false,
fetch: true,
reveal: false,
});
[remote] = await repo.getRemotes({ filter: r => r.url === remoteUrl });
if (remote == null) return;
}
const remoteBranchName = `${remote.name}/${ref}`;
const reference = createReference(remoteBranchName, repo.path, {
refType: 'branch',
name: remoteBranchName,
remote: true,
});
return {
remote: remote,
reference: reference,
};
}
private async onSwitchBranch({ pullRequest }: SwitchToBranchParams) {
const searchedPullRequestWithRemote = this.findSearchedPullRequest(pullRequest);
if (searchedPullRequestWithRemote == null) return Promise.resolve();
const remoteBranch = await this.getRemoteBranch(searchedPullRequestWithRemote);
if (remoteBranch == null) return Promise.resolve();
return RepoActions.switchTo(remoteBranch.remote.repoPath, remoteBranch.reference);
}
private async onOpenWorktree({ pullRequest }: OpenWorktreeParams) {
const baseUri = Uri.parse(pullRequest.refs!.base.url);
const repoAndRemote = this.findSearchedPullRequest(pullRequest)?.repoAndRemote;
const localInfo = repoAndRemote!.repo.folder;
return executeCommand<GHPRPullRequest>(Commands.OpenOrCreateWorktreeForGHPR, {
base: {
repositoryCloneUrl: {
repositoryName: pullRequest.refs!.base.repo,
owner: pullRequest.refs!.base.owner,
url: baseUri,
},
},
githubRepository: {
rootUri: localInfo!.uri,
},
head: {
ref: pullRequest.refs!.head.branch,
sha: pullRequest.refs!.head.sha,
repositoryCloneUrl: {
repositoryName: pullRequest.refs!.head.repo,
owner: pullRequest.refs!.head.owner,
url: Uri.parse(pullRequest.refs!.head.url),
},
},
item: {
number: parseInt(pullRequest.id, 10),
},
});
}
private async onSubscriptionChanged(e: SubscriptionChangeEvent) {
if (e.etag === this._etagSubscription) return;
@ -193,14 +314,15 @@ export class FocusWebview extends WebviewBase {
}
}
private async getMyPullRequests(richRepos: RepoWithRichRemote[]): Promise<SearchedPullRequest[]> {
private async getMyPullRequests(richRepos: RepoWithRichRemote[]): Promise<SearchedPullRequestWithRemote[]> {
const allPrs = [];
for (const { remote } of richRepos) {
for (const richRepo of richRepos) {
const { remote } = richRepo;
const prs = await this.container.git.getMyPullRequests(remote);
if (prs == null) {
continue;
}
allPrs.push(...prs.filter(pr => pr.reasons.length > 0));
allPrs.push(...prs.filter(pr => pr.reasons.length > 0).map(pr => ({ ...pr, repoAndRemote: richRepo })));
}
function getScore(pr: SearchedPullRequest) {

+ 17
- 1
src/plus/webviews/focus/protocol.ts Просмотреть файл

@ -1,7 +1,7 @@
import type { IssueShape } from '../../../git/models/issue';
import type { PullRequestShape } from '../../../git/models/pullRequest';
import type { Subscription } from '../../../subscription';
import { IpcNotificationType } from '../../../webviews/protocol';
import { IpcCommandType, IpcNotificationType } from '../../../webviews/protocol';
export type State = {
isPlus: boolean;
@ -30,6 +30,22 @@ export interface RepoWithRichProvider {
isConnected: boolean;
}
// Commands
export interface OpenWorktreeParams {
pullRequest: PullRequestShape;
}
export const OpenWorktreeCommandType = new IpcCommandType<OpenWorktreeParams>('focus/pr/openWorktree');
export interface SwitchToBranchParams {
pullRequest: PullRequestShape;
}
export const SwitchToBranchCommandType = new IpcCommandType<SwitchToBranchParams>('focus/pr/switchToBranch');
// Notifications
export interface DidChangeStateNotificationParams {
state: State;
}

+ 21
- 2
src/webviews/apps/plus/focus/components/pull-request-row.ts Просмотреть файл

@ -100,8 +100,19 @@ const template = html`
<span class="stat-deleted">-${x => x.pullRequest!.deletions}</span></table-cell
>
<table-cell class="actions">
<a href="${x => x.pullRequest!.url}" title="Open pull request on remote"
><code-icon icon="globe"></code-icon
<a
href="#"
title="Open Worktree..."
aria-label="Open Worktree..."
@click="${(x, c) => x.onOpenWorktreeClick(c.event)}"
><code-icon icon="gl-worktrees-view"></code-icon
></a>
<a
href="#"
title="Switch to Branch..."
aria-label="Switch to Branch..."
@click="${(x, c) => x.onSwitchBranchClick(c.event)}"
><code-icon icon="gl-switch"></code-icon
></a>
</table-cell>
</template>
@ -295,4 +306,12 @@ export class PullRequestRow extends FASTElement {
return assignees;
}
onOpenWorktreeClick(_e: Event) {
this.$emit('open-worktree', this.pullRequest!);
}
onSwitchBranchClick(_e: Event) {
this.$emit('switch-branch', this.pullRequest!);
}
}

+ 23
- 0
src/webviews/apps/plus/focus/focus.ts Просмотреть файл

@ -1,8 +1,11 @@
import { provideVSCodeDesignSystem, vsCodeButton } from '@vscode/webview-ui-toolkit';
import type { PullRequestShape } from '../../../../git/models/pullRequest';
import type { State } from '../../../../plus/webviews/focus/protocol';
import {
DidChangeStateNotificationType,
DidChangeSubscriptionNotificationType,
OpenWorktreeCommandType,
SwitchToBranchCommandType,
} from '../../../../plus/webviews/focus/protocol';
import type { IpcMessage } from '../../../protocol';
import { ExecuteCommandType, onIpc } from '../../../protocol';
@ -65,10 +68,30 @@ export class FocusApp extends App {
this.onPlusActionClicked(e, target),
),
);
disposables.push(
DOM.on<PullRequestRow, PullRequestShape>('pull-request-row', 'open-worktree', (e, target: HTMLElement) =>
this.onOpenWorktree(e, target),
),
);
disposables.push(
DOM.on<PullRequestRow, PullRequestShape>('pull-request-row', 'switch-branch', (e, target: HTMLElement) =>
this.onSwitchBranch(e, target),
),
);
return disposables;
}
private onSwitchBranch(e: CustomEvent<PullRequestShape>, _target: HTMLElement) {
if (e.detail?.refs?.head == null) return;
this.sendCommand(SwitchToBranchCommandType, { pullRequest: e.detail });
}
private onOpenWorktree(e: CustomEvent<PullRequestShape>, _target: HTMLElement) {
if (e.detail?.refs?.head == null) return;
this.sendCommand(OpenWorktreeCommandType, { pullRequest: e.detail });
}
private onDataActionClicked(_e: MouseEvent, target: HTMLElement) {
const action = target.dataset.action;
this.onActionClickedCore(action);

+ 9
- 4
src/webviews/apps/shared/components/code-icon.ts Просмотреть файл

@ -1490,25 +1490,30 @@ const styles = css`
:host([icon='target']):before {
content: '\\ebf8';
}
:host([icon='gl-pinned-filled']):before {
:host([icon^='gl-']) {
font-family: 'glicons';
}
:host([icon='gl-pinned-filled']):before {
content: '\\f11c';
/* TODO: see relative positioning needed in every use-case */
position: relative;
left: 1px;
}
:host([icon='gl-graph']):before {
font-family: 'glicons';
content: '\\f102';
}
:host([icon='gl-list-auto']):before {
font-family: 'glicons';
content: '\\f11a';
}
:host([icon='gl-clock']):before {
font-family: 'glicons';
content: '\\f11d';
}
:host([icon='gl-worktrees-view']):before {
content: '\\f112';
}
:host([icon='gl-switch']):before {
content: '\\f118';
}
@keyframes codicon-spin {
100% {

Загрузка…
Отмена
Сохранить