Repository: jesseduffield/lazygit (language: go)

Issue:
Can the discard functionality use other than git checkout?
**Is your feature request related to a problem? Please describe.**
Some of the repos i work with have pre-checkout hooks and thus the discard functionality becomes really slow. Can discard functionality use any other command than git checkout?

**Describe the solution you'd like**
The discard changes functionality can work the same way but it can be done with so many other commands.

<!--
You may be able to add your desired feature with a custom command. Check out the examples here: https://github.com/jesseduffield/lazygit/wiki/Custom-Commands-Compendium

If a custom command does what you want but you still want to see the feature built-in to lazygit, feel free to paste the custom command into the issue to help us better understand the functionality you want.

We also encourage you to put up a PR yourself! Who cares if you've never written Go before, neither did any of the existing contributors before their first lazygit PR! Check out the PR tutorial here: https://www.youtube.com/watch?v=kNavnhzZHtk&ab_channel=JesseDuffield

Also check out the contributing guide here: https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md
-->



Discussion / hints:
There would be an alternative possible with `git restore` https://git-scm.com/docs/git-restore, but unfortunately the docs still have it tagged as "THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE."

Do you have a particular discard flow in mind? If you want to clear your entire working directory, you can already do a `Dx` when on the files tab to discard everything with a `git reset --hard HEAD && git clean -fd`

If you want to discard a single file, I'm personally not aware of a 3rd option besides `checkout` and `restore`.
@ChrisMcD1,
can we add `-q` flag to discard commands using `git checkout`. this won't trigger any `post-checkout` or `pre-checkout` hooks?
`-q` doesn't feel like the right option to use for this, but `git -c core.hooksPath=/dev/null checkout` seems to me like an appropriate thing to do. The rationale is that we might also have implemented discarding as taking the current patch and applying it in reverse, in which case no checkout hook would run either. (I'm not suggesting we do that.)

Instead of hard-coding `/dev/null` we should probably use `os.DevNull` so that it works on Windows too.

Fix patch:
diff --git a/pkg/commands/git_commands/working_tree.go b/pkg/commands/git_commands/working_tree.go
index d0148bfe95a..3b818d83f77 100644
--- a/pkg/commands/git_commands/working_tree.go
+++ b/pkg/commands/git_commands/working_tree.go
@@ -109,6 +109,10 @@ func (self *WorkingTreeCommands) BeforeAndAfterFileForRename(file *models.File)
 	return beforeFile, afterFile, nil
 }
 
