Repository: jesseduffield/lazygit (language: go)

Issue:
Copy tags to clipboard
**Is your feature request related to a problem? Please describe.**
I recently started using lazygit and it's way more convenient that relying on the default git cli. Thank you all for this great tool!

As part of my workflow I sometimes need to copy-paste git tags, I don't think Lazygit supports that.

**Describe the solution you'd like**
I want to be able to go to the `Tags` list and press C-o to copy the tag name into the clipboard.
I also want to be able to have the option to copy the tags from the `Commit` clipboard menu `y` by pressing `t` there.

**Describe alternatives you've considered**
My alternative is using the regular git cli.

**Additional context**
I started hacking around by myself and I think I was able to implement it successfully.

PR here: https://github.com/jesseduffield/lazygit/pull/4218

Some screenshots:
![Image](https://github.com/user-attachments/assets/d1622a56-fa0b-408e-902b-a95b931897a9)
![Image](https://github.com/user-attachments/assets/1ba1eb70-ef52-4852-9c21-907c89f52fa1)

---

I just learned about the custom commands from the PR template, I think the same functionality might be achieved that way. I can try to go with that route if I'm only one needing this feature 🙈



Discussion / hints:

Fix patch:
diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md
index 449c4b6ec6d..f162014b700 100644
--- a/docs/keybindings/Keybindings_en.md
+++ b/docs/keybindings/Keybindings_en.md
@@ -352,6 +352,7 @@ If you would instead like to start an interactive rebase from the selected commi
 
 | Key | Action | Info |
 |-----|--------|-------------|
+| `` <c-o> `` | Copy tag to clipboard |  |
 | `` <space> `` | Checkout | Checkout the selected tag as a detached HEAD. |
 | `` n `` | New tag | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
 | `` d `` | Delete | View delete options for local/remote tag. |
diff --git a/docs/keybindings/Keybindings_ja.md b/docs/keybindings/Keybindings_ja.md
index 0ccd6e7abe9..a1046c8dc17 100644
--- a/docs/keybindings/Keybindings_ja.md
+++ b/docs/keybindings/Keybindings_ja.md
@@ -182,6 +182,7 @@ If you would instead like to start an interactive rebase from the selected commi
 
 | Key | Action | Info |
 |-----|--------|-------------|
+| `` <c-o> `` | Copy tag to clipboard |  |
 | `` <space> `` | チェックアウト | Checkout the selected tag as a detached HEAD. |
 | `` n `` | タグを作成 | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
 | `` d `` | Delete | View delete options for local/remote tag. |
diff --git a/docs/keybindings/Keybindings_ko.md b/docs/keybindings/Keybindings_ko.md
index e5fb37782d3..50c71971961 100644
--- a/docs/keybindings/Keybindings_ko.md
+++ b/docs/keybindings/Keybindings_ko.md
@@ -323,6 +323,7 @@ If you would instead like to start an interactive rebase from the selected commi
 
 | Key | Action | Info |
 |-----|--------|-------------|
+| `` <c-o> `` | Copy tag to clipboard |  |
 | `` <space> `` | 체크아웃 | Checkout the selected tag as a detached HEAD. |
 | `` n `` | 태그를 생성 | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
 | `` d `` | Delete | View delete options for local/remote tag. |
diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md
index 5951320c072..37bacb20f35 100644
--- a/docs/keybindings/Keybindings_nl.md
+++ b/docs/keybindings/Keybindings_nl.md
@@ -352,6 +352,7 @@ If you would instead like to start an interactive rebase from the selected commi
 
 | Key | Action | Info |
 |-----|--------|-------------|
+| `` <c-o> `` | Copy tag to clipboard |  |
 | `` <space> `` | Uitchecken | Checkout the selected tag as a detached HEAD. |
 | `` n `` | Creëer tag | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
 | `` d `` | Delete | View delete options for local/remote tag. |
diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md
index c22d151d3cf..8db7a0e73ee 100644
--- a/docs/keybindings/Keybindings_pl.md
+++ b/docs/keybindings/Keybindings_pl.md
@@ -333,6 +333,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
 
 | Key | Action | Info |
 |-----|--------|-------------|
+| `` <c-o> `` | Copy tag to clipboard |  |
 | `` <space> `` | Przełącz | Przełącz wybrany tag jako odłączoną głowę (detached HEAD). |
 | `` n `` | Nowy tag | Utwórz nowy tag z bieżącego commita. Zostaniesz poproszony o wprowadzenie nazwy tagu i opcjonalnego opisu. |
 | `` d `` | Usuń | Wyświetl opcje usuwania lokalnego/odległego tagu. |
diff --git a/docs/keybindings/Keybindings_ru.md b/docs/keybindings/Keybindings_ru.md
index a9d977fcb8c..9c17e4c6b92 100644
--- a/docs/keybindings/Keybindings_ru.md
+++ b/docs/keybindings/Keybindings_ru.md
@@ -288,6 +288,7 @@ If you would instead like to start an interactive rebase from the selected commi
 
 | Key | Action | Info |
 |-----|--------|-------------|
+| `` <c-o> `` | Copy tag to clipboard |  |
 | `` <space> `` | Переключить | Checkout the selected tag as a detached HEAD. |
 | `` n `` | Создать тег | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
 | `` d `` | Delete | View delete options for local/remote tag. |
diff --git a/docs/keybindings/Keybindings_zh-CN.md b/docs/keybindings/Keybindings_zh-CN.md
index 6cabb0a3a13..6baea60d035 100644
--- a/docs/keybindings/Keybindings_zh-CN.md
+++ b/docs/keybindings/Keybindings_zh-CN.md
@@ -250,6 +250,7 @@ _图例：`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
 
 | Key | Action | Info |
 |-----|--------|-------------|
+| `` <c-o> `` | Copy tag to clipboard |  |
 | `` <space> `` | 检出 | 检出选择的标签作为分离的HEAD |
 | `` n `` | 创建标签 | 基于当前提交创建一个新标签。你将在弹窗中输入标签名称和描述(可选)。 |
 | `` d `` | 删除 | 查看本地/远程标签的删除选项 |
diff --git a/docs/keybindings/Keybindings_zh-TW.md b/docs/keybindings/Keybindings_zh-TW.md
index c891f6bdcb2..895a81e79fd 100644
--- a/docs/keybindings/Keybindings_zh-TW.md
+++ b/docs/keybindings/Keybindings_zh-TW.md
@@ -284,6 +284,7 @@ If you would instead like to start an interactive rebase from the selected commi
 
 | Key | Action | Info |
 |-----|--------|-------------|
+| `` <c-o> `` | Copy tag to clipboard |  |
 | `` <space> `` | 檢出 | Checkout the selected tag as a detached HEAD. |
 | `` n `` | 建立標籤 | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
 | `` d `` | 刪除 | View delete options for local/remote tag. |
diff --git a/pkg/gui/controllers/basic_commits_controller.go b/pkg/gui/controllers/basic_commits_controller.go
index 797215746c9..fb118b02483 100644
--- a/pkg/gui/controllers/basic_commits_controller.go
+++ b/pkg/gui/controllers/basic_commits_controller.go
@@ -3,6 +3,7 @@ package controllers
 import (
 	"errors"
 	"fmt"
+	"strings"
 
 	"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
 	"github.com/jesseduffield/lazygit/pkg/commands/models"
@@ -122,51 +123,67 @@ func (self *BasicCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
 }
 
 func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) error {
-	return self.c.Menu(types.CreateMenuOptions{
-		Title: self.c.Tr.Actions.CopyCommitAttributeToClipboard,
-		Items: []*types.MenuItem{
-			{
-				Label: self.c.Tr.CommitHash,
-				OnPress: func() error {
-					return self.copyCommitHashToClipboard(commit)
-				},
+	items := []*types.MenuItem{
+		{
+			Label: self.c.Tr.CommitHash,
+			OnPress: func() error {
+				return self.copyCommitHashToClipboard(commit)
 			},
-			{
-				Label: self.c.Tr.CommitSubject,
-				OnPress: func() error {
-					return self.copyCommitSubjectToClipboard(commit)
-				},
-				Key: 's',
+		},
+		{
+			Label: self.c.Tr.CommitSubject,
+			OnPress: func() error {
+				return self.copyCommitSubjectToClipboard(commit)
 			},
-			{
-				Label: self.c.Tr.CommitMessage,
-				OnPress: func() error {
-					return self.copyCommitMessageToClipboard(commit)
-				},
-				Key: 'm',
+			Key: 's',
+		},
+		{
+			Label: self.c.Tr.CommitMessage,
+			OnPress: func() error {
+				return self.copyCommitMessageToClipboard(commit)
 			},
-			{
-				Label: self.c.Tr.CommitURL,
-				OnPress: func() error {
-					return self.copyCommitURLToClipboard(commit)
-				},
-				Key: 'u',
+			Key: 'm',
+		},
+		{
+			Label: self.c.Tr.CommitURL,
+			OnPress: func() error {
+				return self.copyCommitURLToClipboard(commit)
 			},
-			{
-				Label: self.c.Tr.CommitDiff,
-				OnPress: func() error {
-					return self.copyCommitDiffToClipboard(commit)
-				},
-				Key: 'd',
+			Key: 'u',
+		},
+		{
+			Label: self.c.Tr.CommitDiff,
+			OnPress: func() error {
+				return self.copyCommitDiffToClipboard(commit)
 			},
-			{
-				Label: self.c.Tr.CommitAuthor,
-				OnPress: func() error {
-					return self.copyAuthorToClipboard(commit)
-				},
-				Key: 'a',
+			Key: 'd',
+		},
+		{
+			Label: self.c.Tr.CommitAuthor,
+			OnPress: func() error {
+				return self.copyAuthorToClipboard(commit)
 			},
+			Key: 'a',
 		},
+	}
+
+	commitTagsItem := types.MenuItem{
+		Label: self.c.Tr.CommitTags,
+		OnPress: func() error {
+			return self.copyCommitTagsToClipboard(commit)
+		},
+		Key: 't',
+	}
+
+	if len(commit.Tags) == 0 {
+		commitTagsItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.CommitHasNoTags}
+	}
+
+	items = append(items, &commitTagsItem)
+
+	return self.c.Menu(types.CreateMenuOptions{
+		Title: self.c.Tr.Actions.CopyCommitAttributeToClipboard,
+		Items: items,
 	})
 }
 
@@ -257,6 +274,18 @@ func (self *BasicCommitsController) copyCommitSubjectToClipboard(commit *models.
 	return nil
 }
 
+func (self *BasicCommitsController) copyCommitTagsToClipboard(commit *models.Commit) error {
+	message := strings.Join(commit.Tags, "\n")
+
+	self.c.LogAction(self.c.Tr.Actions.CopyCommitTagsToClipboard)
+	if err := self.c.OS().CopyToClipboard(message); err != nil {
+		return err
+	}
+
+	self.c.Toast(self.c.Tr.CommitTagsCopiedToClipboard)
+	return nil
+}
+
 func (self *BasicCommitsController) openInBrowser(commit *models.Commit) error {
 	url, err := self.c.Helpers().Host.GetCommitURL(commit.Hash)
 	if err != nil {
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index 371d039a662..72af2f9fdf1 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -145,6 +145,13 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
 			GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason,
 			Description:       self.c.Tr.CopyBranchNameToClipboard,
 		},
+		{
+			ViewName:          "tags",
+			Key:               opts.GetKey(opts.Config.Universal.CopyToClipboard),
+			Handler:           self.handleCopySelectedSideContextItemCommitHashToClipboard,
+			GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason,
+			Description:       self.c.Tr.CopyTagToClipboard,
+		},
 		{
 			ViewName:          "commits",
 			Key:               opts.GetKey(opts.Config.Universal.CopyToClipboard),
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 3efe269b262..968aa5718fb 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -612,9 +612,11 @@ type TranslationSet struct {
 	CommitMessage                         string
 	CommitSubject                         string
 	CommitAuthor                          string
+	CommitTags                            string
 	CopyCommitAttributeToClipboard        string
 	CopyCommitAttributeToClipboardTooltip string
 	CopyBranchNameToClipboard             string
+	CopyTagToClipboard                    string
 	CopyPathToClipboard                   string
 	CommitPrefixPatternError              string
 	CopySelectedTextToClipboard           string
@@ -674,6 +676,8 @@ type TranslationSet struct {
 	CommitMessageCopiedToClipboard           string
 	CommitSubjectCopiedToClipboard           string
 	CommitAuthorCopiedToClipboard            string
+	CommitTagsCopiedToClipboard              string
+	CommitHasNoTags                          string
 	PatchCopiedToClipboard                   string
 	CopiedToClipboard                        string
 	ErrCannotEditDirectory                   string
@@ -905,6 +909,7 @@ type Actions struct {
 	CopyCommitURLToClipboard          string
 	CopyCommitAuthorToClipboard       string
 	CopyCommitAttributeToClipboard    string
+	CopyCommitTagsToClipboard         string
 	CopyPatchToClipboard              string
 	CustomCommand                     string
 	DiscardAllChangesInDirectory      string
@@ -1627,9 +1632,11 @@ func EnglishTranslationSet() *TranslationSet {
 		CommitMessage:                            "Commit message",
 		CommitSubject:                            "Commit subject",
 		CommitAuthor:                             "Commit author",
+		CommitTags:                               "Commit tags",
 		CopyCommitAttributeToClipboard:           "Copy commit attribute to clipboard",
 		CopyCommitAttributeToClipboardTooltip:    "Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author).",
 		CopyBranchNameToClipboard:                "Copy branch name to clipboard",
+		CopyTagToClipboard:                       "Copy tag to clipboard",
 		CopyPathToClipboard:                      "Copy path to clipboard",
 		CopySelectedTextToClipboard:              "Copy selected text to clipboard",
 		CommitPrefixPatternError:                 "Error in commitPrefix pattern",
@@ -1688,6 +1695,8 @@ func EnglishTranslationSet() *TranslationSet {
 		CommitMessageCopiedToClipboard:           "Commit message copied to clipboard",
 		CommitSubjectCopiedToClipboard:           "Commit subject copied to clipboard",
 		CommitAuthorCopiedToClipboard:            "Commit author copied to clipboard",
+		CommitTagsCopiedToClipboard:              "Commit tags copied to clipboard",
+		CommitHasNoTags:                          "Commit has no tags",
 		PatchCopiedToClipboard:                   "Patch copied to clipboard",
 		CopiedToClipboard:                        "copied to clipboard",
 		ErrCannotEditDirectory:                   "Cannot edit directories: you can only edit individual files",
@@ -1872,6 +1881,7 @@ func EnglishTranslationSet() *TranslationSet {
 			CreateAnnotatedTag:             "Create annotated tag",
 			CopyCommitMessageToClipboard:   "Copy commit message to clipboard",
 			CopyCommitSubjectToClipboard:   "Copy commit subject to clipboard",
+			CopyCommitTagsToClipboard:      "Copy commit tags to clipboard",
 			CopyCommitDiffToClipboard:      "Copy commit diff to clipboard",
 			CopyCommitHashToClipboard:      "Copy full commit hash to clipboard",
 			CopyCommitURLToClipboard:       "Copy commit URL to clipboard",


Test patch:
diff --git a/pkg/integration/tests/commit/copy_tag_to_clipboard.go b/pkg/integration/tests/commit/copy_tag_to_clipboard.go
new file mode 100644
index 00000000000..a88148754ca
--- /dev/null
+++ b/pkg/integration/tests/commit/copy_tag_to_clipboard.go
@@ -0,0 +1,51 @@
+package commit
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+// We're emulating the clipboard by writing to a file called clipboard
+
+var CopyTagToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Copy a commit tag to the clipboard",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig: func(config *config.AppConfig) {
+		// Include delimiters around the text so that we can assert on the entire content
+		config.GetUserConfig().OS.CopyToClipboardCmd = "echo _{{text}}_ > clipboard"
+	},
+
+	SetupRepo: func(shell *Shell) {
+		shell.SetAuthor("John Doe", "john@doe.com")
+		shell.EmptyCommit("commit")
+		shell.CreateLightweightTag("tag1", "HEAD")
+		shell.CreateLightweightTag("tag2", "HEAD")
+	},
+
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Commits().
+			Focus().
+			Lines(
+				Contains("commit").IsSelected(),
+			).
+			Press(keys.Commits.CopyCommitAttributeToClipboard)
+
+		t.ExpectPopup().Menu().
+			Title(Equals("Copy to clipboard")).
+			Select(Contains("Commit tags")).
+			Confirm()
+
+		t.ExpectToast(Equals("Commit tags copied to clipboard"))
+
+		t.Views().Files().
+			Focus().
+			Press(keys.Files.RefreshFiles).
+			Lines(
+				Contains("clipboard").IsSelected(),
+			)
+
+		t.Views().Main().Content(Contains("+_tag2"))
+		t.Views().Main().Content(Contains("+tag1_"))
+	},
+})
diff --git a/pkg/integration/tests/tag/copy_to_clipboard.go b/pkg/integration/tests/tag/copy_to_clipboard.go
new file mode 100644
index 00000000000..f0176df9a92
--- /dev/null
+++ b/pkg/integration/tests/tag/copy_to_clipboard.go
@@ -0,0 +1,39 @@
+package tag
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var CopyToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Copy the tag to the clipboard",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig: func(config *config.AppConfig) {
+		// Include delimiters around the text so that we can assert on the entire content
+		config.GetUserConfig().OS.CopyToClipboardCmd = "echo _{{text}}_ > clipboard"
+	},
+	SetupRepo: func(shell *Shell) {
+		shell.EmptyCommit("one")
+		shell.CreateLightweightTag("tag1", "HEAD")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Tags().
+			Focus().
+			Lines(
+				Contains("tag").IsSelected(),
+			).
+			Press(keys.Universal.CopyToClipboard)
+
+		t.ExpectToast(Equals("'tag1' copied to clipboard"))
+
+		t.Views().Files().
+			Focus().
+			Press(keys.Files.RefreshFiles).
+			Lines(
+				Contains("clipboard").IsSelected(),
+			)
+
+		t.Views().Main().Content(Contains("_tag1_"))
+	},
+})
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index 0736677a80b..8b520e9e4ab 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -93,6 +93,7 @@ var tests = []*components.IntegrationTest{
 	commit.CommitWithNonMatchingBranchName,
 	commit.CommitWithPrefix,
 	commit.CopyAuthorToClipboard,
+	commit.CopyTagToClipboard,
 	commit.CreateAmendCommit,
 	commit.CreateFixupCommitInBranchStack,
 	commit.CreateTag,
@@ -351,6 +352,7 @@ var tests = []*components.IntegrationTest{
 	sync.RenameBranchAndPull,
 	tag.Checkout,
 	tag.CheckoutWhenBranchWithSameNameExists,
+	tag.CopyToClipboard,
 	tag.CreateWhileCommitting,
 	tag.CrudAnnotated,
 	tag.CrudLightweight,