Skip to content

Commit 300c7be

Browse files
committed
Simplify pull single branch logic
Reuse the existing dispatcher fetch() and pull() instead of duplicating the fetch logic. While the original implemnetation worked well, this greatly reduces the diff with respect to upstream, so that we can maintain it long-term. This has the side effect of also fast-forwarding the other branches (not only pulling the selected), which is fine because that is usually the user's intention.
1 parent 7ac1793 commit 300c7be

7 files changed

Lines changed: 59 additions & 225 deletions

File tree

app/src/lib/progress/fetch.ts

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,3 @@ export class FetchProgressParser extends GitProgressParser {
2020
super(steps)
2121
}
2222
}
23-
24-
/**
25-
* Progress steps for a single-branch fetch operation. Unlike a full fetch,
26-
* Highly approximate (some would say outright inaccurate) division
27-
* of the individual progress reporting steps in a fetch operation
28-
*/
29-
const singleBranchFetchSteps = [
30-
{ title: 'remote: Enumerating objects', weight: 0.1 },
31-
{ title: 'remote: Counting objects', weight: 0.2 },
32-
{ title: 'remote: Compressing objects', weight: 0.3 },
33-
{ title: 'remote', weight: 0.4 },
34-
]
35-
36-
/**
37-
* A utility class for interpreting the output from
38-
* `git fetch --progress <remote> <branch>` and turning that into a percentage
39-
* value estimating the overall progress of a single branch fetch.
40-
*/
41-
export class SingleBranchFetchProgressParser extends GitProgressParser {
42-
public constructor() {
43-
super(singleBranchFetchSteps)
44-
}
45-
}

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

Lines changed: 24 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,6 @@ import {
266266
listWorktrees,
267267
unstageAll,
268268
git,
269-
IGitStringExecutionOptions,
270-
IGitStringResult,
271269
} from '../git'
272270
import {
273271
installGlobalLFSFilters,
@@ -453,11 +451,6 @@ import {
453451
gatherCommitContext,
454452
} from '../copilot-conflict-context'
455453
import { resolveWithin } from '../path'
456-
import {
457-
executionOptionsWithProgress,
458-
SingleBranchFetchProgressParser,
459-
} from '../progress'
460-
import { envForRemoteOperation } from '../git/environment'
461454

462455
const LastSelectedRepositoryIDKey = 'last-selected-repository-id'
463456

@@ -6082,171 +6075,6 @@ export class AppStore extends TypedBaseStore<IAppState> {
60826075
})
60836076
}
60846077

