diff --git a/apps/desktop/src/components/v3/FileDependenciesPlugin.svelte b/apps/desktop/src/components/v3/FileDependenciesPlugin.svelte new file mode 100644 index 0000000000..e58396f76c --- /dev/null +++ b/apps/desktop/src/components/v3/FileDependenciesPlugin.svelte @@ -0,0 +1,98 @@ + + + diff --git a/apps/desktop/src/components/v3/FileList.svelte b/apps/desktop/src/components/v3/FileList.svelte index dd112b9d5a..dcffeb2c5a 100644 --- a/apps/desktop/src/components/v3/FileList.svelte +++ b/apps/desktop/src/components/v3/FileList.svelte @@ -9,6 +9,7 @@ import { chunk } from '$lib/utils/array'; import { sortLikeFileTree } from '$lib/worktree/changeTree'; import { getContext } from '@gitbutler/shared/context'; + import type { FileDependencies } from '$lib/dependencies/dependencies'; import type { TreeChange } from '$lib/hunks/change'; import type { SelectionId } from '$lib/selection/key'; @@ -20,9 +21,18 @@ showCheckboxes?: boolean; selectionId: SelectionId; listActive: boolean; + fileDependencies?: Map; }; - const { projectId, changes, listMode, selectionId, showCheckboxes, listActive }: Props = $props(); + const { + projectId, + changes, + listMode, + selectionId, + showCheckboxes, + listActive, + fileDependencies + }: Props = $props(); let currentDisplayIndex = $state(0); @@ -65,6 +75,7 @@ onclick={(e) => { selectFilesInList(e, change, visibleFiles, idSelection, true, idx, selectionId); }} + locked={fileDependencies?.has(change.path)} /> {/snippet} diff --git a/apps/desktop/src/components/v3/FileListItemWrapper.svelte b/apps/desktop/src/components/v3/FileListItemWrapper.svelte index 224072e91e..783644b157 100644 --- a/apps/desktop/src/components/v3/FileListItemWrapper.svelte +++ b/apps/desktop/src/components/v3/FileListItemWrapper.svelte @@ -34,6 +34,7 @@ onclick?: (e: MouseEvent) => void; onkeydown?: (e: KeyboardEvent) => void; onCloseClick?: () => void; + locked?: boolean; } const { diff --git a/apps/desktop/src/components/v3/UnifiedDiffView.svelte b/apps/desktop/src/components/v3/UnifiedDiffView.svelte index ef4998e3e0..9d0108bce6 100644 --- a/apps/desktop/src/components/v3/UnifiedDiffView.svelte +++ b/apps/desktop/src/components/v3/UnifiedDiffView.svelte @@ -1,11 +1,11 @@ + +
{#if diff.type === 'Patch'} {#each diff.subject.hunks as hunk} {@const [staged, stagedLines] = getStageState(hunk)} - {@const [fullyLocked, lineLocks] = getLineLocks( - viewingStackId, - hunk, - fileDependencies?.current.data?.dependencies ?? [] - )} + {@const [fullyLocked, lineLocks] = getLineLocks(viewingStackId, hunk, fileLocks)}
import ScrollableContainer from '$components/ConfigurableScrollableContainer.svelte'; import ReduxResult from '$components/ReduxResult.svelte'; + import FileDependenciesPlugin from '$components/v3/FileDependenciesPlugin.svelte'; import FileList from '$components/v3/FileList.svelte'; import FileListMode from '$components/v3/FileListMode.svelte'; import WorktreeTipsFooter from '$components/v3/WorktreeTipsFooter.svelte'; import noChanges from '$lib/assets/illustrations/no-changes.svg?raw'; import { createCommitStore } from '$lib/commits/contexts'; + import { getSelectableFiles } from '$lib/dependencies/dependencies'; import { Focusable, FocusManager } from '$lib/focus/focusManager.svelte'; import { focusable } from '$lib/focus/focusable.svelte'; + import { isWorkspacePath } from '$lib/routes/routes.svelte'; import { ChangeSelectionService, type SelectedFile } from '$lib/selection/changeSelection.svelte'; import { StackService } from '$lib/stacks/stackService.svelte'; import { UiState } from '$lib/state/uiState.svelte'; @@ -33,6 +36,8 @@ FocusManager ); + let fileDependenciesPlugin = $state>(); + const projectState = $derived(uiState.project(projectId)); const drawerPage = $derived(projectState.drawerPage.get()); const isCommitting = $derived(drawerPage.current === 'new-commit'); @@ -47,6 +52,22 @@ const changesResult = $derived(worktreeService.getChanges(projectId)); const affectedPaths = $derived(changesResult.current.data?.map((c) => c.path)); + const workspacesParams = $derived(isWorkspacePath()); + + // This is the stack ID that's being viewed. Not **necessarily** the stack ID associated with + // the change and diff in question. + const viewingStackId = $derived(workspacesParams?.stackId); + + const fileDependencies = $derived( + fileDependenciesPlugin?.imports.deps?.type === 'multiple' + ? fileDependenciesPlugin.imports.deps.data + : undefined + ); + + const selectableFiles = $derived( + getSelectableFiles(changesResult.current.data, fileDependencies, viewingStackId) + ); + let focusGroup = focusManager.radioGroup({ triggers: [Focusable.UncommittedChanges, Focusable.ChangedFiles] }); @@ -69,14 +90,7 @@ let listMode: 'list' | 'tree' = $state('list'); function selectEverything() { - const affectedPaths = - changesResult.current.data?.map((c) => [c.path, c.pathBytes] as const) ?? []; - const files: SelectedFile[] = affectedPaths.map(([path, pathBytes]) => ({ - path, - pathBytes, - type: 'full' - })); - changeSelection.addMany(files); + changeSelection.addMany(selectableFiles); } function updateCommitSelection() { @@ -104,6 +118,14 @@ let isFooterSticky = $state(false); + + {#snippet children(changes, { stackId, projectId })} @@ -133,6 +155,7 @@ {#if changes.length > 0}
+ dependency.locks.some((lock) => lock.stackId !== currentStack) + ); +} + +/** + * Retrieves a list of selectable files based on the provided changes, file dependencies, and stack ID. + * + * @param changes - An array of `TreeChange` objects representing the changes in the file tree. + * If undefined or empty, an empty array is returned. + * @param fileDependencies - A map where the keys are file paths and the values are `FileDependencies` objects. + * This is used to determine if a file is locked to a specific stack. + * @param stackId - The ID of the current stack. Will be used to determine locks to other stacks. + * + * @returns An array of `SelectedFile` objects representing the files that can be selected. + * Files that are locked to other stacks are excluded from the result. + */ +export function getSelectableFiles( + changes: TreeChange[] | undefined, + fileDependencies: Map | undefined, + currentStack: string | undefined +): SelectedFile[] { + if (!currentStack) { + // Should not happen, but just in case. + throw new Error('Current stack is undefined'); + } + + if (changes === undefined || changes.length === 0) { + // No changes + return []; + } + const selectedFiles: SelectedFile[] = []; + + for (const change of changes) { + const fileDependency = fileDependencies?.get(change.path)?.dependencies; + if ( + fileDependency && + fileDependency.length > 0 && + hunkIsLockedToOtherStack(fileDependency, currentStack) + ) { + // The files are at least partially locked to other stacks, + // so we don't want to select them. + + // TODO: Support partial selection of files and hunks. + continue; + } + + const fullFile = { + path: change.path, + pathBytes: change.pathBytes, + type: 'full' + } as const; + + selectedFiles.push(fullFile); + } + return selectedFiles; +} diff --git a/apps/desktop/src/lib/dependencies/dependencyService.svelte.ts b/apps/desktop/src/lib/dependencies/dependencyService.svelte.ts index 6a8968f481..b4e1585e39 100644 --- a/apps/desktop/src/lib/dependencies/dependencyService.svelte.ts +++ b/apps/desktop/src/lib/dependencies/dependencyService.svelte.ts @@ -3,7 +3,7 @@ import { type FileDependencies, type HunkDependencies } from '$lib/dependencies/dependencies'; -import { createSelectByIds } from '$lib/state/customSelectors'; +import { createSelectByIds, createSelectByIdsWithKey } from '$lib/state/customSelectors'; import { createEntityAdapter, type EntityState } from '@reduxjs/toolkit'; import type { BackendApi, ClientState } from '$lib/state/clientState.svelte'; @@ -28,8 +28,19 @@ export default class DependencyService { return this.api.endpoints.dependencies.useQuery( { projectId, worktreeChangesKey }, { - transform: ({ fileDependencies }) => - fileDependencySelectors.selectByIds(fileDependencies, filePaths) + transform: ({ fileDependencies }) => { + const keyedDepdendencies = fileDependencySelectors.createSelectByIdsWithKey( + fileDependencies, + filePaths + ); + const dependecyMap = new Map(); + for (const { key, value } of keyedDepdendencies) { + if (value) { + dependecyMap.set(key, value); + } + } + return dependecyMap; + } } ); } @@ -68,5 +79,6 @@ const fileDependenciesAdapter = createEntityAdapter({ const fileDependencySelectors = { ...fileDependenciesAdapter.getSelectors(), - selectByIds: createSelectByIds() + selectByIds: createSelectByIds(), + createSelectByIdsWithKey: createSelectByIdsWithKey() }; diff --git a/apps/desktop/src/lib/state/customSelectors.ts b/apps/desktop/src/lib/state/customSelectors.ts index 499087bff2..2dff18b2c2 100644 --- a/apps/desktop/src/lib/state/customSelectors.ts +++ b/apps/desktop/src/lib/state/customSelectors.ts @@ -28,6 +28,18 @@ export function createSelectByIds() { ); } +export function createSelectByIdsWithKey() { + return createSelector( + [(state: EntityState) => state, (state_, ids: string[]) => ids], + (state, ids) => { + return ids.map((id) => { + const entity = state.entities[id]; + return { key: id, value: entity }; + }); + } + ); +} + /** * The main purpose of this function is to enable selecting e.g. the * parent of a branch, or commit.