코드를 커밋하기 전에 coding convention에 맞지 않으면 에러를 내고 commit을 안되게 하는 pre-commit이란 툴이 있다. 


pre-commit이 지원하는 언어/툴은 다음과 같다. 자바, 스칼라는 없지만, go, python, ruby, rust, swift 등이 있다.




docker

docker_image

fail

golang

node

python

python_venv

ruby

rust

swift

pcre

pygrep

script

system






아래와 같이 설치를 진행한다.




$ pip install pre-commit


$ cd kemi_api_디렉토리


$ pre-commit


$ vi .pre-commit-config.yaml

repos:

-   repo: https://github.com/pre-commit/pre-commit-hooks

    rev: v2.0.0

    hooks:

    -   id: trailing-whitespace

    -   id: end-of-file-fixer

    -   id: fix-encoding-pragma

    -   id: double-quote-string-fixer

    -   id: requirements-txt-fixer


$ pre-commit install




/common/exceptions.py 파일에 '# -*- coding: utf-8 -*-' 이 포함되어 있는데. 

일부러 그 주석만 삭제하고 다음 커맨드를 실행한다.




$ git add .

$ git commit -m 'test'

gTrim Trailing Whitespace.................................................Passed

Fix End of Files.........................................................Passed

Fix python encoding pragma...............................................Failed

hookid: fix-encoding-pragma


Files were modified by this hook. Additional output:


Added `# -*- coding: utf-8 -*-` to /common/exceptions.py


Fix double quoted strings................................................Passed

Fix requirements.txt.................................(no files to check)Skipped



파이썬 인코딩 부분에서 failed되고 알려준다. 

때로는 고쳐주기도 한다.




프로젝트 디렉토리의 .git/hook/pre-commit 파일이 하나 생긴다. 


[프로젝트/.git/hooks] ls -al pre-commit

-rwxr-xr-x  1 samuel.kim  staff  5257 11  9 18:19 pre-commit




이 파일로 인해서 git commit을 미리 체크한다.


hook에 대한 내용은 https://git-scm.com/book/ko/v1/Git%EB%A7%9E%EC%B6%A4-Git-%ED%9B%85를 참조한다.



#!/usr/bin/env python

"""File generated by pre-commit: https://pre-commit.com"""

from __future__ import print_function


import distutils.spawn

import os

import subprocess

import sys


# work around https://github.com/Homebrew/homebrew-core/issues/30445

os.environ.pop('__PYVENV_LAUNCHER__', None)


HERE = os.path.dirname(os.path.abspath(__file__))

Z40 = '0' * 40

ID_HASH = '138fd403232d2ddd5efb44317e38bf03'

# start templated

CONFIG = '.pre-commit-config.yaml'

HOOK_TYPE = 'pre-commit'

INSTALL_PYTHON = '/Users/samuel.kim/.pyenv/versions/3.7.0/bin/python3.7'

SKIP_ON_MISSING_CONFIG = False

# end templated



class EarlyExit(RuntimeError):

    pass



class FatalError(RuntimeError):

    pass



def _norm_exe(exe):

    """Necessary for shebang support on windows.


    roughly lifted from `identify.identify.parse_shebang`

    """

    with open(exe, 'rb') as f:

        if f.read(2) != b'#!':

            return ()

        try:

            first_line = f.readline().decode('UTF-8')

        except UnicodeDecodeError:

            return ()


        cmd = first_line.split()

        if cmd[0] == '/usr/bin/env':

            del cmd[0]

        return tuple(cmd)



def _run_legacy():

    if HOOK_TYPE == 'pre-push':

        stdin = getattr(sys.stdin, 'buffer', sys.stdin).read()

    else:

        stdin = None


    legacy_hook = os.path.join(HERE, '{}.legacy'.format(HOOK_TYPE))

    if os.access(legacy_hook, os.X_OK):

        cmd = _norm_exe(legacy_hook) + (legacy_hook,) + tuple(sys.argv[1:])

        proc = subprocess.Popen(cmd, stdin=subprocess.PIPE if stdin else None)

        proc.communicate(stdin)

        return proc.returncode, stdin

    else:

        return 0, stdin



