diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 464440e8456..0cb271edf0a 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -2908,9 +2908,16 @@ export class AppStore extends TypedBaseStore { let contributionTargetDefaultBranch: string | undefined if (selectedRepository instanceof Repository) { + const targetBranch = findContributionTargetDefaultBranch( + selectedRepository, + branchesState + ) + // Show the remote tracking branch name (e.g. "origin/main") in the + // menu label, since we merge from the remote ref, not the local one. contributionTargetDefaultBranch = - findContributionTargetDefaultBranch(selectedRepository, branchesState) - ?.name ?? undefined + targetBranch !== null + ? targetBranch.upstream ?? targetBranch.name + : undefined } // From the menu, we'll offer to force-push whenever it's possible, regardless @@ -5897,6 +5904,22 @@ export class AppStore extends TypedBaseStore { }) } + /** + * Fetch all relevant remotes without fast-forwarding local branches. + * + * This updates remote tracking refs (e.g. origin/main) but leaves local + * branches untouched. Useful when you only need to merge from a remote + * ref without altering the local branch state. + */ + public _fetchWithoutFastForward( + repository: Repository, + fetchType: FetchType + ): Promise { + return this.withRefreshedGitHubRepository(repository, repository => { + return this.performFetch(repository, fetchType, undefined, true) + }) + } + /** * Fetch a particular remote in a repository. * @@ -5923,7 +5946,8 @@ export class AppStore extends TypedBaseStore { private async performFetch( repository: Repository, fetchType: FetchType, - remotes?: IRemote[] + remotes?: IRemote[], + skipFastForward?: boolean ): Promise { await this.withPushPullFetch(repository, async () => { const gitStore = this.gitStoreCache.get(repository) @@ -5954,14 +5978,16 @@ export class AppStore extends TypedBaseStore { ? 'Refreshing Repository' : 'Refreshing repository' - this.updatePushPullFetchProgress(repository, { - kind: 'generic', - title: refreshTitle, - description: 'Fast-forwarding branches', - value: fetchWeight, - }) + if (!skipFastForward) { + this.updatePushPullFetchProgress(repository, { + kind: 'generic', + title: refreshTitle, + description: 'Fast-forwarding branches', + value: fetchWeight, + }) - await this.fastForwardBranches(repository) + await this.fastForwardBranches(repository) + } this.updatePushPullFetchProgress(repository, { kind: 'generic', diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index d4f651c457a..8e5e1026fae 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -29,7 +29,7 @@ import { getNonGitHubUrl, isRepositoryWithGitHubRepository, } from '../models/repository' -import { Branch } from '../models/branch' +import { Branch, BranchType } from '../models/branch' import { PreferencesTab } from '../models/preferences' import { findItemByAccessKey, itemIsSelectable } from '../models/app-menu' import { Account, isDotComAccount } from '../models/account' @@ -659,19 +659,54 @@ export class App extends React.Component { return } - await this.props.dispatcher.fetch(repository, FetchType.UserInitiatedTask) + // Fetch from the remote to update the remote tracking ref (e.g. + // origin/main) without fast-forwarding local branches. + await this.props.dispatcher.fetchWithoutFastForward( + repository, + FetchType.UserInitiatedTask + ) + + // Merge the remote tracking branch (e.g. origin/main) instead of the + // local branch, so that the local default branch is not altered as a + // side effect. + const branchToMerge = this.getRemoteTrackingBranch( + contributionTargetDefaultBranch, + state.branchesState.allBranches + ) this.props.dispatcher.initializeMergeOperation( repository, false, - contributionTargetDefaultBranch + branchToMerge ) const { mergeStatus } = state.compareState - this.props.dispatcher.mergeBranch( - repository, - contributionTargetDefaultBranch, - mergeStatus + this.props.dispatcher.mergeBranch(repository, branchToMerge, mergeStatus) + } + + /** + * Returns the remote tracking branch for a given branch if available. + * For remote branches, returns the branch itself. + * For local branches, finds the corresponding remote branch in allBranches. + * Falls back to the original branch if no remote tracking branch is found. + */ + private getRemoteTrackingBranch( + branch: Branch, + allBranches: ReadonlyArray + ): Branch { + if (branch.type === BranchType.Remote) { + return branch + } + + const upstream = branch.upstream + if (upstream === null) { + return branch + } + + return ( + allBranches.find( + b => b.type === BranchType.Remote && b.name === upstream + ) ?? branch ) } diff --git a/app/src/ui/dispatcher/dispatcher.ts b/app/src/ui/dispatcher/dispatcher.ts index f970c1c1ffa..827452140e1 100644 --- a/app/src/ui/dispatcher/dispatcher.ts +++ b/app/src/ui/dispatcher/dispatcher.ts @@ -788,6 +788,17 @@ export class Dispatcher { return this.appStore._fetch(repository, fetchType) } + /** + * Fetch all refs without fast-forwarding local branches. + * Updates remote tracking refs only, leaving local branches untouched. + */ + public fetchWithoutFastForward( + repository: Repository, + fetchType: FetchType + ): Promise { + return this.appStore._fetchWithoutFastForward(repository, fetchType) + } + /** Publish the repository to GitHub with the given properties. */ public publishRepository( repository: Repository,