From f13756c800a0d94359a634cf0b9093e8226dc642 Mon Sep 17 00:00:00 2001 From: Valentine Kiselev Date: Sun, 7 Aug 2022 19:16:18 +0300 Subject: [PATCH] feature: Add support for git lfs (#306) * feature: Add support for Git LFS Signed-off-by: Valentin Kiselev * docs: Add notes about Git LFS Signed-off-by: Valentin Kiselev --- docs/full_guide.md | 9 +++ internal/git/lfs.go | 35 ++++++++++++ internal/lefthook/run.go | 7 +-- internal/lefthook/runner/execute_unix.go | 9 +++ internal/lefthook/runner/execute_windows.go | 8 +++ internal/lefthook/runner/executor.go | 1 + internal/lefthook/runner/runner.go | 62 ++++++++++++++++++++- internal/lefthook/runner/runner_test.go | 28 ++++++---- 8 files changed, 139 insertions(+), 20 deletions(-) create mode 100644 internal/git/lfs.go diff --git a/docs/full_guide.md b/docs/full_guide.md index 18645384..b6eed910 100644 --- a/docs/full_guide.md +++ b/docs/full_guide.md @@ -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: diff --git a/internal/git/lfs.go b/internal/git/lfs.go new file mode 100644 index 00000000..142e8bf8 --- /dev/null +++ b/internal/git/lfs.go @@ -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 +} diff --git a/internal/lefthook/run.go b/internal/lefthook/run.go index 47dc6022..d1facd78 100644 --- a/internal/lefthook/run.go +++ b/internal/lefthook/run.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "os" - "path/filepath" "strings" "time" @@ -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) }() diff --git a/internal/lefthook/runner/execute_unix.go b/internal/lefthook/runner/execute_unix.go index 4dd87158..1f4d2370 100644 --- a/internal/lefthook/runner/execute_unix.go +++ b/internal/lefthook/runner/execute_unix.go @@ -6,6 +6,7 @@ package runner import ( "bytes" "io" + "os" "os/exec" "path/filepath" "strings" @@ -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() +} diff --git a/internal/lefthook/runner/execute_windows.go b/internal/lefthook/runner/execute_windows.go index b1fe1425..59bef39a 100644 --- a/internal/lefthook/runner/execute_windows.go +++ b/internal/lefthook/runner/execute_windows.go @@ -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() +} diff --git a/internal/lefthook/runner/executor.go b/internal/lefthook/runner/executor.go index 7dccca56..4e10ba67 100644 --- a/internal/lefthook/runner/executor.go +++ b/internal/lefthook/runner/executor.go @@ -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 } diff --git a/internal/lefthook/runner/runner.go b/internal/lefthook/runner/runner.go index ae81e555..9dfde95f 100644 --- a/internal/lefthook/runner/runner.go +++ b/internal/lefthook/runner/runner.go @@ -1,6 +1,7 @@ package runner import ( + "errors" "fmt" "os" "path/filepath" @@ -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) { @@ -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 { diff --git a/internal/lefthook/runner/runner_test.go b/internal/lefthook/runner/runner_test.go index deb4ec19..09570933 100644 --- a/internal/lefthook/runner/runner_test.go +++ b/internal/lefthook/runner/runner_test.go @@ -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) @@ -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 @@ -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{}, @@ -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 @@ -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) } }) }