Skip to content

Commit 1731fcd

Browse files
committed
Merge branch 'upstream-development'
2 parents 14fa02c + 871fea6 commit 1731fcd

8 files changed

Lines changed: 277 additions & 186 deletions

File tree

app/src/lib/feature-flag.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -103,19 +103,14 @@ export const enableCommitMessageGeneration = (account: Account) => {
103103
}
104104

105105
export const enableCopilotSdkCommitMessageGeneration = (account: Account) => {
106-
return enableBetaFeatures()
107-
// IMPORTANT: Leaving this here for now. When the feature is enabled in prod,
108-
// we will rely on the `desktop_enable_copilot_sdk_commit_message_generation`
109-
// feature flag to control the rollout, but we want to be able to enable it in
110-
// beta and other non-production builds regardless of the feature flag.
111-
// Remember to also update build.ts to get Copilot bundled in the production
112-
// build when this happens.
113-
// return (
114-
// enableBetaFeatures() &&
115-
// (account.features ?? []).includes(
116-
// 'desktop_enable_copilot_sdk_commit_message_generation'
117-
// )
118-
// )
106+
// Enabled for all users in beta and development channels, and for users with
107+
// the feature flag enabled in production.
108+
return (
109+
enableBetaFeatures() ||
110+
(account.features ?? []).includes(
111+
'desktop_enable_copilot_sdk_commit_message_generation'
112+
)
113+
)
119114
}
120115

121116
/** Should we enable Copilot-powered merge conflict resolution? */

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

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ import {
242242
listWorktrees,
243243
getMainWorktreePath,
244244
removeWorktree,
245+
moveWorktree,
245246
getCommitRangeDiff,
246247
getCommitRangeChangedFiles,
247248
updateRemoteHEAD,
@@ -257,7 +258,6 @@ import {
257258
IConfigValueOrigin,
258259
unstageAll,
259260
git,
260-
moveWorktree,
261261
} from '../git'
262262
import {
263263
installGlobalLFSFilters,
@@ -3140,13 +3140,30 @@ export class AppStore extends TypedBaseStore<IAppState> {
31403140
this.updateMenuLabelsForSelectedRepository()
31413141
}
31423142

3143+
/**
3144+
* Determine whether the worktree dropdown is currently shown in the toolbar.
3145+
*
3146+
* This mirrors the render condition in `App.renderWorktreeToolbarButton`: the
3147+
* dropdown is shown when worktree support is enabled and either the selected
3148+
* repository has at least one linked worktree (i.e. more than just the main
3149+
* worktree) or the worktree foldout is currently open (which lets the user
3150+
* create their first worktree from the toolbar).
3151+
*/
31433152
private isWorktreeDropdownVisible(): boolean {
3153+
if (!enableWorktreeSupport() || !this.showWorktrees) {
3154+
return false
3155+
}
3156+
3157+
if (this.currentFoldout?.type === FoldoutType.Worktree) {
3158+
return true
3159+
}
3160+
31443161
const repository = this.selectedRepository
31453162
const worktreeCount =
31463163
repository instanceof Repository
31473164
? this.repositoryStateCache.get(repository).worktrees.length
31483165
: 0
3149-
return enableWorktreeSupport() && this.showWorktrees && worktreeCount > 1
3166+
return worktreeCount > 1
31503167
}
31513168

31523169
/**
@@ -3206,8 +3223,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
32063223
this.stashedFilesWidth = constrain(this.stashedFilesWidth, 100, filesMax)
32073224

32083225
// Allocate worktree first (highest priority), then branch, then
3209-
// push-pull. Each subsequent allocation uses the clamped value of the
3210-
// previous to prevent the total from exceeding the available space.
3226+
// push-pull. The foldouts are laid out in this order, so the width
3227+
// constraints should follow the same order. Each subsequent allocation
3228+
// uses the clamped value of the previous to prevent the total from
3229+
// exceeding the available space.
32113230
const worktreeDropdownMax =
32123231
available - defaultBranchDropdownWidth - defaultPushPullButtonWidth
32133232
this.worktreeDropdownWidth = constrain(
@@ -5081,6 +5100,14 @@ export class AppStore extends TypedBaseStore<IAppState> {
50815100
/** This shouldn't be called directly. See `Dispatcher`. */
50825101
public async _showFoldout(foldout: Foldout): Promise<void> {
50835102
this.currentFoldout = foldout
5103+
5104+
// Showing the worktree foldout makes the worktree dropdown visible even
5105+
// when there are no linked worktrees, so the toolbar width allocation has
5106+
// to be recalculated to reserve space for it.
5107+
if (foldout.type === FoldoutType.Worktree) {
5108+
this.updateResizableConstraints()
5109+
}
5110+
50845111
this.emitUpdate()
50855112

50865113
// If the user is opening the repository list and we haven't yet
@@ -5101,7 +5128,14 @@ export class AppStore extends TypedBaseStore<IAppState> {
51015128
return
51025129
}
51035130

5131+
const wasWorktreeFoldout = this.currentFoldout.type === FoldoutType.Worktree
5132+
51045133
this.currentFoldout = null
5134+
5135+
if (wasWorktreeFoldout) {
5136+
this.updateResizableConstraints()
5137+
}
5138+
51055139
this.emitUpdate()
51065140
}
51075141

@@ -5115,7 +5149,14 @@ export class AppStore extends TypedBaseStore<IAppState> {
51155149
return
51165150
}
51175151

5152+
const wasWorktreeFoldout = this.currentFoldout.type === FoldoutType.Worktree
5153+
51185154
this.currentFoldout = null
5155+
5156+
if (wasWorktreeFoldout) {
5157+
this.updateResizableConstraints()
5158+
}
5159+
51195160
this.emitUpdate()
51205161
}
51215162

@@ -6999,6 +7040,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
69997040
throw new Error('Could not find main worktree')
70007041
}
70017042

