Skip to content
Draft
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
1 change: 1 addition & 0 deletions src/@types/vscode.proposed.chatParticipantAdditions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ declare module 'vscode' {
isComplete?: boolean;
toolSpecificData?: ChatTerminalToolInvocationData;
fromSubAgent?: boolean;
presentation?: 'hidden' | 'hiddenAfterComplete' | undefined;

constructor(toolName: string, toolCallId: string, isError?: boolean);
}
Expand Down
5 changes: 5 additions & 0 deletions src/@types/vscode.proposed.chatSessionsProvider.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ declare module 'vscode' {
*/
description?: string | MarkdownString;

/**
* An optional badge that provides additional context about the chat session.
*/
badge?: string | MarkdownString;

/**
* An optional status indicating the current state of the session.
*/
Expand Down
12 changes: 11 additions & 1 deletion src/common/timelineEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export enum EventType {
CrossReferenced,
Closed,
Reopened,
BaseRefChanged,
CopilotStarted,
CopilotFinished,
CopilotFinishedError,
Expand Down Expand Up @@ -156,6 +157,15 @@ export interface ReopenedEvent {
createdAt: string;
}

export interface BaseRefChangedEvent {
id: string;
event: EventType.BaseRefChanged;
actor: IActor;
createdAt: string;
currentRefName: string;
previousRefName: string;
}

export interface SessionPullInfo {
id: number;
host: string;
Expand Down Expand Up @@ -192,4 +202,4 @@ export interface CopilotFinishedErrorEvent {
sessionLink: SessionLinkInfo;
}

export type TimelineEvent = CommitEvent | ReviewEvent | CommentEvent | NewCommitsSinceReviewEvent | MergedEvent | AssignEvent | UnassignEvent | HeadRefDeleteEvent | CrossReferencedEvent | ClosedEvent | ReopenedEvent | CopilotStartedEvent | CopilotFinishedEvent | CopilotFinishedErrorEvent;
export type TimelineEvent = CommitEvent | ReviewEvent | CommentEvent | NewCommitsSinceReviewEvent | MergedEvent | AssignEvent | UnassignEvent | HeadRefDeleteEvent | CrossReferencedEvent | ClosedEvent | ReopenedEvent | BaseRefChangedEvent | CopilotStartedEvent | CopilotFinishedEvent | CopilotFinishedErrorEvent;
42 changes: 4 additions & 38 deletions src/github/createPRViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ import { IAccount, ILabel, IMilestone, IProject, isITeam, ITeam, MergeMethod, Re
import { BaseBranchMetadata, PullRequestGitHelper } from './pullRequestGitHelper';
import { PullRequestModel } from './pullRequestModel';
import { getDefaultMergeMethod } from './pullRequestOverview';
import { getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, getProjectFromQuickPick, reviewersQuickPick } from './quickPicks';
import { branchPicks, getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, getProjectFromQuickPick, reviewersQuickPick } from './quickPicks';
import { getIssueNumberLabelFromParsed, ISSUE_EXPRESSION, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, variableSubstitution } from './utils';
import { DisplayLabel, PreReviewState } from './views';
import { RemoteInfo } from '../../common/types';
import { ChooseBaseRemoteAndBranchResult, ChooseCompareRemoteAndBranchResult, ChooseRemoteAndBranchArgs, CreateParamsNew, CreatePullRequestNew, TitleAndDescriptionArgs } from '../../common/views';
import type { Branch, Ref } from '../api/api';
import type { Branch } from '../api/api';
import { GitHubServerType } from '../common/authentication';
import { emojify, ensureEmojis } from '../common/emoji';
import { commands, contexts } from '../common/executeCommands';
Expand Down Expand Up @@ -812,40 +812,6 @@ export class CreatePullRequestViewProvider extends BaseCreatePullRequestViewProv
});
}

private async branchPicks(githubRepository: GitHubRepository, changeRepoMessage: string, isBase: boolean): Promise<(vscode.QuickPickItem & { remote?: RemoteInfo, branch?: string })[]> {
let branches: (string | Ref)[];
if (isBase) {
// For the base, we only want to show branches from GitHub.
branches = await githubRepository.listBranches(githubRepository.remote.owner, githubRepository.remote.repositoryName);
} else {
// For the compare, we only want to show local branches.
branches = (await this._folderRepositoryManager.repository.getBranches({ remote: false })).filter(branch => branch.name);
}
// TODO: @alexr00 - Add sorting so that the most likely to be used branch (ex main or release if base) is at the top of the list.
const branchPicks: (vscode.QuickPickItem & { remote?: RemoteInfo, branch?: string })[] = branches.map(branch => {
const branchName = typeof branch === 'string' ? branch : branch.name!;
const pick: (vscode.QuickPickItem & { remote: RemoteInfo, branch: string }) = {
iconPath: new vscode.ThemeIcon('git-branch'),
label: branchName,
remote: {
owner: githubRepository.remote.owner,
repositoryName: githubRepository.remote.repositoryName
},
branch: branchName
};
return pick;
});
branchPicks.unshift({
kind: vscode.QuickPickItemKind.Separator,
label: `${githubRepository.remote.owner}/${githubRepository.remote.repositoryName}`
});
branchPicks.unshift({
iconPath: new vscode.ThemeIcon('repo'),
label: changeRepoMessage
});
return branchPicks;
}

private async processRemoteAndBranchResult(githubRepository: GitHubRepository, result: { remote: RemoteInfo, branch: string }, isBase: boolean) {
const [defaultBranch, viewerPermission] = await Promise.all([githubRepository.getDefaultBranch(), githubRepository.getViewerPermission()]);

Expand Down Expand Up @@ -922,7 +888,7 @@ export class CreatePullRequestViewProvider extends BaseCreatePullRequestViewProv
quickPick.placeholder = githubRepository ? branchPlaceholder : remotePlaceholder;
quickPick.show();
quickPick.busy = true;
quickPick.items = githubRepository ? await this.branchPicks(githubRepository, chooseDifferentRemote, isBase) : await this.remotePicks(isBase);
quickPick.items = githubRepository ? await branchPicks(githubRepository, this._folderRepositoryManager, chooseDifferentRemote, isBase) : await this.remotePicks(isBase);
const activeItem = message.args.currentBranch ? quickPick.items.find(item => item.branch === message.args.currentBranch) : undefined;
quickPick.activeItems = activeItem ? [activeItem] : [];
quickPick.busy = false;
Expand All @@ -941,7 +907,7 @@ export class CreatePullRequestViewProvider extends BaseCreatePullRequestViewProv
const selectedRemote = selectedPick as vscode.QuickPickItem & { remote: RemoteInfo };
quickPick.busy = true;
githubRepository = this._folderRepositoryManager.findRepo(repo => repo.remote.owner === selectedRemote.remote.owner && repo.remote.repositoryName === selectedRemote.remote.repositoryName)!;
quickPick.items = await this.branchPicks(githubRepository, chooseDifferentRemote, isBase);
quickPick.items = await branchPicks(githubRepository, this._folderRepositoryManager, chooseDifferentRemote, isBase);
quickPick.placeholder = branchPlaceholder;
quickPick.busy = false;
} else if (selectedPick.branch && selectedPick.remote) {
Expand Down
13 changes: 11 additions & 2 deletions src/github/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ export interface ReopenedEvent {
createdAt: string;
}

export interface BaseRefChangedEvent {
__typename: string;
id: string;
actor: Actor;
createdAt: string;
currentRefName: string;
previousRefName: string;
}

export interface AbbreviatedIssueComment {
author: Account;
body: string;
Expand Down Expand Up @@ -265,7 +274,7 @@ export interface TimelineEventsResponse {
repository: {
pullRequest: {
timelineItems: {
nodes: (MergedEvent | Review | IssueComment | Commit | AssignedEvent | HeadRefDeletedEvent | null)[];
nodes: (MergedEvent | Review | IssueComment | Commit | AssignedEvent | HeadRefDeletedEvent | BaseRefChangedEvent | null)[];
};
};
} | null;
Expand Down Expand Up @@ -1051,7 +1060,7 @@ export interface MergePullRequestResponse {
mergePullRequest: {
pullRequest: PullRequest & {
timelineItems: {
nodes: (MergedEvent | Review | IssueComment | Commit | AssignedEvent | HeadRefDeletedEvent)[]
nodes: (MergedEvent | Review | IssueComment | Commit | AssignedEvent | HeadRefDeletedEvent | BaseRefChangedEvent)[]
}
};
}
Expand Down
1 change: 1 addition & 0 deletions src/github/issueModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface IssueChangeEvent {

draft?: true;
reviewers?: true;
base?: true;
}

export class IssueModel<TItem extends Issue = Issue> extends Disposable {
Expand Down
41 changes: 41 additions & 0 deletions src/github/pullRequestModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
SubmitReviewResponse,
TimelineEventsResponse,
UnresolveReviewThreadResponse,
UpdateIssueResponse,
} from './graphql';
import {
AccountType,
Expand Down Expand Up @@ -1199,6 +1200,46 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
return true;
}

/**
* Update the base branch of the pull request.
* @param newBaseBranch The new base branch name
*/
async updateBaseBranch(newBaseBranch: string): Promise<void> {
Logger.debug(`Updating base branch to ${newBaseBranch} - enter`, PullRequestModel.ID);
try {
const { mutate, schema } = await this.githubRepository.ensure();

const { data } = await mutate<UpdateIssueResponse>({
mutation: schema.UpdatePullRequest,
variables: {
input: {
pullRequestId: this.graphNodeId,
baseRefName: newBaseBranch,
},
},
});

if (data?.updateIssue?.issue) {
// Update the local base branch reference by creating a new GitHubRef instance
const cloneUrl = this.base.repositoryCloneUrl.toString() || '';
this.base = new GitHubRef(
newBaseBranch,
`${this.base.owner}:${newBaseBranch}`,
this.base.sha,
cloneUrl,
this.base.owner,
this.base.name,
this.base.isInOrganization
);
this._onDidChange.fire({ base: true });
}
Logger.debug(`Updating base branch to ${newBaseBranch} - done`, PullRequestModel.ID);
} catch (e) {
Logger.error(`Updating base branch to ${newBaseBranch} failed: ${e}`, PullRequestModel.ID);
throw e;
}
}

/**
* Get existing requests to review.
*/
Expand Down
50 changes: 48 additions & 2 deletions src/github/pullRequestOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import {
import { IssueOverviewPanel } from './issueOverview';
import { isCopilotOnMyBehalf, PullRequestModel } from './pullRequestModel';
import { PullRequestReviewCommon, ReviewContext } from './pullRequestReviewCommon';
import { pickEmail, reviewersQuickPick } from './quickPicks';
import { branchPicks, pickEmail, reviewersQuickPick } from './quickPicks';
import { parseReviewers } from './utils';
import { CancelCodingAgentReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReviewType } from './views';
import { CancelCodingAgentReply, ChangeBaseReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReviewType } from './views';
import { IComment } from '../common/comment';
import { COPILOT_SWE_AGENT, copilotEventToStatus, CopilotPRStatus, mostRecentCopilotEvent } from '../common/copilot';
import { commands, contexts } from '../common/executeCommands';
Expand Down Expand Up @@ -425,6 +425,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
return this.openCommitChanges(message);
case 'pr.delete-review':
return this.deleteReview(message);
case 'pr.change-base-branch':
return this.changeBaseBranch(message);
}
}

Expand Down Expand Up @@ -805,6 +807,50 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
}
}

private async changeBaseBranch(message: IRequestMessage<void>): Promise<void> {
const quickPick = vscode.window.createQuickPick<vscode.QuickPickItem & { branch?: string }>();

try {
quickPick.busy = true;
quickPick.canSelectMany = false;
quickPick.placeholder = vscode.l10n.t('Select a new base branch');
quickPick.show();

quickPick.items = await branchPicks(this._item.githubRepository, this._folderRepositoryManager, undefined, true);

quickPick.busy = false;
const acceptPromise = asPromise<void>(quickPick.onDidAccept).then(() => {
return quickPick.selectedItems[0]?.branch;
});
const hidePromise = asPromise<void>(quickPick.onDidHide);
const selectedBranch = await Promise.race<string | void>([acceptPromise, hidePromise]);
quickPick.busy = true;
quickPick.enabled = false;

if (selectedBranch) {
try {
await this._item.updateBaseBranch(selectedBranch);
const events = await this._getTimeline();
const reply: ChangeBaseReply = {
base: selectedBranch,
events
};
await this._replyMessage(message, reply);
} catch (e) {
Logger.error(formatError(e), PullRequestOverviewPanel.ID);
vscode.window.showErrorMessage(vscode.l10n.t('Changing base branch failed. {0}', formatError(e)));
this._throwError(message, `${formatError(e)}`);
}
}
} catch (e) {
Logger.error(formatError(e), PullRequestOverviewPanel.ID);
vscode.window.showErrorMessage(formatError(e));
} finally {
quickPick.hide();
quickPick.dispose();
}
}

override dispose() {
super.dispose();
disposeAll(this._prListeners);
Expand Down
13 changes: 13 additions & 0 deletions src/github/queriesShared.gql
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,17 @@ fragment ReopenedEvent on ReopenedEvent {
createdAt
}

fragment BaseRefChangedEvent on BaseRefChangedEvent {
id
actor {
...Node
...Actor
}
createdAt
currentRefName
previousRefName
}

fragment Review on PullRequestReview {
id
databaseId
Expand Down Expand Up @@ -312,6 +323,7 @@ query TimelineEvents($owner: String!, $name: String!, $number: Int!, $last: Int
...CrossReferencedEvent
...ClosedEvent
...ReopenedEvent
...BaseRefChangedEvent
}
}
}
Expand Down Expand Up @@ -1230,6 +1242,7 @@ mutation MergePullRequest($input: MergePullRequestInput!, $last: Int = 150) {
...CrossReferencedEvent
...ClosedEvent
...ReopenedEvent
...BaseRefChangedEvent
}
}
}
Expand Down
38 changes: 38 additions & 0 deletions src/github/quickPicks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { GitHubRepository, TeamReviewerRefreshKind } from './githubRepository';
import { AccountType, IAccount, ILabel, IMilestone, IProject, isISuggestedReviewer, isITeam, ISuggestedReviewer, ITeam, reviewerId, ReviewState } from './interface';
import { IssueModel } from './issueModel';
import { DisplayLabel } from './views';
import { RemoteInfo } from '../../common/types';
import { Ref } from '../api/api';
import { COPILOT_ACCOUNTS } from '../common/comment';
import { COPILOT_REVIEWER, COPILOT_REVIEWER_ID, COPILOT_SWE_AGENT } from '../common/copilot';
import { emojify, ensureEmojis } from '../common/emoji';
Expand Down Expand Up @@ -479,4 +481,40 @@ export async function pickEmail(githubRepository: GitHubRepository, current: str

const result = await vscode.window.showQuickPick(getEmails(), { canPickMany: false, title: vscode.l10n.t('Choose an email') });
return result ? result.label : undefined;
}

