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

gabyx / Githooks / 6705

20 Feb 2026 05:15PM UTC coverage: 78.823% (-0.2%) from 78.997%
6705

Pull #184

circleci

gabyx
fix: litning :anchor:
Pull Request #184: chore: linting

282 of 426 new or added lines in 67 files covered. (66.2%)

29 existing lines in 20 files now uncovered.

9350 of 11862 relevant lines covered (78.82%)

1745.57 hits per line

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

73.75
/githooks/git/gitcommon.go
1
package git
2

3
import (
4
        "os"
5
        "os/exec"
6
        "path"
7
        "path/filepath"
8
        "regexp"
9
        "runtime"
10
        "strings"
11

12
        cm "github.com/gabyx/githooks/githooks/common"
13
        strs "github.com/gabyx/githooks/githooks/strings"
14

15
        "github.com/hashicorp/go-version"
16
)
17

18
const (
19
        // NullRef is the null reference used by git during certain hook execution.
20
        NullRef = "0000000000000000000000000000000000000000"
21
)
22

23
var lfsVersionRegex = regexp.MustCompile(`\d+\.\d+\.\d+`)
24

25
// Get Git LFS version.
26
func GetGitLFSVersion() (ver *version.Version, err error) {
944✔
27
        res, err := NewCtx().Get("lfs", "--version")
944✔
28
        if err != nil {
944✔
29
                return nil, err
×
30
        }
×
31

32
        ver, err = version.NewVersion(lfsVersionRegex.FindString(res))
944✔
33

944✔
34
        return
944✔
35
}
36

37
// IsBareRepo returns `true` if `c.GetCwd()` is a bare repository.
38
func (c *Context) IsBareRepo() bool {
1,425✔
39
        out, _ := c.Get("rev-parse", "--is-bare-repository")
1,425✔
40
        return out == GitCVTrue //nolint:nlreturn
1,425✔
41
}
1,425✔
42

43
// IsGitRepo returns `true` if `path` is a git repository (bare or non-bare).
44
func (c *Context) IsGitRepo() bool {
485✔
45
        return c.Check("rev-parse") == nil
485✔
46
}
485✔
47

48
// IsGitDir returns `true` if `c.GetCwd()` is a git repository (bare or non-bare).
49
func (c *Context) IsGitDir() bool {
52✔
50
        s, err := c.Get("rev-parse", "--is-inside-git-dir")
52✔
51

52✔
52
        return err == nil && s == GitCVTrue
52✔
53
}
52✔
54

55
// GetMainWorktree returns the main worktree
56
// based on the current context's working directory.
57
func (c *Context) GetMainWorktree() (string, error) {
79✔
58
        // This feature is kind of buggy in earlier version of git < 2.28.0
79✔
59
        // it returns a git directory instead of the work tree
79✔
60
        // We strip "/.git" from the output.
79✔
61
        trees, err := c.Get("worktree", "list", "--porcelain")
79✔
62
        if err != nil {
79✔
63
                return "", err
×
64
        }
×
65

66
        list := strs.SplitLinesN(trees, 2) //nolint:mnd
79✔
67
        if len(list) == 0 {
79✔
68
                return "", cm.ErrorF("Could not get main worktree in '%s'", c.GetCwd())
×
69
        }
×
70

71
        tree := strings.TrimSuffix(
79✔
72
                strings.TrimSpace(
79✔
73
                        strings.TrimPrefix(list[0], "worktree")),
79✔
74
                "/.git")
79✔
75

79✔
76
        if strs.IsEmpty(tree) {
79✔
77
                return "", cm.ErrorF("Could not get main worktree in '%s'", c.GetCwd())
×
78
        }
×
79

80
        return filepath.ToSlash(tree), nil
79✔
81
}
82