7043+
// Switch to the main worktree before deleting the current one since the
7044+
// current worktree path will be deleted after the switch. Use the
7045+
// resulting repository (with the updated path) for the subsequent
7046+
// remove and refresh calls.
70027047
repository = await this._switchWorktree(repository, main)
70037048
}
70047049

@@ -7014,23 +7059,41 @@ export class AppStore extends TypedBaseStore<IAppState> {
70147059
error: e,
70157060
originalWorktree,
70167061
})
7062+
return
70177063
}
70187064

70197065
await this._refreshWorktrees(repository)
70207066
this.statsStore.increment('worktreeDeletedCount')
70217067
}
70227068

7069+
/** This shouldn't be called directly. See 'Dispatcher'. */
70237070
public async _moveWorktree(
70247071
repository: Repository,
70257072
worktreePath: string,
70267073
newPath: string
70277074
): Promise<void> {
70287075
await moveWorktree(repository, worktreePath, newPath)
7029-
const result = await this.repositoriesStore.switchWorktree(
7030-
repository,
7031-
newPath
7032-
)
7033-
await this._refreshWorktrees(result.repository)
7076+
7077+
// If the worktree being renamed is the currently selected one, switch to
7078+
// its new path so that the subsequent refresh (and any further git calls)
7079+
// operate on the renamed directory rather than the now non-existing one.
7080+
if (repository.path === worktreePath) {
7081+
const result = await this.repositoriesStore.switchWorktree(
7082+
repository,
7083+
newPath
7084+
)
7085+
7086+
// Renaming changes the repository's path and therefore its hash, which
7087+
// is the key used by the state cache. Carry the existing state over to
7088+
// the new identity so we don't reset the UI (e.g. a typed commit
7089+
// message) just because the worktree was renamed.
7090+
this.repositoryStateCache.transferState(repository, result.repository)
7091+
7092+
await this._selectRepository(result.repository)
7093+
await this._refreshWorktrees(result.repository)
7094+
} else {
7095+
await this._refreshWorktrees(repository)
7096+
}
70347097
}
70357098

70367099
public _setWorktreeDropdownWidth(width: number): Promise<void> {

app/src/lib/stores/repository-state-cache.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,29 @@ export class RepositoryStateCache {
285285
})
286286
}
287287

