2525
2626// Tag git tag info.
2727type Tag struct {
28- Name string
29- Date time.Time
28+ Name string
29+ Date time.Time
30+ Annotated bool
3031}
3132
3233// LogRangeType type of log range.
@@ -270,7 +271,6 @@ func (g GitSV) Tag(version semver.Version, annotate, local bool) (string, error)
270271
271272 var tagOpts * git.CreateTagOptions
272273 if annotate {
273- // Create an annotated tag with a message
274274 tagOpts = & git.CreateTagOptions {
275275 Message : tagMsg ,
276276 }
@@ -336,7 +336,9 @@ func (g GitSV) Tags() ([]Tag, error) {
336336
337337 // Try to get annotated tag first
338338 tagObj , err := repo .TagObject (ref .Hash ())
339- if err == nil {
339+
340+ annotated := err == nil
341+ if annotated {
340342 tagDate = tagObj .Tagger .When
341343 } else {
342344 // For lightweight tags, get the commit
@@ -348,8 +350,9 @@ func (g GitSV) Tags() ([]Tag, error) {
348350 }
349351
350352 tags = append (tags , Tag {
351- Name : tagName ,
352- Date : tagDate ,
353+ Name : tagName ,
354+ Date : tagDate ,
355+ Annotated : annotated ,
353356 })
354357
355358 return nil
@@ -366,6 +369,83 @@ func (g GitSV) Tags() ([]Tag, error) {
366369 return tags , nil
367370}
368371
372+ // Retag moves the existing tag with the given name to HEAD. The exact name
373+ // is preserved (no pattern reformatting). If the tag already points to HEAD
374+ // and no annotation upgrade is required, the local rewrite is skipped.
375+ func (g GitSV ) Retag (existingName string , annotate , local bool ) (string , error ) {
376+ repo , err := git .PlainOpen ("." )
377+ if err != nil {
378+ return existingName , fmt .Errorf ("failed to open git repository: %w" , err )
379+ }
380+
381+ head , err := repo .Head ()
382+ if err != nil {
383+ return existingName , fmt .Errorf ("failed to get HEAD reference: %w" , err )
384+ }
385+
386+ tagCommit , existingAnnotated , err := resolveTagCommit (repo , existingName )
387+ if err != nil {
388+ return existingName , fmt .Errorf ("failed to resolve tag %q: %w" , existingName , err )
389+ }
390+
391+ if tagCommit != head .Hash () || (annotate && ! existingAnnotated ) {
392+ tagMsg := fmt .Sprintf ("Version %s" , existingName )
393+
394+ var tagOpts * git.CreateTagOptions
395+ if annotate {
396+ tagOpts = & git.CreateTagOptions {Message : tagMsg }
397+ }
398+
399+ if delErr := repo .DeleteTag (existingName ); delErr != nil && ! errors .Is (delErr , git .ErrTagNotFound ) {
400+ return existingName , fmt .Errorf ("failed to delete existing tag: %w" , delErr )
401+ }
402+
403+ if _ , err = repo .CreateTag (existingName , head .Hash (), tagOpts ); err != nil {
404+ return existingName , fmt .Errorf ("failed to create tag: %w" , err )
405+ }
406+ }
407+
408+ if local {
409+ return existingName , nil
410+ }
411+
412+ remote , err := repo .Remote ("origin" )
413+ if err != nil {
414+ return existingName , fmt .Errorf ("failed to get remote: %w" , err )
415+ }
416+
417+ refSpec := fmt .Sprintf ("refs/tags/%s:refs/tags/%s" , existingName , existingName )
418+
419+ if err = remote .Push (& git.PushOptions {
420+ RefSpecs : []config.RefSpec {config .RefSpec (refSpec )},
421+ Force : true ,
422+ }); err != nil && ! errors .Is (err , git .NoErrAlreadyUpToDate ) {
423+ return existingName , fmt .Errorf ("failed to push tag: %w" , err )
424+ }
425+
426+ return existingName , nil
427+ }
428+
429+ // resolveTagCommit returns the commit hash the named tag points to and
430+ // whether the tag is annotated, handling both annotated and lightweight
431+ // tags.
432+ func resolveTagCommit (repo * git.Repository , name string ) (plumbing.Hash , bool , error ) {
433+ ref , err := repo .Reference (plumbing .ReferenceName ("refs/tags/" + name ), false )
434+ if err != nil {
435+ return plumbing .ZeroHash , false , err
436+ }
437+
438+ if ref == nil {
439+ return plumbing .ZeroHash , false , plumbing .ErrReferenceNotFound
440+ }
441+
442+ if tagObj , terr := repo .TagObject (ref .Hash ()); terr == nil {
443+ return tagObj .Target , true , nil
444+ }
445+
446+ return ref .Hash (), false , nil
447+ }
448+
369449// Branch get git branch.
370450func (g GitSV ) Branch () string {
371451 repo , err := git .PlainOpen ("." )
0 commit comments