+func newCheckoutCommand() *GitCommandBuilder {
+	return NewGitCmd("checkout").Config(fmt.Sprintf("core.hooksPath=%s", os.DevNull))
+}
+
 // DiscardAllFileChanges directly
 func (self *WorkingTreeCommands) DiscardAllFileChanges(file *models.File) error {
 	if file.IsRename() {
@@ -130,7 +134,7 @@ func (self *WorkingTreeCommands) DiscardAllFileChanges(file *models.File) error
 
 	if file.ShortStatus == "AA" {
 		if err := self.cmd.New(
-			NewGitCmd("checkout").Arg("--ours", "--", file.Name).ToArgv(),
+			newCheckoutCommand().Arg("--ours", "--", file.Name).ToArgv(),
 		).Run(); err != nil {
 			return err
 		}
@@ -189,7 +193,7 @@ func (self *WorkingTreeCommands) DiscardUnstagedDirChanges(node IFileNode) error
 			return err
 		}
 
-		cmdArgs := NewGitCmd("checkout").Arg("--", node.GetPath()).ToArgv()
+		cmdArgs := newCheckoutCommand().Arg("--", node.GetPath()).ToArgv()
 		if err := self.cmd.New(cmdArgs).Run(); err != nil {
 			return err
 		}
@@ -222,7 +226,7 @@ func (self *WorkingTreeCommands) RemoveUntrackedDirFiles(node IFileNode) error {
 }
 
 func (self *WorkingTreeCommands) DiscardUnstagedFileChanges(file *models.File) error {
-	cmdArgs := NewGitCmd("checkout").Arg("--", file.Name).ToArgv()
+	cmdArgs := newCheckoutCommand().Arg("--", file.Name).ToArgv()
 	return self.cmd.New(cmdArgs).Run()
 }
 
@@ -315,7 +319,7 @@ func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reve
 
 // CheckoutFile checks out the file for the given commit
 func (self *WorkingTreeCommands) CheckoutFile(commitHash, fileName string) error {
-	cmdArgs := NewGitCmd("checkout").Arg(commitHash, "--", fileName).
+	cmdArgs := newCheckoutCommand().Arg(commitHash, "--", fileName).
 		ToArgv()
 
 	return self.cmd.New(cmdArgs).Run()
@@ -323,7 +327,7 @@ func (self *WorkingTreeCommands) CheckoutFile(commitHash, fileName string) error
 
 // DiscardAnyUnstagedFileChanges discards any unstaged file changes via `git checkout -- .`
 func (self *WorkingTreeCommands) DiscardAnyUnstagedFileChanges() error {
-	cmdArgs := NewGitCmd("checkout").Arg("--", ".").
+	cmdArgs := newCheckoutCommand().Arg("--", ".").
 		ToArgv()
 
 	return self.cmd.New(cmdArgs).Run()


Test patch:
diff --git a/pkg/commands/git_commands/rebase_test.go b/pkg/commands/git_commands/rebase_test.go
index 21ebdf365dc..0d60c8c2f3e 100644
--- a/pkg/commands/git_commands/rebase_test.go
+++ b/pkg/commands/git_commands/rebase_test.go
@@ -149,7 +149,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
 			runner: oscommands.NewFakeRunner(t).
 				ExpectGitArgs([]string{"rebase", "--interactive", "--autostash", "--keep-empty", "--no-autosquash", "--rebase-merges", "abcdef"}, "", nil).
 				ExpectGitArgs([]string{"cat-file", "-e", "HEAD^:test999.txt"}, "", nil).
-				ExpectGitArgs([]string{"checkout", "HEAD^", "--", "test999.txt"}, "", nil).
+				ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "HEAD^", "--", "test999.txt"}, "", nil).
 				ExpectGitArgs([]string{"commit", "--amend", "--no-edit", "--allow-empty"}, "", nil).
 				ExpectGitArgs([]string{"rebase", "--continue"}, "", nil),
 			test: func(err error) {
diff --git a/pkg/commands/git_commands/working_tree_test.go b/pkg/commands/git_commands/working_tree_test.go
index 42f68536b6f..e56818c1b3e 100644
--- a/pkg/commands/git_commands/working_tree_test.go
+++ b/pkg/commands/git_commands/working_tree_test.go
@@ -1,6 +1,8 @@
 package git_commands
 
 import (
+	"fmt"
+	"os"
 	"testing"
 
 	"github.com/go-errors/errors"
@@ -10,6 +12,8 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+var disableHooksFlag = fmt.Sprintf("core.hooksPath=%s", os.DevNull)
+
 func TestWorkingTreeStageFile(t *testing.T) {
 	runner := oscommands.NewFakeRunner(t).
 		ExpectGitArgs([]string{"add", "--", "test.txt"}, "", nil)
@@ -113,7 +117,7 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
 			},
 			removeFile: func(string) error { return nil },
 			runner: oscommands.NewFakeRunner(t).
-				ExpectGitArgs([]string{"checkout", "--", "test"}, "", errors.New("error")),
+				ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "--", "test"}, "", errors.New("error")),
 			expectedError: "error",
 		},
 		{
@@ -125,7 +129,7 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
 			},
 			removeFile: func(string) error { return nil },
 			runner: oscommands.NewFakeRunner(t).
-				ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil),
+				ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "--", "test"}, "", nil),
 			expectedError: "",
 		},
 		{
@@ -138,7 +142,7 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
 			removeFile: func(string) error { return nil },
 			runner: oscommands.NewFakeRunner(t).
 				ExpectGitArgs([]string{"reset", "--", "test"}, "", nil).
-				ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil),
+				ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "--", "test"}, "", nil),
 			expectedError: "",
 		},
 		{
@@ -151,7 +155,7 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
 			removeFile: func(string) error { return nil },
 			runner: oscommands.NewFakeRunner(t).
 				ExpectGitArgs([]string{"reset", "--", "test"}, "", nil).
-				ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil),
+				ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "--", "test"}, "", nil),
 			expectedError: "",
 		},
 		{
@@ -428,7 +432,7 @@ func TestWorkingTreeCheckoutFile(t *testing.T) {
 			commitHash: "11af912",
 			fileName:   "test999.txt",
 			runner: oscommands.NewFakeRunner(t).
-				ExpectGitArgs([]string{"checkout", "11af912", "--", "test999.txt"}, "", nil),
+				ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "11af912", "--", "test999.txt"}, "", nil),
 			test: func(err error) {
 				assert.NoError(t, err)
 			},
@@ -438,7 +442,7 @@ func TestWorkingTreeCheckoutFile(t *testing.T) {
 			commitHash: "11af912",
 			fileName:   "test999.txt",
 			runner: oscommands.NewFakeRunner(t).
-				ExpectGitArgs([]string{"checkout", "11af912", "--", "test999.txt"}, "", errors.New("error")),
+				ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "11af912", "--", "test999.txt"}, "", errors.New("error")),
 			test: func(err error) {
 				assert.Error(t, err)
 			},
@@ -468,7 +472,7 @@ func TestWorkingTreeDiscardUnstagedFileChanges(t *testing.T) {
 			testName: "valid case",
 			file:     &models.File{Name: "test.txt"},
 			runner: oscommands.NewFakeRunner(t).
-				ExpectGitArgs([]string{"checkout", "--", "test.txt"}, "", nil),
+				ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "--", "test.txt"}, "", nil),
 			test: func(err error) {
 				assert.NoError(t, err)
 			},
@@ -495,7 +499,7 @@ func TestWorkingTreeDiscardAnyUnstagedFileChanges(t *testing.T) {
 		{
 			testName: "valid case",
 			runner: oscommands.NewFakeRunner(t).
-				ExpectGitArgs([]string{"checkout", "--", "."}, "", nil),
+				ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "--", "."}, "", nil),
 			test: func(err error) {
 				assert.NoError(t, err)
 			},