mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-28 02:07:09 -04:00
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:
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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"})
|
||||
},
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user