Files
ss-tools/frontend/src/components/git/ConflictResolver.svelte
2026-02-23 20:20:25 +03:00

182 lines
7.3 KiB
Svelte

<!-- [DEF:ConflictResolver:Component] -->
<!--
@SEMANTICS: git, conflict, resolution, merge
@PURPOSE: UI for resolving merge conflicts (Keep Mine / Keep Theirs).
@LAYER: Component
@RELATION: DISPATCHES -> resolve
@INVARIANT: User must resolve all conflicts before saving.
-->
<script>
// [SECTION: IMPORTS]
import { createEventDispatcher } from "svelte";
import { addToast as toast } from "../../lib/toasts.js";
// [/SECTION]
// [SECTION: PROPS]
/** @type {Array<{file_path: string, mine: string, theirs: string}>} */
let {
conflicts = [],
show = false,
} = $props();
// [/SECTION]
// [SECTION: STATE]
const dispatch = createEventDispatcher();
/** @type {Object.<string, 'mine' | 'theirs' | 'manual'>} */
let resolutions = $state({});
// [/SECTION]
// [DEF:resolve:Function]
/**
* @purpose Set resolution strategy for a file.
* @pre file path must exist in conflicts array.
* @post resolutions state is updated for the given file.
* @param {string} file - File path.
* @param {'mine'|'theirs'} strategy - Resolution strategy.
* @side_effect Updates resolutions state.
*/
function resolve(file, strategy) {
console.log(
`[ConflictResolver][Action] Resolving ${file} with ${strategy}`,
);
resolutions[file] = strategy;
resolutions = { ...resolutions }; // Trigger update
}
// [/DEF:resolve:Function]
// [DEF:handleSave:Function]
/**
* @purpose Validate and submit resolutions.
* @pre All conflicts must have a resolution.
* @post 'resolve' event dispatched if valid.
* @side_effect Dispatches event and closes modal.
*/
function handleSave() {
// 1. Guard Clause (@PRE)
const unresolved = conflicts.filter((c) => !resolutions[c.file_path]);
if (unresolved.length > 0) {
console.warn(
`[ConflictResolver][Coherence:Failed] ${unresolved.length} unresolved conflicts`,
);
toast(
`Please resolve all conflicts first. (${unresolved.length} remaining)`,
"error",
);
return;
}
// 2. Implementation
console.log(`[ConflictResolver][Coherence:OK] All conflicts resolved`);
dispatch("resolve", resolutions);
show = false;
}
// [/DEF:handleSave:Function]
</script>
<!-- [SECTION: TEMPLATE] -->
{#if show}
<div
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
>
<div
class="bg-white p-6 rounded-lg shadow-xl w-full max-w-5xl max-h-[90vh] flex flex-col"
>
<h2 class="text-xl font-bold mb-4 text-red-600">
Merge Conflicts Detected
</h2>
<p class="text-gray-600 mb-4">
The following files have conflicts. Please choose how to resolve
them.
</p>
<div class="flex-1 overflow-y-auto space-y-6 mb-4 pr-2">
{#each conflicts as conflict}
<div class="border rounded-lg overflow-hidden">
<div
class="bg-gray-100 px-4 py-2 font-medium border-b flex justify-between items-center"
>
<span>{conflict.file_path}</span>
{#if resolutions[conflict.file_path]}
<span
class="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full uppercase font-bold"
>
Resolved: {resolutions[conflict.file_path]}
</span>
{/if}
</div>
<div
class="grid grid-cols-1 md:grid-cols-2 gap-0 divide-x"
>
<div class="p-0 flex flex-col">
<div
class="bg-blue-50 px-4 py-1 text-[10px] font-bold text-blue-600 uppercase border-b"
>
Your Changes (Mine)
</div>
<div class="p-4 bg-white flex-1 overflow-auto">
<pre
class="text-xs font-mono whitespace-pre">{conflict.mine}</pre>
</div>
<button
class="w-full py-2 text-sm font-medium border-t transition-colors {resolutions[
conflict.file_path
] === 'mine'
? 'bg-blue-600 text-white'
: 'bg-gray-50 hover:bg-blue-50 text-blue-600'}"
onclick={() =>
resolve(conflict.file_path, "mine")}
>
Keep Mine
</button>
</div>
<div class="p-0 flex flex-col">
<div
class="bg-green-50 px-4 py-1 text-[10px] font-bold text-green-600 uppercase border-b"
>
Remote Changes (Theirs)
</div>
<div class="p-4 bg-white flex-1 overflow-auto">
<pre
class="text-xs font-mono whitespace-pre">{conflict.theirs}</pre>
</div>
<button
class="w-full py-2 text-sm font-medium border-t transition-colors {resolutions[
conflict.file_path
] === 'theirs'
? 'bg-green-600 text-white'
: 'bg-gray-50 hover:bg-green-50 text-green-600'}"
onclick={() =>
resolve(conflict.file_path, "theirs")}
>
Keep Theirs
</button>
</div>
</div>
</div>
{/each}
</div>
<div class="flex justify-end space-x-3 pt-4 border-t">
<button
onclick={() => (show = false)}
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded transition-colors"
>
Cancel
</button>
<button
onclick={handleSave}
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors shadow-sm"
>
Resolve & Continue
</button>
</div>
</div>
</div>
{/if}
<!-- [/SECTION] -->
<!-- [/DEF:ConflictResolver:Component] -->