From 20600b9b5c9cf48187a3e09a59796231e3168894 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 6 Jul 2025 13:50:33 +0200 Subject: [PATCH 1/3] Add convenience function ConfirmIf It's a very common pattern in the code base to have some code that we want to run either directly, or with a confirmation, depending on some condition. In most cases this is solved by creating a local helper function that we call either directly or from within the HandleConfirm of the confirmation; provide a convenience helper that makes this easier. --- pkg/gui/popup/popup_handler.go | 14 ++++++++++++++ pkg/gui/types/common.go | 2 ++ 2 files changed, 16 insertions(+) diff --git a/pkg/gui/popup/popup_handler.go b/pkg/gui/popup/popup_handler.go index 7fe77a8da..d8ce654b4 100644 --- a/pkg/gui/popup/popup_handler.go +++ b/pkg/gui/popup/popup_handler.go @@ -115,6 +115,20 @@ func (self *PopupHandler) Confirm(opts types.ConfirmOpts) { }) } +func (self *PopupHandler) ConfirmIf(condition bool, opts types.ConfirmOpts) error { + if condition { + self.createPopupPanelFn(context.Background(), types.CreatePopupPanelOpts{ + Title: opts.Title, + Prompt: opts.Prompt, + HandleConfirm: opts.HandleConfirm, + HandleClose: opts.HandleClose, + }) + return nil + } + + return opts.HandleConfirm() +} + func (self *PopupHandler) Prompt(opts types.PromptOpts) { self.createPopupPanelFn(context.Background(), types.CreatePopupPanelOpts{ Title: opts.Title, diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index fb472ab24..bf7267a8c 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -126,6 +126,8 @@ type IPopupHandler interface { Alert(title string, message string) // Shows a popup asking the user for confirmation. Confirm(opts ConfirmOpts) + // Shows a popup asking the user for confirmation if condition is true; otherwise, the HandleConfirm function is called directly. + ConfirmIf(condition bool, opts ConfirmOpts) error // Shows a popup prompting the user for input. Prompt(opts PromptOpts) WithWaitingStatus(message string, f func(gocui.Task) error) error From f872912c07615b7a5a9f76ce177b629aba4d9301 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 6 Jul 2025 14:25:13 +0200 Subject: [PATCH 2/3] Use ConfirmIf where applicable --- pkg/gui/controllers/branches_controller.go | 8 +- .../controllers/commits_files_controller.go | 60 ++++++------- .../custom_patch_options_menu_action.go | 86 ++++++++----------- pkg/gui/controllers/files_controller.go | 14 ++- pkg/gui/controllers/helpers/commits_helper.go | 11 +-- pkg/gui/controllers/helpers/fixup_helper.go | 38 +++----- pkg/gui/controllers/helpers/tags_helper.go | 61 ++++++------- .../controllers/local_commits_controller.go | 34 +++----- pkg/gui/controllers/quit_actions.go | 9 +- pkg/gui/controllers/staging_controller.go | 13 +-- pkg/gui/controllers/stash_controller.go | 43 ++++------ 11 files changed, 143 insertions(+), 234 deletions(-) diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index 77e22f1c3..241070ef6 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -724,17 +724,11 @@ func (self *BranchesController) rename(branch *models.Branch) error { // I could do an explicit check here for whether the branch is tracking a remote branch // but if we've selected it we'll already know that via Pullables and Pullables. // Bit of a hack but I'm lazy. - if !branch.IsTrackingRemote() { - return promptForNewName() - } - - self.c.Confirm(types.ConfirmOpts{ + return self.c.ConfirmIf(branch.IsTrackingRemote(), types.ConfirmOpts{ Title: self.c.Tr.RenameBranch, Prompt: self.c.Tr.RenameBranchWarning, HandleConfirm: promptForNewName, }) - - return nil } func (self *BranchesController) newBranch(selectedBranch *models.Branch) error { diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index 1e26df210..42e515b08 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -426,20 +426,18 @@ func (self *CommitFilesController) toggleForPatch(selectedNodes []*filetree.Comm } from, to, reverse := self.currentFromToReverseForPatchBuilding() - if self.c.Git().Patch.PatchBuilder.Active() && self.c.Git().Patch.PatchBuilder.NewPatchRequired(from, to, reverse) { - self.c.Confirm(types.ConfirmOpts{ - Title: self.c.Tr.DiscardPatch, - Prompt: self.c.Tr.DiscardPatchConfirm, - HandleConfirm: func() error { + mustDiscardPatch := self.c.Git().Patch.PatchBuilder.Active() && self.c.Git().Patch.PatchBuilder.NewPatchRequired(from, to, reverse) + return self.c.ConfirmIf(mustDiscardPatch, types.ConfirmOpts{ + Title: self.c.Tr.DiscardPatch, + Prompt: self.c.Tr.DiscardPatchConfirm, + HandleConfirm: func() error { + if mustDiscardPatch { self.c.Git().Patch.PatchBuilder.Reset() - return toggle() - }, - }) + } - return nil - } - - return toggle() + return toggle() + }, + }) } func (self *CommitFilesController) toggleAllForPatch(_ *filetree.CommitFileNode) error { @@ -479,32 +477,26 @@ func (self *CommitFilesController) enterCommitFile(node *filetree.CommitFileNode keybindings.Label(self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView)) } - enterTheFile := func() error { - if !self.c.Git().Patch.PatchBuilder.Active() { - if err := self.startPatchBuilder(); err != nil { - return err - } - } - - self.c.Context().Push(self.c.Contexts().CustomPatchBuilder, opts) - return nil - } - from, to, reverse := self.currentFromToReverseForPatchBuilding() - if self.c.Git().Patch.PatchBuilder.Active() && self.c.Git().Patch.PatchBuilder.NewPatchRequired(from, to, reverse) { - self.c.Confirm(types.ConfirmOpts{ - Title: self.c.Tr.DiscardPatch, - Prompt: self.c.Tr.DiscardPatchConfirm, - HandleConfirm: func() error { + mustDiscardPatch := self.c.Git().Patch.PatchBuilder.Active() && self.c.Git().Patch.PatchBuilder.NewPatchRequired(from, to, reverse) + return self.c.ConfirmIf(mustDiscardPatch, types.ConfirmOpts{ + Title: self.c.Tr.DiscardPatch, + Prompt: self.c.Tr.DiscardPatchConfirm, + HandleConfirm: func() error { + if mustDiscardPatch { self.c.Git().Patch.PatchBuilder.Reset() - return enterTheFile() - }, - }) + } - return nil - } + if !self.c.Git().Patch.PatchBuilder.Active() { + if err := self.startPatchBuilder(); err != nil { + return err + } + } - return enterTheFile() + self.c.Context().Push(self.c.Contexts().CustomPatchBuilder, opts) + return nil + }, + }) } func (self *CommitFilesController) handleToggleCommitFileDirCollapsed(node *filetree.CommitFileNode) error { diff --git a/pkg/gui/controllers/custom_patch_options_menu_action.go b/pkg/gui/controllers/custom_patch_options_menu_action.go index ce6c2493f..d6a36c2e1 100644 --- a/pkg/gui/controllers/custom_patch_options_menu_action.go +++ b/pkg/gui/controllers/custom_patch_options_menu_action.go @@ -173,28 +173,19 @@ func (self *CustomPatchOptionsMenuAction) handleMovePatchIntoWorkingTree() error self.returnFocusFromPatchExplorerIfNecessary() - pull := func(stash bool) error { - return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error { - commitIndex := self.getPatchCommitIndex() - self.c.LogAction(self.c.Tr.Actions.MovePatchIntoIndex) - err := self.c.Git().Patch.MovePatchIntoIndex(self.c.Model().Commits, commitIndex, stash) - return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) - }) - } - - if self.c.Helpers().WorkingTree.IsWorkingTreeDirty() { - self.c.Confirm(types.ConfirmOpts{ - Title: self.c.Tr.MustStashTitle, - Prompt: self.c.Tr.MustStashWarning, - HandleConfirm: func() error { - return pull(true) - }, - }) - - return nil - } - - return pull(false) + mustStash := self.c.Helpers().WorkingTree.IsWorkingTreeDirty() + return self.c.ConfirmIf(mustStash, types.ConfirmOpts{ + Title: self.c.Tr.MustStashTitle, + Prompt: self.c.Tr.MustStashWarning, + HandleConfirm: func() error { + return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error { + commitIndex := self.getPatchCommitIndex() + self.c.LogAction(self.c.Tr.Actions.MovePatchIntoIndex) + err := self.c.Git().Patch.MovePatchIntoIndex(self.c.Model().Commits, commitIndex, mustStash) + return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) + }) + }, + }) } func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommit() error { @@ -272,40 +263,31 @@ func (self *CustomPatchOptionsMenuAction) handleApplyPatch(reverse bool) error { affectedUnstagedFiles := self.getAffectedUnstagedFiles() - apply := func() error { - action := self.c.Tr.Actions.ApplyPatch - if reverse { - action = "Apply patch in reverse" - } - self.c.LogAction(action) + mustStageFiles := len(affectedUnstagedFiles) > 0 + return self.c.ConfirmIf(mustStageFiles, types.ConfirmOpts{ + Title: self.c.Tr.MustStageFilesAffectedByPatchTitle, + Prompt: self.c.Tr.MustStageFilesAffectedByPatchWarning, + HandleConfirm: func() error { + action := self.c.Tr.Actions.ApplyPatch + if reverse { + action = "Apply patch in reverse" + } + self.c.LogAction(action) - if len(affectedUnstagedFiles) > 0 { - if err := self.c.Git().WorkingTree.StageFiles(affectedUnstagedFiles, nil); err != nil { + if mustStageFiles { + if err := self.c.Git().WorkingTree.StageFiles(affectedUnstagedFiles, nil); err != nil { + return err + } + } + + if err := self.c.Git().Patch.ApplyCustomPatch(reverse, true); err != nil { return err } - } - if err := self.c.Git().Patch.ApplyCustomPatch(reverse, true); err != nil { - return err - } - - self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) - return nil - } - - if len(affectedUnstagedFiles) > 0 { - self.c.Confirm(types.ConfirmOpts{ - Title: self.c.Tr.MustStageFilesAffectedByPatchTitle, - Prompt: self.c.Tr.MustStageFilesAffectedByPatchWarning, - HandleConfirm: func() error { - return apply() - }, - }) - - return nil - } - - return apply() + self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) + return nil + }, + }) } func (self *CustomPatchOptionsMenuAction) copyPatchToClipboard() error { diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index feca4b674..9f95c86f6 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -794,19 +794,17 @@ func (self *FilesController) handleAmendCommitPress() error { }, }, }) - } else if !self.c.UserConfig().Gui.SkipAmendWarning { - self.c.Confirm(types.ConfirmOpts{ + } + + return self.c.ConfirmIf(!self.c.UserConfig().Gui.SkipAmendWarning, + types.ConfirmOpts{ Title: self.c.Tr.AmendLastCommitTitle, Prompt: self.c.Tr.SureToAmend, HandleConfirm: func() error { return doAmend() }, - }) - - return nil - } - - return doAmend() + }, + ) } func (self *FilesController) isResolvingConflicts() bool { diff --git a/pkg/gui/controllers/helpers/commits_helper.go b/pkg/gui/controllers/helpers/commits_helper.go index 5404ac5c5..306c689a1 100644 --- a/pkg/gui/controllers/helpers/commits_helper.go +++ b/pkg/gui/controllers/helpers/commits_helper.go @@ -256,13 +256,8 @@ func (self *CommitsHelper) pasteCommitMessageFromClipboard() error { return nil } - if currentMessage := self.JoinCommitMessageAndUnwrappedDescription(); currentMessage == "" { - self.SetMessageAndDescriptionInView(message) - return nil - } - - // Confirm before overwriting the commit message - self.c.Confirm(types.ConfirmOpts{ + currentMessage := self.JoinCommitMessageAndUnwrappedDescription() + return self.c.ConfirmIf(currentMessage != "", types.ConfirmOpts{ Title: self.c.Tr.PasteCommitMessageFromClipboard, Prompt: self.c.Tr.SurePasteCommitMessage, HandleConfirm: func() error { @@ -270,6 +265,4 @@ func (self *CommitsHelper) pasteCommitMessageFromClipboard() error { return nil }, }) - - return nil } diff --git a/pkg/gui/controllers/helpers/fixup_helper.go b/pkg/gui/controllers/helpers/fixup_helper.go index 21496cf1c..37a0cf688 100644 --- a/pkg/gui/controllers/helpers/fixup_helper.go +++ b/pkg/gui/controllers/helpers/fixup_helper.go @@ -128,32 +128,22 @@ func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error { // and that's the one we want to select. _, index, _ := self.findCommit(commits, hashGroups[NOT_MERGED][0]) - doIt := func() error { - if !hasStagedChanges { - if err := self.c.Git().WorkingTree.StageAll(); err != nil { - return err + return self.c.ConfirmIf(warnAboutAddedLines, types.ConfirmOpts{ + Title: self.c.Tr.FindBaseCommitForFixup, + Prompt: self.c.Tr.HunksWithOnlyAddedLinesWarning, + HandleConfirm: func() error { + if !hasStagedChanges { + if err := self.c.Git().WorkingTree.StageAll(); err != nil { + return err + } + self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.FILES}}) } - self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.FILES}}) - } - self.c.Contexts().LocalCommits.SetSelection(index) - self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{}) - return nil - } - - if warnAboutAddedLines { - self.c.Confirm(types.ConfirmOpts{ - Title: self.c.Tr.FindBaseCommitForFixup, - Prompt: self.c.Tr.HunksWithOnlyAddedLinesWarning, - HandleConfirm: func() error { - return doIt() - }, - }) - - return nil - } - - return doIt() + self.c.Contexts().LocalCommits.SetSelection(index) + self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{}) + return nil + }, + }) } func (self *FixupHelper) getDiff() (string, bool, error) { diff --git a/pkg/gui/controllers/helpers/tags_helper.go b/pkg/gui/controllers/helpers/tags_helper.go index 022b7969b..6a7e47219 100644 --- a/pkg/gui/controllers/helpers/tags_helper.go +++ b/pkg/gui/controllers/helpers/tags_helper.go @@ -23,43 +23,34 @@ func NewTagsHelper(c *HelperCommon, commitsHelper *CommitsHelper, gpg *GpgHelper } func (self *TagsHelper) OpenCreateTagPrompt(ref string, onCreate func()) error { - doCreateTag := func(tagName string, description string, force bool) error { - var command *oscommands.CmdObj - if description != "" || self.c.Git().Config.GetGpgTagSign() { - self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag) - command = self.c.Git().Tag.CreateAnnotatedObj(tagName, ref, description, force) - } else { - self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag) - command = self.c.Git().Tag.CreateLightweightObj(tagName, ref, force) - } - - return self.gpg.WithGpgHandling(command, git_commands.TagGpgSign, self.c.Tr.CreatingTag, func() error { - return nil - }, []types.RefreshableView{types.COMMITS, types.TAGS}) - } - onConfirm := func(tagName string, description string) error { - if self.c.Git().Tag.HasTag(tagName) { - prompt := utils.ResolvePlaceholderString( - self.c.Tr.ForceTagPrompt, - map[string]string{ - "tagName": tagName, - "cancelKey": self.c.UserConfig().Keybinding.Universal.Return, - "confirmKey": self.c.UserConfig().Keybinding.Universal.Confirm, - }, - ) - self.c.Confirm(types.ConfirmOpts{ - Title: self.c.Tr.ForceTag, - Prompt: prompt, - HandleConfirm: func() error { - return doCreateTag(tagName, description, true) - }, - }) + prompt := utils.ResolvePlaceholderString( + self.c.Tr.ForceTagPrompt, + map[string]string{ + "tagName": tagName, + "cancelKey": self.c.UserConfig().Keybinding.Universal.Return, + "confirmKey": self.c.UserConfig().Keybinding.Universal.Confirm, + }, + ) + force := self.c.Git().Tag.HasTag(tagName) + return self.c.ConfirmIf(force, types.ConfirmOpts{ + Title: self.c.Tr.ForceTag, + Prompt: prompt, + HandleConfirm: func() error { + var command *oscommands.CmdObj + if description != "" || self.c.Git().Config.GetGpgTagSign() { + self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag) + command = self.c.Git().Tag.CreateAnnotatedObj(tagName, ref, description, force) + } else { + self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag) + command = self.c.Git().Tag.CreateLightweightObj(tagName, ref, force) + } - return nil - } - - return doCreateTag(tagName, description, false) + return self.gpg.WithGpgHandling(command, git_commands.TagGpgSign, self.c.Tr.CreatingTag, func() error { + return nil + }, []types.RefreshableView{types.COMMITS, types.TAGS}) + }, + }) } self.commitsHelper.OpenCommitMessagePanel( diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 2886c8cd0..0a0a96988 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -435,17 +435,12 @@ func (self *LocalCommitsController) doRewordEditor() error { } func (self *LocalCommitsController) rewordEditor(commit *models.Commit) error { - if self.c.UserConfig().Gui.SkipRewordInEditorWarning { - return self.doRewordEditor() - } - - self.c.Confirm(types.ConfirmOpts{ - Title: self.c.Tr.RewordInEditorTitle, - Prompt: self.c.Tr.RewordInEditorPrompt, - HandleConfirm: self.doRewordEditor, - }) - - return nil + return self.c.ConfirmIf(!self.c.UserConfig().Gui.SkipRewordInEditorWarning, + types.ConfirmOpts{ + Title: self.c.Tr.RewordInEditorTitle, + Prompt: self.c.Tr.RewordInEditorPrompt, + HandleConfirm: self.doRewordEditor, + }) } func (self *LocalCommitsController) drop(selectedCommits []*models.Commit, startIdx int, endIdx int) error { @@ -749,17 +744,12 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error { } } - if self.c.UserConfig().Gui.SkipAmendWarning { - return handleCommit() - } - - self.c.Confirm(types.ConfirmOpts{ - Title: self.c.Tr.AmendCommitTitle, - Prompt: self.c.Tr.AmendCommitPrompt, - HandleConfirm: handleCommit, - }) - - return nil + return self.c.ConfirmIf(!self.c.UserConfig().Gui.SkipAmendWarning, + types.ConfirmOpts{ + Title: self.c.Tr.AmendCommitTitle, + Prompt: self.c.Tr.AmendCommitPrompt, + HandleConfirm: handleCommit, + }) } func (self *LocalCommitsController) canAmendRange(commits []*models.Commit, start, end int) *types.DisabledReason { diff --git a/pkg/gui/controllers/quit_actions.go b/pkg/gui/controllers/quit_actions.go index 8d5b0c9ca..2775a92a6 100644 --- a/pkg/gui/controllers/quit_actions.go +++ b/pkg/gui/controllers/quit_actions.go @@ -25,19 +25,14 @@ func (self *QuitActions) quitAux() error { return self.confirmQuitDuringUpdate() } - if self.c.UserConfig().ConfirmOnQuit { - self.c.Confirm(types.ConfirmOpts{ + return self.c.ConfirmIf(self.c.UserConfig().ConfirmOnQuit, + types.ConfirmOpts{ Title: "", Prompt: self.c.Tr.ConfirmQuit, HandleConfirm: func() error { return gocui.ErrQuit }, }) - - return nil - } - - return gocui.ErrQuit } func (self *QuitActions) confirmQuitDuringUpdate() error { diff --git a/pkg/gui/controllers/staging_controller.go b/pkg/gui/controllers/staging_controller.go index 44bcffb19..d7198bdcb 100644 --- a/pkg/gui/controllers/staging_controller.go +++ b/pkg/gui/controllers/staging_controller.go @@ -201,19 +201,12 @@ func (self *StagingController) DiscardSelection() error { keybindings.Label(self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView)) } - reset := func() error { return self.applySelectionAndRefresh(true) } - - if !self.staged && !self.c.UserConfig().Gui.SkipDiscardChangeWarning { - self.c.Confirm(types.ConfirmOpts{ + return self.c.ConfirmIf(!self.staged && !self.c.UserConfig().Gui.SkipDiscardChangeWarning, + types.ConfirmOpts{ Title: self.c.Tr.DiscardChangeTitle, Prompt: self.c.Tr.DiscardChangePrompt, - HandleConfirm: reset, + HandleConfirm: func() error { return self.applySelectionAndRefresh(true) }, }) - - return nil - } - - return reset() } func (self *StagingController) applySelectionAndRefresh(reverse bool) error { diff --git a/pkg/gui/controllers/stash_controller.go b/pkg/gui/controllers/stash_controller.go index cd9a613ff..c9a938bb5 100644 --- a/pkg/gui/controllers/stash_controller.go +++ b/pkg/gui/controllers/stash_controller.go @@ -107,32 +107,23 @@ func (self *StashController) context() *context.StashContext { } func (self *StashController) handleStashApply(stashEntry *models.StashEntry) error { - apply := func() error { - self.c.LogAction(self.c.Tr.Actions.Stash) - err := self.c.Git().Stash.Apply(stashEntry.Index) - self.postStashRefresh() - if err != nil { - return err - } - if self.c.UserConfig().Gui.SwitchToFilesAfterStashApply { - self.c.Context().Push(self.c.Contexts().Files, types.OnFocusOpts{}) - } - return nil - } - - if self.c.UserConfig().Gui.SkipStashWarning { - return apply() - } - - self.c.Confirm(types.ConfirmOpts{ - Title: self.c.Tr.StashApply, - Prompt: self.c.Tr.SureApplyStashEntry, - HandleConfirm: func() error { - return apply() - }, - }) - - return nil + return self.c.ConfirmIf(!self.c.UserConfig().Gui.SkipStashWarning, + types.ConfirmOpts{ + Title: self.c.Tr.StashApply, + Prompt: self.c.Tr.SureApplyStashEntry, + HandleConfirm: func() error { + self.c.LogAction(self.c.Tr.Actions.Stash) + err := self.c.Git().Stash.Apply(stashEntry.Index) + self.postStashRefresh() + if err != nil { + return err + } + if self.c.UserConfig().Gui.SwitchToFilesAfterStashApply { + self.c.Context().Push(self.c.Contexts().Files, types.OnFocusOpts{}) + } + return nil + }, + }) } func (self *StashController) handleStashPop(stashEntry *models.StashEntry) error { From 7153305174bb48b05a970fe49619cf67a14b3589 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 6 Jul 2025 14:24:50 +0200 Subject: [PATCH 3/3] Add confirmation for hard reset when there are uncommitted changes --- pkg/gui/controllers/helpers/refs_helper.go | 11 +++++++-- .../controllers/workspace_reset_controller.go | 24 ++++++++++++------- pkg/i18n/english.go | 2 ++ .../tests/branch/reset_to_upstream.go | 5 ++++ 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/pkg/gui/controllers/helpers/refs_helper.go b/pkg/gui/controllers/helpers/refs_helper.go index 993ffb423..68bcbbc33 100644 --- a/pkg/gui/controllers/helpers/refs_helper.go +++ b/pkg/gui/controllers/helpers/refs_helper.go @@ -248,8 +248,15 @@ func (self *RefsHelper) CreateGitResetMenu(name string, ref string) error { style.FgRed.Sprintf("reset --%s %s", row.strength, name), }, OnPress: func() error { - self.c.LogAction("Reset") - return self.ResetToRef(ref, row.strength, []string{}) + return self.c.ConfirmIf(row.strength == "hard" && IsWorkingTreeDirty(self.c.Model().Files), + types.ConfirmOpts{ + Title: self.c.Tr.Actions.HardReset, + Prompt: self.c.Tr.ResetHardConfirmation, + HandleConfirm: func() error { + self.c.LogAction("Reset") + return self.ResetToRef(ref, row.strength, []string{}) + }, + }) }, Key: row.key, Tooltip: row.tooltip, diff --git a/pkg/gui/controllers/workspace_reset_controller.go b/pkg/gui/controllers/workspace_reset_controller.go index 0c74e3ac7..9865e7796 100644 --- a/pkg/gui/controllers/workspace_reset_controller.go +++ b/pkg/gui/controllers/workspace_reset_controller.go @@ -9,6 +9,7 @@ import ( "time" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -150,15 +151,22 @@ func (self *FilesController) createResetMenu() error { red.Sprint("git reset --hard HEAD"), }, OnPress: func() error { - self.c.LogAction(self.c.Tr.Actions.HardReset) - if err := self.c.Git().WorkingTree.ResetHard("HEAD"); err != nil { - return err - } + return self.c.ConfirmIf(helpers.IsWorkingTreeDirty(self.c.Model().Files), + types.ConfirmOpts{ + Title: self.c.Tr.Actions.HardReset, + Prompt: self.c.Tr.ResetHardConfirmation, + HandleConfirm: func() error { + self.c.LogAction(self.c.Tr.Actions.HardReset) + if err := self.c.Git().WorkingTree.ResetHard("HEAD"); err != nil { + return err + } - self.c.Refresh( - types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}, - ) - return nil + self.c.Refresh( + types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}, + ) + return nil + }, + }) }, Key: 'h', }, diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 2bf237193..5a725a677 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -479,6 +479,7 @@ type TranslationSet struct { ResetSoftTooltip string ResetMixedTooltip string ResetHardTooltip string + ResetHardConfirmation string PressEnterToReturn string ViewStashOptions string ViewStashOptionsTooltip string @@ -1502,6 +1503,7 @@ func EnglishTranslationSet() *TranslationSet { ResetSoftTooltip: "Reset HEAD to the chosen commit, and keep the changes between the current and chosen commit as staged changes.", ResetMixedTooltip: "Reset HEAD to the chosen commit, and keep the changes between the current and chosen commit as unstaged changes.", ResetHardTooltip: "Reset HEAD to the chosen commit, and discard all changes between the current and chosen commit, as well as all current modifications in the working tree.", + ResetHardConfirmation: "Are you sure you want to do a hard reset? This will discard all uncommitted changes (both staged and unstaged), which is not undoable.", ViewResetOptions: `Reset`, FileResetOptionsTooltip: "View reset options for working tree (e.g. nuking the working tree).", FixupTooltip: "Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded.", diff --git a/pkg/integration/tests/branch/reset_to_upstream.go b/pkg/integration/tests/branch/reset_to_upstream.go index 18f04257e..e5503f127 100644 --- a/pkg/integration/tests/branch/reset_to_upstream.go +++ b/pkg/integration/tests/branch/reset_to_upstream.go @@ -97,6 +97,11 @@ var ResetToUpstream = NewIntegrationTest(NewIntegrationTestArgs{ Title(Equals("Reset to origin/hard-branch")). Select(Contains("Hard reset")). Confirm() + + t.ExpectPopup().Confirmation(). + Title(Equals("Hard reset")). + Content(Contains("Are you sure you want to do a hard reset?")). + Confirm() }) t.Views().Commits().Lines(Contains("hard commit")) t.Views().Files().IsEmpty()