83
// GetGitDirCommon returns the common Git directory.
84
// For normal repos this points to the `.git` directory.
85
// For worktrees this points to the main worktrees git dir.
86
// The env. variable GIT_COMMON_DIR has especially
87
// be introduced for multiple worktrees, see:
88
// https://github.com/git/git/commit/c7b3a3d2fe2688a30ddb8d516ed000eeda13c24e
89
func (c *Context) GetGitDirCommon() (gitDir string, err error) {
1,978✔
90
        // Git 2.46.x apparently runs `reference-transaction` on `git init`
1,978✔
91
        // where `rev-parse` fails despite the documentation saying it reports `$GIT_COMMON_DIR` if set
1,978✔
92
        // (it is set!) -> Bug was reported.
1,978✔
93
        gitDir = os.Getenv("GIT_COMMON_DIR")
1,978✔
94
        if strs.IsEmpty(gitDir) {
3,956✔
95
                gitDir = os.Getenv("GIT_DIR")
1,978✔
96
                if strs.IsEmpty(gitDir) {
3,948✔
97
                        gitDir, err = c.Get("rev-parse", "--git-common-dir")
1,970✔
98
                        if err != nil {
2,050✔
99
                                return
80✔
100
                        }
80✔
101
                }
102
        }
103

104
        if !filepath.IsAbs(gitDir) {
3,784✔
105
                gitDir = filepath.Join(c.GetCwd(), gitDir)
1,886✔
106
        }
1,886✔
107

108
        gitDir, err = filepath.Abs(gitDir)
1,898✔
109
        if err != nil {
1,898✔
110
                return
×
111
        }
×
112

113
        gitDir = filepath.ToSlash(gitDir)
1,898✔
114

1,898✔
115
        return
1,898✔
116
}
117

118
// GetGitDirWorktree returns the Git directory.
119
// For normal repos this points to the `.git` directory.
120
// For worktrees this points to the actual worktrees git dir `.git/worktrees/<....>/`.
121
func (c *Context) GetGitDirWorktree() (gitDir string, err error) {
2,635✔
122
        // Git 2.46.x apparently runs `reference-transaction` on `git init`
2,635✔
123
        // where `rev-parse` fails despite the documentation saying it reports `$GIT_DIR` if set
2,635✔
124
        // (it is set!) -> Bug was reported.
2,635✔
125
        gitDir = os.Getenv("GIT_DIR")
2,635✔
126
        if strs.IsEmpty(gitDir) {
4,886✔
127
                gitDir, err = c.Get("rev-parse", "--absolute-git-dir")
2,251✔
128
                if err != nil {
2,251✔
129
                        return
×
130
                }
×
131
        }
132

133
        gitDir = filepath.ToSlash(gitDir)
2,635✔
134

2,635✔
135
        return
2,635✔
136
}
137

138
// GetRepoRoot returns the top level directory in a non-bare repository or the
139
// absolute Git directory in a bare repository for `topLevel`.
140
// This is the root level for Githooks.
141
// The `gitDir` is the common Git directory (main Git dir for worktrees).
142
func (c *Context) GetRepoRoot() (topLevel string, gitDir string, gitDirWorktree string, err error) {
1,199✔
143
        if gitDir, err = c.GetGitDirCommon(); err != nil {
1,279✔
144
                return
80✔
145
        }
80✔
146

147
        if gitDirWorktree, err = c.GetGitDirWorktree(); err != nil {
1,119✔
148
                return
×
149
        }
×
150

151
        if c.IsBareRepo() {
1,141✔
152
                topLevel = gitDir
22✔
153
        } else {
1,119✔
154
                if topLevel, err = c.Get("rev-parse", "--show-toplevel"); err != nil {
1,097✔
155
                        return
×
156
                }
×
157
                topLevel = filepath.ToSlash(topLevel)
1,097✔
158
        }
159

160
        return
1,119✔
161
}
162

163
// GetCurrentBranch gets the current branch in repository.
164
func (c *Context) GetCurrentBranch() (string, error) {
188✔
165
        return c.Get("branch", "--show-current")
188✔
166
}
188✔
167

