1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-17 01:42:45 +02:00

Copy tags to clipboard (#4218)

- **PR Description**

This PR adds a new feature that allows users to copy tags to the
clipboard.
It can be used from the Commits "Copy to clipboard" menu and also from
the Tags list.
Closes #4219 

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [x] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
This commit is contained in:
Jesse Duffield
2025-01-30 08:56:07 +11:00
committed by GitHub
14 changed files with 189 additions and 43 deletions

View File

@ -352,6 +352,7 @@ If you would instead like to start an interactive rebase from the selected commi
| Key | Action | Info | | Key | Action | Info |
|-----|--------|-------------| |-----|--------|-------------|
| `` <c-o> `` | Copy tag to clipboard | |
| `` <space> `` | Checkout | Checkout the selected tag as a detached HEAD. | | `` <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. | | `` 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. | | `` d `` | Delete | View delete options for local/remote tag. |

View File

@ -182,6 +182,7 @@ If you would instead like to start an interactive rebase from the selected commi
| Key | Action | Info | | Key | Action | Info |
|-----|--------|-------------| |-----|--------|-------------|
| `` <c-o> `` | Copy tag to clipboard | |
| `` <space> `` | チェックアウト | Checkout the selected tag as a detached HEAD. | | `` <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. | | `` 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. | | `` d `` | Delete | View delete options for local/remote tag. |

View File

@ -323,6 +323,7 @@ If you would instead like to start an interactive rebase from the selected commi
| Key | Action | Info | | Key | Action | Info |
|-----|--------|-------------| |-----|--------|-------------|
| `` <c-o> `` | Copy tag to clipboard | |
| `` <space> `` | 체크아웃 | Checkout the selected tag as a detached HEAD. | | `` <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. | | `` 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. | | `` d `` | Delete | View delete options for local/remote tag. |

View File

@ -352,6 +352,7 @@ If you would instead like to start an interactive rebase from the selected commi
| Key | Action | Info | | Key | Action | Info |
|-----|--------|-------------| |-----|--------|-------------|
| `` <c-o> `` | Copy tag to clipboard | |
| `` <space> `` | Uitchecken | Checkout the selected tag as a detached HEAD. | | `` <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. | | `` 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. | | `` d `` | Delete | View delete options for local/remote tag. |

View File

@ -333,6 +333,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
| Key | Action | Info | | Key | Action | Info |
|-----|--------|-------------| |-----|--------|-------------|
| `` <c-o> `` | Copy tag to clipboard | |
| `` <space> `` | Przełącz | Przełącz wybrany tag jako odłączoną głowę (detached HEAD). | | `` <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. | | `` 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. | | `` d `` | Usuń | Wyświetl opcje usuwania lokalnego/odległego tagu. |

View File

@ -288,6 +288,7 @@ If you would instead like to start an interactive rebase from the selected commi
| Key | Action | Info | | Key | Action | Info |
|-----|--------|-------------| |-----|--------|-------------|
| `` <c-o> `` | Copy tag to clipboard | |
| `` <space> `` | Переключить | Checkout the selected tag as a detached HEAD. | | `` <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. | | `` 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. | | `` d `` | Delete | View delete options for local/remote tag. |

View File

@ -250,6 +250,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| Key | Action | Info | | Key | Action | Info |
|-----|--------|-------------| |-----|--------|-------------|
| `` <c-o> `` | Copy tag to clipboard | |
| `` <space> `` | 检出 | 检出选择的标签作为分离的HEAD | | `` <space> `` | 检出 | 检出选择的标签作为分离的HEAD |
| `` n `` | 创建标签 | 基于当前提交创建一个新标签。你将在弹窗中输入标签名称和描述(可选)。 | | `` n `` | 创建标签 | 基于当前提交创建一个新标签。你将在弹窗中输入标签名称和描述(可选)。 |
| `` d `` | 删除 | 查看本地/远程标签的删除选项 | | `` d `` | 删除 | 查看本地/远程标签的删除选项 |

View File

@ -284,6 +284,7 @@ If you would instead like to start an interactive rebase from the selected commi
| Key | Action | Info | | Key | Action | Info |
|-----|--------|-------------| |-----|--------|-------------|
| `` <c-o> `` | Copy tag to clipboard | |
| `` <space> `` | 檢出 | Checkout the selected tag as a detached HEAD. | | `` <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. | | `` 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. | | `` d `` | 刪除 | View delete options for local/remote tag. |

View File

@ -3,6 +3,7 @@ package controllers
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
@ -122,9 +123,7 @@ func (self *BasicCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
} }
func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) error { func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) error {
return self.c.Menu(types.CreateMenuOptions{ items := []*types.MenuItem{
Title: self.c.Tr.Actions.CopyCommitAttributeToClipboard,
Items: []*types.MenuItem{
{ {
Label: self.c.Tr.CommitHash, Label: self.c.Tr.CommitHash,
OnPress: func() error { OnPress: func() error {
@ -166,7 +165,25 @@ func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) e
}, },
Key: 'a', 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 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 { func (self *BasicCommitsController) openInBrowser(commit *models.Commit) error {
url, err := self.c.Helpers().Host.GetCommitURL(commit.Hash) url, err := self.c.Helpers().Host.GetCommitURL(commit.Hash)
if err != nil { if err != nil {

View File

@ -145,6 +145,13 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason, GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason,
Description: self.c.Tr.CopyBranchNameToClipboard, 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", ViewName: "commits",
Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), Key: opts.GetKey(opts.Config.Universal.CopyToClipboard),

View File

@ -612,9 +612,11 @@ type TranslationSet struct {
CommitMessage string CommitMessage string
CommitSubject string CommitSubject string
CommitAuthor string CommitAuthor string
CommitTags string
CopyCommitAttributeToClipboard string CopyCommitAttributeToClipboard string
CopyCommitAttributeToClipboardTooltip string CopyCommitAttributeToClipboardTooltip string
CopyBranchNameToClipboard string CopyBranchNameToClipboard string
CopyTagToClipboard string
CopyPathToClipboard string CopyPathToClipboard string
CommitPrefixPatternError string CommitPrefixPatternError string
CopySelectedTextToClipboard string CopySelectedTextToClipboard string
@ -674,6 +676,8 @@ type TranslationSet struct {
CommitMessageCopiedToClipboard string CommitMessageCopiedToClipboard string
CommitSubjectCopiedToClipboard string CommitSubjectCopiedToClipboard string
CommitAuthorCopiedToClipboard string CommitAuthorCopiedToClipboard string
CommitTagsCopiedToClipboard string
CommitHasNoTags string
PatchCopiedToClipboard string PatchCopiedToClipboard string
CopiedToClipboard string CopiedToClipboard string
ErrCannotEditDirectory string ErrCannotEditDirectory string
@ -905,6 +909,7 @@ type Actions struct {
CopyCommitURLToClipboard string CopyCommitURLToClipboard string
CopyCommitAuthorToClipboard string CopyCommitAuthorToClipboard string
CopyCommitAttributeToClipboard string CopyCommitAttributeToClipboard string
CopyCommitTagsToClipboard string
CopyPatchToClipboard string CopyPatchToClipboard string
CustomCommand string CustomCommand string
DiscardAllChangesInDirectory string DiscardAllChangesInDirectory string
@ -1627,9 +1632,11 @@ func EnglishTranslationSet() *TranslationSet {
CommitMessage: "Commit message", CommitMessage: "Commit message",
CommitSubject: "Commit subject", CommitSubject: "Commit subject",
CommitAuthor: "Commit author", CommitAuthor: "Commit author",
CommitTags: "Commit tags",
CopyCommitAttributeToClipboard: "Copy commit attribute to clipboard", CopyCommitAttributeToClipboard: "Copy commit attribute to clipboard",
CopyCommitAttributeToClipboardTooltip: "Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author).", CopyCommitAttributeToClipboardTooltip: "Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author).",
CopyBranchNameToClipboard: "Copy branch name to clipboard", CopyBranchNameToClipboard: "Copy branch name to clipboard",
CopyTagToClipboard: "Copy tag to clipboard",
CopyPathToClipboard: "Copy path to clipboard", CopyPathToClipboard: "Copy path to clipboard",
CopySelectedTextToClipboard: "Copy selected text to clipboard", CopySelectedTextToClipboard: "Copy selected text to clipboard",
CommitPrefixPatternError: "Error in commitPrefix pattern", CommitPrefixPatternError: "Error in commitPrefix pattern",
@ -1688,6 +1695,8 @@ func EnglishTranslationSet() *TranslationSet {
CommitMessageCopiedToClipboard: "Commit message copied to clipboard", CommitMessageCopiedToClipboard: "Commit message copied to clipboard",
CommitSubjectCopiedToClipboard: "Commit subject copied to clipboard", CommitSubjectCopiedToClipboard: "Commit subject copied to clipboard",
CommitAuthorCopiedToClipboard: "Commit author 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", PatchCopiedToClipboard: "Patch copied to clipboard",
CopiedToClipboard: "copied to clipboard", CopiedToClipboard: "copied to clipboard",
ErrCannotEditDirectory: "Cannot edit directories: you can only edit individual files", ErrCannotEditDirectory: "Cannot edit directories: you can only edit individual files",
@ -1872,6 +1881,7 @@ func EnglishTranslationSet() *TranslationSet {
CreateAnnotatedTag: "Create annotated tag", CreateAnnotatedTag: "Create annotated tag",
CopyCommitMessageToClipboard: "Copy commit message to clipboard", CopyCommitMessageToClipboard: "Copy commit message to clipboard",
CopyCommitSubjectToClipboard: "Copy commit subject to clipboard", CopyCommitSubjectToClipboard: "Copy commit subject to clipboard",
CopyCommitTagsToClipboard: "Copy commit tags to clipboard",
CopyCommitDiffToClipboard: "Copy commit diff to clipboard", CopyCommitDiffToClipboard: "Copy commit diff to clipboard",
CopyCommitHashToClipboard: "Copy full commit hash to clipboard", CopyCommitHashToClipboard: "Copy full commit hash to clipboard",
CopyCommitURLToClipboard: "Copy commit URL to clipboard", CopyCommitURLToClipboard: "Copy commit URL to clipboard",

View File

@ -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_"))
},
})

View File

@ -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_"))
},
})

View File

@ -93,6 +93,7 @@ var tests = []*components.IntegrationTest{
commit.CommitWithNonMatchingBranchName, commit.CommitWithNonMatchingBranchName,
commit.CommitWithPrefix, commit.CommitWithPrefix,
commit.CopyAuthorToClipboard, commit.CopyAuthorToClipboard,
commit.CopyTagToClipboard,
commit.CreateAmendCommit, commit.CreateAmendCommit,
commit.CreateFixupCommitInBranchStack, commit.CreateFixupCommitInBranchStack,
commit.CreateTag, commit.CreateTag,
@ -351,6 +352,7 @@ var tests = []*components.IntegrationTest{
sync.RenameBranchAndPull, sync.RenameBranchAndPull,
tag.Checkout, tag.Checkout,
tag.CheckoutWhenBranchWithSameNameExists, tag.CheckoutWhenBranchWithSameNameExists,
tag.CopyToClipboard,
tag.CreateWhileCommitting, tag.CreateWhileCommitting,
tag.CrudAnnotated, tag.CrudAnnotated,
tag.CrudLightweight, tag.CrudLightweight,