288+
/**
289+
* Move the entire cached state for a repository from one identity to another.
290+
*
291+
* This is used when a worktree is renamed: the repository's path (and
292+
* therefore its hash) changes, but it still refers to the same working
293+
* directory, so all of the existing in-memory state (working directory
294+
* changes, commit message, history, etc.) should be carried over to the new
295+
* identity rather than reset to its initial values.
296+
*/
297+
public transferState(source: Repository, target: Repository) {
298+
if (source.hash === target.hash) {
299+
return
300+
}
301+
302+
const sourceState = this.repositoryState.get(source.hash)
303+
if (sourceState === undefined) {
304+
return
305+
}
306+
307+
this.repositoryState.set(target.hash, sourceState)
308+
this.repositoryState.delete(source.hash)
309+
}
310+
288311
private sendPullRequestStateNotExistsException() {
289312
sendNonFatalException(
290313
'PullRequestState',

app/src/ui/dispatcher/dispatcher.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,28 @@ export class Dispatcher {
11811181
.catch(e => this.postError(e))
11821182
}
11831183

1184+
/**
1185+
* Rename (move) a worktree to a new path and keep the worktree list in sync.
1186+
* If the worktree being renamed is the currently selected one, the repository
1187+
* is switched to its new path.
1188+
*
1189+
* Returns a value indicating whether the rename succeeded. On failure the
1190+
* error is surfaced to the user via `postError`.
1191+
*/
1192+
public async moveWorktree(
1193+
repository: Repository,
1194+
worktreePath: string,
1195+
newPath: string
1196+
): Promise<boolean> {
1197+
return this.appStore
1198+
._moveWorktree(repository, worktreePath, newPath)
1199+
.then(() => true)
1200+
.catch(e => {
1201+
this.postError(e)
1202+
return false
1203+
})
1204+
}
1205+
11841206
/**
11851207
* Delete a worktree. If the worktree being deleted is the currently selected
11861208
* one, the repository is switched to the main worktree first.
@@ -1195,16 +1217,6 @@ export class Dispatcher {
11951217
.catch(e => this.postError(e))
11961218
}
11971219

1198-
public async moveWorktree(
1199-
repository: Repository,
1200-
worktreePath: string,
1201-
newPath: string
1202-
): Promise<void> {
1203-
await this.appStore
1204-
._moveWorktree(repository, worktreePath, newPath)
1205-
.catch(e => this.postError(e))
1206-
}
1207-
12081220
/**
12091221
* Request deletion of a worktree, showing a confirmation dialog if the
12101222
* user's preferences require it.

app/src/ui/preferences/preferences.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,8 +426,8 @@ export class Preferences extends React.Component<
426426
onDismissed={this.onCancel}
427427
onSubmit={this.onSave}
428428
>
429+
{this.renderDisallowedCharactersError()}
429430
<div className="preferences-container">
430-
{this.renderDisallowedCharactersError()}
431431
<TabBar
432432
onTabClicked={this.onTabClicked}
433433
selectedIndex={this.tabToVisualIndex(this.state.selectedIndex)}

app/src/ui/worktrees/rename-worktree-dialog.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,17 @@ export class RenameWorktreeDialog extends React.Component<
4343

4444
this.setState({ renaming: true })
4545

46-
try {
47-
await this.props.dispatcher.moveWorktree(
48-
repository,
49-
worktreePath,
50-
newPath
51-
)
52-
} finally {
53-
this.setState({ renaming: false })
54-
}
46+
const success = await this.props.dispatcher.moveWorktree(
47+
repository,
48+
worktreePath,
49+
newPath
50+
)
5551

56-
onDismissed()
52+
this.setState({ renaming: false })
53+
54+
if (success) {
55+
onDismissed()
56+
}
5757
}
5858

5959
public render() {

app/styles/ui/_worktrees.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
}
7272

7373
#add-worktree {
74-
width: 450px;
74+
width: 500px;
7575

7676
.autocompletion-popup {
7777
width: 350px;

0 commit comments

Comments
 (0)