From 9d976bcb4e43f18255c7818d7303a96a84f3afa8 Mon Sep 17 00:00:00 2001 From: TK88101 Date: Fri, 3 Apr 2026 00:43:58 +0900 Subject: [PATCH] Add confirmation dialogs for destructive workspace reset actions The workspace reset menu had several destructive actions that executed immediately without confirmation, risking accidental data loss. Add confirmation prompts before: discard unstaged changes (git checkout -- .), discard untracked files (git clean -fd), and discard staged changes (stash and drop). Update the existing integration test to expect the new confirmation dialog. Closes #874 --- .../controllers/workspace_reset_controller.go | 69 ++++++++++++------- pkg/i18n/english.go | 6 ++ .../tests/file/discard_staged_changes.go | 5 ++ 3 files changed, 56 insertions(+), 24 deletions(-) diff --git a/pkg/gui/controllers/workspace_reset_controller.go b/pkg/gui/controllers/workspace_reset_controller.go index 82357922f4a..c66f08b262b 100644 --- a/pkg/gui/controllers/workspace_reset_controller.go +++ b/pkg/gui/controllers/workspace_reset_controller.go @@ -62,14 +62,21 @@ func (self *FilesController) createResetMenu() error { red.Sprint("git checkout -- ."), }, OnPress: func() error { - self.c.LogAction(self.c.Tr.Actions.DiscardUnstagedFileChanges) - if err := self.c.Git().WorkingTree.DiscardAnyUnstagedFileChanges(); err != nil { - return err - } + self.c.Confirm(types.ConfirmOpts{ + Title: self.c.Tr.DiscardAnyUnstagedChanges, + Prompt: self.c.Tr.DiscardAnyUnstagedChangesConfirmation, + HandleConfirm: func() error { + self.c.LogAction(self.c.Tr.Actions.DiscardUnstagedFileChanges) + if err := self.c.Git().WorkingTree.DiscardAnyUnstagedFileChanges(); err != nil { + return err + } - self.c.Refresh( - types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}, - ) + self.c.Refresh( + types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}, + ) + return nil + }, + }) return nil }, Key: 'u', @@ -80,14 +87,21 @@ func (self *FilesController) createResetMenu() error { red.Sprint("git clean -fd"), }, OnPress: func() error { - self.c.LogAction(self.c.Tr.Actions.RemoveUntrackedFiles) - if err := self.c.Git().WorkingTree.RemoveUntrackedFiles(); err != nil { - return err - } + self.c.Confirm(types.ConfirmOpts{ + Title: self.c.Tr.DiscardUntrackedFiles, + Prompt: self.c.Tr.DiscardUntrackedFilesConfirmation, + HandleConfirm: func() error { + self.c.LogAction(self.c.Tr.Actions.RemoveUntrackedFiles) + if err := self.c.Git().WorkingTree.RemoveUntrackedFiles(); err != nil { + return err + } - self.c.Refresh( - types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}, - ) + self.c.Refresh( + types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}, + ) + return nil + }, + }) return nil }, Key: 'c', @@ -99,20 +113,27 @@ func (self *FilesController) createResetMenu() error { }, Tooltip: self.c.Tr.DiscardStagedChangesDescription, OnPress: func() error { - self.c.LogAction(self.c.Tr.Actions.RemoveStagedFiles) if !self.c.Helpers().WorkingTree.IsWorkingTreeDirtyExceptSubmodules() { return errors.New(self.c.Tr.NoTrackedStagedFilesStash) } - if err := self.c.Git().Stash.SaveStagedChanges("[lazygit] tmp stash"); err != nil { - return err - } - if err := self.c.Git().Stash.DropNewest(); err != nil { - return err - } + self.c.Confirm(types.ConfirmOpts{ + Title: self.c.Tr.DiscardStagedChanges, + Prompt: self.c.Tr.DiscardStagedChangesConfirmation, + HandleConfirm: func() error { + self.c.LogAction(self.c.Tr.Actions.RemoveStagedFiles) + if err := self.c.Git().Stash.SaveStagedChanges("[lazygit] tmp stash"); err != nil { + return err + } + if err := self.c.Git().Stash.DropNewest(); err != nil { + return err + } - self.c.Refresh( - types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}, - ) + self.c.Refresh( + types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}, + ) + return nil + }, + }) return nil }, Key: 'S', diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 079f6c991fa..bc7384ec7a3 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -825,6 +825,9 @@ type TranslationSet struct { NukeDescription string NukeTreeConfirmation string DiscardStagedChangesDescription string + DiscardAnyUnstagedChangesConfirmation string + DiscardUntrackedFilesConfirmation string + DiscardStagedChangesConfirmation string EmptyOutput string Patch string CustomPatch string @@ -1944,6 +1947,9 @@ func EnglishTranslationSet() *TranslationSet { NukeDescription: "If you want to make all the changes in the worktree go away, this is the way to do it. If there are dirty submodule changes this will stash those changes in the submodule(s).", NukeTreeConfirmation: "Are you sure you want to nuke the working tree? This will discard all changes in the worktree (staged, unstaged and untracked), which is not undoable.", DiscardStagedChangesDescription: "This will create a new stash entry containing only staged files and then drop it, so that the working tree is left with only unstaged changes", + DiscardAnyUnstagedChangesConfirmation: "Are you sure you want to discard all unstaged changes? This is not undoable.", + DiscardUntrackedFilesConfirmation: "Are you sure you want to discard all untracked files? This is not undoable.", + DiscardStagedChangesConfirmation: "Are you sure you want to discard all staged changes? This is not undoable.", EmptyOutput: "", Patch: "Patch", CustomPatch: "Custom patch", diff --git a/pkg/integration/tests/file/discard_staged_changes.go b/pkg/integration/tests/file/discard_staged_changes.go index 06322567fb6..44601c624e9 100644 --- a/pkg/integration/tests/file/discard_staged_changes.go +++ b/pkg/integration/tests/file/discard_staged_changes.go @@ -41,6 +41,11 @@ var DiscardStagedChanges = NewIntegrationTest(NewIntegrationTestArgs{ t.ExpectPopup().Menu().Title(Equals("")).Select(Contains("Discard staged changes")).Confirm() + t.ExpectPopup().Confirmation(). + Title(Equals("Discard staged changes")). + Content(Equals("Are you sure you want to discard all staged changes? This is not undoable.")). + Confirm() + // staged file has been removed t.Views().Files(). Lines(