feat(p9): commit/push avancé dans l'IDE (staging sélectif, discard, amend, fetch/pull)

- components/workspace/CommitPanel.vue : staging sélectif par fichier (stage/unstage/stage all/unstage all), discard avec confirmation (includeUntracked), zone de commit (message + amend), commit mode 'staged', push, fetch, pull --ff-only (rebase proposé en cas de divergence), gestion 409 ALREADY_PUSHED
- components/workspace/FileRow.vue : ligne de fichier réutilisable (badge statut M/A/D/?/U + clic → diff + slot actions)
- WorkspaceView : CommitPanel remplace ChangedFilesPanel ; @changed → reload des changements
- stores/worktrees : applyWorktree (upsert depuis les mutations git → header live)
- i18n EN+FR (commit.*) ; acceptance-p9.mjs (staging sélectif, amend OK/409, pull ff-only OK + divergence refusée)
This commit is contained in:
2026-06-27 13:53:05 +02:00
parent 75efecf93f
commit 92670a796a
8 changed files with 385 additions and 60 deletions

View File

@@ -0,0 +1,53 @@
<template>
<div
class="group flex items-center gap-2 rounded px-2 py-0.5 text-xs"
:class="active ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-300 hover:bg-zinc-800/60'"
>
<button
type="button"
class="flex min-w-0 flex-1 items-center gap-2 text-left"
:title="c.renamedFrom ? `${c.renamedFrom} → ${c.path}` : c.path"
@click="emit('select')"
>
<span class="w-3 shrink-0 text-center font-mono font-bold" :class="tone">{{ letter }}</span>
<span class="min-w-0 flex-1 truncate font-mono">{{ basename }}<span class="text-zinc-600">{{ dir }}</span></span>
<span v-if="c.insertions" class="shrink-0 text-emerald-500">+{{ c.insertions }}</span>
<span v-if="c.deletions" class="shrink-0 text-rose-500">{{ c.deletions }}</span>
</button>
<span class="flex shrink-0 items-center gap-0.5 opacity-60 group-hover:opacity-100">
<slot />
</span>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import type { FileChange } from '@arboretum/shared';
const props = defineProps<{ c: FileChange; active: boolean; staged?: boolean }>();
const emit = defineEmits<{ select: [] }>();
const letter = computed(() => {
if (props.c.conflicted) return 'U';
if (props.c.untracked) return '?';
const x = props.staged
? props.c.indexStatus
: props.c.worktreeStatus !== '.' && props.c.worktreeStatus !== ' ' && props.c.worktreeStatus
? props.c.worktreeStatus
: props.c.indexStatus;
return (x || 'M').toUpperCase();
});
const tone = computed(() => {
if (props.c.conflicted) return 'text-rose-400';
if (props.c.untracked) return 'text-zinc-400';
return letter.value === 'A' ? 'text-emerald-400' : letter.value === 'D' ? 'text-rose-400' : letter.value === 'R' ? 'text-sky-400' : 'text-amber-400';
});
const basename = computed(() => {
const i = props.c.path.lastIndexOf('/');
return i >= 0 ? props.c.path.slice(i + 1) : props.c.path;
});
const dir = computed(() => {
const i = props.c.path.lastIndexOf('/');
return i >= 0 ? ` ${props.c.path.slice(0, i)}` : '';
});
</script>