168
// FindGitDirs returns Git directories inside `searchDir`.
169
// Paths relative to `searchDir` containing `.dotfiles` (hidden files)
170
// will never be reported. Optionally the output can be sorted.
171
func FindGitDirs(searchDir string) (all []string, err error) {
27✔
172
        candidates, err := cm.Glob(path.Join(searchDir, "**/HEAD"), true)
27✔
173
        if err != nil {
27✔
NEW
174
                return all, err
×
175
        }
×
176

177
        // We obtain a list of HEAD files, e.g.
178
        //         - ~/a/b/normal/.git/HEAD
179
        //         - ~/a/b/normal/.git/.../HEAD  1) filter out
180
        //         - ~/a/b/bare-repo/HEAD
181
        //         - ~/a/b/bare-repo/.../HEAD
182

183
        repos := make(strs.StringSet, len(candidates))
27✔
184
        var dir, relPath string
27✔
185

27✔
186
        // Be consistent here , on windows we might get twice `
27✔
187
        // C:/a/.git` and also `c:/a/.git` in the loop below
27✔
188
        // because of the output of `GetGitDirCommon()``.
27✔
189
        // -> adjust the volume label to UpperCase always for storing
27✔
190
        // the result in the `StringMap`
27✔
191
        adjustVolumeNameCase := runtime.GOOS == cm.WindowsOsName
27✔
192

27✔
193
        // Filter wrong dirs out.
27✔
194
        for i := range candidates {
104✔
195
                dir = path.Dir(candidates[i])
77✔
196
                normalGitDir := path.Base(dir) == ".git"
77✔
197

77✔
198
                relPath, err = filepath.Rel(searchDir, dir) // filepath, because path.Rel is not available.
77✔
199
                if err != nil {
77✔
NEW
200
                        return all, err
×
201
                }
×
202
                relPath = filepath.ToSlash(relPath)
77✔
203

77✔
204
                if normalGitDir && cm.ContainsDotFile(path.Dir(relPath)) ||
77✔
205
                        !normalGitDir && cm.ContainsDotFile(relPath) {
103✔
206
                        // With that we filter out matches which
26✔
207
                        // contain .dotfiles in the relative path to the search dir.
26✔
208
                        continue
26✔
209
                }
210

211
                // gitDir is always an absolute path
212
                gitDir, e := NewCtxAt(dir).GetGitDirCommon()
51✔
213

51✔
214
                if adjustVolumeNameCase && len(gitDir) >= 2 {
51✔
215
                        gitDir = strings.ToUpper(gitDir[0:2]) + gitDir[2:]
×
216
                }
×
217

218
                // Check if its really a git directory.
219
                if e == nil &&
51✔
220
                        NewCtxAt(gitDir).IsGitRepo() {
102✔
221
                        repos.Insert(gitDir)
51✔
222
                }
51✔
223
        }
224

225
        all = repos.ToList()
27✔
226

27✔
227
        return all, err
27✔
228
}
229

230
// Initialize an empty repo at path `repoPath`.
231
func Init(repoPath string, bare bool) error {
185✔
232
        args := []string{"-c", "core.hooksPath=.git/hooks", "init", "--template="}
185✔
233
        if bare {
370✔
234
                args = append(args, "--bare")
185✔
235
        }
185✔
236

237
        // We must not execute this clone command inside a Git repo  (e.g. A)
238
        // due to `core.hooksPath=.git/hooks` which get applied to `A` -> Bug ?:
239
        // https://stackoverflow.com/questions/67273420/why-does-git-execute-hooks-from-an-other-repository
240
        ctx := NewCtxSanitizedAt(repoPath)
185✔
241
        if !cm.IsDirectory(ctx.GetCwd()) {
185✔
242
                if e := os.MkdirAll(ctx.GetCwd(), cm.DefaultFileModeDirectory); e != nil {
×
243
                        return cm.ErrorF("Could not create working directory '%s'.", ctx.GetCwd())
×
244
                }
×
245
        }
246

247
        out, e := ctx.GetCombined(args...)
185✔
248

185✔
249
        if e != nil {
185✔
250
                return cm.ErrorF("Init Git repository in '%s' failed:\n%s", repoPath, out)
×
251
        }
×
252

253
        return nil
185✔
254
}
255

