diff --git a/pkg/commands/git_commands/tag.go b/pkg/commands/git_commands/tag.go index fb87db3b9..2517ce380 100644 --- a/pkg/commands/git_commands/tag.go +++ b/pkg/commands/git_commands/tag.go @@ -12,16 +12,19 @@ func NewTagCommands(gitCommon *GitCommon) *TagCommands { } } -func (self *TagCommands) CreateLightweight(tagName string, ref string) error { - cmdArgs := NewGitCmd("tag").Arg("--", tagName). +func (self *TagCommands) CreateLightweight(tagName string, ref string, force bool) error { + cmdArgs := NewGitCmd("tag"). + ArgIf(force, "--force"). + Arg("--", tagName). ArgIf(len(ref) > 0, ref). ToArgv() return self.cmd.New(cmdArgs).Run() } -func (self *TagCommands) CreateAnnotated(tagName, ref, msg string) error { +func (self *TagCommands) CreateAnnotated(tagName, ref, msg string, force bool) error { cmdArgs := NewGitCmd("tag").Arg(tagName). + ArgIf(force, "--force"). ArgIf(len(ref) > 0, ref). Arg("-m", msg). ToArgv() @@ -29,6 +32,15 @@ func (self *TagCommands) CreateAnnotated(tagName, ref, msg string) error { return self.cmd.New(cmdArgs).Run() } +func (self *TagCommands) HasTag(tagName string) bool { + cmdArgs := NewGitCmd("show-ref"). + Arg("--tags", "--quiet", "--verify", "--"). + Arg("refs/tags/" + tagName). + ToArgv() + + return self.cmd.New(cmdArgs).Run() == nil +} + func (self *TagCommands) Delete(tagName string) error { cmdArgs := NewGitCmd("tag").Arg("-d", tagName). ToArgv() diff --git a/pkg/gui/controllers/helpers/tags_helper.go b/pkg/gui/controllers/helpers/tags_helper.go index 47f2115ce..51957355a 100644 --- a/pkg/gui/controllers/helpers/tags_helper.go +++ b/pkg/gui/controllers/helpers/tags_helper.go @@ -4,6 +4,7 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" ) type TagsHelper struct { @@ -19,16 +20,16 @@ func NewTagsHelper(c *HelperCommon, commitsHelper *CommitsHelper) *TagsHelper { } func (self *TagsHelper) OpenCreateTagPrompt(ref string, onCreate func()) error { - onConfirm := func(tagName string, description string) error { + doCreateTag := func(tagName string, description string, force bool) error { return self.c.WithWaitingStatus(self.c.Tr.CreatingTag, func(gocui.Task) error { if description != "" { self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag) - if err := self.c.Git().Tag.CreateAnnotated(tagName, ref, description); err != nil { + if err := self.c.Git().Tag.CreateAnnotated(tagName, ref, description, force); err != nil { return self.c.Error(err) } } else { self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag) - if err := self.c.Git().Tag.CreateLightweight(tagName, ref); err != nil { + if err := self.c.Git().Tag.CreateLightweight(tagName, ref, force); err != nil { return self.c.Error(err) } } @@ -41,6 +42,28 @@ func (self *TagsHelper) OpenCreateTagPrompt(ref string, onCreate func()) error { }) } + onConfirm := func(tagName string, description string) error { + if self.c.Git().Tag.HasTag(tagName) { + prompt := utils.ResolvePlaceholderString( + self.c.Tr.ForceTagPrompt, + map[string]string{ + "tagName": tagName, + "cancelKey": self.c.UserConfig.Keybinding.Universal.Return, + "confirmKey": self.c.UserConfig.Keybinding.Universal.Confirm, + }, + ) + return self.c.Confirm(types.ConfirmOpts{ + Title: self.c.Tr.ForceTag, + Prompt: prompt, + HandleConfirm: func() error { + return doCreateTag(tagName, description, true) + }, + }) + } else { + return doCreateTag(tagName, description, false) + } + } + return self.commitsHelper.OpenCommitMessagePanel( &OpenCommitMessagePanelOpts{ CommitIndex: context.NoCommitIndex, diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 5aa6bd3c9..274c5328e 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -359,6 +359,8 @@ type TranslationSet struct { PushTag string CreateTag string CreatingTag string + ForceTag string + ForceTagPrompt string FetchRemote string FetchingRemoteStatus string CheckoutCommit string @@ -1102,6 +1104,8 @@ func EnglishTranslationSet() TranslationSet { PushTag: "Push tag", CreateTag: "Create tag", CreatingTag: "Creating tag", + ForceTag: "Force Tag", + ForceTagPrompt: "The tag '{{.tagName}}' exists already. Press {{.cancelKey}} to cancel, or {{.confirmKey}} to overwrite.", FetchRemote: "Fetch remote", FetchingRemoteStatus: "Fetching remote", CheckoutCommit: "Checkout commit", diff --git a/pkg/integration/tests/tag/force_tag_annotated.go b/pkg/integration/tests/tag/force_tag_annotated.go new file mode 100644 index 000000000..1f9e2f09b --- /dev/null +++ b/pkg/integration/tests/tag/force_tag_annotated.go @@ -0,0 +1,47 @@ +package tag + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var ForceTagAnnotated = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Overwrite an annotated tag that already exists", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("first commit") + shell.CreateAnnotatedTag("new-tag", "message", "HEAD") + shell.EmptyCommit("second commit") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("second commit").IsSelected(), + Contains("new-tag").Contains("first commit"), + ). + Press(keys.Commits.CreateTag). + Tap(func() { + t.ExpectPopup().CommitMessagePanel(). + Title(Equals("Tag name")). + Type("new-tag"). + SwitchToDescription(). + Title(Equals("Tag description")). + Type("message"). + SwitchToSummary(). + Confirm() + }). + Tap(func() { + t.ExpectPopup().Confirmation(). + Title(Equals("Force Tag")). + Content(Contains("The tag 'new-tag' exists already. Press to cancel, or to overwrite.")). + Confirm() + }). + Lines( + Contains("new-tag").Contains("second commit"), + DoesNotContain("new-tag").Contains("first commit"), + ) + }, +}) diff --git a/pkg/integration/tests/tag/force_tag_lightweight.go b/pkg/integration/tests/tag/force_tag_lightweight.go new file mode 100644 index 000000000..1f24b624c --- /dev/null +++ b/pkg/integration/tests/tag/force_tag_lightweight.go @@ -0,0 +1,43 @@ +package tag + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var ForceTagLightweight = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Overwrite a lightweight tag that already exists", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("first commit") + shell.CreateLightweightTag("new-tag", "HEAD") + shell.EmptyCommit("second commit") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("second commit").IsSelected(), + Contains("new-tag").Contains("first commit"), + ). + Press(keys.Commits.CreateTag). + Tap(func() { + t.ExpectPopup().CommitMessagePanel(). + Title(Equals("Tag name")). + Type("new-tag"). + Confirm() + }). + Tap(func() { + t.ExpectPopup().Confirmation(). + Title(Equals("Force Tag")). + Content(Contains("The tag 'new-tag' exists already. Press to cancel, or to overwrite.")). + Confirm() + }). + Lines( + Contains("new-tag").Contains("second commit"), + DoesNotContain("new-tag").Contains("first commit"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index dfcd6c0ea..d7870ff49 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -214,6 +214,8 @@ var tests = []*components.IntegrationTest{ tag.CreateWhileCommitting, tag.CrudAnnotated, tag.CrudLightweight, + tag.ForceTagAnnotated, + tag.ForceTagLightweight, tag.Reset, ui.Accordion, ui.DoublePopup,