Delete or push multiple tags at once (#6071)

Make it possible to range-select multiple tags and delete or push them
all at once.
This commit is contained in:
Stefan Haller
2026-09-27 11:24:08 +02:00
committed by GitHub
10 changed files with 364 additions and 123 deletions
+3 -2
View File
@@ -59,9 +59,10 @@ func (self *RemoteCommands) DeleteRemoteBranch(task gocui.Task, remoteName strin
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
}
func (self *RemoteCommands) DeleteRemoteTag(task gocui.Task, remoteName string, tagName string) error {
func (self *RemoteCommands) DeleteRemoteTag(task gocui.Task, remoteName string, tagNames []string) error {
cmdArgs := NewGitCmd("push").
Arg(remoteName, "--delete", "refs/tags/"+tagName).
Arg(remoteName, "--delete").
Arg(lo.Map(tagNames, func(t string, _ int) string { return "refs/tags/" + t })...).
ToArgv()
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
+6 -4
View File
@@ -5,6 +5,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/samber/lo"
)
type TagCommands struct {
@@ -46,15 +47,16 @@ func (self *TagCommands) HasTag(tagName string) bool {
return self.cmd.New(cmdArgs).DontLog().Run() == nil
}
func (self *TagCommands) LocalDelete(tagName string) error {
cmdArgs := NewGitCmd("tag").Arg("-d", tagName).
func (self *TagCommands) LocalDelete(tagNames []string) error {
cmdArgs := NewGitCmd("tag").Arg("-d").Arg(tagNames...).
ToArgv()
return self.cmd.New(cmdArgs).Run()
}
func (self *TagCommands) Push(task gocui.Task, remoteName string, tagName string) error {
cmdArgs := NewGitCmd("push").Arg(remoteName, "tag", tagName).
func (self *TagCommands) Push(task gocui.Task, remoteName string, tagNames []string) error {
cmdArgs := NewGitCmd("push").Arg(remoteName).
Arg(lo.FlatMap(tagNames, func(t string, _ int) []string { return []string{"tag", t} })...).
ToArgv()
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
+1 -12
View File
@@ -504,18 +504,7 @@ func (self *BranchesHelper) PrepareFastForward(branches []*models.Branch) (func(
// Runs f on a worker with all the given branches shown as being in the given
// operation while it runs
func (self *BranchesHelper) WithInlineStatusOnBranches(branches []*models.Branch, operation types.ItemOperation, f func(gocui.Task) error) error {
return self.c.WithInlineStatus(branches[0], operation, context.LOCAL_BRANCHES_CONTEXT_KEY, func(task gocui.Task) error {
for _, branch := range branches[1:] {
self.c.State().SetItemOperation(branch, operation)
}
defer func() {
for _, branch := range branches[1:] {
self.c.State().ClearItemOperation(branch)
}
}()
return f(task)
})
return WithInlineStatusOnItems(self.c, branches, operation, context.LOCAL_BRANCHES_CONTEXT_KEY, f)
}
func (self *BranchesHelper) fetchUpstreamBranches(task gocui.Task, branches []*models.Branch) error {
@@ -94,6 +94,23 @@ func (self *InlineStatusHelper) WithInlineStatus(opts InlineStatusOpts, f func(g
}
}
// Runs f on a worker with all the given items shown as being in the given
// operation while it runs
func WithInlineStatusOnItems[T types.HasUrn](c *HelperCommon, items []T, operation types.ItemOperation, contextKey types.ContextKey, f func(gocui.Task) error) error {
return c.WithInlineStatus(items[0], operation, contextKey, func(task gocui.Task) error {
for _, item := range items[1:] {
c.State().SetItemOperation(item, operation)
}
defer func() {
for _, item := range items[1:] {
c.State().ClearItemOperation(item)
}
}()
return f(task)
})
}
func (self *InlineStatusHelper) start(opts InlineStatusOpts) {
self.c.State().SetItemOperation(opts.Item, opts.Operation)
+117 -105
View File
@@ -7,6 +7,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -61,17 +62,17 @@ func (self *TagsController) GetKeybindings(opts types.KeybindingsOpts) []*types.
},
{
Keys: opts.GetKeys(opts.Config.Universal.Remove),
Handler: self.withItem(self.delete),
Handler: self.withItems(self.delete),
Description: self.c.Tr.Delete,
GetDisabledReason: self.require(self.singleItemSelected()),
GetDisabledReason: self.require(self.itemsSelected()),
Tooltip: self.c.Tr.TagDeleteTooltip,
OpensMenu: true,
DisplayOnScreen: true,
},
{
Keys: opts.GetKeys(opts.Config.Branches.PushTag),
Handler: self.withItem(self.push),
GetDisabledReason: self.require(self.singleItemSelected()),
Handler: self.withItems(self.push),
GetDisabledReason: self.require(self.itemsSelected()),
Description: self.c.Tr.PushTag,
Tooltip: self.c.Tr.PushTagTooltip,
DisplayOnScreen: true,
@@ -164,54 +165,104 @@ func (self *TagsController) checkout(tag *models.Tag) error {
return nil
}
func (self *TagsController) localDelete(tag *models.Tag) error {
func (self *TagsController) localDelete(tags []*models.Tag) error {
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.DeleteLocalTag)
err := self.c.Git().Tag.LocalDelete(tag.Name)
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS, types.TAGS}})
err := self.c.Git().Tag.LocalDelete(tagNames(tags))
self.refreshAfterLocalDelete()
return err
})
}
func (self *TagsController) remoteDelete(tag *models.Tag) error {
title := utils.ResolvePlaceholderString(
self.c.Tr.SelectRemoteTagUpstream,
map[string]string{
"tagName": tag.Name,
func (self *TagsController) remoteDelete(tags []*models.Tag) error {
confirmPromptTemplate := lo.Ternary(len(tags) > 1, self.c.Tr.DeleteRemoteTagsPrompt, self.c.Tr.DeleteRemoteTagPrompt)
return self.confirmRemoteDelete(tags, confirmPromptTemplate, func(task gocui.Task, upstream string) error {
self.c.LogAction(self.c.Tr.Actions.DeleteRemoteTag)
if err := self.c.Git().Remote.DeleteRemoteTag(task, upstream, tagNames(tags)); err != nil {
return err
}
self.c.Toast(lo.Ternary(len(tags) > 1, self.c.Tr.RemoteTagsDeletedMessage, self.c.Tr.RemoteTagDeletedMessage))
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS, types.TAGS}})
return nil
})
}
func (self *TagsController) localAndRemoteDelete(tags []*models.Tag) error {
confirmPromptTemplate := lo.Ternary(len(tags) > 1, self.c.Tr.DeleteLocalAndRemoteTagsPrompt, self.c.Tr.DeleteLocalAndRemoteTagPrompt)
return self.confirmRemoteDelete(tags, confirmPromptTemplate, func(task gocui.Task, upstream string) error {
self.c.LogAction(self.c.Tr.Actions.DeleteRemoteTag)
if err := self.c.Git().Remote.DeleteRemoteTag(task, upstream, tagNames(tags)); err != nil {
return err
}
self.c.LogAction(self.c.Tr.Actions.DeleteLocalTag)
if err := self.c.Git().Tag.LocalDelete(tagNames(tags)); err != nil {
return err
}
self.refreshAfterLocalDelete()
return nil
})
}
// Refreshes after deleting local tags. If the tags were a range selection,
// this also collapses it to its first line; otherwise it would select the tags
// that moved up into the place of the deleted ones. Collapsing it in the Then
// of a batched refresh draws the shorter list and the new selection in the
// same frame.
func (self *TagsController) refreshAfterLocalDelete() {
self.c.RefreshFromWorker(types.RefreshOptions{
Scope: []types.RefreshableView{types.COMMITS, types.TAGS},
BatchUIUpdates: true,
Then: func() error {
if self.context().IsSelectingRange() {
self.context().CollapseRangeSelectionToTop()
self.c.PostRefreshUpdate(self.context())
}
return nil
},
)
})
}
// Asks for the remote to delete the tags from and for a confirmation, and then
// runs deleteTags on a worker with the tags shown as being deleted.
// confirmPromptTemplate can use the placeholder upstream, and tagName if there
// is only one tag.
func (self *TagsController) confirmRemoteDelete(
tags []*models.Tag,
confirmPromptTemplate string,
deleteTags func(task gocui.Task, upstream string) error,
) error {
var title string
if len(tags) == 1 {
title = utils.ResolvePlaceholderString(
self.c.Tr.SelectRemoteTagUpstream,
map[string]string{
"tagName": tags[0].Name,
},
)
} else {
title = self.c.Tr.SelectRemoteTagsUpstream
}
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.DeleteRemoteTagPrompt,
confirmPromptTemplate,
map[string]string{
"tagName": tag.Name,
"tagName": tags[0].Name,
"upstream": upstream,
},
)
self.c.Confirm(types.ConfirmOpts{
Title: confirmTitle,
Title: self.deleteTagsTitle(tags),
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.Toast(self.c.Tr.RemoteTagDeletedMessage)
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS, types.TAGS}})
return nil
return helpers.WithInlineStatusOnItems(self.c.HelperCommon, tags, types.ItemOperationDeleting, context.TAGS_CONTEXT_KEY, func(task gocui.Task) error {
return deleteTags(task, upstream)
})
},
})
@@ -223,116 +274,73 @@ 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
}
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS, types.TAGS}})
return nil
})
},
})
return nil
},
})
return nil
}
func (self *TagsController) delete(tag *models.Tag) error {
menuTitle := utils.ResolvePlaceholderString(
self.c.Tr.DeleteTagTitle,
map[string]string{
"tagName": tag.Name,
},
)
func (self *TagsController) delete(tags []*models.Tag) error {
menuItems := []*types.MenuItem{
{
Label: self.c.Tr.DeleteLocalTag,
Label: lo.Ternary(len(tags) > 1, self.c.Tr.DeleteLocalTags, self.c.Tr.DeleteLocalTag),
Keys: menuKey('c'),
OnPress: func() error {
return self.localDelete(tag)
return self.localDelete(tags)
},
},
{
Label: self.c.Tr.DeleteRemoteTag,
Label: lo.Ternary(len(tags) > 1, self.c.Tr.DeleteRemoteTags, self.c.Tr.DeleteRemoteTag),
Keys: menuKey('r'),
OpensMenu: true,
OnPress: func() error {
return self.remoteDelete(tag)
return self.remoteDelete(tags)
},
},
{
Label: self.c.Tr.DeleteLocalAndRemoteTag,
Label: lo.Ternary(len(tags) > 1, self.c.Tr.DeleteLocalAndRemoteTags, self.c.Tr.DeleteLocalAndRemoteTag),
Keys: menuKey('b'),
OpensMenu: true,
OnPress: func() error {
return self.localAndRemoteDelete(tag)
return self.localAndRemoteDelete(tags)
},
},
}
return self.c.Menu(types.CreateMenuOptions{
Title: menuTitle,
Title: self.deleteTagsTitle(tags),
Items: menuItems,
})
}
func (self *TagsController) push(tag *models.Tag) error {
title := utils.ResolvePlaceholderString(
self.c.Tr.PushTagTitle,
func (self *TagsController) deleteTagsTitle(tags []*models.Tag) string {
if len(tags) > 1 {
return self.c.Tr.DeleteTagsTitle
}
return utils.ResolvePlaceholderString(
self.c.Tr.DeleteTagTitle,
map[string]string{
"tagName": tag.Name,
"tagName": tags[0].Name,
},
)
}
func (self *TagsController) push(tags []*models.Tag) error {
var title string
if len(tags) == 1 {
title = utils.ResolvePlaceholderString(
self.c.Tr.PushTagTitle,
map[string]string{
"tagName": tags[0].Name,
},
)
} else {
title = self.c.Tr.PushTagsTitle
}
self.c.Prompt(types.PromptOpts{
Title: title,
InitialContent: "origin",
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteSuggestionsFunc(),
HandleConfirm: func(response string) error {
return self.c.WithInlineStatus(tag, types.ItemOperationPushing, context.TAGS_CONTEXT_KEY, func(task gocui.Task) error {
return helpers.WithInlineStatusOnItems(self.c.HelperCommon, tags, types.ItemOperationPushing, context.TAGS_CONTEXT_KEY, func(task gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.PushTag)
return self.c.Git().Tag.Push(task, response, tag.Name)
return self.c.Git().Tag.Push(task, response, tagNames(tags))
})
},
})
@@ -354,3 +362,7 @@ func (self *TagsController) create() error {
func (self *TagsController) context() *context.TagsContext {
return self.c.Contexts().Tags
}
func tagNames(tags []*models.Tag) []string {
return lo.Map(tags, func(tag *models.Tag, _ int) string { return tag.Name })
}
+18
View File
@@ -609,14 +609,23 @@ type TranslationSet struct {
LightweightTag string
AnnotatedTag string
DeleteTagTitle string
DeleteTagsTitle string
DeleteLocalTag string
DeleteLocalTags string
DeleteRemoteTag string
DeleteRemoteTags string
DeleteLocalAndRemoteTag string
DeleteLocalAndRemoteTags string
SelectRemoteTagUpstream string
SelectRemoteTagsUpstream string
DeleteRemoteTagPrompt string
DeleteRemoteTagsPrompt string
DeleteLocalAndRemoteTagPrompt string
DeleteLocalAndRemoteTagsPrompt string
RemoteTagDeletedMessage string
RemoteTagsDeletedMessage string
PushTagTitle string
PushTagsTitle string
PushTag string
PushTagTooltip string
NewTag string
@@ -1789,14 +1798,23 @@ func EnglishTranslationSet() *TranslationSet {
AnnotatedTag: "Annotated tag",
LightweightTag: "Lightweight tag",
DeleteTagTitle: "Delete tag '{{.tagName}}'?",
DeleteTagsTitle: "Delete selected tags?",
DeleteLocalTag: "Delete local tag",
DeleteLocalTags: "Delete local tags",
DeleteRemoteTag: "Delete remote tag",
DeleteRemoteTags: "Delete remote tags",
DeleteLocalAndRemoteTag: "Delete local and remote tag",
DeleteLocalAndRemoteTags: "Delete local and remote tags",
RemoteTagDeletedMessage: "Remote tag deleted",
RemoteTagsDeletedMessage: "Remote tags deleted",
SelectRemoteTagUpstream: "Remote from which to remove tag '{{.tagName}}':",
SelectRemoteTagsUpstream: "Remote from which to remove the selected tags:",
DeleteRemoteTagPrompt: "Are you sure you want to delete the remote tag '{{.tagName}}' from '{{.upstream}}'?",
DeleteRemoteTagsPrompt: "Are you sure you want to delete the selected tags from '{{.upstream}}'?",
DeleteLocalAndRemoteTagPrompt: "Are you sure you want to delete '{{.tagName}}' from both your machine and from '{{.upstream}}'?",
DeleteLocalAndRemoteTagsPrompt: "Are you sure you want to delete the selected tags from both your machine and from '{{.upstream}}'?",
PushTagTitle: "Remote to push tag '{{.tagName}}' to:",
PushTagsTitle: "Remote to push the selected tags to:",
// Using 'push tag' rather than just 'push' to disambiguate from a global push
PushTag: "Push tag",
PushTagTooltip: "Push the selected tag to a remote. You'll be prompted to select a remote.",
+14
View File
@@ -6,6 +6,7 @@ import (
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/samber/lo"
)
type Git struct {
@@ -27,6 +28,19 @@ func (self *Git) RemoteTagDeleted(ref string, tagName string) *Git {
})
}
// Asserts the names of all tags on the remote, in alphabetical order
func (self *Git) RemoteTagNames(remote string, expectedNames []string) *Git {
return self.expect([]string{"git", "ls-remote", "--tags", "--refs", remote}, func(output string) (bool, string) {
names := lo.FilterMap(strings.Split(output, "\n"), func(line string, _ int) (string, bool) {
_, ref, found := strings.Cut(line, "\t")
return strings.TrimPrefix(ref, "refs/tags/"), found
})
actual := strings.Join(names, "\n")
expected := strings.Join(expectedNames, "\n")
return actual == expected, fmt.Sprintf("Expected the tags on %s to be\n%s\nbut got\n%s", remote, expected, actual)
})
}
func (self *Git) assert(cmdArgs []string, expected string) *Git {
self.expect(cmdArgs, func(output string) (bool, string) {
return output == expected, fmt.Sprintf("Expected current branch name to be '%s', but got '%s'", expected, output)
@@ -0,0 +1,133 @@
package tag
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var DeleteMultiple = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Delete a range selection of tags remotely, locally and remotely, and locally",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("initial commit")
shell.CloneIntoRemote("origin")
// The tags are sorted by date, newest first. Give each tag's commit a
// date of its own so that the order is fixed.
for i := 1; i <= 8; i++ {
shell.EmptyCommitWithDate(fmt.Sprintf("commit %02d", i), fmt.Sprintf("2024-01-%02d 10:00:00", i))
shell.CreateLightweightTag(fmt.Sprintf("tag-%02d", i), "HEAD")
}
for _, tag := range []string{"tag-03", "tag-04", "tag-05", "tag-06"} {
shell.PushBranch("origin", "refs/tags/"+tag) // abusing PushBranch to push a tag
}
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Tags().
Focus().
Lines(
Contains("tag-08").IsSelected(),
Contains("tag-07"),
Contains("tag-06"),
Contains("tag-05"),
Contains("tag-04"),
Contains("tag-03"),
Contains("tag-02"),
Contains("tag-01"),
).
NavigateToLine(Contains("tag-06")).
Press(keys.Universal.RangeSelectDown).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().
Menu().
Title(Equals("Delete selected tags?")).
Select(Contains("Delete remote tags")).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("Remote from which to remove the selected tags:")).
InitialText(Equals("origin")).
Confirm()
t.ExpectPopup().
Confirmation().
Title(Equals("Delete selected tags?")).
Content(Equals("Are you sure you want to delete the selected tags from 'origin'?")).
Confirm()
t.ExpectToast(Equals("Remote tags deleted"))
}).
// The local tags are still there, so the selection stays as it is
Lines(
Contains("tag-08"),
Contains("tag-07"),
Contains("tag-06").IsSelected(),
Contains("tag-05").IsSelected(),
Contains("tag-04"),
Contains("tag-03"),
Contains("tag-02"),
Contains("tag-01"),
).
Tap(func() {
t.Git().
RemoteTagDeleted("origin", "tag-06").
RemoteTagDeleted("origin", "tag-05")
}).
NavigateToLine(Contains("tag-04")).
Press(keys.Universal.RangeSelectDown).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().
Menu().
Title(Equals("Delete selected tags?")).
Select(Contains("Delete local and remote tags")).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("Remote from which to remove the selected tags:")).
InitialText(Equals("origin")).
Confirm()
t.ExpectPopup().
Confirmation().
Title(Equals("Delete selected tags?")).
Content(Equals("Are you sure you want to delete the selected tags from both your machine and from 'origin'?")).
Confirm()
}).
Lines(
Contains("tag-08"),
Contains("tag-07"),
Contains("tag-06"),
Contains("tag-05"),
Contains("tag-02").IsSelected(),
Contains("tag-01"),
).
Tap(func() {
t.Git().
RemoteTagDeleted("origin", "tag-04").
RemoteTagDeleted("origin", "tag-03")
}).
NavigateToLine(Contains("tag-07")).
Press(keys.Universal.RangeSelectDown).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().
Menu().
Title(Equals("Delete selected tags?")).
Select(Contains("Delete local tags")).
Confirm()
}).
Lines(
Contains("tag-08"),
Contains("tag-05").IsSelected(),
Contains("tag-02"),
Contains("tag-01"),
)
},
})
@@ -0,0 +1,53 @@
package tag
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var PushMultiple = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Push a range selection of tags",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("initial commit")
shell.CloneIntoRemote("origin")
// The tags are sorted by date, newest first. Give each tag's commit a
// date of its own so that the order is fixed.
for i := 1; i <= 4; i++ {
shell.EmptyCommitWithDate(fmt.Sprintf("commit %02d", i), fmt.Sprintf("2024-01-%02d 10:00:00", i))
shell.CreateLightweightTag(fmt.Sprintf("tag-%02d", i), "HEAD")
}
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Tags().
Focus().
Lines(
Contains("tag-04").IsSelected(),
Contains("tag-03"),
Contains("tag-02"),
Contains("tag-01"),
).
NavigateToLine(Contains("tag-03")).
Press(keys.Universal.RangeSelectDown).
Press(keys.Branches.PushTag).
Tap(func() {
t.ExpectPopup().Prompt().
Title(Equals("Remote to push the selected tags to:")).
InitialText(Equals("origin")).
Confirm()
}).
Lines(
Contains("tag-04"),
Contains("tag-03").IsSelected(),
Contains("tag-02").IsSelected(),
Contains("tag-01"),
)
t.Git().RemoteTagNames("origin", []string{"tag-02", "tag-03"})
},
})
+2
View File
@@ -533,9 +533,11 @@ var tests = []*components.IntegrationTest{
tag.CrudAnnotated,
tag.CrudLightweight,
tag.DeleteLocalAndRemote,
tag.DeleteMultiple,
tag.DeleteRemoteTagWhenBranchWithSameNameExists,
tag.ForceTagAnnotated,
tag.ForceTagLightweight,
tag.PushMultiple,
tag.Reset,
tag.ResetToDuplicateNamedBranch,
ui.Accordion,