256
// Clone an URL to a path `repoPath`.
257
func Clone(repoPath string, url string, branch string, depth int) error {
242✔
258
        // Its important to not use any template directory here to not
242✔
259
        // install accidentally Githooks run-wrappers.
242✔
260
        // We set the `core.hooksPath` explicitly to its internal
242✔
261
        // hooks directory to not interfere
242✔
262
        // with global settings.
242✔
263
        // Also this installs LFS hooks, which comes handy for certain shared hook repos
242✔
264
        // with prebuilt binaries.
242✔
265
        args := []string{"clone", "-c", "core.hooksPath=.git/hooks", "--template=", "--single-branch"}
242✔
266

242✔
267
        if branch != "" {
248✔
268
                args = append(args, "--branch", branch)
6✔
269
        }
6✔
270

271
        if depth > 0 {
292✔
272
                args = append(args, strs.Fmt("--depth=%v", depth))
50✔
273
        }
50✔
274

275
        args = append(args, []string{url, repoPath}...)
242✔
276

242✔
277
        // We must not execute this clone command inside a Git repo  (e.g. A)
242✔
278
        // due to `core.hooksPath=.git/hooks` which get applied to `A` -> Bug ?:
242✔
279
        // https://stackoverflow.com/questions/67273420/why-does-git-execute-hooks-from-an-other-repository
242✔
280
        ctx := NewCtxSanitizedAt(path.Dir(repoPath))
242✔
281
        if !cm.IsDirectory(ctx.GetCwd()) {
458✔
282
                if e := os.MkdirAll(ctx.GetCwd(), cm.DefaultFileModeDirectory); e != nil {
216✔
283
                        return cm.ErrorF("Could not create working directory '%s'.", ctx.GetCwd())
×
284
                }
×
285
        }
286

287
        out, e := ctx.GetCombined(args...)
242✔
288

242✔
289
        if e != nil {
242✔
290
                return cm.ErrorF("Cloning of '%s' [branch: '%s']\ninto '%s' failed:\n%s", url, branch, repoPath, out)
×
291
        }
×
292

293
        return nil
242✔
294
}
295

296
// Pull executes a pull in `repoPath`.
297
func (c *Context) Pull(remote string) error {
6✔
298
        out, e := c.GetCombined("pull", remote)
6✔
299
        if e != nil {
6✔
300
                return cm.ErrorF("Pulling '%s' in '%s' failed:\n%s", remote, c.GetCwd(), out)
×
301
        }
×
302

303
        return nil
6✔
304
}
305

306
// FetchBranch executes a fetch of a `branch` from the `remote` in `repoPath`.
307
// This command sadly does not automatically (git 2.30) fetch the tags on this branch
308
// automatically. Use `tagPattern` to specify explicitly which tags to fetch.
309
func (c *Context) FetchBranch(remote string, branch string, tagPattern string) error {
282✔
310
        cmd := []string{"fetch", "--force", "--prune", "--prune-tags", "--no-tags", remote, branch}
282✔
311

282✔
312
        if strs.IsNotEmpty(tagPattern) {
564✔
313
                cmd = append(cmd, strs.Fmt("refs/tags/%s:refs/tags/%s", tagPattern, tagPattern))
282✔
314
        }
282✔
315

316
        out, e := c.GetCombined(cmd...)
282✔
317
        if e != nil {
282✔
318
                return cm.ErrorF("Fetching of '%s' from '%s'\nin '%s' failed:\n%s",
×
319
                        branch, remote, c.GetCwd(), out)
×
320
        }
×
321

322
        return nil
282✔
323
}
324

