1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-13 01:30:53 +02:00

Add option to delete local and remote tag (#4217)

- **PR Description**

Option to delete both local and remote tag, closes #4203.

- **Please check if the PR fulfills these requirements**

* [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))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc

<!--
Be sure to name your PR with an imperative e.g. 'Add worktrees view'
see https://github.com/jesseduffield/lazygit/releases/tag/v0.40.0 for
examples
-->
This commit is contained in:
Jesse Duffield
2025-01-30 09:36:12 +11:00
committed by GitHub
5 changed files with 145 additions and 0 deletions

View File

@ -177,6 +177,59 @@ func (self *TagsController) remoteDelete(tag *models.Tag) error {
return nil 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 { func (self *TagsController) delete(tag *models.Tag) error {
menuTitle := utils.ResolvePlaceholderString( menuTitle := utils.ResolvePlaceholderString(
self.c.Tr.DeleteTagTitle, self.c.Tr.DeleteTagTitle,
@ -201,6 +254,14 @@ func (self *TagsController) delete(tag *models.Tag) error {
return self.remoteDelete(tag) 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{ return self.c.Menu(types.CreateMenuOptions{

View File

@ -518,8 +518,10 @@ type TranslationSet struct {
DeleteTagTitle string DeleteTagTitle string
DeleteLocalTag string DeleteLocalTag string
DeleteRemoteTag string DeleteRemoteTag string
DeleteLocalAndRemoteTag string
SelectRemoteTagUpstream string SelectRemoteTagUpstream string
DeleteRemoteTagPrompt string DeleteRemoteTagPrompt string
DeleteLocalAndRemoteTagPrompt string
RemoteTagDeletedMessage string RemoteTagDeletedMessage string
PushTagTitle string PushTagTitle string
PushTag string PushTag string
@ -1539,9 +1541,11 @@ func EnglishTranslationSet() *TranslationSet {
DeleteTagTitle: "Delete tag '{{.tagName}}'?", DeleteTagTitle: "Delete tag '{{.tagName}}'?",
DeleteLocalTag: "Delete local tag", DeleteLocalTag: "Delete local tag",
DeleteRemoteTag: "Delete remote tag", DeleteRemoteTag: "Delete remote tag",
DeleteLocalAndRemoteTag: "Delete local and remote tag",
RemoteTagDeletedMessage: "Remote tag deleted", RemoteTagDeletedMessage: "Remote tag deleted",
SelectRemoteTagUpstream: "Remote from which to remove tag '{{.tagName}}':", SelectRemoteTagUpstream: "Remote from which to remove tag '{{.tagName}}':",
DeleteRemoteTagPrompt: "Are you sure you want to delete the remote tag '{{.tagName}}' from '{{.upstream}}'?", 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:", PushTagTitle: "Remote to push tag '{{.tagName}}' to:",
// Using 'push tag' rather than just 'push' to disambiguate from a global push // Using 'push tag' rather than just 'push' to disambiguate from a global push
PushTag: "Push tag", PushTag: "Push tag",

View File

@ -190,6 +190,10 @@ func (self *Shell) Revert(ref string) *Shell {
return self.RunCommand([]string{"git", "revert", ref}) 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 { func (self *Shell) CreateLightweightTag(name string, ref string) *Shell {
return self.RunCommand([]string{"git", "tag", name, ref}) return self.RunCommand([]string{"git", "tag", name, ref})
} }

View File

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

View File

@ -356,6 +356,7 @@ var tests = []*components.IntegrationTest{
tag.CreateWhileCommitting, tag.CreateWhileCommitting,
tag.CrudAnnotated, tag.CrudAnnotated,
tag.CrudLightweight, tag.CrudLightweight,
tag.DeleteLocalAndRemote,
tag.ForceTagAnnotated, tag.ForceTagAnnotated,
tag.ForceTagLightweight, tag.ForceTagLightweight,
tag.Reset, tag.Reset,