export async function branchPicks(githubRepository: GitHubRepository, folderRepoManager: FolderRepositoryManager, changeRepoMessage: string | undefined, isBase: boolean): Promise<(vscode.QuickPickItem & { remote?: RemoteInfo, branch?: string })[]> {
let branches: (string | Ref)[];
if (isBase) {
// For the base, we only want to show branches from GitHub.
branches = await githubRepository.listBranches(githubRepository.remote.owner, githubRepository.remote.repositoryName);
} else {
// For the compare, we only want to show local branches.
branches = (await folderRepoManager.repository.getBranches({ remote: false })).filter(branch => branch.name);
}
// TODO: @alexr00 - Add sorting so that the most likely to be used branch (ex main or release if base) is at the top of the list.
const branchPicks: (vscode.QuickPickItem & { remote?: RemoteInfo, branch?: string })[] = branches.map(branch => {
const branchName = typeof branch === 'string' ? branch : branch.name!;
const pick: (vscode.QuickPickItem & { remote: RemoteInfo, branch: string }) = {
iconPath: new vscode.ThemeIcon('git-branch'),
label: branchName,
remote: {
owner: githubRepository.remote.owner,
repositoryName: githubRepository.remote.repositoryName
},
branch: branchName
};
return pick;
});
branchPicks.unshift({
kind: vscode.QuickPickItemKind.Separator,
label: `${githubRepository.remote.owner}/${githubRepository.remote.repositoryName}`
});
if (changeRepoMessage) {
branchPicks.unshift({
iconPath: new vscode.ThemeIcon('repo'),
label: changeRepoMessage
});
}
return branchPicks;
}
Loading