diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 59f62727cdc..3401b27e4de 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -200,6 +200,11 @@ import { } from '../../ui/lib/diff-mode' import { pathExists } from '../../ui/lib/path-exists' import { updateStore } from '../../ui/lib/update-store' +import { + getPreferredWorktreePath, + clearPreferredWorktreePath, +} from '../worktree-preferences' +import { normalizePath } from '../helpers/path' import { resizableComponentClass } from '../../ui/resizable' import { BypassReasonType } from '../../ui/secret-scanning/bypass-push-protection-dialog' import { findContributionTargetDefaultBranch } from '../branch' @@ -2143,20 +2148,57 @@ export class AppStore extends TypedBaseStore { this.selectedRepository = repository - this.emitUpdate() this.stopBackgroundFetching() this.stopPullRequestUpdater() this._clearBanner() this.stopBackgroundPruner() if (repository == null) { + this.emitUpdate() return Promise.resolve(null) } if (!(repository instanceof Repository)) { + this.emitUpdate() return Promise.resolve(null) } + // When returning to a repository that has worktrees, restore the + // previously active linked worktree so the user doesn't always land + // on the main worktree after switching repos. + if (!repository.isLinkedWorktree) { + const repoPath = normalizePath(repository.path) + const preferredPath = getPreferredWorktreePath(repoPath) + + if (preferredPath && preferredPath !== repoPath) { + const linkedRepo = this.repositories.find( + r => + r instanceof Repository && normalizePath(r.path) === preferredPath + ) + + if (linkedRepo instanceof Repository) { + repository = linkedRepo + this.selectedRepository = repository + } else { + const exists = await pathExists(preferredPath) + if (exists) { + const addedRepos = await this._addRepositories( + [preferredPath], + repository.login + ) + if (addedRepos.length > 0) { + repository = addedRepos[0] + this.selectedRepository = repository + } + } else { + clearPreferredWorktreePath(repoPath) + } + } + } + } + + this.emitUpdate() + if (persistSelection) { setNumber(LastSelectedRepositoryIDKey, repository.id) } @@ -7219,6 +7261,23 @@ export class AppStore extends TypedBaseStore { return } + if (repository instanceof Repository) { + if (repository.isLinkedWorktree) { + const repoPath = normalizePath(repository.path) + const mainRepo = this.repositories.find( + r => + r instanceof Repository && + !r.isLinkedWorktree && + getPreferredWorktreePath(normalizePath(r.path)) === repoPath + ) + if (mainRepo instanceof Repository) { + clearPreferredWorktreePath(normalizePath(mainRepo.path)) + } + } else { + clearPreferredWorktreePath(normalizePath(repository.path)) + } + } + const allRepositories = await this.repositoriesStore.getAll() if (allRepositories.length === 0) { this._closeFoldout(FoldoutType.Repository) diff --git a/app/src/lib/worktree-preferences.ts b/app/src/lib/worktree-preferences.ts new file mode 100644 index 00000000000..a8ee2124811 --- /dev/null +++ b/app/src/lib/worktree-preferences.ts @@ -0,0 +1,59 @@ +import { normalizePath } from './helpers/path' + +const StorageKey = 'worktree-active-paths' + +function getPreferences(): Record { + try { + const raw = localStorage.getItem(StorageKey) + return raw ? JSON.parse(raw) : {} + } catch { + return {} + } +} + +function savePreferences(prefs: Record) { + localStorage.setItem(StorageKey, JSON.stringify(prefs)) +} + +/** + * Get the preferred worktree path for a given main worktree path. + * Returns null if no preference is stored (defaults to main worktree). + */ +export function getPreferredWorktreePath( + mainWorktreePath: string +): string | null { + const prefs = getPreferences() + return prefs[normalizePath(mainWorktreePath)] ?? null +} + +/** + * Store the user's active worktree choice for a repository. + * If the active path is the main worktree itself, the preference is cleared + * so the repo defaults to its main worktree on next visit. + */ +export function setPreferredWorktreePath( + mainWorktreePath: string, + activeWorktreePath: string +) { + const prefs = getPreferences() + const normalizedMain = normalizePath(mainWorktreePath) + const normalizedActive = normalizePath(activeWorktreePath) + + if (normalizedMain === normalizedActive) { + delete prefs[normalizedMain] + } else { + prefs[normalizedMain] = normalizedActive + } + + savePreferences(prefs) +} + +/** + * Clear any stored worktree preference for a repository so it + * defaults to the main worktree on next visit. + */ +export function clearPreferredWorktreePath(mainWorktreePath: string) { + const prefs = getPreferences() + delete prefs[normalizePath(mainWorktreePath)] + savePreferences(prefs) +} diff --git a/app/src/ui/toolbar/worktree-dropdown.tsx b/app/src/ui/toolbar/worktree-dropdown.tsx index 061f60f2c56..5fd30a27743 100644 --- a/app/src/ui/toolbar/worktree-dropdown.tsx +++ b/app/src/ui/toolbar/worktree-dropdown.tsx @@ -18,6 +18,7 @@ import { PopupType } from '../../models/popup' import { Resizable } from '../resizable' import { enableResizingToolbarButtons } from '../../lib/feature-flag' import { normalizePath } from '../../lib/helpers/path' +import { setPreferredWorktreePath } from '../../lib/worktree-preferences' interface IWorktreeDropdownProps { readonly dispatcher: Dispatcher @@ -54,6 +55,13 @@ export class WorktreeDropdown extends React.Component< dispatcher.closeFoldout(FoldoutType.Worktree) + const { allWorktrees } = this.props.repositoryState.worktreesState + const mainWorktree = allWorktrees.find(wt => wt.type === 'main') + + if (mainWorktree) { + setPreferredWorktreePath(mainWorktree.path, worktree.path) + } + const existingRepo = repositories.find( r => r instanceof Repository && normalizePath(r.path) === worktreePath ) diff --git a/app/src/ui/worktrees/delete-worktree-dialog.tsx b/app/src/ui/worktrees/delete-worktree-dialog.tsx index 2080e3326c5..d30c2ccd239 100644 --- a/app/src/ui/worktrees/delete-worktree-dialog.tsx +++ b/app/src/ui/worktrees/delete-worktree-dialog.tsx @@ -8,6 +8,10 @@ import { Ref } from '../lib/ref' import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group' import { removeWorktree, getMainWorktreePath } from '../../lib/git/worktree' import { normalizePath } from '../../lib/helpers/path' +import { + getPreferredWorktreePath, + clearPreferredWorktreePath, +} from '../../lib/worktree-preferences' interface IDeleteWorktreeDialogProps { readonly repository: Repository @@ -66,16 +70,19 @@ export class DeleteWorktreeDialog extends React.Component< const isDeletingCurrentWorktree = normalizePath(repository.path) === normalizePath(worktreePath) + const mainPathForCleanup = await getMainWorktreePath(repository) + try { if (isDeletingCurrentWorktree) { // When deleting the currently selected worktree, we must switch away // first. Otherwise git runs from the directory being deleted and the // app is left pointing at a non-existent path. - const mainPath = await getMainWorktreePath(repository) - if (mainPath === null) { + if (mainPathForCleanup === null) { throw new Error('Could not find main worktree') } + const mainPath = mainPathForCleanup + const addedRepos = await dispatcher.addRepositories( [mainPath], repository.login @@ -90,6 +97,7 @@ export class DeleteWorktreeDialog extends React.Component< await dispatcher.removeRepository(repository, false) } else { await removeWorktree(repository, worktreePath) + await dispatcher.refreshRepository(repository) } } catch (e) { dispatcher.postError(e) @@ -97,6 +105,12 @@ export class DeleteWorktreeDialog extends React.Component< return } + const resolvedMainPath = mainPathForCleanup ?? repository.path + const preferred = getPreferredWorktreePath(resolvedMainPath) + if (preferred && normalizePath(preferred) === normalizePath(worktreePath)) { + clearPreferredWorktreePath(resolvedMainPath) + } + this.props.onDismissed() } }