Skip to content

Commit 5068acc

Browse files
Copilotalexr00
andauthored
Add base branch editing to PR overview (#8232)
* Initial plan * Initial plan for base branch editing feature Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add base branch editing functionality Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Fix TypeScript errors in base branch editing feature Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Address code review feedback - use immutable GitHubRef update Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add test for updateBaseBranch method Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Reuse branch picks code * Add 4px top margin to edit button and include base branch in reply Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Use a type * Add BaseRefChanged timeline event and include in ChangeBaseReply Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Make base branch clickable instead of using edit icon button Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Use code styling for base branch text and secondary button styling Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Fix stying * Remove silly test * Fix bad merge --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent e5c0627 commit 5068acc

14 files changed

Lines changed: 1227 additions & 1034 deletions

File tree

src/common/timelineEvent.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export enum EventType {
2222
CrossReferenced,
2323
Closed,
2424
Reopened,
25+
BaseRefChanged,
2526
CopilotStarted,
2627
CopilotFinished,
2728
CopilotFinishedError,
@@ -156,6 +157,15 @@ export interface ReopenedEvent {
156157
createdAt: string;
157158
}
158159

160+
export interface BaseRefChangedEvent {
161+
id: string;
162+
event: EventType.BaseRefChanged;
163+
actor: IActor;
164+
createdAt: string;
165+
currentRefName: string;
166+
previousRefName: string;
167+
}
168+
159169
export interface SessionPullInfo {
160170
id: number;
161171
host: string;
@@ -192,4 +202,4 @@ export interface CopilotFinishedErrorEvent {
192202
sessionLink: SessionLinkInfo;
193203
}
194204

195-
export type TimelineEvent = CommitEvent | ReviewEvent | CommentEvent | NewCommitsSinceReviewEvent | MergedEvent | AssignEvent | UnassignEvent | HeadRefDeleteEvent | CrossReferencedEvent | ClosedEvent | ReopenedEvent | CopilotStartedEvent | CopilotFinishedEvent | CopilotFinishedErrorEvent;
205+
export type TimelineEvent = CommitEvent | ReviewEvent | CommentEvent | NewCommitsSinceReviewEvent | MergedEvent | AssignEvent | UnassignEvent | HeadRefDeleteEvent | CrossReferencedEvent | ClosedEvent | ReopenedEvent | BaseRefChangedEvent | CopilotStartedEvent | CopilotFinishedEvent | CopilotFinishedErrorEvent;

src/github/createPRViewProvider.ts

Lines changed: 4 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import { IAccount, ILabel, IMilestone, IProject, isITeam, ITeam, MergeMethod, Re
1515
import { BaseBranchMetadata, PullRequestGitHelper } from './pullRequestGitHelper';
1616
import { PullRequestModel } from './pullRequestModel';
1717
import { getDefaultMergeMethod } from './pullRequestOverview';
18-
import { getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, getProjectFromQuickPick, reviewersQuickPick } from './quickPicks';
18+
import { branchPicks, getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, getProjectFromQuickPick, reviewersQuickPick } from './quickPicks';
1919
import { getIssueNumberLabelFromParsed, ISSUE_EXPRESSION, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, variableSubstitution } from './utils';
2020
import { ChangeTemplateReply, DisplayLabel, PreReviewState } from './views';
2121
import { RemoteInfo } from '../../common/types';
2222
import { ChooseBaseRemoteAndBranchResult, ChooseCompareRemoteAndBranchResult, ChooseRemoteAndBranchArgs, CreateParamsNew, CreatePullRequestNew, TitleAndDescriptionArgs } from '../../common/views';
23-
import type { Branch, Ref } from '../api/api';
23+
import type { Branch } from '../api/api';
2424
import { GitHubServerType } from '../common/authentication';
2525
import { emojify, ensureEmojis } from '../common/emoji';
2626
import { commands, contexts } from '../common/executeCommands';
@@ -133,12 +133,6 @@ export abstract class BaseCreatePullRequestViewProvider<T extends BasePullReques
133133
return repo.getRepoAccessAndMergeMethods(refetch);
134134
}
135135

136-
protected getRecentlyUsedBranches(owner: string, repositoryName: string): string[] {
137-
const repoKey = `${owner}/${repositoryName}`;
138-
const state = this._folderRepositoryManager.context.workspaceState.get<RecentlyUsedBranchesState>(RECENTLY_USED_BRANCHES, { branches: {} });
139-
return state.branches[repoKey] || [];
140-
}
141-
142136
protected saveRecentlyUsedBranch(owner: string, repositoryName: string, branchName: string): void {
143137
const repoKey = `${owner}/${repositoryName}`;
144138
const state = this._folderRepositoryManager.context.workspaceState.get<RecentlyUsedBranchesState>(RECENTLY_USED_BRANCHES, { branches: {} });
@@ -901,77 +895,6 @@ export class CreatePullRequestViewProvider extends BaseCreatePullRequestViewProv
901895
});
902896
}
903897

904-
private async branchPicks(githubRepository: GitHubRepository, changeRepoMessage: string, isBase: boolean): Promise<(vscode.QuickPickItem & { remote?: RemoteInfo, branch?: string })[]> {
905-
let branches: (string | Ref)[];
906-
if (isBase) {
907-
// For the base, we only want to show branches from GitHub.
908-
branches = await githubRepository.listBranches(githubRepository.remote.owner, githubRepository.remote.repositoryName);
909-
} else {
910-
// For the compare, we only want to show local branches.
911-
branches = (await this._folderRepositoryManager.repository.getBranches({ remote: false })).filter(branch => branch.name);
912-
}
913-
914-
const branchNames = branches.map(branch => typeof branch === 'string' ? branch : branch.name!);
915-
916-
// Get recently used branches for base branches only
917-
let recentBranches: string[] = [];
918-
let otherBranches: string[] = branchNames;
919-
if (isBase) {
920-
const recentlyUsed = this.getRecentlyUsedBranches(githubRepository.remote.owner, githubRepository.remote.repositoryName);
921-
// Include all recently used branches, even if they're not in the current branch list
922-
// This allows showing branches that weren't fetched due to timeout
923-
recentBranches = recentlyUsed;
924-
// Remove recently used branches from the main list (if they exist there)
925-
otherBranches = branchNames.filter(name => !recentBranches.includes(name));
926-
}
927-
928-
const branchPicks: (vscode.QuickPickItem & { remote?: RemoteInfo, branch?: string })[] = [];
929-
930-
// Add recently used branches section
931-
if (recentBranches.length > 0) {
932-
branchPicks.push({
933-
kind: vscode.QuickPickItemKind.Separator,
934-
label: vscode.l10n.t('Recently Used')
935-
});
936-
recentBranches.forEach(branchName => {
937-
branchPicks.push({
938-
iconPath: new vscode.ThemeIcon('git-branch'),
939-
label: branchName,
940-
remote: {
941-
owner: githubRepository.remote.owner,
942-
repositoryName: githubRepository.remote.repositoryName
943-
},
944-
branch: branchName
945-
});
946-
});
947-
}
948-
949-
// Add all other branches section
950-
if (otherBranches.length > 0) {
951-
branchPicks.push({
952-
kind: vscode.QuickPickItemKind.Separator,
953-
label: recentBranches.length > 0 ? vscode.l10n.t('All Branches') : `${githubRepository.remote.owner}/${githubRepository.remote.repositoryName}`
954-
});
955-
otherBranches.forEach(branchName => {
956-
branchPicks.push({
957-
iconPath: new vscode.ThemeIcon('git-branch'),
958-
label: branchName,
959-
remote: {
960-
owner: githubRepository.remote.owner,
961-
repositoryName: githubRepository.remote.repositoryName
962-
},
963-
branch: branchName
964-
});
965-
});
966-
}
967-
968-
branchPicks.unshift({
969-
iconPath: new vscode.ThemeIcon('repo'),
970-
label: changeRepoMessage
971-
});
972-
return branchPicks;
973-
}
974-
975898
private async processRemoteAndBranchResult(githubRepository: GitHubRepository, result: { remote: RemoteInfo, branch: string }, isBase: boolean) {
976899
const [defaultBranch, viewerPermission] = await Promise.all([githubRepository.getDefaultBranch(), githubRepository.getViewerPermission()]);
977900

@@ -1052,7 +975,7 @@ export class CreatePullRequestViewProvider extends BaseCreatePullRequestViewProv
1052975
quickPick.placeholder = githubRepository ? branchPlaceholder : remotePlaceholder;
1053976
quickPick.show();
1054977
quickPick.busy = true;
1055-
quickPick.items = githubRepository ? await this.branchPicks(githubRepository, chooseDifferentRemote, isBase) : await this.remotePicks(isBase);
978+
quickPick.items = githubRepository ? await branchPicks(githubRepository, this._folderRepositoryManager, chooseDifferentRemote, isBase) : await this.remotePicks(isBase);
1056979
const activeItem = message.args.currentBranch ? quickPick.items.find(item => item.branch === message.args.currentBranch) : undefined;
1057980
quickPick.activeItems = activeItem ? [activeItem] : [];
1058981
quickPick.busy = false;
@@ -1071,7 +994,7 @@ export class CreatePullRequestViewProvider extends BaseCreatePullRequestViewProv
1071994
const selectedRemote = selectedPick as vscode.QuickPickItem & { remote: RemoteInfo };
1072995
quickPick.busy = true;
1073996
githubRepository = this._folderRepositoryManager.findRepo(repo => repo.remote.owner === selectedRemote.remote.owner && repo.remote.repositoryName === selectedRemote.remote.repositoryName)!;
1074-
quickPick.items = await this.branchPicks(githubRepository, chooseDifferentRemote, isBase);
997+
quickPick.items = await branchPicks(githubRepository, this._folderRepositoryManager, chooseDifferentRemote, isBase);
1075998
quickPick.placeholder = branchPlaceholder;
1076999
quickPick.busy = false;
10771000
} else if (selectedPick.branch && selectedPick.remote) {

src/github/graphql.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ export interface ReopenedEvent {
6868
createdAt: string;
6969
}
7070

71+
export interface BaseRefChangedEvent {
72+
__typename: string;
73+
id: string;
74+
actor: Actor;
75+
createdAt: string;
76+
currentRefName: string;
77+
previousRefName: string;
78+
}
79+
7180
export interface AbbreviatedIssueComment {
7281
author: Account;
7382
body: string;
@@ -270,7 +279,7 @@ export interface TimelineEventsResponse {
270279
repository: {
271280
pullRequest: {
272281
timelineItems: {
273-
nodes: (MergedEvent | Review | IssueComment | Commit | AssignedEvent | HeadRefDeletedEvent | null)[];
282+
nodes: (MergedEvent | Review | IssueComment | Commit | AssignedEvent | HeadRefDeletedEvent | BaseRefChangedEvent | null)[];
274283
};
275284
};
276285
} | null;
@@ -1076,7 +1085,7 @@ export interface MergePullRequestResponse {
10761085
mergePullRequest: {
10771086
pullRequest: PullRequest & {
10781087
timelineItems: {
1079-
nodes: (MergedEvent | Review | IssueComment | Commit | AssignedEvent | HeadRefDeletedEvent)[]
1088+
nodes: (MergedEvent | Review | IssueComment | Commit | AssignedEvent | HeadRefDeletedEvent | BaseRefChangedEvent)[]
10801089
}
10811090
};
10821091
}

src/github/issueModel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface IssueChangeEvent {
4141

4242
draft?: true;
4343
reviewers?: true;
44+
base?: true;
4445
}
4546

4647
export class IssueModel<TItem extends Issue = Issue> extends Disposable {

src/github/pullRequestModel.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
SubmitReviewResponse,
4444
TimelineEventsResponse,
4545
UnresolveReviewThreadResponse,
46+
UpdateIssueResponse,
4647
} from './graphql';
4748
import {
4849
AccountType,
@@ -1230,6 +1231,46 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
12301231
return true;
12311232
}
12321233

1234+
/**
1235+
* Update the base branch of the pull request.
1236+
* @param newBaseBranch The new base branch name
1237+
*/
1238+
async updateBaseBranch(newBaseBranch: string): Promise<void> {
1239+
Logger.debug(`Updating base branch to ${newBaseBranch} - enter`, PullRequestModel.ID);
1240+
try {
1241+
const { mutate, schema } = await this.githubRepository.ensure();
1242+
1243+
const { data } = await mutate<UpdateIssueResponse>({
1244+
mutation: schema.UpdatePullRequest,
1245+
variables: {
1246+
input: {
1247+
pullRequestId: this.graphNodeId,
1248+
baseRefName: newBaseBranch,
1249+
},
1250+
},
1251+
});
1252+
1253+
if (data?.updateIssue?.issue) {
1254+
// Update the local base branch reference by creating a new GitHubRef instance
1255+
const cloneUrl = this.base.repositoryCloneUrl.toString() || '';
1256+
this.base = new GitHubRef(
1257+
newBaseBranch,
1258+
`${this.base.owner}:${newBaseBranch}`,
1259+
this.base.sha,
1260+
cloneUrl,
1261+
this.base.owner,
1262+
this.base.name,
1263+
this.base.isInOrganization
1264+
);
1265+
this._onDidChange.fire({ base: true });
1266+
}
1267+
Logger.debug(`Updating base branch to ${newBaseBranch} - done`, PullRequestModel.ID);
1268+
} catch (e) {
1269+
Logger.error(`Updating base branch to ${newBaseBranch} failed: ${e}`, PullRequestModel.ID);
1270+
throw e;
1271+
}
1272+
}
1273+
12331274
/**
12341275
* Get existing requests to review.
12351276
*/

0 commit comments

Comments
 (0)