325
// GetCommits gets all commits in the ancestry path starting from `firstSHA` (excluded in the result)
326
// up to and including `lastSHA`.
327
func (c *Context) GetCommits(firstSHA string, lastSHA string) ([]string, error) {
45✔
328
        return c.GetSplit("rev-list", "--ancestry-path", strs.Fmt("%s..%s", firstSHA, lastSHA))
45✔
329
}
45✔
330

331
// GetCommitLog gets all commits in the ancestry path starting from `firstSHA` (excluded in the result)
332
// up to and including `lastSHA`.
333
func (c *Context) GetCommitLog(commitSHA string, format string) (string, error) {
×
334
        return c.Get("log", strs.Fmt("--format=%s", format), commitSHA)
×
335
}
×
336

337
// GetRemoteURLAndBranch reports the `remote`s `url` and
338
// the current `branch` of HEAD.
339
func (c *Context) GetRemoteURLAndBranch(remote string) (currentURL string, currentBranch string, err error) {
92✔
340
        currentURL = c.GetConfig("remote."+remote+".url", LocalScope)
92✔
341
        currentBranch, err = c.Get("symbolic-ref", "-q", "--short", HEAD)
92✔
342

92✔
343
        return
92✔
344
}
92✔
345

346
// PullOrClone either executes a pull in `repoPath` or if not
347
// existing, clones to this path.
348
func PullOrClone(
349
        repoPath string,
350
        url string,
351
        branch string,
352
        depth int,
353
        repoCheck func(*Context) error) (isNewClone bool, err error) {
56✔
354
        gitx := NewCtxSanitizedAt(repoPath)
56✔
355
        if gitx.IsGitRepo() {
62✔
356
                isNewClone = false
6✔
357

6✔
358
                if repoCheck != nil {
6✔
359
                        if err = repoCheck(gitx); err != nil {
×
360
                                return
×
361
                        }
×
362
                }
363

364
                err = gitx.Pull("origin")
6✔
365
        } else {
50✔
366
                isNewClone = true
50✔
367

50✔
368
                if err = os.RemoveAll(repoPath); err != nil {
50✔
369
                        err = cm.ErrorF("Could not remove directory '%s'.", repoPath)
×
NEW
370
                        return //nolint:nlreturn
×
371
                }
×
372

373
                err = Clone(repoPath, url, branch, depth)
50✔
374
        }
375

376
        return
56✔
377
}
378

379
// RepoCheck is the function which is executed before a fetch.
380
// Arguments 1 and 2 are `url`, `branch`.
381
// Return an error to abort the action.
382
// Return `true` to trigger a complete reclone.
383
// Available ConfigScope's.
384
type RepoCheck = func(Context, string, string) (bool, error)
385

386
// FetchOrClone either executes a fetch in `repoPath` or if not
387
// existing, clones to this path.
388
// The callback `repoCheck` before a fetch can trigger a reclone.
389
func FetchOrClone(
390
        repoPath string,
391
        url string, branch string,
392
        depth int,
393
        tagPattern string,
394
        repoCheck RepoCheck) (isNewClone bool, gitx *Context, err error) {
282✔
395
        gitx = NewCtxSanitizedAt(repoPath)
282✔
396

282✔
397
        if gitx.IsGitRepo() {
374✔
398
                isNewClone = false
92✔
399

92✔
400
                if repoCheck != nil {
184✔
401
                        var reclone bool
92✔
402
                        if reclone, err = repoCheck(*gitx, url, branch); err != nil {
92✔
403
                                return
×
404
                        }
×
405

406
                        isNewClone = reclone
92✔
407
                }
408
        } else {
190✔
409
                isNewClone = true
190✔
410
        }
190✔
411

412
        if isNewClone {
472✔
413
                if err = os.RemoveAll(repoPath); err != nil {
190✔
414
                        return
×
415
                }
×
416
                err = Clone(repoPath, url, branch, depth)
190✔
417
        }
418

419
        if err != nil {
282✔
420
                return
×
421
        }
×
422

423
        err = gitx.FetchBranch("origin", branch, tagPattern)
282✔
424

282✔
425
        return
282✔
426
}
427

