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

evilmartians / lefthook / 8293101502

15 Mar 2024 08:06AM UTC coverage: 77.962% (-0.3%) from 78.228%
8293101502

Pull #676

github

web-flow
Merge 93f327845 into e9573bbd7
Pull Request #676: fix: try ignoring empty patch files on pre commit hook

8 of 19 new or added lines in 2 files covered. (42.11%)

1 existing line in 1 file now uncovered.

2395 of 3072 relevant lines covered (77.96%)

3.62 hits per line

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

73.45
/internal/git/repository.go
1
package git
2

3
import (
4
        "fmt"
5
        "os"
6
        "path/filepath"
7
        "regexp"
8
        "strings"
9

10
        "github.com/spf13/afero"
11
)
12

13
const (
14
        stashMessage      = "lefthook auto backup"
15
        unstagedPatchName = "lefthook-unstaged.patch"
16
        infoDirMode       = 0o775
17
        minStatusLen      = 3
18
)
19

20
var (
21
        headBranchRegexp = regexp.MustCompile(`HEAD -> (?P<name>.*)$`)
22
        cmdPushFilesBase = []string{"git", "diff", "--name-only", "HEAD", "@{push}"}
23
        cmdPushFilesHead = []string{"git", "diff", "--name-only", "HEAD"}
24
        cmdStagedFiles   = []string{"git", "diff", "--name-only", "--cached", "--diff-filter=ACMR"}
25
        cmdStatusShort   = []string{"git", "status", "--short"}
26
        cmdListStash     = []string{"git", "stash", "list"}
27
        cmdRootPath      = []string{"git", "rev-parse", "--show-toplevel"}
28
        cmdHooksPath     = []string{"git", "rev-parse", "--git-path", "hooks"}
29
        cmdInfoPath      = []string{"git", "rev-parse", "--git-path", "info"}
30
        cmdGitPath       = []string{"git", "rev-parse", "--git-dir"}
31
        cmdAllFiles      = []string{"git", "ls-files", "--cached"}
32
        cmdCreateStash   = []string{"git", "stash", "create"}
33
        cmdStageFiles    = []string{"git", "add"}
34
        cmdRemotes       = []string{"git", "branch", "--remotes"}
35
        cmdHideUnstaged  = []string{"git", "checkout", "--force", "--"}
36
)
37

38
// Repository represents a git repository.
39
type Repository struct {
40
        Fs                afero.Fs
41
        Git               Exec
42
        HooksPath         string
43
        RootPath          string
44
        GitPath           string
45
        InfoPath          string
46
        unstagedPatchPath string
47
        headBranch        string
48
}
49

50
// NewRepository returns a Repository or an error, if git repository it not initialized.
51
func NewRepository(fs afero.Fs, git Exec) (*Repository, error) {
3✔
52
        rootPath, err := git.Cmd(cmdRootPath)
3✔
53
        if err != nil {
3✔
54
                return nil, err
×
55
        }
×
56

57
        hooksPath, err := git.Cmd(cmdHooksPath)
3✔
58
        if err != nil {
3✔
59
                return nil, err
×
60
        }
×
61
        if exists, _ := afero.DirExists(fs, filepath.Join(rootPath, hooksPath)); exists {
6✔
62
                hooksPath = filepath.Join(rootPath, hooksPath)
3✔
63
        }
3✔
64

65
        infoPath, err := git.Cmd(cmdInfoPath)
3✔
66
        if err != nil {
3✔
67
                return nil, err
×
68
        }
×
69
        infoPath = filepath.Clean(infoPath)
3✔
70
        if exists, _ := afero.DirExists(fs, infoPath); !exists {
3✔
71
                err = fs.Mkdir(infoPath, infoDirMode)
×
72
                if err != nil {
×
73
                        return nil, err
×
74
                }
×
75
        }
76

77
        gitPath, err := git.Cmd(cmdGitPath)
3✔
78
        if err != nil {
3✔
79
                return nil, err
×
80
        }
×
81
        if !filepath.IsAbs(gitPath) {
6✔
82
                gitPath = filepath.Join(rootPath, gitPath)
3✔
83
        }
3✔
84

85
        git.SetRootPath(rootPath)
3✔
86

3✔
87
        return &Repository{
3✔
88
                Fs:                fs,
3✔
89
                Git:               git,
3✔
90
                HooksPath:         hooksPath,
3✔
91
                RootPath:          rootPath,
3✔
92
                GitPath:           gitPath,
3✔
93
                InfoPath:          infoPath,
3✔
94
                unstagedPatchPath: filepath.Join(infoPath, unstagedPatchName),
3✔
95
        }, nil
3✔
96
}
97

