1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-08-06 22:33:07 +02:00

Show annotation information for selected tag (#4663)

- **PR Description**

In the Tags panel, show information about the selected tag above the
commit log. This is mostly useful for annotated tags.

Fixes #4659.
This commit is contained in:
Stefan Haller
2025-06-30 16:14:20 +02:00
committed by GitHub
5 changed files with 83 additions and 24 deletions

View File

@ -57,3 +57,20 @@ func (self *TagCommands) Push(task gocui.Task, remoteName string, tagName string
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run() return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
} }
// Return info about an annotated tag in the format:
//
// Tagger: tagger name <tagger email>
// TaggerDate: tagger date
//
// Tag message
//
// Should only be called for annotated tags.
func (self *TagCommands) ShowAnnotationInfo(tagName string) (string, error) {
cmdArgs := NewGitCmd("for-each-ref").
Arg("--format=Tagger: %(taggername) %(taggeremail)%0aTaggerDate: %(taggerdate)%0a%0a%(contents)").
Arg("refs/tags/" + tagName).
ToArgv()
return self.cmd.New(cmdArgs).RunWithOutput()
}

View File

@ -1,7 +1,7 @@
package git_commands package git_commands
import ( import (
"regexp" "strings"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
@ -26,9 +26,13 @@ func NewTagLoader(
} }
func (self *TagLoader) GetTags() ([]*models.Tag, error) { func (self *TagLoader) GetTags() ([]*models.Tag, error) {
// get remote branches, sorted by creation date (descending) // get tags, sorted by creation date (descending)
// see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt // see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt
cmdArgs := NewGitCmd("tag").Arg("--list", "-n", "--sort=-creatordate").ToArgv() cmdArgs := NewGitCmd("for-each-ref").
Arg("--sort=-creatordate").
Arg("--format=%(refname)%00%(objecttype)%00%(contents:subject)").
Arg("refs/tags").
ToArgv()
tagsOutput, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput() tagsOutput, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
if err != nil { if err != nil {
return nil, err return nil, err
@ -36,20 +40,20 @@ func (self *TagLoader) GetTags() ([]*models.Tag, error) {
split := utils.SplitLines(tagsOutput) split := utils.SplitLines(tagsOutput)
lineRegex := regexp.MustCompile(`^([^\s]+)(\s+)?(.*)$`) tags := lo.FilterMap(split, func(line string, _ int) (*models.Tag, bool) {
fields := strings.SplitN(line, "\x00", 3)
tags := lo.Map(split, func(line string, _ int) *models.Tag { if len(fields) != 3 {
matches := lineRegex.FindStringSubmatch(line) return nil, false
tagName := matches[1]
message := ""
if len(matches) > 3 {
message = matches[3]
} }
tagName := fields[0]
objectType := fields[1]
message := fields[2]
return &models.Tag{ return &models.Tag{
Name: tagName, Name: strings.TrimPrefix(tagName, "refs/tags/"),
Message: message, Message: message,
} IsAnnotated: objectType == "tag",
}, true
}) })
return tags, nil return tags, nil

View File

@ -9,10 +9,9 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
const tagsOutput = `tag1 this is my message const tagsOutput = "refs/tags/tag1\x00tag\x00this is my message\n" +
tag2 "refs/tags/tag2\x00commit\x00\n" +
tag3 this is my other message "refs/tags/tag3\x00tag\x00this is my other message\n"
`
func TestGetTags(t *testing.T) { func TestGetTags(t *testing.T) {
type scenario struct { type scenario struct {
@ -26,18 +25,18 @@ func TestGetTags(t *testing.T) {
{ {
testName: "should return no tags if there are none", testName: "should return no tags if there are none",
runner: oscommands.NewFakeRunner(t). runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"tag", "--list", "-n", "--sort=-creatordate"}, "", nil), ExpectGitArgs([]string{"for-each-ref", "--sort=-creatordate", "--format=%(refname)%00%(objecttype)%00%(contents:subject)", "refs/tags"}, "", nil),
expectedTags: []*models.Tag{}, expectedTags: []*models.Tag{},
expectedError: nil, expectedError: nil,
}, },
{ {
testName: "should return tags if present", testName: "should return tags if present",
runner: oscommands.NewFakeRunner(t). runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"tag", "--list", "-n", "--sort=-creatordate"}, tagsOutput, nil), ExpectGitArgs([]string{"for-each-ref", "--sort=-creatordate", "--format=%(refname)%00%(objecttype)%00%(contents:subject)", "refs/tags"}, tagsOutput, nil),
expectedTags: []*models.Tag{ expectedTags: []*models.Tag{
{Name: "tag1", Message: "this is my message"}, {Name: "tag1", Message: "this is my message", IsAnnotated: true},
{Name: "tag2", Message: ""}, {Name: "tag2", Message: "", IsAnnotated: false},
{Name: "tag3", Message: "this is my other message"}, {Name: "tag3", Message: "this is my other message", IsAnnotated: true},
}, },
expectedError: nil, expectedError: nil,
}, },

View File

@ -3,9 +3,13 @@ package models
// Tag : A git tag // Tag : A git tag
type Tag struct { type Tag struct {
Name string Name string
// this is either the first line of the message of an annotated tag, or the // this is either the first line of the message of an annotated tag, or the
// first line of a commit message for a lightweight tag // first line of a commit message for a lightweight tag
Message string Message string
// true if this is an annotated tag, false if it's a lightweight tag
IsAnnotated bool
} }
func (t *Tag) FullRefName() string { func (t *Tag) FullRefName() string {

View File

@ -1,11 +1,16 @@
package controllers package controllers
import ( import (
"fmt"
"strings"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
) )
type TagsController struct { type TagsController struct {
@ -96,7 +101,8 @@ func (self *TagsController) GetOnRenderToMain() func() {
task = types.NewRenderStringTask("No tags") task = types.NewRenderStringTask("No tags")
} else { } else {
cmdObj := self.c.Git().Branch.GetGraphCmdObj(tag.FullRefName()) cmdObj := self.c.Git().Branch.GetGraphCmdObj(tag.FullRefName())
task = types.NewRunCommandTask(cmdObj.GetCmd()) prefix := self.getTagInfo(tag) + "\n\n---\n\n"
task = types.NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix)
} }
self.c.RenderToMainViews(types.RefreshMainOpts{ self.c.RenderToMainViews(types.RefreshMainOpts{
@ -110,6 +116,35 @@ func (self *TagsController) GetOnRenderToMain() func() {
} }
} }
func (self *TagsController) getTagInfo(tag *models.Tag) string {
if tag.IsAnnotated {
info := fmt.Sprintf("%s: %s", self.c.Tr.AnnotatedTag, style.AttrBold.Sprint(style.FgYellow.Sprint(tag.Name)))
output, err := self.c.Git().Tag.ShowAnnotationInfo(tag.Name)
if err == nil {
info += "\n\n" + strings.TrimRight(filterOutPgpSignature(output), "\n")
}
return info
}
return fmt.Sprintf("%s: %s", self.c.Tr.LightweightTag, style.AttrBold.Sprint(style.FgYellow.Sprint(tag.Name)))
}
func filterOutPgpSignature(output string) string {
lines := strings.Split(output, "\n")
inPgpSignature := false
filteredLines := lo.Filter(lines, func(line string, _ int) bool {
if line == "-----END PGP SIGNATURE-----" {
inPgpSignature = false
return false
}
if line == "-----BEGIN PGP SIGNATURE-----" {
inPgpSignature = true
}
return !inPgpSignature
})
return strings.Join(filteredLines, "\n")
}
func (self *TagsController) checkout(tag *models.Tag) error { func (self *TagsController) checkout(tag *models.Tag) error {
self.c.LogAction(self.c.Tr.Actions.CheckoutTag) self.c.LogAction(self.c.Tr.Actions.CheckoutTag)
if err := self.c.Helpers().Refs.CheckoutRef(tag.FullRefName(), types.CheckoutRefOptions{}); err != nil { if err := self.c.Helpers().Refs.CheckoutRef(tag.FullRefName(), types.CheckoutRefOptions{}); err != nil {