|
7 | 7 | import {
|
8 | 8 | conflictEntryHint,
|
9 | 9 | getConflictState,
|
10 |
| - getInitialFileStatus, |
11 |
| - type ConflictEntryPresence, |
12 |
| - type ConflictState |
| 10 | + type ConflictEntryPresence |
13 | 11 | } from '$lib/conflictEntryPresence';
|
14 |
| - import { type RemoteFile } from '$lib/files/file'; |
15 |
| - import { UncommitedFilesWatcher } from '$lib/files/watcher'; |
| 12 | + import { FileService } from '$lib/files/fileService'; |
16 | 13 | import { ModeService, type EditModeMetadata } from '$lib/mode/modeService';
|
17 | 14 | import { vscodePath } from '$lib/project/project';
|
18 | 15 | import { ProjectsService } from '$lib/project/projectsService';
|
19 | 16 | import { SETTINGS, type Settings } from '$lib/settings/userSettings';
|
20 | 17 | import { UserService } from '$lib/user/userService';
|
21 |
| - import { computeFileStatus } from '$lib/utils/fileStatus'; |
| 18 | + import { computeChangeStatus } from '$lib/utils/fileStatus'; |
22 | 19 | import { getEditorUri, openExternalUrl } from '$lib/utils/url';
|
23 | 20 | import { getContext } from '@gitbutler/shared/context';
|
24 | 21 | import { getContextStoreBySymbol } from '@gitbutler/shared/context';
|
|
28 | 25 | import Modal from '@gitbutler/ui/Modal.svelte';
|
29 | 26 | import Avatar from '@gitbutler/ui/avatar/Avatar.svelte';
|
30 | 27 | import FileListItem from '@gitbutler/ui/file/FileListItem.svelte';
|
| 28 | + import { isDefined } from '@gitbutler/ui/utils/typeguards'; |
31 | 29 | import { SvelteSet } from 'svelte/reactivity';
|
| 30 | + import { |
| 31 | + derived, |
| 32 | + fromStore, |
| 33 | + readable, |
| 34 | + toStore, |
| 35 | + type Readable, |
| 36 | + type Writable |
| 37 | + } from 'svelte/store'; |
| 38 | + import type { FileInfo } from '$lib/files/file'; |
| 39 | + import type { TreeChange } from '$lib/hunks/change'; |
32 | 40 | import type { FileStatus } from '@gitbutler/ui/file/types';
|
33 |
| - import type { Writable } from 'svelte/store'; |
34 | 41 |
|
35 | 42 | type Props = {
|
36 | 43 | projectId: string;
|
|
43 | 50 | const projectResult = $derived(projectService.getProject(projectId));
|
44 | 51 |
|
45 | 52 | const remoteCommitService = getContext(CommitService);
|
46 |
| - const uncommitedFileWatcher = getContext(UncommitedFilesWatcher); |
47 | 53 | const modeService = getContext(ModeService);
|
48 | 54 | const userSettings = getContextStoreBySymbol<Settings, Writable<Settings>>(SETTINGS);
|
49 |
| -
|
50 |
| - const uncommitedFiles = $derived(uncommitedFileWatcher.uncommittedFiles(projectId)); |
| 55 | + const fileService = getContext(FileService); |
51 | 56 |
|
52 | 57 | const userService = getContext(UserService);
|
53 | 58 | const user = userService.user;
|
54 | 59 |
|
55 | 60 | let modeServiceAborting = $state<'inert' | 'loading' | 'completed'>('inert');
|
56 | 61 | let modeServiceSaving = $state<'inert' | 'loading' | 'completed'>('inert');
|
57 | 62 |
|
58 |
| - let initialFiles = $state<[RemoteFile, ConflictEntryPresence | undefined][]>([]); |
| 63 | + const initialFiles = $derived(modeService.initialEditModeState({ projectId })); |
| 64 | + const uncommittedFiles = $derived(modeService.changesSinceInitialEditState({ projectId })); |
| 65 | +
|
| 66 | + function readFromWorkspace( |
| 67 | + filePath: string, |
| 68 | + projectId: string |
| 69 | + ): Readable<{ data: FileInfo; isLarge: boolean } | undefined> { |
| 70 | + return readable(undefined as { data: FileInfo; isLarge: boolean } | undefined, (set) => { |
| 71 | + fileService.readFromWorkspace(filePath, projectId).then(set); |
| 72 | + }); |
| 73 | + } |
| 74 | +
|
59 | 75 | let commit = $state<Commit>();
|
60 | 76 |
|
61 | 77 | async function getCommitData() {
|
|
79 | 95 | let contextMenu = $state<ReturnType<typeof FileContextMenu> | undefined>(undefined);
|
80 | 96 | let confirmSaveModal = $state<ReturnType<typeof Modal> | undefined>(undefined);
|
81 | 97 |
|
82 |
| - $effect(() => { |
83 |
| - modeService.getInitialIndexState().then((files) => { |
84 |
| - initialFiles = files; |
85 |
| - }); |
86 |
| - }); |
87 |
| -
|
88 | 98 | interface FileEntry {
|
89 |
| - conflicted: boolean; |
90 |
| - name: string; |
91 | 99 | path: string;
|
92 | 100 | status?: FileStatus;
|
| 101 | + conflicted: boolean; |
93 | 102 | conflictHint?: string;
|
94 |
| - conflictState?: ConflictState; |
95 |
| - conflictEntryPresence?: ConflictEntryPresence; |
96 | 103 | }
|
97 | 104 |
|
98 | 105 | const initialFileMap = $derived(
|
99 |
| - new Map<string, RemoteFile>(initialFiles.map(([file]) => [file.path, file])) |
100 |
| - ); |
101 |
| -
|
102 |
| - const uncommitedFileMap = $derived( |
103 |
| - new Map<string, RemoteFile>($uncommitedFiles.map(([file]) => [file.path, file])) |
| 106 | + new Map<string, { file: TreeChange; conflictEntryPresence?: ConflictEntryPresence }>( |
| 107 | + initialFiles.current?.data?.map(([f, c]) => [ |
| 108 | + f.path, |
| 109 | + { file: f, conflictEntryPresence: c } |
| 110 | + ]) || [] |
| 111 | + ) |
104 | 112 | );
|
105 | 113 |
|
106 | 114 | const files = $derived.by(() => {
|
| 115 | + if (!initialFiles.current.data || !uncommittedFiles.current.data) return []; |
| 116 | +
|
107 | 117 | const outputMap = new Map<string, FileEntry>();
|
108 | 118 |
|
109 |
| - // Create output |
110 |
| - { |
111 |
| - initialFiles.forEach(([initialFile, conflictEntryPresence]) => { |
112 |
| - const conflictState = |
113 |
| - conflictEntryPresence && getConflictState(initialFile, conflictEntryPresence); |
114 |
| -
|
115 |
| - const uncommitedFileChange = uncommitedFileMap.get(initialFile.path); |
116 |
| -
|
117 |
| - outputMap.set(initialFile.path, { |
118 |
| - name: initialFile.filename, |
119 |
| - path: initialFile.path, |
120 |
| - conflicted: !!conflictEntryPresence, |
121 |
| - conflictHint: conflictEntryPresence |
122 |
| - ? conflictEntryHint(conflictEntryPresence) |
123 |
| - : undefined, |
124 |
| - status: getInitialFileStatus(uncommitedFileChange, conflictEntryPresence), |
125 |
| - conflictState, |
126 |
| - conflictEntryPresence |
127 |
| - }); |
| 119 | + initialFiles.current.data.forEach(([initialFile, conflictEntryPresence]) => { |
| 120 | + outputMap.set(initialFile.path, { |
| 121 | + path: initialFile.path, |
| 122 | + conflicted: !!conflictEntryPresence, |
| 123 | + conflictHint: conflictEntryPresence ? conflictEntryHint(conflictEntryPresence) : undefined |
128 | 124 | });
|
| 125 | + }); |
129 | 126 |
|
130 |
| - $uncommitedFiles.forEach(([uncommitedFile]) => { |
131 |
| - const existingFile = initialFileMap.get(uncommitedFile.path); |
132 |
| - determineOutput: { |
133 |
| - if (existingFile) { |
134 |
| - const fileChanged = existingFile.hunks.some( |
135 |
| - (hunk) => !uncommitedFile.hunks.map((hunk) => hunk.diff).includes(hunk.diff) |
136 |
| - ); |
137 |
| -
|
138 |
| - if (fileChanged) { |
139 |
| - // All initial entries should have been added to the map, |
140 |
| - // so we can safely assert that it will be present |
141 |
| - const outputFile = outputMap.get(uncommitedFile.path)!; |
142 |
| - if (outputFile.conflicted && outputFile.conflictEntryPresence) { |
143 |
| - outputFile.conflictState = getConflictState( |
144 |
| - uncommitedFile, |
145 |
| - outputFile.conflictEntryPresence |
146 |
| - ); |
147 |
| - } |
148 |
| -
|
149 |
| - if (!outputFile.conflicted) { |
150 |
| - outputFile.status = 'M'; |
151 |
| - } |
152 |
| - } |
153 |
| - break determineOutput; |
154 |
| - } |
155 |
| -
|
156 |
| - outputMap.set(uncommitedFile.path, { |
157 |
| - name: uncommitedFile.filename, |
158 |
| - path: uncommitedFile.path, |
159 |
| - conflicted: false, |
160 |
| - status: computeFileStatus(uncommitedFile) |
161 |
| - }); |
| 127 | + uncommittedFiles.current.data.forEach((uncommitedFile) => { |
| 128 | + const outputFile = outputMap.get(uncommitedFile.path)!; |
| 129 | + if (outputFile) { |
| 130 | + // We don't want to set a status if the file is |
| 131 | + // conflicted because it will _always_ show up as |
| 132 | + // modified |
| 133 | + if (!outputFile.conflicted) { |
| 134 | + outputFile.status = computeChangeStatus(uncommitedFile); |
162 | 135 | }
|
| 136 | + return; |
| 137 | + } |
| 138 | +
|
| 139 | + outputMap.set(uncommitedFile.path, { |
| 140 | + path: uncommitedFile.path, |
| 141 | + conflicted: false, |
| 142 | + status: computeChangeStatus(uncommitedFile) |
163 | 143 | });
|
164 |
| - } |
| 144 | + }); |
165 | 145 |
|
166 | 146 | const orderedOutput = Array.from(outputMap.values());
|
167 | 147 | orderedOutput.sort((a, b) => {
|
|
181 | 161 | const conflictedFiles = $derived(files.filter((file) => file.conflicted));
|
182 | 162 |
|
183 | 163 | let manuallyResolvedFiles = new SvelteSet<string>();
|
| 164 | + const filesWithConflictedStatues = $derived( |
| 165 | + conflictedFiles.map((f) => [f, isConflicted(f)] as [FileEntry, Readable<boolean>]) |
| 166 | + ); |
184 | 167 | const stillConflictedFiles = $derived(
|
185 |
| - conflictedFiles.filter( |
186 |
| - (file) => !manuallyResolvedFiles.has(file.path) && file.conflictState !== 'resolved' |
187 |
| - ) |
| 168 | + filesWithConflictedStatues.filter(([_, status]) => fromStore(status).current).map(([f]) => f) |
188 | 169 | );
|
189 | 170 |
|
190 |
| - function isConflicted(file: FileEntry): boolean { |
191 |
| - return ( |
192 |
| - file.conflicted && file.conflictState !== 'resolved' && !manuallyResolvedFiles.has(file.path) |
193 |
| - ); |
| 171 | + function isConflicted(fileEntry: FileEntry): Readable<boolean> { |
| 172 | + const file = readFromWorkspace(fileEntry.path, projectId); |
| 173 | + const conflictState = derived(file, (file) => { |
| 174 | + if (!isDefined(file?.data.content)) return 'unknown'; |
| 175 | + const { conflictEntryPresence } = initialFileMap.get(fileEntry.path) || {}; |
| 176 | + if (!conflictEntryPresence) return 'unknown'; |
| 177 | + return getConflictState(conflictEntryPresence, file.data.content); |
| 178 | + }); |
| 179 | +
|
| 180 | + const manuallyResolved = toStore(() => manuallyResolvedFiles.has(fileEntry.path)); |
| 181 | +
|
| 182 | + return derived([conflictState, manuallyResolved], ([conflictState, manuallyResolved]) => { |
| 183 | + return fileEntry.conflicted && conflictState === 'conflicted' && !manuallyResolved; |
| 184 | + }); |
194 | 185 | }
|
195 | 186 |
|
196 | 187 | async function abort() {
|
197 | 188 | modeServiceAborting = 'loading';
|
198 | 189 |
|
199 | 190 | try {
|
200 |
| - await modeService.abortEditAndReturnToWorkspace(); |
| 191 | + await modeService.abortEditAndReturnToWorkspace({ projectId }); |
201 | 192 | modeServiceAborting = 'completed';
|
202 | 193 | } finally {
|
203 | 194 | modeServiceAborting = 'inert';
|
|
208 | 199 | modeServiceSaving = 'loading';
|
209 | 200 |
|
210 | 201 | try {
|
211 |
| - await modeService.saveEditAndReturnToWorkspace(); |
| 202 | + await modeService.saveEditAndReturnToWorkspace({ projectId }); |
212 | 203 | modeServiceSaving = 'completed';
|
213 | 204 | } finally {
|
214 | 205 | modeServiceAborting = 'inert';
|
|
285 | 276 | }}
|
286 | 277 | >
|
287 | 278 | {#each files as file (file.path)}
|
| 279 | + {@const conflictedStore = isConflicted(file)} |
| 280 | + {@const conflicted = fromStore(conflictedStore).current} |
288 | 281 | <div class="file">
|
289 | 282 | <FileListItem
|
290 | 283 | filePath={file.path}
|
291 | 284 | fileStatus={file.status}
|
292 |
| - conflicted={isConflicted(file)} |
| 285 | + {conflicted} |
293 | 286 | onresolveclick={file.conflicted
|
294 | 287 | ? () => manuallyResolvedFiles.add(file.path)
|
295 | 288 | : undefined}
|
|
0 commit comments