98
// StagedFiles returns a list of staged files
99
// or an error if git command fails.
100
func (r *Repository) StagedFiles() ([]string, error) {
3✔
101
        return r.FilesByCommand(cmdStagedFiles)
3✔
102
}
3✔
103

104
// StagedFiles returns a list of all files in repository
105
// or an error if git command fails.
106
func (r *Repository) AllFiles() ([]string, error) {
3✔
107
        return r.FilesByCommand(cmdAllFiles)
3✔
108
}
3✔
109

110
// PushFiles returns a list of files that are ready to be pushed
111
// or an error if git command fails.
112
func (r *Repository) PushFiles() ([]string, error) {
×
113
        res, err := r.FilesByCommand(cmdPushFilesBase)
×
114
        if err == nil {
×
115
                return res, nil
×
116
        }
×
117

118
        if len(r.headBranch) == 0 {
×
119
                branches, err := r.Git.CmdLines(cmdRemotes)
×
120
                if err != nil {
×
121
                        return nil, err
×
122
                }
×
123
                for _, branch := range branches {
×
124
                        if !headBranchRegexp.MatchString(branch) {
×
125
                                continue
×
126
                        }
127

128
                        matches := headBranchRegexp.FindStringSubmatch(branch)
×
129
                        r.headBranch = matches[headBranchRegexp.SubexpIndex("name")]
×
130
                        break
×
131
                }
132
        }
133
        return r.FilesByCommand(append(cmdPushFilesHead, r.headBranch))
×
134
}
135

136
// PartiallyStagedFiles returns the list of files that have both staged and
137
// unstaged changes.
138
// See https://git-scm.com/docs/git-status#_short_format.
139
func (r *Repository) PartiallyStagedFiles() ([]string, error) {
6✔
140
        lines, err := r.Git.CmdLines(cmdStatusShort)
6✔
141
        if err != nil {
6✔
142
                return []string{}, err
×
143
        }
×
144

145
        partiallyStaged := make([]string, 0)
6✔
146

6✔
147
        for _, line := range lines {
12✔
148
                if len(line) < minStatusLen {
6✔
149
                        continue
×
150
                }
151

152
                index := line[0]
6✔
153
                workingTree := line[1]
6✔
154

6✔
155
                filename := line[3:]
6✔
156
                idx := strings.Index(filename, "->")
6✔
157
                if idx != -1 {
9✔
158
                        filename = filename[idx+3:]
3✔
159
                }
3✔
160

161
                if index != ' ' && index != '?' && workingTree != ' ' && workingTree != '?' && len(filename) > 0 {
11✔
162
                        partiallyStaged = append(partiallyStaged, filename)
5✔
163
                }
5✔
164
        }
165

166
        return partiallyStaged, nil
6✔
167
}
168

169
func (r *Repository) SaveUnstaged(files []string) error {
2✔
170
        _, err := r.Git.Cmd(
2✔
171
                append([]string{
2✔
172
                        "git",
2✔
173
                        "diff",
2✔
174
                        "--binary",          // support binary files
2✔
175
                        "--unified=0",       // do not add lines around diff for consistent behavior
2✔
176
                        "--no-color",        // disable colors for consistent behavior
2✔
177
                        "--no-ext-diff",     // disable external diff tools for consistent behavior
2✔
178
                        "--src-prefix=a/",   // force prefix for consistent behavior
2✔
179
                        "--dst-prefix=b/",   // force prefix for consistent behavior
2✔
180
                        "--patch",           // output a patch that can be applied
2✔
181
                        "--submodule=short", // always use the default short format for submodules
2✔
182
                        "--output",
2✔
183
                        r.unstagedPatchPath,
2✔
184
                        "--",
2✔
185
                }, files...),
2✔
186
        )
2✔
187

2✔
188
        return err
2✔
189
}
2✔
190

191
func (r *Repository) HideUnstaged(files []string) error {
2✔
192
        _, err := r.Git.Cmd(append(cmdHideUnstaged, files...))
2✔
193

2✔
194
        return err
2✔
195
}
2✔
196

