From 7db8fb8e9c78532273cb0c22aceef6b01f5b341d Mon Sep 17 00:00:00 2001 From: Anvar Umuraliev Date: Mon, 27 Jan 2025 17:43:48 +0100 Subject: [PATCH] Add option to delete local and remote tag --- pkg/gui/controllers/tags_controller.go | 61 +++++++++++++++ pkg/i18n/english.go | 4 + pkg/integration/components/shell.go | 4 + .../tests/tag/delete_local_and_remote.go | 75 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 5 files changed, 145 insertions(+) create mode 100644 pkg/integration/tests/tag/delete_local_and_remote.go diff --git a/pkg/gui/controllers/tags_controller.go b/pkg/gui/controllers/tags_controller.go index 372fa8e0a..8a207478e 100644 --- a/pkg/gui/controllers/tags_controller.go +++ b/pkg/gui/controllers/tags_controller.go @@ -177,6 +177,59 @@ func (self *TagsController) remoteDelete(tag *models.Tag) error { return nil } +func (self *TagsController) localAndRemoteDelete(tag *models.Tag) error { + title := utils.ResolvePlaceholderString( + self.c.Tr.SelectRemoteTagUpstream, + map[string]string{ + "tagName": tag.Name, + }, + ) + + self.c.Prompt(types.PromptOpts{ + Title: title, + InitialContent: "origin", + FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteSuggestionsFunc(), + HandleConfirm: func(upstream string) error { + confirmTitle := utils.ResolvePlaceholderString( + self.c.Tr.DeleteTagTitle, + map[string]string{ + "tagName": tag.Name, + }, + ) + confirmPrompt := utils.ResolvePlaceholderString( + self.c.Tr.DeleteLocalAndRemoteTagPrompt, + map[string]string{ + "tagName": tag.Name, + "upstream": upstream, + }, + ) + + self.c.Confirm(types.ConfirmOpts{ + Title: confirmTitle, + Prompt: confirmPrompt, + HandleConfirm: func() error { + return self.c.WithInlineStatus(tag, types.ItemOperationDeleting, context.TAGS_CONTEXT_KEY, func(task gocui.Task) error { + self.c.LogAction(self.c.Tr.Actions.DeleteRemoteTag) + if err := self.c.Git().Remote.DeleteRemoteTag(task, upstream, tag.Name); err != nil { + return err + } + + self.c.LogAction(self.c.Tr.Actions.DeleteLocalTag) + if err := self.c.Git().Tag.LocalDelete(tag.Name); err != nil { + return err + } + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS}}) + }) + }, + }) + + return nil + }, + }) + + return nil +} + func (self *TagsController) delete(tag *models.Tag) error { menuTitle := utils.ResolvePlaceholderString( self.c.Tr.DeleteTagTitle, @@ -201,6 +254,14 @@ func (self *TagsController) delete(tag *models.Tag) error { return self.remoteDelete(tag) }, }, + { + Label: self.c.Tr.DeleteLocalAndRemoteTag, + Key: 'b', + OpensMenu: true, + OnPress: func() error { + return self.localAndRemoteDelete(tag) + }, + }, } return self.c.Menu(types.CreateMenuOptions{ diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 968aa5718..87744f24d 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -518,8 +518,10 @@ type TranslationSet struct { DeleteTagTitle string DeleteLocalTag string DeleteRemoteTag string + DeleteLocalAndRemoteTag string SelectRemoteTagUpstream string DeleteRemoteTagPrompt string + DeleteLocalAndRemoteTagPrompt string RemoteTagDeletedMessage string PushTagTitle string PushTag string @@ -1539,9 +1541,11 @@ func EnglishTranslationSet() *TranslationSet { DeleteTagTitle: "Delete tag '{{.tagName}}'?", DeleteLocalTag: "Delete local tag", DeleteRemoteTag: "Delete remote tag", + DeleteLocalAndRemoteTag: "Delete local and remote tag", RemoteTagDeletedMessage: "Remote tag deleted", SelectRemoteTagUpstream: "Remote from which to remove tag '{{.tagName}}':", DeleteRemoteTagPrompt: "Are you sure you want to delete the remote tag '{{.tagName}}' from '{{.upstream}}'?", + DeleteLocalAndRemoteTagPrompt: "Are you sure you want to delete '{{.tagName}}' from both your machine and from '{{.upstream}}'?", PushTagTitle: "Remote to push tag '{{.tagName}}' to:", // Using 'push tag' rather than just 'push' to disambiguate from a global push PushTag: "Push tag", diff --git a/pkg/integration/components/shell.go b/pkg/integration/components/shell.go index 4fb2d5f52..faf58e64a 100644 --- a/pkg/integration/components/shell.go +++ b/pkg/integration/components/shell.go @@ -190,6 +190,10 @@ func (self *Shell) Revert(ref string) *Shell { return self.RunCommand([]string{"git", "revert", ref}) } +func (self *Shell) AssertRemoteTagNotFound(upstream, name string) *Shell { + return self.RunCommandExpectError([]string{"git", "ls-remote", "--exit-code", upstream, fmt.Sprintf("refs/tags/%s", name)}) +} + func (self *Shell) CreateLightweightTag(name string, ref string) *Shell { return self.RunCommand([]string{"git", "tag", name, ref}) } diff --git a/pkg/integration/tests/tag/delete_local_and_remote.go b/pkg/integration/tests/tag/delete_local_and_remote.go new file mode 100644 index 000000000..35b9bc25d --- /dev/null +++ b/pkg/integration/tests/tag/delete_local_and_remote.go @@ -0,0 +1,75 @@ +package tag + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var DeleteLocalAndRemote = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Create and delete both local and remote annotated tag", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("initial commit") + shell.CloneIntoRemote("origin") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Tags(). + Focus(). + IsEmpty(). + Press(keys.Universal.New). + Tap(func() { + t.ExpectPopup().CommitMessagePanel(). + Title(Equals("Tag name")). + Type("new-tag"). + SwitchToDescription(). + Title(Equals("Tag description")). + Type("message"). + SwitchToSummary(). + Confirm() + }). + Lines( + MatchesRegexp(`new-tag.*message`).IsSelected(), + ). + Press(keys.Universal.Push). + Tap(func() { + t.ExpectPopup().Prompt(). + Title(Equals("Remote to push tag 'new-tag' to:")). + InitialText(Equals("origin")). + SuggestionLines( + Contains("origin"), + ). + Confirm() + }). + Press(keys.Universal.Remove). + Tap(func() { + t.ExpectPopup(). + Menu(). + Title(Equals("Delete tag 'new-tag'?")). + Select(Contains("Delete local and remote tag")). + Confirm() + }). + Tap(func() { + t.ExpectPopup().Prompt(). + Title(Equals("Remote from which to remove tag 'new-tag':")). + InitialText(Equals("origin")). + SuggestionLines( + Contains("origin"), + ). + Confirm() + }). + Tap(func() { + t.ExpectPopup(). + Confirmation(). + Title(Equals("Delete tag 'new-tag'?")). + Content(Equals("Are you sure you want to delete 'new-tag' from both your machine and from 'origin'?")). + Confirm() + }). + IsEmpty(). + Press(keys.Universal.New). + Tap(func() { + t.Shell().AssertRemoteTagNotFound("origin", "new-tag") + }) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 8b520e9e4..54a78bd1e 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -356,6 +356,7 @@ var tests = []*components.IntegrationTest{ tag.CreateWhileCommitting, tag.CrudAnnotated, tag.CrudLightweight, + tag.DeleteLocalAndRemote, tag.ForceTagAnnotated, tag.ForceTagLightweight, tag.Reset,