6085-
public async _fetchSingleBranch(
6086-
repository: Repository,
6087-
branch: Branch
6088-
): Promise<void> {
6089-
const state = this.repositoryStateCache.get(repository)
6090-
// Don't allow concurrent network operations.
6091-
if (state.isPushPullFetchInProgress) {
6092-
this._showPopup({
6093-
type: PopupType.Error,
6094-
error: new Error(
6095-
'Another push/pull/fetch request is in progress.\nTry again after the ongoing request is finished'
6096-
),
6097-
})
6098-
return
6099-
}
6100-
6101-
return this.withRefreshedGitHubRepository(repository, repo => {
6102-
return this.performFetchSingleBranch(repo, branch)
6103-
})
6104-
}
6105-
6106-
/** This shouldn't be called directly. See `Dispatcher`. */
6107-
private async performFetchSingleBranch(
6108-
repository: Repository,
6109-
branch: Branch
6110-
) {
6111-
const isRemote = branch.type === BranchType.Remote
6112-
6113-
const remoteName = isRemote ? branch.remoteName : branch.upstreamRemoteName
6114-
const remoteBranchName = isRemote
6115-
? branch.nameWithoutRemote
6116-
: branch.upstreamWithoutRemote
6117-
6118-
if (!remoteName) {
6119-
throw new Error('Remote name not found')
6120-
}
6121-
if (!remoteBranchName) {
6122-
throw new Error('Remote branch not found')
6123-
}
6124-
6125-
const isBackgroundTask = false
6126-
const gitStore = this.gitStoreCache.get(repository)
6127-
6128-
// repository.url
6129-
const remote = { name: remoteName, url: 'file://' }
6130-
6131-
const progressCb = (progress: IFetchProgress) => {
6132-
this.updatePushPullFetchProgress(repository, progress)
6133-
}
6134-
const progressTitle = isRemote
6135-
? `Fetching ${branch.name}`
6136-
: `Fetching ${remoteBranchName}`
6137-
const kind = 'fetch'
6138-
6139-
const fetchFn = async (isRemote: boolean): Promise<IGitStringResult> => {
6140-
let opts: IGitStringExecutionOptions = {
6141-
successExitCodes: new Set([0]),
6142-
}
6143-
if (remote.url) {
6144-
opts = {
6145-
...opts,
6146-
env: await envForRemoteOperation(remote.url),
6147-
}
6148-
}
6149-
opts = await executionOptionsWithProgress(
6150-
{ ...opts, trackLFSProgress: true, isBackgroundTask },
6151-
new SingleBranchFetchProgressParser(),
6152-
progress => {
6153-
if (progress.kind === 'context') {
6154-
const text = progress.text
6155-
if (
6156-
!text.startsWith('remote: Counting objects') &&
6157-
!text.startsWith('remote: Compressing objects')
6158-
) {
6159-
return
6160-
}
6161-
}
6162-
6163-
const description =
6164-
progress.kind === 'progress' ? progress.details.text : progress.text
6165-
const value = progress.percent
6166-
6167-
progressCb({
6168-
kind,
6169-
title: progressTitle,
6170-
description,
6171-
value,
6172-
remote: remote.name,
6173-
})
6174-
}
6175-
)
6176-
const flags = isRemote
6177-
? ['fetch', '--progress', '--recurse-submodules=on-demand', remoteName]
6178-
: [
6179-
'fetch',
6180-
'--progress',
6181-
'--show-forced-updates',
6182-
// '--no-write-fetch-head',
6183-
'--recurse-submodules=on-demand',
6184-
remoteName,
6185-
]
6186-
6187-
const branchTarget = isRemote
6188-
? remoteBranchName
6189-
: `${remoteBranchName}:${remoteBranchName}`
6190-
const actionName = isRemote ? 'fetchRemoteBranch' : 'fetchLocalBranch'
6191-
6192-
const executionOpts = isRemote
6193-
? opts
6194-
: {
6195-
...opts,
6196-
successExitCodes: new Set([0, 1]),
6197-
}
6198-
6199-
return await git(
6200-
[...flags, branchTarget],
6201-
repository.path,
6202-
actionName,
6203-
executionOpts
6204-
)
6205-
}
6206-
6207-
const execFetchFn = async () => {
6208-
// Initial progress
6209-
progressCb({
6210-
kind,
6211-
title: progressTitle,
6212-
value: 0,
6213-
remote: remote.name,
6214-
})
6215-
6216-
await gitStore.performFailableOperation(
6217-
async () => {
6218-
const result = await fetchFn(isRemote)
6219-
if (
6220-
!isRemote &&
6221-
result &&
6222-
(result.stderr?.includes('rejected') ||
6223-
result.stderr?.includes('non-fast-forward'))
6224-
) {
6225-
this.emitError(
6226-
new ErrorWithMetadata(new Error(result.stderr), { repository })
6227-
)
6228-
}
6229-
6230-
await this._refreshRepository(repository)
6231-
},
6232-
{
6233-
backgroundTask: isBackgroundTask,
6234-
}
6235-
)
6236-
}
6237-
6238-
try {
6239-
await this.withPushPullFetch(repository, execFetchFn)
6240-
} catch (error) {
6241-
const errorWithMetadata = new ErrorWithMetadata(error, {
6242-
repository,
6243-
})
6244-
this.emitError(errorWithMetadata)
6245-
} finally {
6246-
this.updatePushPullFetchProgress(repository, null)
6247-
}
6248-
}
6249-
62506078
public async _resetHardToUpstream(repository: Repository): Promise<void> {
62516079
const { branchesState } = this.repositoryStateCache.get(repository)
62526080
const { tip } = branchesState
@@ -6778,6 +6606,30 @@ export class AppStore extends TypedBaseStore<IAppState> {
67786606
})
67796607
}
67806608

6609+
/**
6610+
* Attempts to fast-forward a branch to its upstream.
6611+
*
6612+
* @returns true if it was successful
6613+
*/
6614+
public async _fastForwardBranch(
6615+
repository: Repository,
6616+
branch: Branch
6617+
): Promise<void> {
6618+
if (branch.upstream === null || branch.isGone) {
6619+
throw new Error(`The branch '${branch.name}' has not been published yet.`)
6620+
}
6621+
await this._fetch(repository, FetchType.UserInitiatedTask)
6622+
const stillDifferingBranches = await getBranchesDifferingFromUpstream(
6623+
repository
6624+
)
6625+
const stillDiffers = stillDifferingBranches.some(b => b.ref === branch.ref)
6626+
if (stillDiffers) {
6627+
throw new Error(
6628+
`The branch '${branch.name}' cannot be fast-forwarded. Switch to it and pull it manually.`
6629+
)
6630+
}
6631+
}
6632+
67816633
/**
67826634
* Fetch a particular remote in a repository.
67836635
*

app/src/ui/branches/branch-list-item-context-menu.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@ interface IBranchContextMenuConfig {
77
name: string
88
nameWithoutRemote: string
99
isLocal: boolean
10-
isCurrentBranch: boolean
1110
repoType: RepoType | undefined
1211
isInUseByOtherWorktree: boolean
1312
onRenameBranch?: (branchName: string) => void
1413
onViewBranchOnGitHub?: () => void
1514
onViewPullRequestOnGitHub?: () => void
1615
onSetAsDefaultBranch?: (branchName: string) => void
1716
onDeleteBranch?: (branchName: string) => void
18-
onFetchSingleBranch?: (branchName: string) => void
17+
onPullSingleBranch?: (branchName: string) => void
1918
}
2019

2120
export function generateBranchContextMenuItems(
@@ -25,17 +24,17 @@ export function generateBranchContextMenuItems(
2524
name,
2625
nameWithoutRemote,
2726
isLocal,
28-
isCurrentBranch,
2927
repoType,
3028
isInUseByOtherWorktree,
3129
onRenameBranch,
3230
onViewBranchOnGitHub,
3331
onViewPullRequestOnGitHub,
3432
onSetAsDefaultBranch,
3533
onDeleteBranch,
36-
onFetchSingleBranch,
34+
onPullSingleBranch,
3735
} = config
3836
const items = new Array<IMenuItem>()
37+
3938
if (onRenameBranch !== undefined) {
4039
items.push({
4140
label: 'Rename…',
@@ -70,12 +69,11 @@ export function generateBranchContextMenuItems(
7069
})
7170
}
7271

73-
// This should be the selected branch.
74-
if (!isCurrentBranch && onFetchSingleBranch !== undefined) {
72+
if (onPullSingleBranch) {
7573
items.push({ type: 'separator' })
7674
items.push({
77-
label: getSingleFetchBranchLabel(),
78-
action: () => onFetchSingleBranch(name),
75+
label: __DARWIN__ ? 'Pull branch' : 'Pull branch',
76+
action: () => onPullSingleBranch(name),
7977
enabled: true,
8078
})
8179
}
@@ -117,7 +115,3 @@ function getViewPullRequestLabel(repoType: RepoType): string {
117115
return assertNever(repoType, `Unknown repo type: ${repoType}`)
118116
}
119117
}
120-
121-
function getSingleFetchBranchLabel(): string {
122-
return __DARWIN__ ? 'Fetch Branch' : 'Fetch branch'
123-
}

app/src/ui/branches/branch-list.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ interface IBranchListProps {
145145
/** Optional: Callback for if delete context menu should exist */
146146
readonly onDeleteBranch?: (branchName: string) => void
147147

