Skip to content

Commit

Permalink
feature: Add support for git lfs (#306)
Browse files Browse the repository at this point in the history
* feature: Add support for Git LFS

Signed-off-by: Valentin Kiselev <mrexox@evilmartians.com>

* docs: Add notes about Git LFS

Signed-off-by: Valentin Kiselev <mrexox@evilmartians.com>
  • Loading branch information
mrexox authored Aug 7, 2022
1 parent 4521c56 commit f13756c
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 20 deletions.
9 changes: 9 additions & 0 deletions docs/full_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,15 @@ SHA1=$3
# ...
```

## Git LFS support

Lefthook runs LFS hooks internally for the following hooks:

- post-checkout
- post-commit
- post-merge
- pre-push

## Change directory for script files

You can do this through this config keys:
Expand Down
35 changes: 35 additions & 0 deletions internal/git/lfs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package git

import (
"os/exec"
)

const (
LFSRequiredFile = ".lfs-required"
LFSConfigFile = ".lfsconfig"
)

var lfsHooks = [...]string{
"post-checkout",
"post-commit",
"post-merge",
"pre-push",
}

// IsLFSAvailable returns 'true' if git-lfs is installed.
func IsLFSAvailable() bool {
_, err := exec.LookPath("git-lfs")

return err == nil
}

// IsLFSHook returns whether the hookName is supported by Git LFS.
func IsLFSHook(hookName string) bool {
for _, lfsHookName := range lfsHooks {
if lfsHookName == hookName {
return true
}
}

return false
}
7 changes: 2 additions & 5 deletions internal/lefthook/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"

Expand Down Expand Up @@ -110,10 +109,8 @@ Run 'lefthook install' manually.`,

go func() {
run.RunAll(
[]string{
filepath.Join(cfg.SourceDir, hookName),
filepath.Join(cfg.SourceDirLocal, hookName),
},
hookName,
[]string{cfg.SourceDir, cfg.SourceDirLocal},
)
close(resultChan)
}()
Expand Down
9 changes: 9 additions & 0 deletions internal/lefthook/runner/execute_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package runner
import (
"bytes"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
Expand All @@ -30,3 +31,11 @@ func (e CommandExecutor) Execute(root string, args []string) (*bytes.Buffer, err

return out, command.Wait()
}

func (e CommandExecutor) RawExecute(command string, args ...string) error {
cmd := exec.Command(command, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

return cmd.Run()
}
8 changes: 8 additions & 0 deletions internal/lefthook/runner/execute_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@ func (e CommandExecutor) Execute(root string, args []string) (*bytes.Buffer, err
}
return &out, command.Wait()
}

func (e CommandExecutor) RawExecute(command string, args ...string) error {
cmd := exec.Command(command, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

return cmd.Run()
}
1 change: 1 addition & 0 deletions internal/lefthook/runner/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ import (
// It is used here for testing purpose mostly.
type Executor interface {
Execute(root string, args []string) (*bytes.Buffer, error)
RawExecute(command string, args ...string) error
}
62 changes: 59 additions & 3 deletions internal/lefthook/runner/runner.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package runner

import (
"errors"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -50,15 +51,25 @@ func NewRunner(
}
}

func (r *Runner) RunAll(scriptDirs []string) {
func (r *Runner) RunAll(hookName string, sourceDirs []string) {
if err := r.runLFSHook(hookName); err != nil {
log.Error(err)
}

log.StartSpinner()
defer log.StopSpinner()

scriptDirs := make([]string, len(sourceDirs))
for _, sourceDir := range sourceDirs {
scriptDirs = append(scriptDirs, filepath.Join(
r.repo.RootPath, sourceDir, hookName,
))
}

for _, dir := range scriptDirs {
r.runScripts(dir)
}
r.runCommands()

log.StopSpinner()
}

func (r *Runner) fail(name, text string) {
Expand All @@ -70,6 +81,51 @@ func (r *Runner) success(name string) {
r.resultChan <- resultSuccess(name)
}

func (r *Runner) runLFSHook(hookName string) error {
if !git.IsLFSHook(hookName) {
return nil
}

lfsRequiredFile := filepath.Join(r.repo.RootPath, git.LFSRequiredFile)
lfsConfigFile := filepath.Join(r.repo.RootPath, git.LFSConfigFile)

requiredExists, err := afero.Exists(r.repo.Fs, lfsRequiredFile)
if err != nil {
return err
}
configExists, err := afero.Exists(r.repo.Fs, lfsConfigFile)
if err != nil {
return err
}

if git.IsLFSAvailable() {
log.Debugf(
"Executing LFS Hook: `git lfs %s %s", hookName, strings.Join(r.args, " "),
)
err := r.exec.RawExecute(
"git",
append(
[]string{"lfs", hookName},
r.args...,
)...,
)
if err != nil {
return errors.New("git-lfs command failed")
}
} else if requiredExists || configExists {
log.Errorf(
"This repository requires Git LFS, but 'git-lfs' wasn't found.\n"+
"Install 'git-lfs' or consider reviewing the files:\n"+
" - %s\n"+
" - %s\n",
lfsRequiredFile, lfsConfigFile,
)
return errors.New("git-lfs is required")
}

return nil
}

func (r *Runner) runScripts(dir string) {
files, err := afero.ReadDir(r.fs, dir) // ReadDir already sorts files by .Name()
if err != nil || len(files) == 0 {
Expand Down
28 changes: 16 additions & 12 deletions internal/lefthook/runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ func (e TestExecutor) Execute(root string, args []string) (out *bytes.Buffer, er
return
}

func (e TestExecutor) RawExecute(command string, args ...string) error {
return nil
}

func TestRunAll(t *testing.T) {
hookName := "pre-commit"

root, err := filepath.Abs("src")
if err != nil {
t.Errorf("unexpected error: %s", err)
Expand All @@ -44,7 +50,7 @@ func TestRunAll(t *testing.T) {
for i, tt := range [...]struct {
name string
args []string
scriptDirs []string
sourceDirs []string
existingFiles []string
hook *config.Hook
success, fail []Result
Expand Down Expand Up @@ -179,13 +185,11 @@ func TestRunAll(t *testing.T) {
fail: []Result{{Name: "test", Status: StatusErr, Text: "try 'success'"}},
},
{
name: "with simple scripts",
scriptDirs: []string{
filepath.Join(root, config.DefaultSourceDir),
},
name: "with simple scripts",
sourceDirs: []string{config.DefaultSourceDir},
existingFiles: []string{
filepath.Join(root, config.DefaultSourceDir, "script.sh"),
filepath.Join(root, config.DefaultSourceDir, "failing.js"),
filepath.Join(root, config.DefaultSourceDir, hookName, "script.sh"),
filepath.Join(root, config.DefaultSourceDir, hookName, "failing.js"),
},
hook: &config.Hook{
Commands: map[string]*config.Command{},
Expand Down Expand Up @@ -226,7 +230,7 @@ func TestRunAll(t *testing.T) {
}

t.Run(fmt.Sprintf("%d: %s", i, tt.name), func(t *testing.T) {
runner.RunAll(tt.scriptDirs)
runner.RunAll(hookName, tt.sourceDirs)
close(resultChan)

var success, fail []Result
Expand All @@ -238,12 +242,12 @@ func TestRunAll(t *testing.T) {
}
}

if !resultsMatch(success, tt.success) {
t.Errorf("success results are not matching")
if !resultsMatch(tt.success, success) {
t.Errorf("success results are not matching\n Need: %v\n Was: %v", tt.success, success)
}

if !resultsMatch(fail, tt.fail) {
t.Errorf("fail results are not matching")
if !resultsMatch(tt.fail, fail) {
t.Errorf("fail results are not matching:\n Need: %v\n Was: %v", tt.fail, fail)
}
})
}
Expand Down

0 comments on commit f13756c

Please sign in to comment.