From 497d380b180efe1be6ff0be7dd8d72269169b2a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Sep 2025 09:58:55 +0000 Subject: [PATCH 1/2] Initial plan From b22627f15be9177e9620ed4505dfdb5476b4f2f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Sep 2025 10:14:42 +0000 Subject: [PATCH 2/2] Implement additive executable file icons with symlink/submodule support Co-authored-by: Byron <63622+Byron@users.noreply.github.com> --- apps/desktop/src/components/FileList.svelte | 3 + .../src/components/FileListItemWrapper.svelte | 4 + .../src/lib/components/file/FileIcon.svelte | 91 +++++++++++++++++-- .../lib/components/file/FileListItem.svelte | 9 +- .../src/lib/components/file/FileName.svelte | 9 +- .../lib/components/file/FileViewHeader.svelte | 9 +- .../ui/src/lib/components/file/fileIcons.ts | 5 +- .../src/lib/components/file/fileTypeUtils.ts | 85 +++++++++++++++++ 8 files changed, 191 insertions(+), 24 deletions(-) create mode 100644 packages/ui/src/lib/components/file/fileTypeUtils.ts diff --git a/apps/desktop/src/components/FileList.svelte b/apps/desktop/src/components/FileList.svelte index ca2bc1d5f7..bbaa39eee0 100644 --- a/apps/desktop/src/components/FileList.svelte +++ b/apps/desktop/src/components/FileList.svelte @@ -11,6 +11,7 @@ import { editPatch } from '$lib/editMode/editPatchUtils'; import { abbreviateFolders, changesToFileTree } from '$lib/files/filetreeV3'; import { type TreeChange, isExecutableStatus } from '$lib/hunks/change'; + import { getFileType } from '@gitbutler/ui/components/file/fileTypeUtils'; import { MODE_SERVICE } from '$lib/mode/modeService'; import { showToast } from '$lib/notifications/toasts'; import { FILE_SELECTION_MANAGER } from '$lib/selection/fileSelectionManager.svelte'; @@ -221,6 +222,7 @@ {#snippet fileTemplate(change: TreeChange, idx: number, depth: number = 0)} {@const isExecutable = isExecutableStatus(change.status)} + {@const fileType = getFileType(change.status)} {@const selected = idSelection.has(change.path, selectionId)} handleKeyDown(change, idx, e), autoAction: true }} onclick={(e) => { diff --git a/apps/desktop/src/components/FileListItemWrapper.svelte b/apps/desktop/src/components/FileListItemWrapper.svelte index 2de966de9e..5270793674 100644 --- a/apps/desktop/src/components/FileListItemWrapper.svelte +++ b/apps/desktop/src/components/FileListItemWrapper.svelte @@ -33,6 +33,7 @@ linesRemoved?: number; depth?: number; executable?: boolean; + fileType?: 'regular' | 'executable' | 'symlink' | 'submodule'; focusableOpts?: FocusableOptions; showCheckbox?: boolean; draggable?: boolean; @@ -58,6 +59,7 @@ listMode, depth, executable, + fileType, showCheckbox, conflictEntries, draggable, @@ -165,6 +167,7 @@ linesRemoved={lineChangesStat?.removed} fileStatusTooltip={previousTooltipText} {executable} + {fileType} oncontextmenu={(e) => { e.stopPropagation(); e.preventDefault(); @@ -186,6 +189,7 @@ indeterminate={checkStatus.current === 'indeterminate'} {depth} {executable} + {fileType} draggable={!draggableDisabled} {onkeydown} {hideBorder} diff --git a/packages/ui/src/lib/components/file/FileIcon.svelte b/packages/ui/src/lib/components/file/FileIcon.svelte index 161ccf1f6f..fe4d213216 100644 --- a/packages/ui/src/lib/components/file/FileIcon.svelte +++ b/packages/ui/src/lib/components/file/FileIcon.svelte @@ -1,26 +1,99 @@ - +
+ + + {#if showExecutableOverlay()} + executable + {/if} +
diff --git a/packages/ui/src/lib/components/file/FileListItem.svelte b/packages/ui/src/lib/components/file/FileListItem.svelte index e828694132..206efa49b4 100644 --- a/packages/ui/src/lib/components/file/FileListItem.svelte +++ b/packages/ui/src/lib/components/file/FileListItem.svelte @@ -4,7 +4,6 @@ import Checkbox from '$components/Checkbox.svelte'; import Icon from '$components/Icon.svelte'; import Tooltip from '$components/Tooltip.svelte'; - import ExecutableLabel from '$components/file/ExecutableLabel.svelte'; import FileIndent from '$components/file/FileIndent.svelte'; import FileName from '$components/file/FileName.svelte'; import FileStatusBadge from '$components/file/FileStatusBadge.svelte'; @@ -35,6 +34,7 @@ active?: boolean; hideBorder?: boolean; executable?: boolean; + fileType?: 'regular' | 'executable' | 'symlink' | 'submodule'; actionOpts?: FocusableOptions; oncheckclick?: (e: MouseEvent) => void; oncheck?: ( @@ -72,6 +72,7 @@ listMode = 'list', depth, executable, + fileType, actionOpts, oncheck, oncheckclick, @@ -130,7 +131,7 @@ {/if} - +
{#if locked} @@ -171,10 +172,6 @@ {/if} - {#if executable} - - {/if} - {#if fileStatus} {/if} diff --git a/packages/ui/src/lib/components/file/FileName.svelte b/packages/ui/src/lib/components/file/FileName.svelte index 4cbdd4bb9c..4993a7094e 100644 --- a/packages/ui/src/lib/components/file/FileName.svelte +++ b/packages/ui/src/lib/components/file/FileName.svelte @@ -7,9 +7,14 @@ filePath: string; hideFilePath?: boolean; textSize?: '12' | '13'; + fileType?: 'regular' | 'executable' | 'symlink' | 'submodule'; + /** + * @deprecated Use fileType prop instead + */ + executable?: boolean; } - let { filePath, textSize = '12', hideFilePath }: Props = $props(); + let { filePath, textSize = '12', hideFilePath, fileType, executable }: Props = $props(); const fileNameAndPath = $derived(splitFilePath(filePath)); const filePathParts = $derived({ first: fileNameAndPath.path.split('/').slice(0, -1).join('/'), @@ -18,7 +23,7 @@
- + {fileNameAndPath.filename} diff --git a/packages/ui/src/lib/components/file/FileViewHeader.svelte b/packages/ui/src/lib/components/file/FileViewHeader.svelte index 4f05239be9..07fd517f4e 100644 --- a/packages/ui/src/lib/components/file/FileViewHeader.svelte +++ b/packages/ui/src/lib/components/file/FileViewHeader.svelte @@ -3,7 +3,6 @@ import Button from '$components/Button.svelte'; import Icon from '$components/Icon.svelte'; import LineStats from '$components/LineStats.svelte'; - import ExecutableLabel from '$components/file/ExecutableLabel.svelte'; import FileName from '$components/file/FileName.svelte'; import FileStatusBadge from '$components/file/FileStatusBadge.svelte'; import type { FileStatus } from '$components/file/types'; @@ -18,6 +17,7 @@ linesRemoved?: number; conflicted?: boolean; executable?: boolean; + fileType?: 'regular' | 'executable' | 'symlink' | 'submodule'; transparent?: boolean; noPaddings?: boolean; oncontextmenu?: (e: MouseEvent) => void; @@ -34,6 +34,7 @@ linesRemoved = 0, conflicted, executable, + fileType, transparent, noPaddings, oncontextmenu, @@ -63,7 +64,7 @@ {/if}
- +
@@ -71,10 +72,6 @@ {/if} - {#if executable} - - {/if} - {#if fileStatus} {/if} diff --git a/packages/ui/src/lib/components/file/fileIcons.ts b/packages/ui/src/lib/components/file/fileIcons.ts index 1babd6a7df..b60a0a1c23 100644 --- a/packages/ui/src/lib/components/file/fileIcons.ts +++ b/packages/ui/src/lib/components/file/fileIcons.ts @@ -325,5 +325,8 @@ export const fileIcons = { xml: '\n', yaml: '\n', yarn: '\n', - zig: '\n' + zig: '\n', + symlink: '\n', + submodule: '\n', + 'executable-overlay': 'x\n' } as Record; diff --git a/packages/ui/src/lib/components/file/fileTypeUtils.ts b/packages/ui/src/lib/components/file/fileTypeUtils.ts new file mode 100644 index 0000000000..e7f1525b43 --- /dev/null +++ b/packages/ui/src/lib/components/file/fileTypeUtils.ts @@ -0,0 +1,85 @@ +/** + * Utilities for determining file types from TreeChange objects and ChangeState + */ + +export type FileType = 'regular' | 'executable' | 'symlink' | 'submodule'; + +/** + * Determines the file type from a ChangeState object or status + */ +export function getFileType(status: any): FileType { + // Handle the different status types: Addition, Deletion, Modification, Rename + const state = getRelevantState(status); + + if (!state || !state.kind) { + return 'regular'; + } + + switch (state.kind) { + case 'Link': + return 'symlink'; + case 'Commit': + return 'submodule'; + case 'BlobExecutable': + return 'executable'; + case 'Blob': + default: + return 'regular'; + } +} + +/** + * Gets the relevant state from a status object + */ +function getRelevantState(status: any): any { + if (!status || !status.subject) { + return null; + } + + const subject = status.subject; + + // For additions, use the state + if (subject.state) { + return subject.state; + } + + // For deletions, we might need the previous state + if (subject.previousState) { + return subject.previousState; + } + + return null; +} + +/** + * Checks if a file is executable (but not a symlink or submodule) + */ +export function isExecutableFile(status: any): boolean { + const fileType = getFileType(status); + return fileType === 'executable'; +} + +/** + * Checks if a file is a symlink + */ +export function isSymlink(status: any): boolean { + const fileType = getFileType(status); + return fileType === 'symlink'; +} + +/** + * Checks if a file is a submodule + */ +export function isSubmodule(status: any): boolean { + const fileType = getFileType(status); + return fileType === 'submodule'; +} + +/** + * Checks if a file should show an executable overlay + * (executable files that are not symlinks or submodules) + */ +export function shouldShowExecutableOverlay(status: any): boolean { + const fileType = getFileType(status); + return fileType === 'executable'; +} \ No newline at end of file