Skip to content

Commit 580b56d

Browse files
committed
Merge branch 'main' into copilot/fix-branch-selection-issue
2 parents 5c10ed4 + 8fa502f commit 580b56d

18 files changed

+380
-76
lines changed

package.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@
243243
"default": [
244244
{
245245
"label": "%githubPullRequests.queries.copilotOnMyBehalf%",
246-
"query": "repo:${owner}/${repository} is:open author:copilot involves:${user}"
246+
"query": "repo:${owner}/${repository} is:open author:copilot assignee:${user}"
247247
},
248248
{
249249
"label": "Local Pull Request Branches",
@@ -321,6 +321,11 @@
321321
"default": true,
322322
"description": "%githubPullRequests.defaultDeletionMethod.selectRemote.description%"
323323
},
324+
"githubPullRequests.deleteBranchAfterMerge": {
325+
"type": "boolean",
326+
"default": false,
327+
"description": "%githubPullRequests.deleteBranchAfterMerge.description%"
328+
},
324329
"githubPullRequests.terminalLinksHandler": {
325330
"type": "string",
326331
"enum": [
@@ -711,6 +716,21 @@
711716
"default": "on",
712717
"markdownDescription": "%githubIssues.useBranchForIssues.markdownDescription%"
713718
},
719+
"githubIssues.workingBaseBranch": {
720+
"type": "string",
721+
"enum": [
722+
"currentBranch",
723+
"defaultBranch",
724+
"prompt"
725+
],
726+
"enumDescriptions": [
727+
"%githubIssues.workingBaseBranch.currentBranch%",
728+
"%githubIssues.workingBaseBranch.defaultBranch%",
729+
"%githubIssues.workingBaseBranch.prompt%"
730+
],
731+
"default": "currentBranch",
732+
"markdownDescription": "%githubIssues.workingBaseBranch.markdownDescription%"
733+
},
714734
"githubIssues.issueCompletionFormatScm": {
715735
"type": "string",
716736
"default": "${issueTitle}\nFixes ${issueNumberLabel}",

package.nls.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"githubPullRequests.hideViewedFiles.description": "Hide files that have been marked as viewed in the pull request changes tree.",
4040
"githubPullRequests.defaultDeletionMethod.selectLocalBranch.description": "When true, the option to delete the local branch will be selected by default when deleting a branch from a pull request.",
4141
"githubPullRequests.defaultDeletionMethod.selectRemote.description": "When true, the option to delete the remote will be selected by default when deleting a branch from a pull request.",
42+
"githubPullRequests.deleteBranchAfterMerge.description": "Automatically delete the branch after merging a pull request. This setting only applies when the pull request is merged through this extension.",
4243
"githubPullRequests.terminalLinksHandler.description": "Default handler for terminal links.",
4344
"githubPullRequests.terminalLinksHandler.github": "Create the pull request on GitHub",
4445
"githubPullRequests.terminalLinksHandler.vscode": "Create the pull request in VS Code",
@@ -130,6 +131,15 @@
130131
"githubIssues.useBranchForIssues.on": "A branch will always be checked out when you start working on an issue. If the branch doesn't exist, it will be created.",
131132
"githubIssues.useBranchForIssues.off": "A branch will not be created when you start working on an issue. If you have worked on an issue before and a branch was created for it, that same branch will be checked out.",
132133
"githubIssues.useBranchForIssues.prompt": "A prompt will show for setting the name of the branch that will be created and checked out.",
134+
"githubIssues.workingBaseBranch.markdownDescription": {
135+
"message": "Determines which branch to use as the base when creating a new branch for an issue. This setting controls what branch the new issue branch is created from.",
136+
"comment": [
137+
"Describes the base branch selection for issue branches"
138+
]
139+
},
140+
"githubIssues.workingBaseBranch.currentBranch": "Create the issue branch from the current branch without switching to the default branch first.",
141+
"githubIssues.workingBaseBranch.defaultBranch": "Always switch to the default branch before creating the issue branch.",
142+
"githubIssues.workingBaseBranch.prompt": "Prompt which branch to use as the base when creating an issue branch.",
133143
"githubIssues.issueCompletionFormatScm.markdownDescription": {
134144
"message": "Sets the format of issue completions in the SCM inputbox. \n- `${user}` will be replace with the currently logged in username \n- `${issueNumber}` will be replaced with the current issue number \n- `${issueNumberLabel}` will be replaced with a label formatted as #number or owner/repository#number, depending on whether the issue is in the current repository",
135145
"comment": [

src/common/settingKeys.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const DEFAULT_MERGE_METHOD = 'defaultMergeMethod';
3333
export const DEFAULT_DELETION_METHOD = 'defaultDeletionMethod';
3434
export const SELECT_LOCAL_BRANCH = 'selectLocalBranch';
3535
export const SELECT_REMOTE = 'selectRemote';
36+
export const DELETE_BRANCH_AFTER_MERGE = 'deleteBranchAfterMerge';
3637
export const REMOTES = 'remotes';
3738
export const PULL_PR_BRANCH_BEFORE_CHECKOUT = 'pullPullRequestBranchBeforeCheckout';
3839
export type PullPRBranchVariants = 'never' | 'pull' | 'pullAndMergeBase' | 'pullAndUpdateBase' | true | false;
@@ -49,6 +50,7 @@ export const IGNORE_USER_COMPLETION_TRIGGER = 'ignoreUserCompletionTrigger';
4950
export const CREATE_INSERT_FORMAT = 'createInsertFormat';
5051
export const ISSUE_BRANCH_TITLE = 'issueBranchTitle';
5152
export const USE_BRANCH_FOR_ISSUES = 'useBranchForIssues';
53+
export const WORKING_BASE_BRANCH = 'workingBaseBranch';
5254
export const WORKING_ISSUE_FORMAT_SCM = 'workingIssueFormatScm';
5355
export const IGNORE_COMPLETION_TRIGGER = 'ignoreCompletionTrigger';
5456
export const ISSUE_COMPLETION_FORMAT_SCM = 'issueCompletionFormatScm';

src/extensionState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const NEVER_SHOW_PULL_NOTIFICATION = 'github.pullRequest.pullNotification
1313
export const REPO_KEYS = 'github.pullRequest.repos';
1414
export const PREVIOUS_CREATE_METHOD = 'github.pullRequest.previousCreateMethod';
1515
export const LAST_USED_EMAIL = 'github.pullRequest.lastUsedEmail';
16+
export const BRANCHES_ASSOCIATED_WITH_PRS = 'github.pullRequest.branchesAssociatedWithPRs';
1617
export const RECENTLY_USED_BRANCHES = 'github.pullRequest.recentlyUsedBranches';
1718

1819
export interface RepoState {

src/github/activityBarViewProvider.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { MergeArguments, PullRequest, ReviewType } from './views';
1515
import { IComment } from '../common/comment';
1616
import { emojify, ensureEmojis } from '../common/emoji';
1717
import { disposeAll } from '../common/lifecycle';
18+
import { DELETE_BRANCH_AFTER_MERGE, PR_SETTINGS_NAMESPACE } from '../common/settingKeys';
1819
import { ReviewEvent } from '../common/timelineEvent';
1920
import { formatError } from '../common/utils';
2021
import { generateUuid } from '../common/uuid';
@@ -409,6 +410,13 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W
409410

410411
if (!result.merged) {
411412
vscode.window.showErrorMessage(vscode.l10n.t('Merging pull request failed: {0}', result?.message ?? ''));
413+
} else {
414+
// Check if auto-delete branch setting is enabled
415+
const deleteBranchAfterMerge = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<boolean>(DELETE_BRANCH_AFTER_MERGE, false);
416+
if (deleteBranchAfterMerge) {
417+
// Automatically delete the branch after successful merge
418+
await PullRequestReviewCommon.autoDeleteBranchesAfterMerge(this._folderRepositoryManager, this._item);
419+
}
412420
}
413421

414422
this._replyMessage(message, {

src/github/folderRepositoryManager.ts

Lines changed: 125 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import { EventType } from '../common/timelineEvent';
5454
import { Schemes } from '../common/uri';
5555
import { AsyncPredicate, batchPromiseAll, compareIgnoreCase, formatError, Predicate } from '../common/utils';
5656
import { PULL_REQUEST_OVERVIEW_VIEW_TYPE } from '../common/webview';
57-
import { LAST_USED_EMAIL, NEVER_SHOW_PULL_NOTIFICATION, REPO_KEYS, ReposState } from '../extensionState';
57+
import { BRANCHES_ASSOCIATED_WITH_PRS, LAST_USED_EMAIL, NEVER_SHOW_PULL_NOTIFICATION, REPO_KEYS, ReposState } from '../extensionState';
5858
import { git } from '../gitProviders/gitCommands';
5959
import { IThemeWatcher } from '../themeWatcher';
6060
import { CreatePullRequestHelper } from '../view/createPullRequestHelper';
@@ -576,6 +576,11 @@ export class FolderRepositoryManager extends Disposable {
576576
this.getAssignableUsers(repositoriesAdded.length > 0);
577577
if (isAuthenticated && activeRemotes.length) {
578578
this.state = ReposManagerState.RepositoriesLoaded;
579+
// On first activation, associate local branches with PRs
580+
// Do this asynchronously to not block the main flow
581+
this.associateLocalBranchesWithPRsOnFirstActivation().catch(e => {
582+
Logger.error(`Failed to associate branches with PRs: ${e}`, this.id);
583+
});
579584
} else if (!isAuthenticated) {
580585
this.state = ReposManagerState.NeedsAuthentication;
581586
}
@@ -953,6 +958,112 @@ export class FolderRepositoryManager extends Disposable {
953958
return models.filter(value => value !== undefined) as PullRequestModel[];
954959
}
955960

961+
/**
962+
* On first activation, iterate through local branches and associate them with PRs if they match.
963+
* This helps discover PRs that were created before the extension was installed or in other ways.
964+
*/
965+
private async associateLocalBranchesWithPRsOnFirstActivation(): Promise<void> {
966+
const stateKey = `${BRANCHES_ASSOCIATED_WITH_PRS}.${this.repository.rootUri.fsPath}`;
967+
const hasRun = this.context.globalState.get<boolean>(stateKey, false);
968+
969+
if (hasRun) {
970+
Logger.debug('Branch association has already run for this workspace folder', this.id);
971+
return;
972+
}
973+
974+
Logger.appendLine('First activation: associating local branches with PRs', this.id);
975+
976+
const githubRepositories = this._githubRepositories;
977+
if (!githubRepositories || !githubRepositories.length || !this.repository.getRefs) {
978+
Logger.debug('No GitHub repositories or getRefs not available, skipping branch association', this.id);
979+
await this.context.globalState.update(stateKey, true);
980+
return;
981+
}
982+
983+
try {
984+
// Only check the 3 most recently used branches to minimize API calls
985+
const localBranches = (await this.repository.getRefs({
986+
pattern: 'refs/heads/',
987+
sort: 'committerdate',
988+
count: 10
989+
}))
990+
.filter(r => r.name !== undefined)
991+
.map(r => r.name!);
992+
993+
Logger.debug(`Found ${localBranches.length} local branches to check`, this.id);
994+
995+
const associationResults: boolean[] = [];
996+
997+
// Process all branches (max 3) in parallel
998+
const chunkResults = await Promise.all(localBranches.map(async branchName => {
999+
try {
1000+
// Check if this branch already has PR metadata
1001+
const existingMetadata = await PullRequestGitHelper.getMatchingPullRequestMetadataForBranch(
1002+
this.repository,
1003+
branchName,
1004+
);
1005+
1006+
if (existingMetadata) {
1007+
// Branch already has PR metadata, skip
1008+
return false;
1009+
}
1010+
1011+
// Get the branch to check its upstream
1012+
const branch = await this.repository.getBranch(branchName);
1013+
if (!branch.upstream) {
1014+
// No upstream, can't match to a PR
1015+
return false;
1016+
}
1017+
1018+
// Try to find a matching PR on GitHub
1019+
const remoteName = branch.upstream.remote;
1020+
const upstreamBranchName = branch.upstream.name;
1021+
1022+
const githubRepo = githubRepositories.find(
1023+
repo => repo.remote.remoteName === remoteName,
1024+
);
1025+
1026+
if (!githubRepo) {
1027+
return false;
1028+
}
1029+
1030+
// Get the metadata of the GitHub repository to find owner
1031+
const metadata = await githubRepo.getMetadata();
1032+
if (!metadata?.owner) {
1033+
return false;
1034+
}
1035+
1036+
// Search for a PR with this head branch
1037+
const matchingPR = await githubRepo.getPullRequestForBranch(upstreamBranchName, metadata.owner.login);
1038+
1039+
if (matchingPR) {
1040+
Logger.appendLine(`Found PR #${matchingPR.number} for branch ${branchName}, associating...`, this.id);
1041+
await PullRequestGitHelper.associateBranchWithPullRequest(
1042+
this.repository,
1043+
matchingPR,
1044+
branchName,
1045+
);
1046+
return true;
1047+
}
1048+
return false;
1049+
} catch (e) {
1050+
Logger.debug(`Error checking branch ${branchName}: ${e}`, this.id);
1051+
// Continue with other branches even if one fails
1052+
return false;
1053+
}
1054+
}));
1055+
associationResults.push(...chunkResults);
1056+
1057+
const associatedCount = associationResults.filter(r => r).length;
1058+
Logger.appendLine(`Branch association complete: ${associatedCount} branches associated with PRs`, this.id);
1059+
} catch (e) {
1060+
Logger.error(`Error during branch association: ${e}`, this.id);
1061+
} finally {
1062+
// Mark as complete even if there were errors
1063+
await this.context.globalState.update(stateKey, true);
1064+
}
1065+
}
1066+
9561067
async getLabels(issue?: IssueModel, repoInfo?: { owner: string; repo: string }): Promise<ILabel[]> {
9571068
const repo = issue
9581069
? issue.githubRepository
@@ -1086,6 +1197,10 @@ export class FolderRepositoryManager extends Disposable {
10861197

10871198
const activeGitHubRemotes = await this.getActiveGitHubRemotes(this._allGitHubRemotes);
10881199

1200+
// Check if user has explicitly configured remotes (not using defaults)
1201+
const remotesConfig = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).inspect<string[]>(REMOTES);
1202+
const hasUserConfiguredRemotes = !!(remotesConfig?.globalValue || remotesConfig?.workspaceValue || remotesConfig?.workspaceFolderValue);
1203+
10891204
const githubRepositories = this._githubRepositories.filter(repo => {
10901205
if (!activeGitHubRemotes.find(r => r.equals(repo.remote))) {
10911206
return false;
@@ -1148,15 +1263,17 @@ export class FolderRepositoryManager extends Disposable {
11481263

11491264
pageInformation.hasMorePages = itemData.hasMorePages;
11501265

1151-
// Break early if
1266+
// Determine if we should break early from the loop:
11521267
// 1) we've received data AND
11531268
// 2) either we're fetching just the next page (case 2)
11541269
// OR we're fetching all (cases 1&3), and we've fetched as far as we had previously (or further, in case 1).
1155-
if (
1156-
itemData.items.length &&
1157-
(options.fetchNextPage ||
1158-
((options.fetchNextPage === false) && !options.fetchOnePagePerRepo && (pagesFetched >= getTotalFetchedPages())))
1159-
) {
1270+
// 3) AND the user hasn't explicitly configured remotes (if they have, we should search all of them)
1271+
const hasReceivedData = itemData.items.length > 0;
1272+
const isFetchingNextPage = options.fetchNextPage;
1273+
const hasReachedPreviousFetchLimit = (options.fetchNextPage === false) && !options.fetchOnePagePerRepo && (pagesFetched >= getTotalFetchedPages());
1274+
const shouldBreakEarly = hasReceivedData && (isFetchingNextPage || hasReachedPreviousFetchLimit) && !hasUserConfiguredRemotes;
1275+
1276+
if (shouldBreakEarly) {
11601277
if (getTotalFetchedPages() === 0) {
11611278
// We're in case 1, manually set number of pages we looked through until we found first results.
11621279
setTotalFetchedPages(pagesFetched);
@@ -1173,7 +1290,7 @@ export class FolderRepositoryManager extends Disposable {
11731290

11741291
return {
11751292
items: itemData.items,
1176-
hasMorePages: false,
1293+
hasMorePages: itemData.hasMorePages,
11771294
hasUnsearchedRepositories: false,
11781295
totalCount: itemData.totalCount
11791296
};

src/github/pullRequestGitHelper.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ export interface BaseBranchMetadata {
3333
branch: string;
3434
}
3535

36+
export type BranchInfo = {
37+
branch: string;
38+
remote?: string;
39+
createdForPullRequest?: boolean;
40+
remoteInUse?: boolean;
41+
};
42+
3643
export class PullRequestGitHelper {
3744
static ID = 'PullRequestGitHelper';
3845
static async checkoutFromFork(
@@ -202,12 +209,7 @@ export class PullRequestGitHelper {
202209
static async getBranchNRemoteForPullRequest(
203210
repository: Repository,
204211
pullRequest: PullRequestModel,
205-
): Promise<{
206-
branch: string;
207-
remote?: string;
208-
createdForPullRequest?: boolean;
209-
remoteInUse?: boolean;
210-
} | null> {
212+
): Promise<BranchInfo | null> {
211213
let branchName: string | null = null;
212214
try {
213215
const key = PullRequestGitHelper.buildPullRequestMetadata(pullRequest);

src/github/pullRequestOverview.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { COPILOT_SWE_AGENT, copilotEventToStatus, CopilotPRStatus, mostRecentCop
3131
import { commands, contexts } from '../common/executeCommands';
3232
import { disposeAll } from '../common/lifecycle';
3333
import Logger from '../common/logger';
34-
import { DEFAULT_MERGE_METHOD, PR_SETTINGS_NAMESPACE } from '../common/settingKeys';
34+
import { DEFAULT_MERGE_METHOD, DELETE_BRANCH_AFTER_MERGE, PR_SETTINGS_NAMESPACE } from '../common/settingKeys';
3535
import { ITelemetry } from '../common/telemetry';
3636
import { EventType, ReviewEvent, SessionLinkInfo, TimelineEvent } from '../common/timelineEvent';
3737
import { asPromise, formatError } from '../common/utils';
@@ -626,6 +626,13 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
626626

627627
if (!result.merged) {
628628
vscode.window.showErrorMessage(`Merging pull request failed: ${result.message}`);
629+
} else {
630+
// Check if auto-delete branch setting is enabled
631+
const deleteBranchAfterMerge = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<boolean>(DELETE_BRANCH_AFTER_MERGE, false);
632+
if (deleteBranchAfterMerge) {
633+
// Automatically delete the branch after successful merge
634+
await PullRequestReviewCommon.autoDeleteBranchesAfterMerge(this._folderRepositoryManager, this._item);
635+
}
629636
}
630637

631638
const mergeResult: MergeResult = {

0 commit comments

Comments
 (0)