11<script setup lang="ts">
22import { ref , computed , onMounted } from " vue" ;
3- import { gitListTags , gitDeleteTag , gitPushTags , gitDeleteRemoteTag , gitRemoteInfo , type GitTag } from " ../utils/backend" ;
3+ import { gitListTags , gitDeleteTag , gitPushTags , gitDeleteRemoteTag , gitRemoteInfo , gitRetagTo , gitForcePushTag , type GitTag } from " ../utils/backend" ;
44import { useI18n } from " ../composables/useI18n" ;
55import BaseModal from " ./BaseModal.vue" ;
66
@@ -23,6 +23,13 @@ const remoteName = ref("origin");
2323const busyTag = ref <string | null >(null ); // name of tag currently being acted on
2424const busyPushAll = ref (false );
2525const confirmDelete = ref <{ name: string ; withRemote: boolean } | null >(null );
26+ const retagDialog = ref <{
27+ name: string ;
28+ isAnnotated: boolean ;
29+ ref: string ;
30+ message: string ;
31+ forcePush: boolean ;
32+ } | null >(null );
2633
2734// ─── Load ────────────────────────────────────────────────
2835async function load() {
@@ -98,6 +105,35 @@ async function pushTag(name: string) {
98105 }
99106}
100107
108+ function openRetagDialog(tag : GitTag ) {
109+ retagDialog .value = {
110+ name: tag .name ,
111+ isAnnotated: tag .isAnnotated ,
112+ ref: " HEAD" ,
113+ message: tag .message ?? " " ,
114+ forcePush: hasRemote .value ,
115+ };
116+ }
117+
118+ async function confirmRetag() {
119+ if (! retagDialog .value ) return ;
120+ const { name, isAnnotated, ref, message, forcePush } = retagDialog .value ;
121+ busyTag .value = name ;
122+ error .value = null ;
123+ try {
124+ await gitRetagTo (props .cwd , name , ref .trim () || " HEAD" , isAnnotated ? message : undefined );
125+ if (forcePush && hasRemote .value ) {
126+ await gitForcePushTag (props .cwd , remoteName .value , name );
127+ }
128+ retagDialog .value = null ;
129+ await load ();
130+ } catch (err : any ) {
131+ error .value = err ?.message ?? String (err );
132+ } finally {
133+ busyTag .value = null ;
134+ }
135+ }
136+
101137async function pushAllTags() {
102138 if (! hasRemote .value ) return ;
103139 busyPushAll .value = true ;
@@ -183,6 +219,18 @@ async function pushAllTags() {
183219 </div >
184220 </div >
185221 <div class =" tp-item-actions" >
222+ <!-- Retag: move tag to a different ref -->
223+ <button
224+ class =" tp-action-btn"
225+ :disabled =" busyTag === tag.name"
226+ :title =" t('tags.retagTitle')"
227+ @click =" openRetagDialog(tag)"
228+ >
229+ <svg width =" 13" height =" 13" viewBox =" 0 0 16 16" fill =" none" aria-hidden =" true" >
230+ <path d =" M2 8h9M8 5l3 3-3 3" stroke =" currentColor" stroke-width =" 1.4" stroke-linecap =" round" stroke-linejoin =" round" />
231+ <circle cx =" 13" cy =" 8" r =" 1.5" fill =" currentColor" stroke =" none" />
232+ </svg >
233+ </button >
186234 <button
187235 v-if =" hasRemote"
188236 class =" tp-action-btn"
@@ -209,6 +257,52 @@ async function pushAllTags() {
209257 </li >
210258 </ul >
211259
260+ <!-- Retag dialog -->
261+ <BaseModal
262+ v-if =" retagDialog "
263+ :title =" t (' tags.retagTitle' )"
264+ :subtitle =" retagDialog .name "
265+ size="sm"
266+ @close =" retagDialog = null "
267+ >
268+ <div class =" tp-form" >
269+ <label class =" tp-form-label" >{{ t('tags.retagRefLabel') }}</label >
270+ <input
271+ v-model =" retagDialog.ref"
272+ class =" tp-form-input"
273+ placeholder =" HEAD"
274+ autofocus
275+ @keydown.enter =" confirmRetag"
276+ @keydown.escape =" retagDialog = null"
277+ />
278+ <p class =" tp-form-hint" >{{ t('tags.retagRefHint') }}</p >
279+
280+ <template v-if =" retagDialog .isAnnotated " >
281+ <label class =" tp-form-label" >{{ t('tags.retagMessageLabel') }}</label >
282+ <textarea
283+ v-model =" retagDialog.message"
284+ class =" tp-form-textarea"
285+ rows =" 3"
286+ />
287+ </template >
288+
289+ <label v-if =" hasRemote" class =" tp-check-label tp-check-label--warn" >
290+ <input type =" checkbox" v-model =" retagDialog.forcePush" />
291+ <span >{{ t('tags.retagForcePush', remoteName) }}</span >
292+ </label >
293+ </div >
294+ <template #footer >
295+ <button class =" bm-btn bm-btn--ghost" @click =" retagDialog = null" >{{ t('common.cancel') }}</button >
296+ <button
297+ class =" bm-btn bm-btn--primary"
298+ :disabled =" busyTag === retagDialog.name"
299+ @click =" confirmRetag"
300+ >
301+ {{ busyTag === retagDialog.name ? t('common.loading') : t('tags.retagConfirm') }}
302+ </button >
303+ </template >
304+ </BaseModal >
305+
212306 <!-- Delete confirmation sub-modal -->
213307 <BaseModal
214308 v-if =" confirmDelete "
@@ -421,12 +515,58 @@ async function pushAllTags() {
421515 border-left : 3px solid var (--color-danger , #ef4444 );
422516}
423517
424- /* ─── Delete confirm ────── ──────────────────────────────── */
518+ /* ─── Retag / Delete forms ──────────────────────────────── */
425519.tp-check-label {
426520 display : flex ;
427521 align-items : center ;
428522 gap : var (--space-2 );
429523 font-size : var (--font-size-sm );
430524 cursor : pointer ;
431525}
526+
527+ .tp-check-label--warn {
528+ color : var (--color-warning , #f59e0b );
529+ margin-top : var (--space-3 );
530+ }
531+
532+ .tp-form {
533+ display : flex ;
534+ flex-direction : column ;
535+ gap : var (--space-2 );
536+ }
537+
538+ .tp-form-label {
539+ font-size : var (--font-size-sm );
540+ font-weight : var (--font-weight-medium );
541+ color : var (--color-text );
542+ margin-top : var (--space-2 );
543+ }
544+
545+ .tp-form-input ,
546+ .tp-form-textarea {
547+ width : 100% ;
548+ padding : var (--space-3 ) var (--space-4 );
549+ font-size : var (--font-size-sm );
550+ font-family : inherit ;
551+ background : var (--color-bg );
552+ color : var (--color-text );
553+ border : 1px solid var (--color-border );
554+ border-radius : var (--radius-md );
555+ outline : none ;
556+ box-sizing : border-box ;
557+ transition : border-color var (--transition-fast ), box-shadow var (--transition-fast );
558+ resize : vertical ;
559+ }
560+
561+ .tp-form-input :focus ,
562+ .tp-form-textarea :focus {
563+ border-color : var (--color-accent );
564+ box-shadow : 0 0 0 3px var (--color-accent-soft );
565+ }
566+
567+ .tp-form-hint {
568+ font-size : var (--font-size-xs );
569+ color : var (--color-text-muted );
570+ margin : 0 ;
571+ }
432572 </style >
0 commit comments