Skip to content

Commit f5ee15f

Browse files
Copilotalexr00
andauthored
Associate local branches with PRs on first activation (#8227)
* Initial plan * Initial plan for associating local branches with PRs on first activation Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Implement branch association logic for first activation Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Address code review feedback - document chunk size rationale Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Fix comment - clarify chunk size rationale Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Limit branch association to 3 most recently used branches Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Bump it to 10 --------- 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 381a8b1 commit f5ee15f

File tree

2 files changed

+113
-1
lines changed

2 files changed

+113
-1
lines changed

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

1718
export interface RepoState {
1819
mentionableUsers?: IAccount[];

src/github/folderRepositoryManager.ts

Lines changed: 112 additions & 1 deletion
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

0 commit comments

Comments
 (0)