Skip to content

Commit 58bc6b5

Browse files
guplemclaude
andcommitted
feat: merge from remote tracking branch in Update from Default Branch
Instead of merging the local default branch (e.g. `main`), merge the remote tracking branch (e.g. `origin/main`) after fetching. This avoids altering local branches as a side effect of the update operation. Changes: - Fetch without fast-forwarding local branches before merging - Find and merge the remote tracking branch instead of the local one - Update the menu label to show the remote ref (e.g. "Update from origin/main") Follow-up to PR #111 as suggested by @pol-rivero. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5aa44a0 commit 58bc6b5

3 files changed

Lines changed: 97 additions & 14 deletions

File tree

app/src/lib/stores/app-store.ts

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2590,9 +2590,16 @@ export class AppStore extends TypedBaseStore<IAppState> {
25902590

25912591
let contributionTargetDefaultBranch: string | undefined
25922592
if (selectedRepository instanceof Repository) {
2593+
const targetBranch = findContributionTargetDefaultBranch(
2594+
selectedRepository,
2595+
branchesState
2596+
)
2597+
// Show the remote tracking branch name (e.g. "origin/main") in the
2598+
// menu label, since we merge from the remote ref, not the local one.
25932599
contributionTargetDefaultBranch =
2594-
findContributionTargetDefaultBranch(selectedRepository, branchesState)
2595-
?.name ?? undefined
2600+
targetBranch !== null
2601+
? targetBranch.upstream ?? targetBranch.name
2602+
: undefined
25962603
}
25972604

25982605
// From the menu, we'll offer to force-push whenever it's possible, regardless
@@ -5377,6 +5384,27 @@ export class AppStore extends TypedBaseStore<IAppState> {
53775384
})
53785385
}
53795386

5387+
/**
5388+
* Fetch all relevant remotes without fast-forwarding local branches.
5389+
*
5390+
* This updates remote tracking refs (e.g. origin/main) but leaves local
5391+
* branches untouched. Useful when you only need to merge from a remote
5392+
* ref without altering the local branch state.
5393+
*/
5394+
public _fetchWithoutFastForward(
5395+
repository: Repository,
5396+
fetchType: FetchType
5397+
): Promise<void> {
5398+
return this.withRefreshedGitHubRepository(repository, repository => {
5399+
return this.performFetch(
5400+
repository,
5401+
fetchType,
5402+
undefined,
5403+
true
5404+
)
5405+
})
5406+
}
5407+
53805408
/**
53815409
* Fetch a particular remote in a repository.
53825410
*
@@ -5403,7 +5431,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
54035431
private async performFetch(
54045432
repository: Repository,
54055433
fetchType: FetchType,
5406-
remotes?: IRemote[]
5434+
remotes?: IRemote[],
5435+
skipFastForward?: boolean
54075436
): Promise<void> {
54085437
await this.withPushPullFetch(repository, async () => {
54095438
const gitStore = this.gitStoreCache.get(repository)
@@ -5434,14 +5463,16 @@ export class AppStore extends TypedBaseStore<IAppState> {
54345463
? 'Refreshing Repository'
54355464
: 'Refreshing repository'
54365465

5437-
this.updatePushPullFetchProgress(repository, {
5438-
kind: 'generic',
5439-
title: refreshTitle,
5440-
description: 'Fast-forwarding branches',
5441-
value: fetchWeight,
5442-
})
5466+
if (!skipFastForward) {
5467+
this.updatePushPullFetchProgress(repository, {
5468+
kind: 'generic',
5469+
title: refreshTitle,
5470+
description: 'Fast-forwarding branches',
5471+
value: fetchWeight,
5472+
})
54435473

5444-
await this.fastForwardBranches(repository)
5474+
await this.fastForwardBranches(repository)
5475+
}
54455476

54465477
this.updatePushPullFetchProgress(repository, {
54475478
kind: 'generic',

app/src/ui/app.tsx

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
getNonForkGitHubRepository,
3434
isRepositoryWithGitHubRepository,
3535
} from '../models/repository'
36-
import { Branch } from '../models/branch'
36+
import { Branch, BranchType } from '../models/branch'
3737
import { PreferencesTab } from '../models/preferences'
3838
import { findItemByAccessKey, itemIsSelectable } from '../models/app-menu'
3939
import { Account, isDotComAccount } from '../models/account'
@@ -635,7 +635,7 @@ export class App extends React.Component<IAppProps, IAppState> {
635635
updateStore.checkForUpdates(inBackground, skipGuidCheck)
636636
}
637637

638-
private updateBranchWithContributionTargetBranch() {
638+
private async updateBranchWithContributionTargetBranch() {
639639
const { selectedState } = this.state
640640
if (
641641
selectedState == null ||
@@ -654,20 +654,61 @@ export class App extends React.Component<IAppProps, IAppState> {
654654
return
655655
}
656656

657+
// Fetch from the remote to update the remote tracking ref (e.g.
658+
// origin/main) without fast-forwarding local branches.
659+
await this.props.dispatcher.fetchWithoutFastForward(
660+
repository,
661+
FetchType.UserInitiatedTask
662+
)
663+
664+
// Merge the remote tracking branch (e.g. origin/main) instead of the
665+
// local branch, so that the local default branch is not altered as a
666+
// side effect.
667+
const branchToMerge = this.getRemoteTrackingBranch(
668+
contributionTargetDefaultBranch,
669+
state.branchesState.allBranches
670+
)
671+
657672
this.props.dispatcher.initializeMergeOperation(
658673
repository,
659674
false,
660-
contributionTargetDefaultBranch
675+
branchToMerge
661676
)
662677

663678
const { mergeStatus } = state.compareState
664679
this.props.dispatcher.mergeBranch(
665680
repository,
666-
contributionTargetDefaultBranch,
681+
branchToMerge,
667682
mergeStatus
668683
)
669684
}
670685

686+
/**
687+
* Returns the remote tracking branch for a given branch if available.
688+
* For remote branches, returns the branch itself.
689+
* For local branches, finds the corresponding remote branch in allBranches.
690+
* Falls back to the original branch if no remote tracking branch is found.
691+
*/
692+
private getRemoteTrackingBranch(
693+
branch: Branch,
694+
allBranches: ReadonlyArray<Branch>
695+
): Branch {
696+
if (branch.type === BranchType.Remote) {
697+
return branch
698+
}
699+
700+
const upstream = branch.upstream
701+
if (upstream === null) {
702+
return branch
703+
}
704+
705+
return (
706+
allBranches.find(
707+
b => b.type === BranchType.Remote && b.name === upstream
708+
) ?? branch
709+
)
710+
}
711+
671712
private mergeBranch(isSquash: boolean = false) {
672713
const selectedState = this.state.selectedState
673714
if (

app/src/ui/dispatcher/dispatcher.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,17 @@ export class Dispatcher {
747747
return this.appStore._fetch(repository, fetchType)
748748
}
749749

750+
/**
751+
* Fetch all refs without fast-forwarding local branches.
752+
* Updates remote tracking refs only, leaving local branches untouched.
753+
*/
754+
public fetchWithoutFastForward(
755+
repository: Repository,
756+
fetchType: FetchType
757+
): Promise<void> {
758+
return this.appStore._fetchWithoutFastForward(repository, fetchType)
759+
}
760+
750761
/** Publish the repository to GitHub with the given properties. */
751762
public publishRepository(
752763
repository: Repository,

0 commit comments

Comments
 (0)