feat(worktrees): création intelligente (auto) + commit/push/promotion + grille agrandissable
- addWorktree résout la branche par dépôt (branchExists) : checkout si présente, suivi de origin/<b> si seulement remote, sinon création (-b) depuis la branche par défaut. Corrige le 'fatal: invalid reference' sur les groupes hétérogènes ; newBranch -> mode (déprécié, mappé en route).
- Nouvelles opérations git : commit (add -A), push (upstream auto), promotion « en principal » (la branche du worktree devient le checkout principal, sans merge ni conflit, ancienne branche conservée). Routes /worktrees/{commit,push,promote} auditées + GET /repos/:id/branches.
- Web : actions Commit/Push/Promouvoir sur WorktreeCard, sélecteur de branche de base (datalist) dans RepoSection et GroupSessionModal, agrandissement d'un terminal au choix dans TerminalGrid.
- Court-circuite la session de groupe quand aucun worktree n'a pu être créé (fini le masquage par « no resolvable worktree »).
- Tests : git.ts (branchExists/listBranches/commitAll/auto), worktree-manager (commit/promote/branches) ; acceptance P5 étend le scénario worktree de groupe sur branche neuve + commit + promotion.
This commit is contained in:
@@ -62,6 +62,41 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- actions git : commit / push / promotion en principal (sans ouvrir le terminal) -->
|
||||
<div v-if="canPush || worktree.git.dirtyCount || canPromote" class="mt-1 flex flex-wrap items-center gap-2">
|
||||
<form v-if="committingOpen" class="flex flex-wrap items-center gap-1" @submit.prevent="onCommit">
|
||||
<input v-model="commitMessage" class="input w-44 text-xs" :placeholder="t('worktrees.commitPlaceholder')" />
|
||||
<button type="submit" class="btn-primary text-xs" :disabled="!!gitBusy || commitMessage.trim() === ''">
|
||||
{{ gitBusy === 'commit' ? t('worktrees.committing') : t('worktrees.commit') }}
|
||||
</button>
|
||||
<button type="button" class="btn text-xs" @click="committingOpen = false">{{ t('common.cancel') }}</button>
|
||||
</form>
|
||||
<button v-else-if="worktree.git.dirtyCount" class="btn text-xs" :disabled="!!gitBusy" @click="committingOpen = true">
|
||||
{{ t('worktrees.commit') }}
|
||||
</button>
|
||||
|
||||
<button v-if="canPush" class="btn text-xs" :disabled="!!gitBusy" @click="onPush">
|
||||
{{ gitBusy === 'push' ? t('worktrees.pushing') : t('worktrees.push') }}<span v-if="worktree.git.ahead"> ↑{{ worktree.git.ahead }}</span>
|
||||
</button>
|
||||
|
||||
<button v-if="canPromote && !promoteConfirming" class="btn text-xs" :disabled="!!gitBusy" @click="promoteConfirming = true">
|
||||
{{ t('worktrees.promote') }}
|
||||
</button>
|
||||
|
||||
<span v-if="gitError" class="text-xs text-amber-400">{{ gitError }}</span>
|
||||
</div>
|
||||
|
||||
<!-- confirmation explicite de la promotion (action destructive : ce worktree sera supprimé) -->
|
||||
<div v-if="promoteConfirming" class="mt-1 rounded border border-amber-700/40 bg-amber-950/20 p-2 text-xs text-amber-200">
|
||||
{{ promoteConfirmText }}
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<button class="btn-primary text-xs" :disabled="!!gitBusy" @click="onPromote">
|
||||
{{ gitBusy === 'promote' ? t('worktrees.promoting') : forcePromoteNeeded ? t('worktrees.forcePromote') : t('worktrees.promote') }}
|
||||
</button>
|
||||
<button class="btn text-xs" @click="cancelPromote">{{ t('common.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- réponse rapide aux dialogues des sessions corrélées en attente (sans ouvrir le terminal) -->
|
||||
<DialogPrompt v-for="ws in waitingSessions" :key="`dlg-${ws.id}`" :session="ws" class="mt-2" />
|
||||
</div>
|
||||
@@ -135,6 +170,73 @@ const branchLabel = computed(() =>
|
||||
props.worktree.branch ?? (props.worktree.detached ? `${t('worktrees.detached')} ${props.worktree.head.slice(0, 7)}` : '—'),
|
||||
);
|
||||
|
||||
// --- actions git : commit / push / promotion en principal ---
|
||||
const gitBusy = ref<null | 'commit' | 'push' | 'promote'>(null);
|
||||
const gitError = ref<string | null>(null);
|
||||
const committingOpen = ref(false);
|
||||
const commitMessage = ref('');
|
||||
const promoteConfirming = ref(false);
|
||||
const forcePromoteNeeded = ref(false);
|
||||
|
||||
const canPush = computed(() => !!props.worktree.branch && !props.worktree.detached);
|
||||
const canPromote = computed(() => !props.worktree.isMain && !!props.worktree.branch && !props.worktree.detached);
|
||||
const promoteConfirmText = computed(() => t('worktrees.promoteConfirm', { branch: props.worktree.branch ?? '' }));
|
||||
|
||||
function gitMsg(err: unknown): string {
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
function cancelPromote(): void {
|
||||
promoteConfirming.value = false;
|
||||
forcePromoteNeeded.value = false;
|
||||
}
|
||||
|
||||
async function onCommit(): Promise<void> {
|
||||
if (gitBusy.value || commitMessage.value.trim() === '') return;
|
||||
gitBusy.value = 'commit';
|
||||
gitError.value = null;
|
||||
try {
|
||||
await store.commitWorktree(props.worktree.repoId, props.worktree.path, commitMessage.value.trim());
|
||||
committingOpen.value = false;
|
||||
commitMessage.value = '';
|
||||
} catch (err) {
|
||||
gitError.value = gitMsg(err);
|
||||
} finally {
|
||||
gitBusy.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function onPush(): Promise<void> {
|
||||
if (gitBusy.value) return;
|
||||
gitBusy.value = 'push';
|
||||
gitError.value = null;
|
||||
try {
|
||||
await store.pushWorktree(props.worktree.repoId, props.worktree.path);
|
||||
} catch (err) {
|
||||
gitError.value = gitMsg(err);
|
||||
} finally {
|
||||
gitBusy.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function onPromote(): Promise<void> {
|
||||
if (gitBusy.value) return;
|
||||
gitBusy.value = 'promote';
|
||||
gitError.value = null;
|
||||
try {
|
||||
await store.promoteWorktree(props.worktree.repoId, props.worktree.path, forcePromoteNeeded.value);
|
||||
// succès : la branche est passée en principal et ce worktree est supprimé → la carte se démonte.
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 409) {
|
||||
forcePromoteNeeded.value = true; // arbre modifié → proposer la promotion forcée
|
||||
gitError.value = err.message;
|
||||
} else {
|
||||
gitError.value = gitMsg(err);
|
||||
}
|
||||
} finally {
|
||||
gitBusy.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function reset(): void {
|
||||
confirming.value = false;
|
||||
forceNeeded.value = false;
|
||||
|
||||
Reference in New Issue
Block a user