Repository: pre-commit/pre-commit
Pull request #473 (2017-01-24 21:32:51)

Add first class support for golang hooks


Patch:
diff --git a/pre_commit/git.py b/pre_commit/git.py
index d3946c5..d4277e7 100644
--- a/pre_commit/git.py
+++ b/pre_commit/git.py
@@ -32,6 +32,11 @@ def get_git_dir(git_root):
     ))
 
 
+def get_remote_url(git_root):
+    ret = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root)[1]
+    return ret.strip()
+
+
 def is_in_merge_conflict():
     git_dir = get_git_dir('.')
     return (
diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py
index cc7af88..f441ddd 100644
--- a/pre_commit/languages/all.py
+++ b/pre_commit/languages/all.py
@@ -1,6 +1,7 @@
 from __future__ import unicode_literals
 
 from pre_commit.languages import docker
+from pre_commit.languages import golang
 from pre_commit.languages import node
 from pre_commit.languages import pcre
 from pre_commit.languages import python
@@ -43,6 +44,7 @@ from pre_commit.languages import system
 
 languages = {
     'docker': docker,
+    'golang': golang,
     'node': node,
     'pcre': pcre,
     'python': python,
diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py
index 2e9129a..fd24995 100644
--- a/pre_commit/languages/docker.py
+++ b/pre_commit/languages/docker.py
@@ -60,9 +60,8 @@ def install_environment(
     assert repo_cmd_runner.exists('Dockerfile'), (
         'No Dockerfile was found in the hook repository'
     )
-    assert version == 'default', (
-        'Pre-commit does not support language_version for docker '
-    )
+    helpers.assert_version_default('docker', version)
+    helpers.assert_no_additional_deps('docker', additional_dependencies)
     assert_docker_available()
 
     directory = repo_cmd_runner.path(
diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py
new file mode 100644
index 0000000..ff4775d
--- /dev/null
+++ b/pre_commit/languages/golang.py
@@ -0,0 +1,72 @@
+from __future__ import unicode_literals
+
+import contextlib
+import os.path
+
+from pre_commit import git
+from pre_commit.envcontext import envcontext
+from pre_commit.envcontext import Var
+from pre_commit.languages import helpers
+from pre_commit.util import clean_path_on_failure
+from pre_commit.util import cmd_output
+from pre_commit.xargs import xargs
+
+
+ENVIRONMENT_DIR = 'golangenv'
+
+
+def get_env_patch(venv):  # pragma: windows no cover
+    return (
+        ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))),
+    )
+
+
+@contextlib.contextmanager
+def in_env(repo_cmd_runner):  # pragma: windows no cover
+    envdir = repo_cmd_runner.path(
+        helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
+    )
+    with envcontext(get_env_patch(envdir)):
+        yield
+
+
+def guess_go_dir(remote_url):
+    if remote_url.endswith('.git'):
+        remote_url = remote_url[:-1 * len('.git')]
+    remote_url = remote_url.replace(':', '/')
+    looks_like_url = '//' in remote_url or '@' in remote_url
+    if looks_like_url:
+        _, _, remote_url = remote_url.rpartition('//')
+        _, _, remote_url = remote_url.rpartition('@')
+        return remote_url
+    else:
+        return 'unknown_src_dir'
+
+
+def install_environment(
+        repo_cmd_runner,
+        version='default',
+        additional_dependencies=(),
+):  # pragma: windows no cover
+    helpers.assert_version_default('golang', version)
+    helpers.assert_no_additional_deps('golang', additional_dependencies)
+    directory = repo_cmd_runner.path(
+        helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
+    )
+
+    with clean_path_on_failure(directory):
+        remote = git.get_remote_url(repo_cmd_runner.path())
+        repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote))
+
+        # Clone into the goenv we'll create
+        helpers.run_setup_cmd(
+            repo_cmd_runner, ('git', 'clone', '.', repo_src_dir),
+        )
+
+        env = dict(os.environ, GOPATH=directory)
+        cmd_output('go', 'get', './...', cwd=repo_src_dir, env=env)
+
+
+def run_hook(repo_cmd_runner, hook, file_args):  # pragma: windows no cover
+    with in_env(repo_cmd_runner):
+        return xargs(helpers.to_cmd(hook), file_args)
diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py
index a035c47..a6c93de 100644
--- a/pre_commit/languages/helpers.py
+++ b/pre_commit/languages/helpers.py
@@ -18,3 +18,18 @@ def environment_dir(ENVIRONMENT_DIR, language_version):
 
 def to_cmd(hook):
     return tuple(shlex.split(hook['entry'])) + tuple(hook['args'])