148-
/** Optional: Callback if pull option for remote branch context menu should exist */
149-
readonly onFetchSingleBranch?: (branchName: string) => void
148+
/** Optional: Callback if option to fetch branches should exist */
149+
readonly onPullSingleBranch?: (branchName: string) => void
150150
}
151151

152152
/** The Branches list component. */
@@ -241,7 +241,7 @@ export class BranchList extends React.Component<IBranchListProps> {
241241
onRenameBranch,
242242
onDeleteBranch,
243243
onSetAsDefaultBranch,
244-
onFetchSingleBranch,
244+
onPullSingleBranch,
245245
} = this.props
246246

247247
if (
@@ -255,11 +255,11 @@ export class BranchList extends React.Component<IBranchListProps> {
255255
const { type, name, nameWithoutRemote } = item.branch
256256
const isLocal = type === BranchType.Local
257257
const isInUseByOtherWorktree = !!this.inUseByOtherWorktreeName(item)
258+
258259
const items = generateBranchContextMenuItems({
259260
name,
260261
nameWithoutRemote,
261262
isLocal,
262-
isCurrentBranch: item.branch.name === this.props.currentBranch?.name,
263263
repoType: this.props.repository.gitHubRepository?.type,
264264
isInUseByOtherWorktree,
265265
onRenameBranch,
@@ -268,7 +268,7 @@ export class BranchList extends React.Component<IBranchListProps> {
268268
? undefined
269269
: onSetAsDefaultBranch,
270270
onDeleteBranch,
271-
onFetchSingleBranch,
271+
onPullSingleBranch,
272272
})
273273

274274
showContextualMenu(items)

app/src/ui/branches/branches-container.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ interface IBranchesContainerProps {
5353
readonly onRenameBranch: (branchName: string) => void
5454
readonly onSetAsDefaultBranch: (branchName: string) => void
5555
readonly onDeleteBranch: (branchName: string) => void
56-
readonly onFetchSingleBranch: (branchName: string) => void
56+
readonly onPullSingleBranch: (branchName: string) => void
5757

5858
readonly branchSortOrder: BranchSortOrder
5959

@@ -294,7 +294,7 @@ export class BranchesContainer extends React.Component<
294294
onRenameBranch={this.props.onRenameBranch}
295295
onSetAsDefaultBranch={this.props.onSetAsDefaultBranch}
296296
onDeleteBranch={this.props.onDeleteBranch}
297-
onFetchSingleBranch={this.props.onFetchSingleBranch}
297+
onPullSingleBranch={this.props.onPullSingleBranch}
298298
/>
299299
)
300300
case BranchesTab.PullRequests: {

app/src/ui/dispatcher/dispatcher.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -821,12 +821,11 @@ export class Dispatcher {
821821
return this.appStore._pull(repository)
822822
}
823823

824-
/** Pull remote branch by name */
825-
public fetchSingleBranch(
824+
public fastForwardBranch(
826825
repository: Repository,
827826
branch: Branch
828827
): Promise<void> {
829-
return this.appStore._fetchSingleBranch(repository, branch)
828+
return this.appStore._fastForwardBranch(repository, branch)
830829
}
831830

832831
public async pullAllRepositories(): Promise<void> {

0 commit comments

Comments
 (0)