def _validate_config():

    cmd = ('git', 'rev-parse', '--show-toplevel')

    top_level = subprocess.check_output(cmd).decode('UTF-8').strip()

    cfg = os.path.join(top_level, CONFIG)

    if os.path.isfile(cfg):

        pass

    elif SKIP_ON_MISSING_CONFIG or os.getenv('PRE_COMMIT_ALLOW_NO_CONFIG'):

        print(

            '`{}` config file not found. '

            'Skipping `pre-commit`.'.format(CONFIG),

        )

        raise EarlyExit()

    else:

        raise FatalError(

            'No {} file was found\n'

            '- To temporarily silence this, run '

            '`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n'

            '- To permanently silence this, install pre-commit with the '

            '--allow-missing-config option\n'

            '- To uninstall pre-commit run '

            '`pre-commit uninstall`'.format(CONFIG),

        )



def _exe():

    with open(os.devnull, 'wb') as devnull:

        for exe in (INSTALL_PYTHON, sys.executable):

            try:

                if not subprocess.call(

                        (exe, '-c', 'import pre_commit.main'),

                        stdout=devnull, stderr=devnull,

                ):

                    return (exe, '-m', 'pre_commit.main', 'run')

            except OSError:

                pass


    if distutils.spawn.find_executable('pre-commit'):

        return ('pre-commit', 'run')


    raise FatalError(

        '`pre-commit` not found.  Did you forget to activate your virtualenv?',

    )



def _rev_exists(rev):

    return not subprocess.call(('git', 'rev-list', '--quiet', rev))



def _pre_push(stdin):

    remote = sys.argv[1]


    opts = ()

    for line in stdin.decode('UTF-8').splitlines():

        _, local_sha, _, remote_sha = line.split()

        if local_sha == Z40:

            continue

        elif remote_sha != Z40 and _rev_exists(remote_sha):

            opts = ('--origin', local_sha, '--source', remote_sha)

        else:

            # ancestors not found in remote

            ancestors = subprocess.check_output((

                'git', 'rev-list', local_sha, '--topo-order', '--reverse',

                '--not', '--remotes={}'.format(remote),

            )).decode().strip()

            if not ancestors:

                continue

            else:

                first_ancestor = ancestors.splitlines()[0]

                cmd = ('git', 'rev-list', '--max-parents=0', local_sha)

                roots = set(subprocess.check_output(cmd).decode().splitlines())

                if first_ancestor in roots:

                    # pushing the whole tree including root commit

                    opts = ('--all-files',)

                else:

                    cmd = ('git', 'rev-parse', '{}^'.format(first_ancestor))

                    source = subprocess.check_output(cmd).decode().strip()

                    opts = ('--origin', local_sha, '--source', source)


    if opts:

        return opts

    else:

        # An attempt to push an empty changeset

        raise EarlyExit()



def _opts(stdin):

    fns = {

        'commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]),

        'pre-commit': lambda _: (),

        'pre-push': _pre_push,

    }

    stage = HOOK_TYPE.replace('pre-', '')

    return ('--config', CONFIG, '--hook-stage', stage) + fns[HOOK_TYPE](stdin)



def main():

    retv, stdin = _run_legacy()

    try:

        _validate_config()

        return retv | subprocess.call(_exe() + _opts(stdin))

    except EarlyExit:

        return retv

    except FatalError as e:

        print(e.args[0])

        return 1



if __name__ == '__main__':

    exit(main())




만약 flake8라는 파이썬 코딩 컨벤션 강제 툴을 적용하려면. .pre-commit-config.yaml 파일의 hooks id를 추가한다.



$ vi .pre-commit-config.yaml

repos:

-   repo: https://github.com/pre-commit/pre-commit-hooks

    rev: v2.0.0

    hooks:

    -   id: trailing-whitespace

    -   id: end-of-file-fixer

    -   id: fix-encoding-pragma

    -   id: double-quote-string-fixer

    -   id: requirements-txt-fixer

    -   id: flake8






Posted by '김용환'
,