+
+
+def assert_version_default(binary, version):
+    if version != 'default':
+        raise AssertionError(
+            'For now, pre-commit requires system-installed {}'.format(binary),
+        )
+
+
+def assert_no_additional_deps(lang, additional_deps):
+    if additional_deps:
+        raise AssertionError(
+            'For now, pre-commit does not support '
+            'additional_dependencies for {}'.format(lang),
+        )
diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py
index d362e01..4d171c5 100644
--- a/pre_commit/languages/swift.py
+++ b/pre_commit/languages/swift.py
@@ -33,9 +33,8 @@ def install_environment(
         version='default',
         additional_dependencies=(),
 ):  # pragma: windows no cover
-    assert version == 'default', (
-        'Pre-commit does not support language_version for docker '
-    )
+    helpers.assert_version_default('swift', version)
+    helpers.assert_no_additional_deps('swift', additional_dependencies)
     directory = repo_cmd_runner.path(
         helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
     )
diff --git a/tox.ini b/tox.ini
index 4063b93..805e293 100644
--- a/tox.ini
+++ b/tox.ini
@@ -5,7 +5,7 @@ envlist = py27,py34,py35,pypy
 
 [testenv]
 deps = -rrequirements-dev.txt
-passenv = HOME HOMEPATH PROGRAMDATA TERM
+passenv = GOROOT HOME HOMEPATH PROGRAMDATA TERM
 setenv =
     VIRTUALENV_NO_DOWNLOAD = 1
     GIT_AUTHOR_NAME = "test"


Test patch:
diff --git a/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..206733b
--- /dev/null
+++ b/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,5 @@
+-   id: golang-hook
+    name: golang example hook
+    entry: golang-hello-world
+    language: golang
+    files: ''
diff --git a/testing/resources/golang_hooks_repo/golang-hello-world/main.go b/testing/resources/golang_hooks_repo/golang-hello-world/main.go
new file mode 100644
index 0000000..1e3c591
--- /dev/null
+++ b/testing/resources/golang_hooks_repo/golang-hello-world/main.go
@@ -0,0 +1,17 @@
+package main
+
+
+import (
+	"fmt"
+	"github.com/BurntSushi/toml"
+)
+
+type Config struct {
+	What string
+}
+
+func main() {
+	var conf Config
+	toml.Decode("What = 'world'\n", &conf)
+	fmt.Printf("hello %v\n", conf.What)
+}
diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py
new file mode 100644
index 0000000..e0c9ab4
--- /dev/null
+++ b/tests/languages/golang_test.py
@@ -0,0 +1,22 @@
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import pytest
+
+from pre_commit.languages.golang import guess_go_dir
+
+
+@pytest.mark.parametrize(
+    ('url', 'expected'),
+    (
+        ('/im/a/path/on/disk', 'unknown_src_dir'),
+        ('git@github.com:golang/lint', 'github.com/golang/lint'),
+        ('git://github.com/golang/lint', 'github.com/golang/lint'),
+        ('http://github.com/golang/lint', 'github.com/golang/lint'),
+        ('https://github.com/golang/lint', 'github.com/golang/lint'),
+        ('ssh://git@github.com/golang/lint', 'github.com/golang/lint'),
+        ('git@github.com:golang/lint.git', 'github.com/golang/lint'),
+    ),
+)
+def test_guess_go_dir(url, expected):
+    assert guess_go_dir(url) == expected
diff --git a/tests/repository_test.py b/tests/repository_test.py
index 203852c..9830c58 100644
--- a/tests/repository_test.py
+++ b/tests/repository_test.py
@@ -257,6 +257,14 @@ def test_swift_hook(tempdir_factory, store):
     )
 
 
+@pytest.mark.integration
+def test_golang_hook(tempdir_factory, store):
+    _test_hook_repo(
+        tempdir_factory, store, 'golang_hooks_repo',
+        'golang-hook', [], b'hello world\n',
+    )
+
+
 @pytest.mark.integration
 def test_missing_executable(tempdir_factory, store):
     _test_hook_repo(