197
func (r *Repository) RestoreUnstaged() error {
3✔
198
        if ok, _ := afero.Exists(r.Fs, r.unstagedPatchPath); !ok {
6✔
199
                return nil
3✔
200
        }
3✔
201

202
        stat, err := r.Fs.Stat(r.unstagedPatchPath)
2✔
203
        if err != nil {
2✔
NEW
204
                return err
×
NEW
205
        }
×
206

207
        if stat.Size() == 0 {
2✔
NEW
208
                err = r.Fs.Remove(r.unstagedPatchPath)
×
NEW
209
                if err != nil {
×
NEW
210
                        return fmt.Errorf("couldn't remove the patch %s: %w", r.unstagedPatchPath, err)
×
NEW
211
                }
×
212

NEW
213
                return nil
×
214
        }
215

216
        _, err = r.Git.Cmd([]string{
2✔
217
                "git",
2✔
218
                "apply",
2✔
219
                "-v",
2✔
220
                "--whitespace=nowarn",
2✔
221
                "--recount",
2✔
222
                "--unidiff-zero",
2✔
223
                r.unstagedPatchPath,
2✔
224
        })
2✔
225
        if err != nil {
2✔
NEW
226
                return fmt.Errorf("couldn't apply the patch %s: %w", r.unstagedPatchPath, err)
×
NEW
227
        }
×
228

229
        err = r.Fs.Remove(r.unstagedPatchPath)
2✔
230
        if err != nil {
2✔
NEW
231
                return fmt.Errorf("couldn't remove the patch %s: %w", r.unstagedPatchPath, err)
×
UNCOV
232
        }
×
233

234
        return nil
2✔
235
}
236

237
func (r *Repository) StashUnstaged() error {
2✔
238
        stashHash, err := r.Git.Cmd(cmdCreateStash)
2✔
239
        if err != nil {
4✔
240
                return err
2✔
241
        }
2✔
242

243
        _, err = r.Git.Cmd([]string{
2✔
244
                "git",
2✔
245
                "stash",
2✔
246
                "store",
2✔
247
                "--quiet",
2✔
248
                "--message",
2✔
249
                stashMessage,
2✔
250
                stashHash,
2✔
251
        })
2✔
252
        if err != nil {
2✔
253
                return err
×
254
        }
×
255

256
        return nil
2✔
257
}
258

259
func (r *Repository) DropUnstagedStash() error {
3✔
260
        lines, err := r.Git.CmdLines(cmdListStash)
3✔
261
        if err != nil {
3✔
262
                return err
×
263
        }
×
264

265
        stashRegexp := regexp.MustCompile(`^(?P<stash>[^ ]+):\s*` + stashMessage)
3✔
266
        for i := range lines {
6✔
267
                line := lines[len(lines)-i-1]
3✔
268
                matches := stashRegexp.FindStringSubmatch(line)
3✔
269
                if len(matches) == 0 {
6✔
270
                        continue
3✔
271
                }
272

273
                stashID := stashRegexp.SubexpIndex("stash")
2✔
274

2✔
275
                if len(matches[stashID]) > 0 {
4✔
276
                        _, err := r.Git.Cmd([]string{
2✔
277
                                "git",
2✔
278
                                "stash",
2✔
279
                                "drop",
2✔
280
                                "--quiet",
2✔
281
                                matches[stashID],
2✔
282
                        })
2✔
283
                        if err != nil {
2✔
284
                                return err
×
285
                        }
×
286
                }
287
        }
288

289
        return nil
3✔
290
}
291

292
func (r *Repository) AddFiles(files []string) error {
3✔
293
        if len(files) == 0 {
3✔
294
                return nil
×
295
        }
×
296

297
        _, err := r.Git.Cmd(
3✔
298
                append(cmdStageFiles, files...),
3✔
299
        )
3✔
300

3✔
301
        return err
3✔
302
}
303

304
// FilesByCommand accepts git command and returns its result as a list of filepaths.
305
func (r *Repository) FilesByCommand(command []string) ([]string, error) {
3✔
306
        lines, err := r.Git.CmdLines(command)
3✔
307
        if err != nil {
3✔
308
                return nil, err
×
309
        }
×
310

311
        return r.extractFiles(lines)
3✔
312
}
313

314
func (r *Repository) extractFiles(lines []string) ([]string, error) {
3✔
315
        var files []string
3✔
316

3✔
317
        for _, line := range lines {
6✔
318
                file := strings.TrimSpace(line)
3✔
319
                if len(file) == 0 {
3✔
320
                        continue
×
321
                }
322

323
                isFile, err := r.isFile(file)
3✔
324
                if err != nil {
3✔
325
                        return nil, err
×
326
                }
×
327
                if isFile {
6✔
328
                        files = append(files, file)
3✔
329
                }
3✔
330
        }
331

332
        return files, nil
3✔
333
}
334

335
func (r *Repository) isFile(path string) (bool, error) {
3✔
336
        if !strings.HasPrefix(path, r.RootPath) {
6✔
337
                path = filepath.Join(r.RootPath, path)
3✔
338
        }
3✔
339
        stat, err := r.Fs.Stat(path)
3✔
340
        if err != nil {
3✔
341
                if os.IsNotExist(err) {
×
342
                        return false, nil
×
343
                }
×
344
                return false, err
×
345
        }
346

347
        return !stat.IsDir(), nil
3✔
348
}
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

© 2025 Coveralls, Inc