428
// IsRefReachable reports if `ref` (can be branch/tag/commit) is contained starting
429
// from `startRef`.
430
func IsRefReachable(gitx *Context, startRef string, ref string) (bool, error) {
282✔
431
        i, err := gitx.GetExitCode("merge-base", "--is-ancestor", ref, startRef)
282✔
432

282✔
433
        return i == 0, err
282✔
434
}
282✔
435

436
// GetTags gets the tags  at `commitSHA`.
437
func GetTags(gitx *Context, commitSHA string) ([]string, error) {
380✔
438
        if strs.IsEmpty(commitSHA) {
380✔
439
                commitSHA = HEAD
×
440
        }
×
441

442
        return gitx.GetSplit("tag", "--points-at", commitSHA)
380✔
443
}
444

445
// GetVersionAt gets the version & tag from the tags at `commitSHA`.
446
func GetVersionAt(gitx *Context, commitSHA string) (*version.Version, string, error) {
380✔
447
        tags, err := GetTags(gitx, commitSHA)
380✔
448
        if err != nil {
380✔
449
                return nil, "", err
×
450
        }
×
451

452
        for _, tag := range tags {
731✔
453
                ver, e := version.NewVersion(tag)
351✔
454
                if e == nil && ver != nil {
702✔
455
                        return ver, tag, nil
351✔
456
                }
351✔
457
        }
458

459
        return nil, "", nil
29✔
460
}
461

462
// GetVersion gets the semantic version and its tag.
463
func GetVersion(gitx *Context, commitSHA string, matchPattern string) (v *version.Version, tag string, err error) {
×
464
        if commitSHA == HEAD {
×
465
                commitSHA, err = GetCommitSHA(gitx, HEAD)
×
466
                if err != nil {
×
467
                        return
×
468
                }
×
469
        }
470

471
        tag, err = gitx.Get("describe", "--tags", "--abbrev=0", "--match", matchPattern)
×
472
        if err != nil {
×
473
                return
×
474
        }
×
475
        ver := tag
×
476

×
477
        // Get number of commits ahead.
×
478
        commitsAhead, err := gitx.Get("rev-list", "--count", strs.Fmt("%s..%s", ver, commitSHA))
×
479
        if err != nil {
×
480
                return
×
481
        }
×
482

483
        if commitsAhead != "0" {
×
484
                ver = strs.Fmt("%s+%s.%s", ver, commitsAhead, commitSHA[:7])
×
485
        }
×
486

487
        ver = strings.TrimPrefix(ver, "v")
×
488
        v, err = version.NewVersion(ver)
×
489

×
490
        return v, tag, err
×
491
}
492

493
// GetCommitSHA gets the commit SHA1 of the ref.
494
func GetCommitSHA(gitx *Context, ref string) (string, error) {
×
495
        if strs.IsEmpty(ref) {
×
496
                ref = HEAD
×
497
        }
×
498

499
        return gitx.Get("rev-parse", ref)
×
500
}
501

502
// GetLFSConfigFile gets the LFS config file inside the repository and
503
// `true` if existing.
504
func GetLFSConfigFile(repoDir string) (string, bool) {
118✔
505
        s := path.Join(repoDir, ".lfsconfig")
118✔
506

118✔
507
        return s, cm.IsFile(s)
118✔
508
}
118✔
509

510
// IsLFSAvailable tells if git-lfs is available in the path.
511
func IsLFSAvailable() bool {
1,062✔
512
        _, err := exec.LookPath("git-lfs")
1,062✔
513

1,062✔
514
        return err == nil
1,062✔
515
}
1,062✔
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