From a91fe517d36d34aeef2e64b229e64919e2c63f16 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 12 Sep 2024 18:35:20 +0200 Subject: [PATCH 1/6] Remove obsolete TODO comment Looks perfectly internationalized to me. --- pkg/gui/controllers/branches_controller.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index 5a7561f32..0ed0456ae 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -532,7 +532,6 @@ func (self *BranchesController) promptWorktreeBranchDelete(selectedBranch *model return nil } - // TODO: i18n title := utils.ResolvePlaceholderString(self.c.Tr.BranchCheckedOutByWorktree, map[string]string{ "worktreeName": worktree.Name, "branchName": selectedBranch.Name, From d2b6f938587930d1eefe160736ccd5df784f5511 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 12 Sep 2024 19:02:38 +0200 Subject: [PATCH 2/6] Remove unused texts --- pkg/i18n/english.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 2e8efda34..b7668d4a8 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -472,7 +472,6 @@ type TranslationSet struct { RemoveRemoteTooltip string RemoveRemotePrompt string DeleteRemoteBranch string - DeleteRemoteBranchMessage string DeleteRemoteBranchTooltip string SetAsUpstream string SetAsUpstreamTooltip string @@ -849,7 +848,6 @@ type Actions struct { CheckoutBranch string ForceCheckoutBranch string DeleteLocalBranch string - DeleteBranch string Merge string SquashMerge string RebaseBranch string @@ -1463,7 +1461,6 @@ func EnglishTranslationSet() *TranslationSet { RemoveRemoteTooltip: `Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected.`, RemoveRemotePrompt: "Are you sure you want to remove remote", DeleteRemoteBranch: "Delete remote branch", - DeleteRemoteBranchMessage: "Are you sure you want to delete remote branch", DeleteRemoteBranchTooltip: "Delete the remote branch from the remote.", SetAsUpstream: "Set as upstream", SetAsUpstreamTooltip: "Set the selected remote branch as the upstream of the checked-out branch.", @@ -1797,7 +1794,6 @@ func EnglishTranslationSet() *TranslationSet { CheckoutBranch: "Checkout branch", ForceCheckoutBranch: "Force checkout branch", DeleteLocalBranch: "Delete local branch", - DeleteBranch: "Delete branch", Merge: "Merge", SquashMerge: "Squash merge", RebaseBranch: "Rebase branch", From 7e7309f97e22935aaf8d6c4eefd544312c311e0f Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 12 Sep 2024 18:59:40 +0200 Subject: [PATCH 3/6] Add question marks to questions --- pkg/i18n/english.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index b7668d4a8..09b4eda1c 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -1459,7 +1459,7 @@ func EnglishTranslationSet() *TranslationSet { EditRemoteUrl: `Enter updated remote url for {{.remoteName}}:`, RemoveRemote: `Remove remote`, RemoveRemoteTooltip: `Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected.`, - RemoveRemotePrompt: "Are you sure you want to remove remote", + RemoveRemotePrompt: "Are you sure you want to remove remote?", DeleteRemoteBranch: "Delete remote branch", DeleteRemoteBranchTooltip: "Delete the remote branch from the remote.", SetAsUpstream: "Set as upstream", @@ -1477,7 +1477,7 @@ func EnglishTranslationSet() *TranslationSet { ViewUpstreamRebaseOptionsTooltip: "View options for rebasing the checked-out branch onto {{upstream}}. Note: this will not rebase the selected branch onto the upstream, it will rebase the checked-out branch onto the upstream.", UpstreamGenericName: "upstream of selected branch", SetUpstreamTitle: "Set upstream branch", - SetUpstreamMessage: "Are you sure you want to set the upstream branch of '{{.checkedOut}}' to '{{.selected}}'", + SetUpstreamMessage: "Are you sure you want to set the upstream branch of '{{.checkedOut}}' to '{{.selected}}'?", EditRemoteTooltip: "Edit the selected remote's name or URL.", TagCommit: "Tag commit", TagCommitTooltip: "Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description.", From be3683ccc8ec3d12d4c7278966d31e0da87ce13a Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 15 Sep 2024 16:53:04 +0200 Subject: [PATCH 4/6] Add test that demonstrates bug with deleting remote branch with different name It's maybe not very common, but it's totally possible for a remote branch to have a different name than the local branch. This test shows that we don't support this properly when deleting the remote branch. --- ...elete_remote_branch_with_different_name.go | 56 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 2 files changed, 57 insertions(+) create mode 100644 pkg/integration/tests/branch/delete_remote_branch_with_different_name.go diff --git a/pkg/integration/tests/branch/delete_remote_branch_with_different_name.go b/pkg/integration/tests/branch/delete_remote_branch_with_different_name.go new file mode 100644 index 000000000..cd9183b2a --- /dev/null +++ b/pkg/integration/tests/branch/delete_remote_branch_with_different_name.go @@ -0,0 +1,56 @@ +package branch + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var DeleteRemoteBranchWithDifferentName = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Delete a remote branch that has a different name than the local branch", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + }, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("one") + shell.CloneIntoRemote("origin") + shell.NewBranch("mybranch-local") + shell.PushBranchAndSetUpstream("origin", "mybranch-local:mybranch-remote") + shell.Checkout("master") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Focus(). + Lines( + Contains("master").IsSelected(), + Contains("mybranch-local ✓"), + ). + SelectNextItem(). + Press(keys.Universal.Remove). + Tap(func() { + t.ExpectPopup(). + Menu(). + Title(Equals("Delete branch 'mybranch-local'?")). + Select(Contains("Delete remote branch")). + Confirm() + }). + Tap(func() { + t.ExpectPopup(). + Confirmation(). + /* EXPECTED: + Title(Equals("Delete branch 'mybranch-remote'?")). + Content(Equals("Are you sure you want to delete the remote branch 'mybranch-remote' from 'origin'?")). + ACTUAL: */ + Title(Equals("Delete branch 'mybranch-local'?")). + Content(Equals("Are you sure you want to delete the remote branch 'mybranch-local' from 'origin'?")). + Confirm() + }). + Lines( + Contains("master"), + /* EXPECTED: + Contains("mybranch-local (upstream gone)").IsSelected(), + ACTUAL: */ + Contains("mybranch-local ✓").IsSelected(), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 281b0a2b3..6c6d484cf 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -43,6 +43,7 @@ var tests = []*components.IntegrationTest{ branch.CreateTag, branch.Delete, branch.DeleteRemoteBranchWithCredentialPrompt, + branch.DeleteRemoteBranchWithDifferentName, branch.DetachedHead, branch.NewBranchAutostash, branch.NewBranchFromRemoteTrackingDifferentName, From c4e5995cb94070fd9a736ac9afb5ec7a6cf52c49 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 12 Sep 2024 19:16:40 +0200 Subject: [PATCH 5/6] Fix bug with deleting remote branch whose name doesn't match local branch --- pkg/gui/controllers/branches_controller.go | 2 +- .../branch/delete_remote_branch_with_different_name.go | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index 0ed0456ae..c7cba1302 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -581,7 +581,7 @@ func (self *BranchesController) localDelete(branch *models.Branch) error { } func (self *BranchesController) remoteDelete(branch *models.Branch) error { - return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(branch.UpstreamRemote, branch.Name) + return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(branch.UpstreamRemote, branch.UpstreamBranch) } func (self *BranchesController) forceDelete(branch *models.Branch) error { diff --git a/pkg/integration/tests/branch/delete_remote_branch_with_different_name.go b/pkg/integration/tests/branch/delete_remote_branch_with_different_name.go index cd9183b2a..91f4ede27 100644 --- a/pkg/integration/tests/branch/delete_remote_branch_with_different_name.go +++ b/pkg/integration/tests/branch/delete_remote_branch_with_different_name.go @@ -37,20 +37,13 @@ var DeleteRemoteBranchWithDifferentName = NewIntegrationTest(NewIntegrationTestA Tap(func() { t.ExpectPopup(). Confirmation(). - /* EXPECTED: Title(Equals("Delete branch 'mybranch-remote'?")). Content(Equals("Are you sure you want to delete the remote branch 'mybranch-remote' from 'origin'?")). - ACTUAL: */ - Title(Equals("Delete branch 'mybranch-local'?")). - Content(Equals("Are you sure you want to delete the remote branch 'mybranch-local' from 'origin'?")). Confirm() }). Lines( Contains("master"), - /* EXPECTED: Contains("mybranch-local (upstream gone)").IsSelected(), - ACTUAL: */ - Contains("mybranch-local ✓").IsSelected(), ) }, }) From c712b1d0fe0a45788bd32786ab3efa2a3dabc9a9 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 8 Sep 2024 16:05:47 +0200 Subject: [PATCH 6/6] Better local branch delete confirmation Currently we try to delete a branch normally, and if git returns an error and its output contains the text "branch -D", then we prompt the user to force delete, and try again using -D. Besides just being ugly, this has the disadvantage that git's logic to decide whether a branch is merged is not very good; it only considers a branch merged if it is either reachable from the current head, or from its own upstream. In many cases I want to delete a branch that has been merged to master, but I don't have master checked out, so the current branch is really irrelevant, and it should rather (or in addition) check whether the branch is reachable from one of the main branches. The problem is that git doesn't know what those are. But lazygit does, so make the check on our side, prompt the user if necessary, and always use -D. This is both cleaner, and works better. See this mailing list discussion for more: https://lore.kernel.org/git/bf6308ce-3914-4b85-a04b-4a9716bac538@haller-berlin.de/ --- pkg/commands/git_commands/branch.go | 24 ++++ pkg/gui/controllers.go | 2 +- pkg/gui/controllers/branches_controller.go | 81 +------------ .../controllers/helpers/branches_helper.go | 99 +++++++++++++++- pkg/integration/tests/branch/delete.go | 109 ++++++++++++++---- 5 files changed, 207 insertions(+), 108 deletions(-) diff --git a/pkg/commands/git_commands/branch.go b/pkg/commands/git_commands/branch.go index c63087a80..99229b12b 100644 --- a/pkg/commands/git_commands/branch.go +++ b/pkg/commands/git_commands/branch.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/mgutz/str" @@ -260,3 +261,26 @@ func (self *BranchCommands) AllBranchesLogCmdObj() oscommands.ICmdObj { return self.cmd.New(str.ToArgv(candidates[i])).DontLog() } + +func (self *BranchCommands) IsBranchMerged(branch *models.Branch, mainBranches *MainBranches) (bool, error) { + branchesToCheckAgainst := []string{"HEAD"} + if branch.RemoteBranchStoredLocally() { + branchesToCheckAgainst = append(branchesToCheckAgainst, fmt.Sprintf("%s@{upstream}", branch.Name)) + } + branchesToCheckAgainst = append(branchesToCheckAgainst, mainBranches.Get()...) + + cmdArgs := NewGitCmd("rev-list"). + Arg("--max-count=1"). + Arg(branch.Name). + Arg(lo.Map(branchesToCheckAgainst, func(branch string, _ int) string { + return fmt.Sprintf("^%s", branch) + })...). + ToArgv() + + stdout, _, err := self.cmd.New(cmdArgs).RunWithOutputs() + if err != nil { + return false, err + } + + return stdout == "", nil +} diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index c784b4a1d..5f38b0443 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -107,7 +107,7 @@ func (gui *Gui) resetHelpersAndControllers() { Files: helpers.NewFilesHelper(helperCommon), WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, refsHelper, commitsHelper, gpgHelper), Tags: helpers.NewTagsHelper(helperCommon, commitsHelper), - BranchesHelper: helpers.NewBranchesHelper(helperCommon), + BranchesHelper: helpers.NewBranchesHelper(helperCommon, worktreeHelper), GPG: helpers.NewGpgHelper(helperCommon), MergeAndRebase: rebaseHelper, MergeConflicts: mergeConflictsHelper, diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index c7cba1302..93b3b93e1 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -3,7 +3,6 @@ package controllers import ( "errors" "fmt" - "strings" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" @@ -521,92 +520,14 @@ func (self *BranchesController) createNewBranchWithName(newBranchName string) er return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, KeepBranchSelectionIndex: true}) } -func (self *BranchesController) checkedOutByOtherWorktree(branch *models.Branch) bool { - return git_commands.CheckedOutByOtherWorktree(branch, self.c.Model().Worktrees) -} - -func (self *BranchesController) 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.c.Helpers().Worktree.Switch(worktree, context.LOCAL_BRANCHES_CONTEXT_KEY) - }, - }, - { - Label: self.c.Tr.DetachWorktree, - Tooltip: self.c.Tr.DetachWorktreeTooltip, - OnPress: func() error { - return self.c.Helpers().Worktree.Detach(worktree) - }, - }, - { - Label: self.c.Tr.RemoveWorktree, - OnPress: func() error { - return self.c.Helpers().Worktree.Remove(worktree, false) - }, - }, - }, - }) -} - func (self *BranchesController) localDelete(branch *models.Branch) error { - if self.checkedOutByOtherWorktree(branch) { - return self.promptWorktreeBranchDelete(branch) - } - - return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(_ gocui.Task) error { - self.c.LogAction(self.c.Tr.Actions.DeleteLocalBranch) - err := self.c.Git().Branch.LocalDelete(branch.Name, false) - if err != nil && strings.Contains(err.Error(), "git branch -D ") { - return self.forceDelete(branch) - } - if err != nil { - return err - } - return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}}) - }) + return self.c.Helpers().BranchesHelper.ConfirmLocalDelete(branch) } func (self *BranchesController) remoteDelete(branch *models.Branch) error { return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(branch.UpstreamRemote, branch.UpstreamBranch) } -func (self *BranchesController) forceDelete(branch *models.Branch) error { - title := self.c.Tr.ForceDeleteBranchTitle - message := utils.ResolvePlaceholderString( - self.c.Tr.ForceDeleteBranchMessage, - map[string]string{ - "selectedBranchName": branch.Name, - }, - ) - - self.c.Confirm(types.ConfirmOpts{ - Title: title, - Prompt: message, - HandleConfirm: func() error { - if err := self.c.Git().Branch.LocalDelete(branch.Name, true); err != nil { - return err - } - return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}}) - }, - }) - - return nil -} - func (self *BranchesController) delete(branch *models.Branch) error { checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef() diff --git a/pkg/gui/controllers/helpers/branches_helper.go b/pkg/gui/controllers/helpers/branches_helper.go index 635b94f20..68976f7a3 100644 --- a/pkg/gui/controllers/helpers/branches_helper.go +++ b/pkg/gui/controllers/helpers/branches_helper.go @@ -4,20 +4,68 @@ import ( "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" ) type BranchesHelper struct { - c *HelperCommon + c *HelperCommon + worktreeHelper *WorktreeHelper } -func NewBranchesHelper(c *HelperCommon) *BranchesHelper { +func NewBranchesHelper(c *HelperCommon, worktreeHelper *WorktreeHelper) *BranchesHelper { return &BranchesHelper{ - c: c, + c: c, + worktreeHelper: worktreeHelper, } } +func (self *BranchesHelper) ConfirmLocalDelete(branch *models.Branch) error { + if self.checkedOutByOtherWorktree(branch) { + return self.promptWorktreeBranchDelete(branch) + } + + isMerged, err := self.c.Git().Branch.IsBranchMerged(branch, self.c.Model().MainBranches) + 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) + if err := self.c.Git().Branch.LocalDelete(branch.Name, true); err != nil { + return err + } + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}}) + }) + } + + if isMerged { + return doDelete() + } + + title := self.c.Tr.ForceDeleteBranchTitle + message := utils.ResolvePlaceholderString( + self.c.Tr.ForceDeleteBranchMessage, + map[string]string{ + "selectedBranchName": branch.Name, + }, + ) + + self.c.Confirm(types.ConfirmOpts{ + Title: title, + Prompt: message, + HandleConfirm: func() error { + return doDelete() + }, + }) + + return nil +} + func (self *BranchesHelper) ConfirmDeleteRemote(remoteName string, branchName string) error { title := utils.ResolvePlaceholderString( self.c.Tr.DeleteBranchTitle, @@ -52,3 +100,48 @@ func (self *BranchesHelper) ConfirmDeleteRemote(remoteName string, branchName st 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) + }, + }, + }, + }) +} diff --git a/pkg/integration/tests/branch/delete.go b/pkg/integration/tests/branch/delete.go index f81eb0609..aab872957 100644 --- a/pkg/integration/tests/branch/delete.go +++ b/pkg/integration/tests/branch/delete.go @@ -15,27 +15,43 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{ CloneIntoRemote("origin"). EmptyCommit("blah"). NewBranch("branch-one"). + EmptyCommit("on branch-one 01"). PushBranchAndSetUpstream("origin", "branch-one"). + EmptyCommit("on branch-one 02"). + Checkout("master"). + Merge("branch-one"). // branch-one is contained in master, so no delete confirmation NewBranch("branch-two"). - PushBranchAndSetUpstream("origin", "branch-two"). - EmptyCommit("deletion blocker"). - NewBranch("branch-three") + EmptyCommit("on branch-two 01"). + PushBranchAndSetUpstream("origin", "branch-two"). // branch-two is contained in its own upstream, so no delete confirmation either + NewBranchFrom("branch-three", "master"). + EmptyCommit("on branch-three 01"). + NewBranch("current-head"). // branch-three is contained in the current head, so no delete confirmation + EmptyCommit("on current-head"). + NewBranchFrom("branch-four", "master"). + EmptyCommit("on branch-four 01"). + PushBranchAndSetUpstream("origin", "branch-four"). + EmptyCommit("on branch-four 02"). // branch-four is not contained in any of these, so we get a delete confirmation + Checkout("current-head") }, Run: func(t *TestDriver, keys config.KeybindingConfig) { t.Views().Branches(). Focus(). Lines( - MatchesRegexp(`\*.*branch-three`).IsSelected(), - MatchesRegexp(`branch-two`), - MatchesRegexp(`branch-one`), - MatchesRegexp(`master`), + Contains("current-head").IsSelected(), + Contains("branch-four ↑1"), + Contains("branch-three"), + Contains("branch-two ✓"), + Contains("master"), + Contains("branch-one ↑1"), ). + + // Deleting the current branch is not possible Press(keys.Universal.Remove). Tap(func() { t.ExpectPopup(). Menu(). Tooltip(Contains("You cannot delete the checked out branch!")). - Title(Equals("Delete branch 'branch-three'?")). + Title(Equals("Delete branch 'current-head'?")). Select(Contains("Delete local branch")). Confirm(). Tap(func() { @@ -43,8 +59,51 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{ }). Cancel() }). + + // Delete branch-four. This is the only branch that is not fully merged, so we get + // a confirmation popup. SelectNextItem(). Press(keys.Universal.Remove). + Tap(func() { + t.ExpectPopup(). + Menu(). + Title(Equals("Delete branch 'branch-four'?")). + Select(Contains("Delete local branch")). + Confirm() + t.ExpectPopup(). + Confirmation(). + Title(Equals("Force delete branch")). + Content(Equals("'branch-four' is not fully merged. Are you sure you want to delete it?")). + Confirm() + }). + Lines( + Contains("current-head"), + Contains("branch-three").IsSelected(), + Contains("branch-two ✓"), + Contains("master"), + Contains("branch-one ↑1"), + ). + + // Delete branch-three. This branch is contained in the current head, so this just works + // without any confirmation. + Press(keys.Universal.Remove). + Tap(func() { + t.ExpectPopup(). + Menu(). + Title(Equals("Delete branch 'branch-three'?")). + Select(Contains("Delete local branch")). + Confirm() + }). + Lines( + Contains("current-head"), + Contains("branch-two ✓").IsSelected(), + Contains("master"), + Contains("branch-one ↑1"), + ). + + // Delete branch-two. This branch is contained in its own upstream, so this just works + // without any confirmation. + Press(keys.Universal.Remove). Tap(func() { t.ExpectPopup(). Menu(). @@ -52,18 +111,14 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{ Select(Contains("Delete local branch")). Confirm() }). - Tap(func() { - t.ExpectPopup(). - Confirmation(). - Title(Equals("Force delete branch")). - Content(Equals("'branch-two' is not fully merged. Are you sure you want to delete it?")). - Confirm() - }). Lines( - MatchesRegexp(`\*.*branch-three`), - MatchesRegexp(`branch-one`).IsSelected(), - MatchesRegexp(`master`), + Contains("current-head"), + Contains("master").IsSelected(), + Contains("branch-one ↑1"), ). + + // Delete remote branch of branch-one. We only get the normal remote branch confirmation for this one. + SelectNextItem(). Press(keys.Universal.Remove). Tap(func() { t.ExpectPopup(). @@ -87,7 +142,10 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{ t.Views(). RemoteBranches(). - Lines(Equals("branch-two")). + Lines( + Equals("branch-four"), + Equals("branch-two"), + ). Press(keys.Universal.Return) t.Views(). @@ -95,10 +153,13 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{ Focus() }). Lines( - MatchesRegexp(`\*.*branch-three`), - MatchesRegexp(`branch-one \(upstream gone\)`).IsSelected(), - MatchesRegexp(`master`), + Contains("current-head"), + Contains("master"), + Contains("branch-one (upstream gone)").IsSelected(), ). + + // Delete local branch of branch-one. Even though its upstream is gone, we don't get a confirmation + // because it is contained in master. Press(keys.Universal.Remove). Tap(func() { t.ExpectPopup(). @@ -108,8 +169,8 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{ Confirm() }). Lines( - MatchesRegexp(`\*.*branch-three`), - MatchesRegexp(`master`).IsSelected(), + Contains("current-head"), + Contains("master").IsSelected(), ) }, })