mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-03 00:57:52 +02:00
Refresh is one of those functions that shouldn't require error handling (similar to triggering a redraw of the UI, see https://github.com/jesseduffield/lazygit/issues/3887). As far as I see, the only reason why Refresh can currently return an error is that the Then function returns one. The actual refresh errors, e.g. from the git calls that are made to fetch data, are already logged and swallowed. Most of the Then functions do only UI stuff such as selecting a list item, and always return nil; there's only one that can return an error (updating the rebase todo file in LocalCommitsController.startInteractiveRebaseWithEdit); it's not a critical error if this fails, it is only used for setting rebase todo items to "edit" when you start an interactive rebase by pressing 'e' on a range selection of commits. We simply log this error instead of returning it.
305 lines
9.1 KiB
Go
305 lines
9.1 KiB
Go
package helpers
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type BranchesHelper struct {
|
|
c *HelperCommon
|
|
worktreeHelper *WorktreeHelper
|
|
}
|
|
|
|
func NewBranchesHelper(c *HelperCommon, worktreeHelper *WorktreeHelper) *BranchesHelper {
|
|
return &BranchesHelper{
|
|
c: c,
|
|
worktreeHelper: worktreeHelper,
|
|
}
|
|
}
|
|
|
|
func (self *BranchesHelper) ConfirmLocalDelete(branches []*models.Branch) error {
|
|
if len(branches) > 1 {
|
|
if lo.SomeBy(branches, func(branch *models.Branch) bool { return self.checkedOutByOtherWorktree(branch) }) {
|
|
return errors.New(self.c.Tr.SomeBranchesCheckedOutByWorktreeError)
|
|
}
|
|
} else if self.checkedOutByOtherWorktree(branches[0]) {
|
|
return self.promptWorktreeBranchDelete(branches[0])
|
|
}
|
|
|
|
allBranchesMerged, err := self.allBranchesMerged(branches)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
doDelete := func() error {
|
|
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(_ gocui.Task) error {
|
|
self.c.LogAction(self.c.Tr.Actions.DeleteLocalBranch)
|
|
branchNames := lo.Map(branches, func(branch *models.Branch, _ int) string { return branch.Name })
|
|
if err := self.c.Git().Branch.LocalDelete(branchNames, true); err != nil {
|
|
return err
|
|
}
|
|
selectionStart, _ := self.c.Contexts().Branches.GetSelectionRange()
|
|
self.c.Contexts().Branches.SetSelectedLineIdx(selectionStart)
|
|
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}})
|
|
return nil
|
|
})
|
|
}
|
|
|
|
if allBranchesMerged {
|
|
return doDelete()
|
|
}
|
|
|
|
title := self.c.Tr.ForceDeleteBranchTitle
|
|
var message string
|
|
if len(branches) == 1 {
|
|
message = utils.ResolvePlaceholderString(
|
|
self.c.Tr.ForceDeleteBranchMessage,
|
|
map[string]string{
|
|
"selectedBranchName": branches[0].Name,
|
|
},
|
|
)
|
|
} else {
|
|
message = self.c.Tr.ForceDeleteBranchesMessage
|
|
}
|
|
|
|
self.c.Confirm(types.ConfirmOpts{
|
|
Title: title,
|
|
Prompt: message,
|
|
HandleConfirm: func() error {
|
|
return doDelete()
|
|
},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *BranchesHelper) ConfirmDeleteRemote(remoteBranches []*models.RemoteBranch) error {
|
|
var title string
|
|
if len(remoteBranches) == 1 {
|
|
title = utils.ResolvePlaceholderString(
|
|
self.c.Tr.DeleteBranchTitle,
|
|
map[string]string{
|
|
"selectedBranchName": remoteBranches[0].Name,
|
|
},
|
|
)
|
|
} else {
|
|
title = self.c.Tr.DeleteBranchesTitle
|
|
}
|
|
var prompt string
|
|
if len(remoteBranches) == 1 {
|
|
prompt = utils.ResolvePlaceholderString(
|
|
self.c.Tr.DeleteRemoteBranchPrompt,
|
|
map[string]string{
|
|
"selectedBranchName": remoteBranches[0].Name,
|
|
"upstream": remoteBranches[0].RemoteName,
|
|
},
|
|
)
|
|
} else {
|
|
prompt = self.c.Tr.DeleteRemoteBranchesPrompt
|
|
}
|
|
self.c.Confirm(types.ConfirmOpts{
|
|
Title: title,
|
|
Prompt: prompt,
|
|
HandleConfirm: func() error {
|
|
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(task gocui.Task) error {
|
|
if err := self.deleteRemoteBranches(remoteBranches, task); err != nil {
|
|
return err
|
|
}
|
|
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
|
|
return nil
|
|
})
|
|
},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *BranchesHelper) ConfirmLocalAndRemoteDelete(branches []*models.Branch) error {
|
|
if lo.SomeBy(branches, func(branch *models.Branch) bool { return self.checkedOutByOtherWorktree(branch) }) {
|
|
return errors.New(self.c.Tr.SomeBranchesCheckedOutByWorktreeError)
|
|
}
|
|
|
|
allBranchesMerged, err := self.allBranchesMerged(branches)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var prompt string
|
|
if len(branches) == 1 {
|
|
prompt = utils.ResolvePlaceholderString(
|
|
self.c.Tr.DeleteLocalAndRemoteBranchPrompt,
|
|
map[string]string{
|
|
"localBranchName": branches[0].Name,
|
|
"remoteBranchName": branches[0].UpstreamBranch,
|
|
"remoteName": branches[0].UpstreamRemote,
|
|
},
|
|
)
|
|
} else {
|
|
prompt = self.c.Tr.DeleteLocalAndRemoteBranchesPrompt
|
|
}
|
|
|
|
if !allBranchesMerged {
|
|
if len(branches) == 1 {
|
|
prompt += "\n\n" + utils.ResolvePlaceholderString(
|
|
self.c.Tr.ForceDeleteBranchMessage,
|
|
map[string]string{
|
|
"selectedBranchName": branches[0].Name,
|
|
},
|
|
)
|
|
} else {
|
|
prompt += "\n\n" + self.c.Tr.ForceDeleteBranchesMessage
|
|
}
|
|
}
|
|
|
|
self.c.Confirm(types.ConfirmOpts{
|
|
Title: self.c.Tr.DeleteLocalAndRemoteBranch,
|
|
Prompt: prompt,
|
|
HandleConfirm: func() error {
|
|
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(task gocui.Task) error {
|
|
// Delete the remote branches first so that we keep the local ones
|
|
// in case of failure
|
|
remoteBranches := lo.Map(branches, func(branch *models.Branch, _ int) *models.RemoteBranch {
|
|
return &models.RemoteBranch{Name: branch.UpstreamBranch, RemoteName: branch.UpstreamRemote}
|
|
})
|
|
if err := self.deleteRemoteBranches(remoteBranches, task); err != nil {
|
|
return err
|
|
}
|
|
|
|
self.c.LogAction(self.c.Tr.Actions.DeleteLocalBranch)
|
|
branchNames := lo.Map(branches, func(branch *models.Branch, _ int) string { return branch.Name })
|
|
if err := self.c.Git().Branch.LocalDelete(branchNames, true); err != nil {
|
|
return err
|
|
}
|
|
|
|
selectionStart, _ := self.c.Contexts().Branches.GetSelectionRange()
|
|
self.c.Contexts().Branches.SetSelectedLineIdx(selectionStart)
|
|
|
|
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
|
|
return nil
|
|
})
|
|
},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func ShortBranchName(fullBranchName string) string {
|
|
return strings.TrimPrefix(strings.TrimPrefix(fullBranchName, "refs/heads/"), "refs/remotes/")
|
|
}
|
|
|
|
func (self *BranchesHelper) checkedOutByOtherWorktree(branch *models.Branch) bool {
|
|
return git_commands.CheckedOutByOtherWorktree(branch, self.c.Model().Worktrees)
|
|
}
|
|
|
|
func (self *BranchesHelper) worktreeForBranch(branch *models.Branch) (*models.Worktree, bool) {
|
|
return git_commands.WorktreeForBranch(branch, self.c.Model().Worktrees)
|
|
}
|
|
|
|
func (self *BranchesHelper) promptWorktreeBranchDelete(selectedBranch *models.Branch) error {
|
|
worktree, ok := self.worktreeForBranch(selectedBranch)
|
|
if !ok {
|
|
self.c.Log.Error("promptWorktreeBranchDelete out of sync with list of worktrees")
|
|
return nil
|
|
}
|
|
|
|
title := utils.ResolvePlaceholderString(self.c.Tr.BranchCheckedOutByWorktree, map[string]string{
|
|
"worktreeName": worktree.Name,
|
|
"branchName": selectedBranch.Name,
|
|
})
|
|
return self.c.Menu(types.CreateMenuOptions{
|
|
Title: title,
|
|
Items: []*types.MenuItem{
|
|
{
|
|
Label: self.c.Tr.SwitchToWorktree,
|
|
OnPress: func() error {
|
|
return self.worktreeHelper.Switch(worktree, context.LOCAL_BRANCHES_CONTEXT_KEY)
|
|
},
|
|
},
|
|
{
|
|
Label: self.c.Tr.DetachWorktree,
|
|
Tooltip: self.c.Tr.DetachWorktreeTooltip,
|
|
OnPress: func() error {
|
|
return self.worktreeHelper.Detach(worktree)
|
|
},
|
|
},
|
|
{
|
|
Label: self.c.Tr.RemoveWorktree,
|
|
OnPress: func() error {
|
|
return self.worktreeHelper.Remove(worktree, false)
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func (self *BranchesHelper) allBranchesMerged(branches []*models.Branch) (bool, error) {
|
|
allBranchesMerged := true
|
|
for _, branch := range branches {
|
|
isMerged, err := self.c.Git().Branch.IsBranchMerged(branch, self.c.Model().MainBranches)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !isMerged {
|
|
allBranchesMerged = false
|
|
break
|
|
}
|
|
}
|
|
return allBranchesMerged, nil
|
|
}
|
|
|
|
func (self *BranchesHelper) deleteRemoteBranches(remoteBranches []*models.RemoteBranch, task gocui.Task) error {
|
|
remotes := lo.GroupBy(remoteBranches, func(branch *models.RemoteBranch) string { return branch.RemoteName })
|
|
for remote, branches := range remotes {
|
|
self.c.LogAction(self.c.Tr.Actions.DeleteRemoteBranch)
|
|
branchNames := lo.Map(branches, func(branch *models.RemoteBranch, _ int) string { return branch.Name })
|
|
if err := self.c.Git().Remote.DeleteRemoteBranch(task, remote, branchNames); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *BranchesHelper) AutoForwardBranches() error {
|
|
if self.c.UserConfig().Git.AutoForwardBranches == "none" {
|
|
return nil
|
|
}
|
|
|
|
branches := self.c.Model().Branches
|
|
if len(branches) == 0 {
|
|
return nil
|
|
}
|
|
|
|
allBranches := self.c.UserConfig().Git.AutoForwardBranches == "allBranches"
|
|
updateCommands := ""
|
|
// The first branch is the currently checked out branch; skip it
|
|
for _, branch := range branches[1:] {
|
|
if branch.RemoteBranchStoredLocally() && (allBranches || lo.Contains(self.c.UserConfig().Git.MainBranches, branch.Name)) {
|
|
isStrictlyBehind := branch.IsBehindForPull() && !branch.IsAheadForPull()
|
|
if isStrictlyBehind {
|
|
updateCommands += fmt.Sprintf("update %s %s %s\n", branch.FullRefName(), branch.FullUpstreamRefName(), branch.CommitHash)
|
|
}
|
|
}
|
|
}
|
|
|
|
if updateCommands == "" {
|
|
return nil
|
|
}
|
|
|
|
self.c.LogAction(self.c.Tr.Actions.AutoForwardBranches)
|
|
self.c.LogCommand(strings.TrimRight(updateCommands, "\n"), false)
|
|
err := self.c.Git().Branch.UpdateBranchRefs(updateCommands)
|
|
|
|
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES}, Mode: types.SYNC})
|
|
|
|
return err
|
|
}
|