Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion app/src/lib/stores/app-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -2143,20 +2148,57 @@ export class AppStore extends TypedBaseStore<IAppState> {

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)
}
Expand Down Expand Up @@ -7219,6 +7261,23 @@ export class AppStore extends TypedBaseStore<IAppState> {
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)
Expand Down
59 changes: 59 additions & 0 deletions app/src/lib/worktree-preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { normalizePath } from './helpers/path'

const StorageKey = 'worktree-active-paths'

function getPreferences(): Record<string, string> {
try {
const raw = localStorage.getItem(StorageKey)
return raw ? JSON.parse(raw) : {}
} catch {
return {}
}
}

function savePreferences(prefs: Record<string, string>) {
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)
}
8 changes: 8 additions & 0 deletions app/src/ui/toolbar/worktree-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
Expand Down
18 changes: 16 additions & 2 deletions app/src/ui/worktrees/delete-worktree-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -90,13 +97,20 @@ 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)
this.setState({ isDeleting: false })
return
}

const resolvedMainPath = mainPathForCleanup ?? repository.path
const preferred = getPreferredWorktreePath(resolvedMainPath)
if (preferred && normalizePath(preferred) === normalizePath(worktreePath)) {
clearPreferredWorktreePath(resolvedMainPath)
}

this.props.onDismissed()
}
}
Loading