• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

evilmartians / lefthook / 14331394303

08 Apr 2025 10:36AM UTC coverage: 75.011% (-0.2%) from 75.227%
14331394303

push

github

web-flow
fix: fix command execution error on Windows #989 (#992)

* fix: fix command execution error on Windows #989
// This commit fixes an issue where Lefthook was unable to execute commands

- changed:   internal/lefthook/runner/exec/execute_windows.go
  fix: fullPathSh: change `sh.exe` for git bash installed in other directories
  fix: execute: cmdStr is dont escpape filename double quote, so replace
			    these to escaped double quote

* fix(Execute): fallback to Git-based paths when sh.exe is missing

Resolved a bug where sh.exe couldn't be found, causing shell command execution to fail.
Introduced helper functions `getFullPathSh` and `findExecutableDir` to locate sh.exe
via git-bash.exe, git.exe, or a default Git installation directory.

This ensures `CommandExecutor.Execute` works in environments where sh.exe is not in PATH.

Additional context:
- When git-bash.exe is found in a directory, sh.exe typically exists in its "bin" subdirectory.
- When git.exe is located at ".../cmd/git.exe", sh.exe is often found at the corresponding".

27 of 43 new or added lines in 1 file covered. (62.79%)

19 existing lines in 1 file now uncovered.

3332 of 4442 relevant lines covered (75.01%)

3.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.11
/internal/git/command_executor.go
1
package git
2

3
import (
4
        "bytes"
5
        "fmt"
6
        "path/filepath"
7
        "strings"
8

9
        "github.com/evilmartians/lefthook/internal/log"
10
        "github.com/evilmartians/lefthook/internal/system"
11
)
12

13
// CommandExecutor provides some methods that take some effect on execution and/or result data.
14
type CommandExecutor struct {
15
        cmd           system.Command
16
        root          string
17
        onlyDebugLogs bool
18
}
19

20
// NewExecutor returns an object that executes given commands in the OS.
21
func NewExecutor(cmd system.Command) *CommandExecutor {
3✔
22
        return &CommandExecutor{cmd: cmd}
3✔
23
}
3✔
24

25
func (c CommandExecutor) WithoutEnvs(envs ...string) CommandExecutor {
2✔
26
        return CommandExecutor{cmd: c.cmd.WithoutEnvs(envs...), root: c.root}
2✔
27
}
2✔
28

UNCOV
29
func (c CommandExecutor) OnlyDebugLogs() CommandExecutor {
×
UNCOV
30
        return CommandExecutor{cmd: c.cmd, root: c.root, onlyDebugLogs: true}
×
UNCOV
31
}
×
32

33
// Cmd runs plain string command. Trims spaces around output.
34
func (c CommandExecutor) Cmd(cmd []string) (string, error) {
3✔
35
        out, err := c.execute(cmd, c.root)
3✔
36
        if err != nil {
5✔
37
                return "", err
2✔
38
        }
2✔
39

40
        return strings.TrimSpace(out), nil
3✔
41
}
42

43
// BatchedCmd runs the command with any number of appended arguments batched in chunks to match the OS limits.
44
func (c CommandExecutor) BatchedCmd(cmd []string, args []string) (string, error) {
3✔
45
        maxlen := system.MaxCmdLen()
3✔
46
        result := strings.Builder{}
3✔
47

3✔
48
        argsBatched := batchByLength(args, maxlen-len(cmd))
3✔
49
        for i, batch := range argsBatched {
6✔
50
                out, err := c.Cmd(append(cmd, batch...))
3✔
51
                if err != nil {
3✔
UNCOV
52
                        return "", fmt.Errorf("error in batch %d: %w", i, err)
×
UNCOV
53
                }
×
54
                result.WriteString(out)
3✔
55
                result.WriteString("\n")
3✔
56
        }
57

58
        return result.String(), nil
3✔
59
}
60

61
// CmdLines runs plain string command, returns its output split by newline.
62
func (c CommandExecutor) CmdLines(cmd []string) ([]string, error) {
6✔
63
        out, err := c.execute(cmd, c.root)
6✔
64
        if err != nil {
6✔
UNCOV
65
                return nil, err
×
UNCOV
66
        }
×
67

68
        return strings.Split(strings.TrimSpace(out), "\n"), nil
6✔
69
}
70

71
// CmdLines runs plain string command, returns its output split by newline.
72
func (c CommandExecutor) CmdLinesWithinFolder(cmd []string, folder string) ([]string, error) {
3✔
73
        root := filepath.Join(c.root, folder)
3✔
74
        out, err := c.execute(cmd, root)
3✔
75
        if err != nil {
3✔
UNCOV
76
                return nil, err
×
UNCOV
77
        }
×
78

79
        return strings.Split(strings.TrimSpace(out), "\n"), nil
3✔
80
}
81

82
func (c CommandExecutor) execute(cmd []string, root string) (string, error) {
6✔
83
        out := new(bytes.Buffer)
6✔
84
        errOut := new(bytes.Buffer)
6✔
85
        err := c.cmd.Run(cmd, root, system.NullReader, out, errOut)
6✔
86
        outString := out.String()
6✔
87
        errString := errOut.String()
6✔
88

6✔
89
        if len(outString) > 0 {
12✔
90
                log.Builder(log.DebugLevel).
6✔
91
                        Add("[lefthook] stdout: ", outString).
6✔
92
                        Log()
6✔
93
        }
6✔
94

95
        if err != nil {
8✔
96
                if len(errString) > 0 {
4✔
97
                        logLevel := log.ErrorLevel
2✔
98
                        if c.onlyDebugLogs {
2✔
UNCOV
99
                                logLevel = log.DebugLevel
×
UNCOV
100
                        }
×
101
                        log.Builder(logLevel).
2✔
102
                                Add("> ", strings.Join(cmd, " ")).
2✔
103
                                Add("  ", errString).
2✔
104
                                Log()
2✔
105
                }
106
        }
107

108
        return outString, err
6✔
109
}
110

111
func batchByLength(s []string, length int) [][]string {
3✔
112
        batches := make([][]string, 0)
3✔
113

3✔
114
        var acc, prev int
3✔
115
        for i := range s {
6✔
116
                acc += len(s[i])
3✔
117
                if acc > length {
3✔
UNCOV
118
                        if i == prev {
×
UNCOV
119
                                batches = append(batches, s[prev:i+1])
×
UNCOV
120
                                prev = i + 1
×
UNCOV
121
                        } else {
×
UNCOV
122
                                batches = append(batches, s[prev:i])
×
UNCOV
123
                                prev = i
×
UNCOV
124
                        }
×
UNCOV
125
                        acc = len(s[i])
×
126
                }
127
        }
128
        if acc > 0 {
6✔
129
                batches = append(batches, s[prev:])
3✔
130
        }
3✔
131

132
        return batches
3✔
133
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc