From 06ebcae8a68631a720251b59b9c65c040b6dad92 Mon Sep 17 00:00:00 2001 From: Andrew Hare Date: Tue, 25 Feb 2020 12:40:40 -0700 Subject: [PATCH 01/13] Initial design of using pre-commit for Git hooks --- dvc/command/install.py | 9 ++++- dvc/repo/install.py | 4 +- dvc/scm/base.py | 9 ++++- dvc/scm/git/__init__.py | 81 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 97 insertions(+), 6 deletions(-) diff --git a/dvc/command/install.py b/dvc/command/install.py index 1dea3fcb07..39c33802e6 100644 --- a/dvc/command/install.py +++ b/dvc/command/install.py @@ -11,7 +11,7 @@ class CmdInstall(CmdBase): def run(self): try: - self.repo.install() + self.repo.install(self.args.use_pre_commit_tool) except Exception: logger.exception("failed to install DVC Git hooks") return 1 @@ -27,4 +27,11 @@ def add_parser(subparsers, parent_parser): help=INSTALL_HELP, formatter_class=argparse.RawDescriptionHelpFormatter, ) + install_parser.add_argument( + "--use-pre-commit-tool", + action="store_true", + default=False, + help="Install DVC hooks using pre-commit " + "(https://pre-commit.com) if it is installed.", + ) install_parser.set_defaults(func=CmdInstall) diff --git a/dvc/repo/install.py b/dvc/repo/install.py index cff23a15ff..38612d608d 100644 --- a/dvc/repo/install.py +++ b/dvc/repo/install.py @@ -1,2 +1,2 @@ -def install(self): - self.scm.install() +def install(self, use_pre_commit_tool): + self.scm.install(use_pre_commit_tool) diff --git a/dvc/scm/base.py b/dvc/scm/base.py index 779b1009fa..21f2add151 100644 --- a/dvc/scm/base.py +++ b/dvc/scm/base.py @@ -114,8 +114,13 @@ def list_all_commits(self): # pylint: disable=no-self-use """Returns a list of commits in the repo.""" return [] - def install(self): - """Adds dvc commands to SCM hooks for the repo.""" + def install(self, use_pre_commit_tool): + """ + Adds dvc commands to SCM hooks for the repo. + + If use_pre_commit_tool is set and pre-commit is + installed it will be used to install the hooks. + """ def cleanup_ignores(self): """ diff --git a/dvc/scm/git/__init__.py b/dvc/scm/git/__init__.py index efcf5b5aa6..81c85b2b1e 100644 --- a/dvc/scm/git/__init__.py +++ b/dvc/scm/git/__init__.py @@ -2,10 +2,14 @@ import logging import os +from shutil import which +from subprocess import check_call +import yaml from funcy import cached_property from pathspec.patterns import GitWildMatchPattern +from dvc.exceptions import DvcException from dvc.exceptions import GitHookAlreadyExistsError from dvc.scm.base import Base from dvc.scm.base import CloneError, FileNotInRepoError, RevError, SCMError @@ -273,7 +277,82 @@ def _install_hook(self, name, preconditions, cmd): os.chmod(hook, 0o777) - def install(self): + def _install_with_pre_commit_tool(self): + if not which("pre-commit"): + raise DvcException("pre-commit is not installed") + + check_call("pre-commit install", shell=True) + + pre_commit_entry = ( + 'bash -c "#!/bin/sh\\n' + 'if [ -n "$(git ls-files --full-name .dvc)" ]; ' + 'then exec dvc status; fi"' + ) + + pre_push_entry = ( + 'bash -c "#!/bin/sh\\n' + 'if [ -n "$(git ls-files --full-name .dvc)" ]; å' + 'then exec dvc push; fi"' + ) + + # post_checkout_entry = ( + # 'bash -c "#!/bin/sh\\n' + # 'if [ -n "$(git ls-files --full-name .dvc)" ] ' + # '&& [ "$3" = "1" ] && [ ! -d .git/rebase-merge ]; ' + # 'then exec dvc checkout; fi"' + # ) + + conf = { + "repos": [ + { + "repo": "local", + "hooks": [ + { + "id": "dvc-pre-commit", + "name": "DVC Pre Commit", + "entry": pre_commit_entry, + "language": "system", + "stages": ["commit"], + }, + { + "id": "dvc-pre-push", + "name": "DVC Pre Push", + "entry": pre_push_entry, + "language": "system", + "stages": ["push"], + }, + # TODO(andrewhare): Enable this with latest + # pre-commit changes. + # { + # "id": "dvc-post-checkout", + # "name": "DVC Post Checkout", + # "entry": post_checkout_entry, + # "language": "system", + # "always_run": True, + # "stages": ["post-checkout"], + # } + ], + } + ] + } + + conf_yaml = ".pre-commit-config.yaml" + with open(conf_yaml, "w+") as f: + existing_conf = yaml.safe_load(f) + if existing_conf: + # TODO(andrewhare): Do an intelligent merge of `conf` + # and `existing_conf` if the user already has a + # pre-commit config YAML. + pass + yaml.dump(conf, f) + + os.chmod(conf_yaml, 0o777) + + def install(self, use_pre_commit_tool): + if use_pre_commit_tool: + self._install_with_pre_commit_tool() + return + self._verify_dvc_hooks() self._install_hook( From 0327d6375e1c68888a7d84d09f06323cc377a0b4 Mon Sep 17 00:00:00 2001 From: Andrew Hare Date: Thu, 5 Mar 2020 15:49:33 -0700 Subject: [PATCH 02/13] Refactor to reuse existing code and provide merge capabilities --- dvc/scm/git/__init__.py | 130 ++++++++++--------------- dvc/scm/git/pre_commit_tool.py | 40 ++++++++ tests/unit/scm/test_pre_commit_tool.py | 31 ++++++ 3 files changed, 122 insertions(+), 79 deletions(-) create mode 100644 dvc/scm/git/pre_commit_tool.py create mode 100644 tests/unit/scm/test_pre_commit_tool.py diff --git a/dvc/scm/git/__init__.py b/dvc/scm/git/__init__.py index 81c85b2b1e..fe9a747f6e 100644 --- a/dvc/scm/git/__init__.py +++ b/dvc/scm/git/__init__.py @@ -2,9 +2,10 @@ import logging import os +import yaml from shutil import which from subprocess import check_call -import yaml +from pathlib import Path from funcy import cached_property from pathspec.patterns import GitWildMatchPattern @@ -13,6 +14,8 @@ from dvc.exceptions import GitHookAlreadyExistsError from dvc.scm.base import Base from dvc.scm.base import CloneError, FileNotInRepoError, RevError, SCMError +from dvc.scm.git.pre_commit_tool import pre_commit_tool_conf +from dvc.scm.git.pre_commit_tool import merge_pre_commit_tool_confs from dvc.scm.git.tree import GitTree from dvc.utils import fix_env, is_binary, relpath from dvc.utils.fs import path_isin @@ -257,7 +260,7 @@ def list_tags(self): def list_all_commits(self): return [c.hexsha for c in self.repo.iter_commits("--all")] - def _install_hook(self, name, preconditions, cmd): + def _install_hook(self, name, preconditions, cmd, hook_path_fn): # only run in dvc repo in_dvc_repo = '[ -n "$(git ls-files --full-name .dvc)" ]' @@ -265,7 +268,7 @@ def _install_hook(self, name, preconditions, cmd): " && ".join([in_dvc_repo] + preconditions), cmd ) - hook = self._hook_path(name) + hook = hook_path_fn(name) if os.path.isfile(hook): with open(hook, "r+") as fobj: @@ -277,83 +280,15 @@ def _install_hook(self, name, preconditions, cmd): os.chmod(hook, 0o777) - def _install_with_pre_commit_tool(self): - if not which("pre-commit"): - raise DvcException("pre-commit is not installed") - - check_call("pre-commit install", shell=True) - - pre_commit_entry = ( - 'bash -c "#!/bin/sh\\n' - 'if [ -n "$(git ls-files --full-name .dvc)" ]; ' - 'then exec dvc status; fi"' - ) - - pre_push_entry = ( - 'bash -c "#!/bin/sh\\n' - 'if [ -n "$(git ls-files --full-name .dvc)" ]; å' - 'then exec dvc push; fi"' - ) + def install(self, use_pre_commit_tool): + self._verify_dvc_hooks() - # post_checkout_entry = ( - # 'bash -c "#!/bin/sh\\n' - # 'if [ -n "$(git ls-files --full-name .dvc)" ] ' - # '&& [ "$3" = "1" ] && [ ! -d .git/rebase-merge ]; ' - # 'then exec dvc checkout; fi"' - # ) - - conf = { - "repos": [ - { - "repo": "local", - "hooks": [ - { - "id": "dvc-pre-commit", - "name": "DVC Pre Commit", - "entry": pre_commit_entry, - "language": "system", - "stages": ["commit"], - }, - { - "id": "dvc-pre-push", - "name": "DVC Pre Push", - "entry": pre_push_entry, - "language": "system", - "stages": ["push"], - }, - # TODO(andrewhare): Enable this with latest - # pre-commit changes. - # { - # "id": "dvc-post-checkout", - # "name": "DVC Post Checkout", - # "entry": post_checkout_entry, - # "language": "system", - # "always_run": True, - # "stages": ["post-checkout"], - # } - ], - } - ] - } - - conf_yaml = ".pre-commit-config.yaml" - with open(conf_yaml, "w+") as f: - existing_conf = yaml.safe_load(f) - if existing_conf: - # TODO(andrewhare): Do an intelligent merge of `conf` - # and `existing_conf` if the user already has a - # pre-commit config YAML. - pass - yaml.dump(conf, f) - - os.chmod(conf_yaml, 0o777) + hook_path_fn = self._hook_path - def install(self, use_pre_commit_tool): if use_pre_commit_tool: - self._install_with_pre_commit_tool() - return - - self._verify_dvc_hooks() + hook_path_fn = self._pre_commit_tool_hook_path + path = Path(self._pre_commit_tool_hooks_home) + path.mkdir(parents=True, exist_ok=True) self._install_hook( "post-checkout", @@ -366,9 +301,32 @@ def install(self, use_pre_commit_tool): "[ ! -d .git/rebase-merge ]", ], "checkout", + hook_path_fn, ) - self._install_hook("pre-commit", [], "status") - self._install_hook("pre-push", [], "push") + self._install_hook("pre-commit", [], "status", hook_path_fn) + self._install_hook("pre-push", [], "push", hook_path_fn) + + if use_pre_commit_tool: + self._integrate_pre_commit_tool() + + def _integrate_pre_commit_tool(self): + if not which("pre-commit"): + raise DvcException("pre-commit is not installed") + + conf = pre_commit_tool_conf( + self._pre_commit_tool_hook_path("pre-commit"), + self._pre_commit_tool_hook_path("push"), + self._pre_commit_tool_hook_path("post-checkout"), + ) + + conf_yaml = os.path.join(self.root_dir, ".pre-commit-config.yaml") + if not os.path.isfile(conf_yaml): + check_call("pre-commit install", shell=True) + + with open(conf_yaml, "w+") as conf_yaml_f: + existing_conf = yaml.safe_load(conf_yaml_f) + conf = merge_pre_commit_tool_confs(existing_conf, conf) + yaml.dump(conf, conf_yaml_f) def cleanup_ignores(self): for path in self.ignored_paths: @@ -451,9 +409,17 @@ def close(self): def _hooks_home(self): return os.path.join(self.root_dir, self.GIT_DIR, "hooks") + @cached_property + def _pre_commit_tool_hooks_home(self): + # TODO(andrewhare): Is there a const somewhere for ".dvc/tmp"? + return os.path.join(".dvc", "tmp", "hooks") + def _hook_path(self, name): return os.path.join(self._hooks_home, name) + def _pre_commit_tool_hook_path(self, name): + return os.path.join(self._pre_commit_tool_hooks_home, name) + def _verify_hook(self, name): if os.path.exists(self._hook_path(name)): raise GitHookAlreadyExistsError(name) @@ -462,3 +428,9 @@ def _verify_dvc_hooks(self): self._verify_hook("post-checkout") self._verify_hook("pre-commit") self._verify_hook("pre-push") + + def _verify_pre_commit_tool(self): + if not which("pre-commit"): + raise DvcException("pre-commit is not installed") + + check_call("pre-commit install", shell=True) diff --git a/dvc/scm/git/pre_commit_tool.py b/dvc/scm/git/pre_commit_tool.py new file mode 100644 index 0000000000..45244b14a7 --- /dev/null +++ b/dvc/scm/git/pre_commit_tool.py @@ -0,0 +1,40 @@ +def pre_commit_tool_conf(pre_commit_path, push_path, post_checkout_path): + return { + "repos": [ + { + "repo": "local", + "hooks": [ + { + "id": "dvc-pre-commit", + "name": "DVC Pre Commit", + "entry": pre_commit_path, + "language": "script", + "stages": ["commit"], + }, + { + "id": "dvc-pre-push", + "name": "DVC Pre Push", + "entry": push_path, + "language": "script", + "stages": ["push"], + }, + { + "id": "dvc-post-checkout", + "name": "DVC Post Checkout", + "entry": post_checkout_path, + "language": "script", + "stages": ["checkout"], + }, + ], + } + ] + } + + +def merge_pre_commit_tool_confs(existing_conf, conf): + if not existing_conf or not "repos" in existing_conf: + return conf + + existing_conf["repos"].append(conf["repos"][0]) + return existing_conf + diff --git a/tests/unit/scm/test_pre_commit_tool.py b/tests/unit/scm/test_pre_commit_tool.py new file mode 100644 index 0000000000..f353c0c97f --- /dev/null +++ b/tests/unit/scm/test_pre_commit_tool.py @@ -0,0 +1,31 @@ +from dvc.scm.git.pre_commit_tool import pre_commit_tool_conf +from dvc.scm.git.pre_commit_tool import merge_pre_commit_tool_confs + +from unittest import TestCase + + +class TestPreCommitTool(TestCase): + def setUp(self): + self.conf = pre_commit_tool_conf("a", "b", "c") + + def test_merge_pre_commit_tool_confs_empty(self): + existing_conf = None + merged_conf = merge_pre_commit_tool_confs(existing_conf, self.conf) + self.assertEqual(self.conf, merged_conf) + + def test_merge_pre_commit_tool_confs_invalid_yaml(self): + existing_conf = "some invalid yaml" + merged_conf = merge_pre_commit_tool_confs(existing_conf, self.conf) + self.assertEqual(self.conf, merged_conf) + + def test_merge_pre_commit_tool_confs_no_repos(self): + existing_conf = {"foo": [1, 2, 3]} + merged_conf = merge_pre_commit_tool_confs(existing_conf, self.conf) + self.assertEqual(self.conf, merged_conf) + + def test_merge_pre_commit_tool_confs(self): + existing_conf = {"repos": [{}]} + merged_conf = merge_pre_commit_tool_confs(existing_conf, self.conf) + # Merging the new conf in should append the new repo to the end of + # the existing repos array on the existing conf. + self.assertEqual(self.conf["repos"][0], merged_conf["repos"][1]) From 083421306b8740a79be8810b468a7a883bbfd126 Mon Sep 17 00:00:00 2001 From: Andrew Hare Date: Thu, 5 Mar 2020 16:15:06 -0700 Subject: [PATCH 03/13] Linter fixes --- dvc/scm/git/pre_commit_tool.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dvc/scm/git/pre_commit_tool.py b/dvc/scm/git/pre_commit_tool.py index 45244b14a7..01143e4da4 100644 --- a/dvc/scm/git/pre_commit_tool.py +++ b/dvc/scm/git/pre_commit_tool.py @@ -32,9 +32,8 @@ def pre_commit_tool_conf(pre_commit_path, push_path, post_checkout_path): def merge_pre_commit_tool_confs(existing_conf, conf): - if not existing_conf or not "repos" in existing_conf: + if not existing_conf or "repos" not in existing_conf: return conf existing_conf["repos"].append(conf["repos"][0]) return existing_conf - From 62651d1bd957f707c5b48331329e07d69741e313 Mon Sep 17 00:00:00 2001 From: Andrew Hare Date: Thu, 5 Mar 2020 16:40:50 -0700 Subject: [PATCH 04/13] More cleanup --- dvc/scm/git/__init__.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/dvc/scm/git/__init__.py b/dvc/scm/git/__init__.py index fe9a747f6e..ea1e32b516 100644 --- a/dvc/scm/git/__init__.py +++ b/dvc/scm/git/__init__.py @@ -260,7 +260,8 @@ def list_tags(self): def list_all_commits(self): return [c.hexsha for c in self.repo.iter_commits("--all")] - def _install_hook(self, name, preconditions, cmd, hook_path_fn): + @staticmethod + def _install_hook(name, preconditions, cmd, hook_path_fn): # only run in dvc repo in_dvc_repo = '[ -n "$(git ls-files --full-name .dvc)" ]' @@ -287,7 +288,7 @@ def install(self, use_pre_commit_tool): if use_pre_commit_tool: hook_path_fn = self._pre_commit_tool_hook_path - path = Path(self._pre_commit_tool_hooks_home) + path = Path(self._pre_commit_tool_hooks_home()) path.mkdir(parents=True, exist_ok=True) self._install_hook( @@ -321,7 +322,7 @@ def _integrate_pre_commit_tool(self): conf_yaml = os.path.join(self.root_dir, ".pre-commit-config.yaml") if not os.path.isfile(conf_yaml): - check_call("pre-commit install", shell=True) + check_call(["pre-commit", "install"]) with open(conf_yaml, "w+") as conf_yaml_f: existing_conf = yaml.safe_load(conf_yaml_f) @@ -409,16 +410,15 @@ def close(self): def _hooks_home(self): return os.path.join(self.root_dir, self.GIT_DIR, "hooks") - @cached_property - def _pre_commit_tool_hooks_home(self): - # TODO(andrewhare): Is there a const somewhere for ".dvc/tmp"? + @staticmethod + def _pre_commit_tool_hooks_home(): return os.path.join(".dvc", "tmp", "hooks") def _hook_path(self, name): return os.path.join(self._hooks_home, name) def _pre_commit_tool_hook_path(self, name): - return os.path.join(self._pre_commit_tool_hooks_home, name) + return os.path.join(self._pre_commit_tool_hooks_home(), name) def _verify_hook(self, name): if os.path.exists(self._hook_path(name)): @@ -428,9 +428,3 @@ def _verify_dvc_hooks(self): self._verify_hook("post-checkout") self._verify_hook("pre-commit") self._verify_hook("pre-push") - - def _verify_pre_commit_tool(self): - if not which("pre-commit"): - raise DvcException("pre-commit is not installed") - - check_call("pre-commit install", shell=True) From 880f51a329f8337ce2194d48c3b5d176d1a955ff Mon Sep 17 00:00:00 2001 From: Ruslan Kuprieiev Date: Mon, 16 Mar 2020 13:14:09 +0200 Subject: [PATCH 05/13] scm: make use_precommit_tool optional --- dvc/scm/base.py | 2 +- dvc/scm/git/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dvc/scm/base.py b/dvc/scm/base.py index 21f2add151..0806b25e1e 100644 --- a/dvc/scm/base.py +++ b/dvc/scm/base.py @@ -114,7 +114,7 @@ def list_all_commits(self): # pylint: disable=no-self-use """Returns a list of commits in the repo.""" return [] - def install(self, use_pre_commit_tool): + def install(self, use_pre_commit_tool=False): """ Adds dvc commands to SCM hooks for the repo. diff --git a/dvc/scm/git/__init__.py b/dvc/scm/git/__init__.py index ea1e32b516..0c6c506534 100644 --- a/dvc/scm/git/__init__.py +++ b/dvc/scm/git/__init__.py @@ -281,7 +281,7 @@ def _install_hook(name, preconditions, cmd, hook_path_fn): os.chmod(hook, 0o777) - def install(self, use_pre_commit_tool): + def install(self, use_pre_commit_tool=False): self._verify_dvc_hooks() hook_path_fn = self._hook_path From 13d4ac5ee5a3423487980d1c3663ab666bbea92c Mon Sep 17 00:00:00 2001 From: Ruslan Kuprieiev Date: Mon, 16 Mar 2020 13:17:52 +0200 Subject: [PATCH 06/13] dvc: install our own hooks --- .pre-commit-config.yaml | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d27f67a26a..aaa9727127 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,21 @@ repos: -- repo: https://github.com/ambv/black - rev: 19.10b0 - hooks: - - id: black - language_version: python3 -- repo: https://gitlab.com/pycqa/flake8 - rev: master - hooks: - - id: flake8 - language_version: python3 -- repo: https://github.com/lovesegfault/beautysh - rev: master - hooks: - - id: beautysh - language_version: python3 - args: [-i, '2'] # 2-space indentaion +- hooks: + - entry: .dvc/tmp/hooks/pre-commit + id: dvc-pre-commit + language: script + name: DVC Pre Commit + stages: + - commit + - entry: .dvc/tmp/hooks/push + id: dvc-pre-push + language: script + name: DVC Pre Push + stages: + - push + - entry: .dvc/tmp/hooks/post-checkout + id: dvc-post-checkout + language: script + name: DVC Post Checkout + stages: + - post-checkout + repo: local From e8b037e8f31626f862961d4fb57b08bf0ef5193f Mon Sep 17 00:00:00 2001 From: Ruslan Kuprieiev Date: Mon, 16 Mar 2020 13:18:33 +0200 Subject: [PATCH 07/13] install: fix typo --- dvc/scm/git/pre_commit_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dvc/scm/git/pre_commit_tool.py b/dvc/scm/git/pre_commit_tool.py index 01143e4da4..17f018d078 100644 --- a/dvc/scm/git/pre_commit_tool.py +++ b/dvc/scm/git/pre_commit_tool.py @@ -23,7 +23,7 @@ def pre_commit_tool_conf(pre_commit_path, push_path, post_checkout_path): "name": "DVC Post Checkout", "entry": post_checkout_path, "language": "script", - "stages": ["checkout"], + "stages": ["post-checkout"], }, ], } From 497e0a6d06bf27a2ecd894180e8faf8e7d748553 Mon Sep 17 00:00:00 2001 From: Ruslan Kuprieiev Date: Mon, 16 Mar 2020 13:40:02 +0200 Subject: [PATCH 08/13] install: fix bugs in pre-commit integration --- .pre-commit-config.yaml | 18 ++++++++++++++++++ dvc/scm/git/__init__.py | 13 +++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aaa9727127..7d5db2f729 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,22 @@ repos: +- hooks: + - id: black + language_version: python3 + repo: https://github.com/ambv/black + rev: 19.10b0 +- hooks: + - id: flake8 + language_version: python3 + repo: https://gitlab.com/pycqa/flake8 + rev: master +- hooks: + - args: + - -i + - '2' + id: beautysh + language_version: python3 + repo: https://github.com/lovesegfault/beautysh + rev: master - hooks: - entry: .dvc/tmp/hooks/pre-commit id: dvc-pre-commit diff --git a/dvc/scm/git/__init__.py b/dvc/scm/git/__init__.py index 0c6c506534..2c063cd3d3 100644 --- a/dvc/scm/git/__init__.py +++ b/dvc/scm/git/__init__.py @@ -324,10 +324,15 @@ def _integrate_pre_commit_tool(self): if not os.path.isfile(conf_yaml): check_call(["pre-commit", "install"]) - with open(conf_yaml, "w+") as conf_yaml_f: - existing_conf = yaml.safe_load(conf_yaml_f) - conf = merge_pre_commit_tool_confs(existing_conf, conf) - yaml.dump(conf, conf_yaml_f) + existing_conf = {} + if os.path.exists(conf_yaml): + with open(conf_yaml, "r") as fobj: + existing_conf = yaml.safe_load(fobj) + + conf = merge_pre_commit_tool_confs(existing_conf, conf) + + with open(conf_yaml, "w+") as fobj: + yaml.dump(conf, fobj) def cleanup_ignores(self): for path in self.ignored_paths: From 770aad438289e5162892780fe9c003cf3e945495 Mon Sep 17 00:00:00 2001 From: Ruslan Kuprieiev Date: Mon, 16 Mar 2020 13:50:09 +0200 Subject: [PATCH 09/13] install: don't forget 'always_run' for post-checkout --- .pre-commit-config.yaml | 3 ++- dvc/scm/git/pre_commit_tool.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7d5db2f729..046f37b379 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,8 @@ repos: name: DVC Pre Push stages: - push - - entry: .dvc/tmp/hooks/post-checkout + - always_run: true + entry: .dvc/tmp/hooks/post-checkout id: dvc-post-checkout language: script name: DVC Post Checkout diff --git a/dvc/scm/git/pre_commit_tool.py b/dvc/scm/git/pre_commit_tool.py index 17f018d078..9b8a2a3ee3 100644 --- a/dvc/scm/git/pre_commit_tool.py +++ b/dvc/scm/git/pre_commit_tool.py @@ -24,6 +24,7 @@ def pre_commit_tool_conf(pre_commit_path, push_path, post_checkout_path): "entry": post_checkout_path, "language": "script", "stages": ["post-checkout"], + "always_run": True, }, ], } From 37a2576d9b70c2c94a113b2194bd5e7203e6f2b1 Mon Sep 17 00:00:00 2001 From: Ruslan Kuprieiev Date: Mon, 16 Mar 2020 13:52:32 +0200 Subject: [PATCH 10/13] install: no need to run pre-commit install if conf file doesn't exist --- dvc/scm/git/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dvc/scm/git/__init__.py b/dvc/scm/git/__init__.py index 2c063cd3d3..5d1db4249d 100644 --- a/dvc/scm/git/__init__.py +++ b/dvc/scm/git/__init__.py @@ -4,7 +4,6 @@ import os import yaml from shutil import which -from subprocess import check_call from pathlib import Path from funcy import cached_property @@ -321,8 +320,6 @@ def _integrate_pre_commit_tool(self): ) conf_yaml = os.path.join(self.root_dir, ".pre-commit-config.yaml") - if not os.path.isfile(conf_yaml): - check_call(["pre-commit", "install"]) existing_conf = {} if os.path.exists(conf_yaml): From 7aec2fe4e12ec46e22e197f4c2f56447f520c539 Mon Sep 17 00:00:00 2001 From: Ruslan Kuprieiev Date: Mon, 16 Mar 2020 13:57:25 +0200 Subject: [PATCH 11/13] install: fix bugs --- .pre-commit-config.yaml | 2 +- dvc/scm/git/__init__.py | 6 +++--- dvc/scm/git/pre_commit_tool.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 046f37b379..6b3dafef94 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: name: DVC Pre Commit stages: - commit - - entry: .dvc/tmp/hooks/push + - entry: .dvc/tmp/hooks/pre-push id: dvc-pre-push language: script name: DVC Pre Push diff --git a/dvc/scm/git/__init__.py b/dvc/scm/git/__init__.py index 5d1db4249d..fb721911f0 100644 --- a/dvc/scm/git/__init__.py +++ b/dvc/scm/git/__init__.py @@ -281,14 +281,14 @@ def _install_hook(name, preconditions, cmd, hook_path_fn): os.chmod(hook, 0o777) def install(self, use_pre_commit_tool=False): - self._verify_dvc_hooks() - hook_path_fn = self._hook_path if use_pre_commit_tool: hook_path_fn = self._pre_commit_tool_hook_path path = Path(self._pre_commit_tool_hooks_home()) path.mkdir(parents=True, exist_ok=True) + else: + self._verify_dvc_hooks() self._install_hook( "post-checkout", @@ -315,7 +315,7 @@ def _integrate_pre_commit_tool(self): conf = pre_commit_tool_conf( self._pre_commit_tool_hook_path("pre-commit"), - self._pre_commit_tool_hook_path("push"), + self._pre_commit_tool_hook_path("pre-push"), self._pre_commit_tool_hook_path("post-checkout"), ) diff --git a/dvc/scm/git/pre_commit_tool.py b/dvc/scm/git/pre_commit_tool.py index 9b8a2a3ee3..1fe4bf1c38 100644 --- a/dvc/scm/git/pre_commit_tool.py +++ b/dvc/scm/git/pre_commit_tool.py @@ -1,4 +1,4 @@ -def pre_commit_tool_conf(pre_commit_path, push_path, post_checkout_path): +def pre_commit_tool_conf(pre_commit_path, pre_push_path, post_checkout_path): return { "repos": [ { @@ -14,7 +14,7 @@ def pre_commit_tool_conf(pre_commit_path, push_path, post_checkout_path): { "id": "dvc-pre-push", "name": "DVC Pre Push", - "entry": push_path, + "entry": pre_push_path, "language": "script", "stages": ["push"], }, From 0a794956491e64b86d298b37517033867fa23e20 Mon Sep 17 00:00:00 2001 From: Ruslan Kuprieiev Date: Mon, 16 Mar 2020 16:33:48 +0200 Subject: [PATCH 12/13] install: use python hooks --- .pre-commit-config.yaml | 27 +++---- dvc/cli.py | 2 + dvc/command/git_hook.py | 127 +++++++++++++++++++++++++++++++++ dvc/scm/git/__init__.py | 119 ++++++++++++------------------ dvc/scm/git/pre_commit_tool.py | 40 ----------- tests/func/test_install.py | 6 +- 6 files changed, 191 insertions(+), 130 deletions(-) create mode 100644 dvc/command/git_hook.py delete mode 100644 dvc/scm/git/pre_commit_tool.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6b3dafef94..1a4be954b0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,23 +18,24 @@ repos: repo: https://github.com/lovesegfault/beautysh rev: master - hooks: - - entry: .dvc/tmp/hooks/pre-commit - id: dvc-pre-commit - language: script - name: DVC Pre Commit + - args: + - pre-commit + id: dvc + language_version: python3 stages: - commit - - entry: .dvc/tmp/hooks/pre-push - id: dvc-pre-push - language: script - name: DVC Pre Push + - args: + - pre-push + id: dvc + language_version: python3 stages: - push - always_run: true - entry: .dvc/tmp/hooks/post-checkout - id: dvc-post-checkout - language: script - name: DVC Post Checkout + args: + - post-checkout + id: dvc + language_version: python3 stages: - post-checkout - repo: local + repo: https://github.com/andrewhare/dvc + rev: WIP-pre-commit-tool diff --git a/dvc/cli.py b/dvc/cli.py index 5ab81ad215..17d43a974b 100644 --- a/dvc/cli.py +++ b/dvc/cli.py @@ -34,6 +34,7 @@ unprotect, update, version, + git_hook, ) from .command.base import fix_subparsers from .exceptions import DvcParserError @@ -72,6 +73,7 @@ diff, version, update, + git_hook, ] diff --git a/dvc/command/git_hook.py b/dvc/command/git_hook.py new file mode 100644 index 0000000000..571e17127e --- /dev/null +++ b/dvc/command/git_hook.py @@ -0,0 +1,127 @@ +import logging +import os + +from dvc.utils import format_link +from dvc.command.base import CmdBaseNoRepo, fix_subparsers +from dvc.exceptions import NotDvcRepoError + +logger = logging.getLogger(__name__) + + +class CmdHookBase(CmdBaseNoRepo): + def run(self): + from dvc.repo import Repo + + try: + repo = Repo() + repo.close() + except NotDvcRepoError: + return 0 + + return self._run() + + +class CmdPreCommit(CmdHookBase): + def _run(self): + from dvc.main import main + + return main(["status"]) + + +class CmdPostCheckout(CmdHookBase): + def run(self): + # checking out some reference and not specific file. + if self.args.flag != "1": + return 0 + + # make sure we are not in the middle of a rebase/merge, so we + # don't accidentally break it with an unsuccessful checkout. + # Note that git hooks are always running in repo root. + if os.path.isdir(os.path.join(".git", "rebase-merge")): + return 0 + + from dvc.main import main + + return main(["checkout"]) + + +class CmdPrePush(CmdHookBase): + def run(self): + from dvc.main import main + + return main(["push"]) + + +def add_parser(subparsers, parent_parser): + GIT_HOOK_HELP = "Run GIT hook." + + git_hook_parser = subparsers.add_parser( + "git-hook", + parents=[parent_parser], + description=GIT_HOOK_HELP, + add_help=False, + ) + + git_hook_subparsers = git_hook_parser.add_subparsers( + dest="cmd", + help="Use `dvc daemon CMD --help` for command-specific help.", + ) + + fix_subparsers(git_hook_subparsers) + + PRE_COMMIT_HELP = "Run pre-commit GIT hook." + pre_commit_parser = git_hook_subparsers.add_parser( + "pre-commit", + parents=[parent_parser], + description=PRE_COMMIT_HELP, + help=PRE_COMMIT_HELP, + ) + pre_commit_parser.set_defaults(func=CmdPreCommit) + + POST_CHECKOUT_HELP = "Run post-checkout GIT hook." + post_checkout_parser = git_hook_subparsers.add_parser( + "post-checkout", + parents=[parent_parser], + description=POST_CHECKOUT_HELP, + help=POST_CHECKOUT_HELP, + ) + post_checkout_parser.add_argument( + "old_ref", + help="Old ref provided by GIT (see {})".format( + format_link("https://git-scm.com/docs/githooks#_post_checkout") + ), + ) + post_checkout_parser.add_argument( + "new_ref", + help="New ref provided by GIT (see {})".format( + format_link("https://git-scm.com/docs/githooks#_post_checkout") + ), + ) + post_checkout_parser.add_argument( + "flag", + help="Flag provided by GIT (see {})".format( + format_link("https://git-scm.com/docs/githooks#_post_checkout") + ), + ) + post_checkout_parser.set_defaults(func=CmdPostCheckout) + + PRE_PUSH_HELP = "Run pre-push GIT hook." + pre_push_parser = git_hook_subparsers.add_parser( + "pre-push", + parents=[parent_parser], + description=PRE_PUSH_HELP, + help=PRE_PUSH_HELP, + ) + pre_push_parser.add_argument( + "name", + help="Name provided by GIT (see {})".format( + format_link("https://git-scm.com/docs/githooks#_pre_push") + ), + ) + pre_push_parser.add_argument( + "location", + help="Location provided by GIT (see {})".format( + format_link("https://git-scm.com/docs/githooks#_pre_push") + ), + ) + pre_push_parser.set_defaults(func=CmdPrePush) diff --git a/dvc/scm/git/__init__.py b/dvc/scm/git/__init__.py index fb721911f0..193a04fb60 100644 --- a/dvc/scm/git/__init__.py +++ b/dvc/scm/git/__init__.py @@ -3,18 +3,13 @@ import logging import os import yaml -from shutil import which -from pathlib import Path from funcy import cached_property from pathspec.patterns import GitWildMatchPattern -from dvc.exceptions import DvcException from dvc.exceptions import GitHookAlreadyExistsError from dvc.scm.base import Base from dvc.scm.base import CloneError, FileNotInRepoError, RevError, SCMError -from dvc.scm.git.pre_commit_tool import pre_commit_tool_conf -from dvc.scm.git.pre_commit_tool import merge_pre_commit_tool_confs from dvc.scm.git.tree import GitTree from dvc.utils import fix_env, is_binary, relpath from dvc.utils.fs import path_isin @@ -259,77 +254,60 @@ def list_tags(self): def list_all_commits(self): return [c.hexsha for c in self.repo.iter_commits("--all")] - @staticmethod - def _install_hook(name, preconditions, cmd, hook_path_fn): - # only run in dvc repo - in_dvc_repo = '[ -n "$(git ls-files --full-name .dvc)" ]' - - command = "if {}; then exec dvc {}; fi".format( - " && ".join([in_dvc_repo] + preconditions), cmd - ) - - hook = hook_path_fn(name) - - if os.path.isfile(hook): - with open(hook, "r+") as fobj: - if command not in fobj.read(): - fobj.write("{command}\n".format(command=command)) - else: - with open(hook, "w+") as fobj: - fobj.write("#!/bin/sh\n" "{command}\n".format(command=command)) + def _install_hook(self, name): + hook = self._hook_path(name) + with open(hook, "w+") as fobj: + fobj.write("#!/bin/sh\nexec dvc git-hook {} $@\n".format(name)) os.chmod(hook, 0o777) def install(self, use_pre_commit_tool=False): - hook_path_fn = self._hook_path - - if use_pre_commit_tool: - hook_path_fn = self._pre_commit_tool_hook_path - path = Path(self._pre_commit_tool_hooks_home()) - path.mkdir(parents=True, exist_ok=True) - else: + if not use_pre_commit_tool: self._verify_dvc_hooks() + self._install_hook("post-checkout") + self._install_hook("pre-commit") + self._install_hook("pre-push") + return - self._install_hook( - "post-checkout", - [ - # checking out some reference and not specific file. - '[ "$3" = "1" ]', - # make sure we are not in the middle of a rebase/merge, so we - # don't accidentally break it with an unsuccessful checkout. - # Note that git hooks are always running in repo root. - "[ ! -d .git/rebase-merge ]", + config_path = os.path.join(self.root_dir, ".pre-commit-config.yaml") + + config = {} + if os.path.exists(config_path): + with open(config_path, "r") as fobj: + config = yaml.safe_load(fobj) + + entry = { + "repo": "https://github.com/andrewhare/dvc", # FIXME + "rev": "WIP-pre-commit-tool", + "hooks": [ + { + "id": "dvc", + "language_version": "python3", + "args": ["pre-commit"], + "stages": ["commit"], + }, + { + "id": "dvc", + "language_version": "python3", + "args": ["pre-push"], + "stages": ["push"], + }, + { + "id": "dvc", + "language_version": "python3", + "args": ["post-checkout"], + "stages": ["post-checkout"], + "always_run": True, + }, ], - "checkout", - hook_path_fn, - ) - self._install_hook("pre-commit", [], "status", hook_path_fn) - self._install_hook("pre-push", [], "push", hook_path_fn) + } - if use_pre_commit_tool: - self._integrate_pre_commit_tool() - - def _integrate_pre_commit_tool(self): - if not which("pre-commit"): - raise DvcException("pre-commit is not installed") - - conf = pre_commit_tool_conf( - self._pre_commit_tool_hook_path("pre-commit"), - self._pre_commit_tool_hook_path("pre-push"), - self._pre_commit_tool_hook_path("post-checkout"), - ) - - conf_yaml = os.path.join(self.root_dir, ".pre-commit-config.yaml") - - existing_conf = {} - if os.path.exists(conf_yaml): - with open(conf_yaml, "r") as fobj: - existing_conf = yaml.safe_load(fobj) - - conf = merge_pre_commit_tool_confs(existing_conf, conf) + if entry in config["repos"]: + return - with open(conf_yaml, "w+") as fobj: - yaml.dump(conf, fobj) + config["repos"].append(entry) + with open(config_path, "w+") as fobj: + yaml.dump(config, fobj) def cleanup_ignores(self): for path in self.ignored_paths: @@ -412,16 +390,9 @@ def close(self): def _hooks_home(self): return os.path.join(self.root_dir, self.GIT_DIR, "hooks") - @staticmethod - def _pre_commit_tool_hooks_home(): - return os.path.join(".dvc", "tmp", "hooks") - def _hook_path(self, name): return os.path.join(self._hooks_home, name) - def _pre_commit_tool_hook_path(self, name): - return os.path.join(self._pre_commit_tool_hooks_home(), name) - def _verify_hook(self, name): if os.path.exists(self._hook_path(name)): raise GitHookAlreadyExistsError(name) diff --git a/dvc/scm/git/pre_commit_tool.py b/dvc/scm/git/pre_commit_tool.py deleted file mode 100644 index 1fe4bf1c38..0000000000 --- a/dvc/scm/git/pre_commit_tool.py +++ /dev/null @@ -1,40 +0,0 @@ -def pre_commit_tool_conf(pre_commit_path, pre_push_path, post_checkout_path): - return { - "repos": [ - { - "repo": "local", - "hooks": [ - { - "id": "dvc-pre-commit", - "name": "DVC Pre Commit", - "entry": pre_commit_path, - "language": "script", - "stages": ["commit"], - }, - { - "id": "dvc-pre-push", - "name": "DVC Pre Push", - "entry": pre_push_path, - "language": "script", - "stages": ["push"], - }, - { - "id": "dvc-post-checkout", - "name": "DVC Post Checkout", - "entry": post_checkout_path, - "language": "script", - "stages": ["post-checkout"], - "always_run": True, - }, - ], - } - ] - } - - -def merge_pre_commit_tool_confs(existing_conf, conf): - if not existing_conf or "repos" not in existing_conf: - return conf - - existing_conf["repos"].append(conf["repos"][0]) - return existing_conf diff --git a/tests/func/test_install.py b/tests/func/test_install.py index aedd3ddda8..f2ed61ab91 100644 --- a/tests/func/test_install.py +++ b/tests/func/test_install.py @@ -19,9 +19,9 @@ def test_create_hooks(self, scm, dvc): scm.install() hooks_with_commands = [ - ("post-checkout", "exec dvc checkout"), - ("pre-commit", "exec dvc status"), - ("pre-push", "exec dvc push"), + ("post-checkout", "exec dvc git-hook post-checkout"), + ("pre-commit", "exec dvc git-hook pre-commit"), + ("pre-push", "exec dvc git-hook pre-push"), ] for fname, command in hooks_with_commands: From 63fcc6614022856838fdb5f9560414845e7d3a51 Mon Sep 17 00:00:00 2001 From: Ruslan Kuprieiev Date: Mon, 16 Mar 2020 17:15:56 +0200 Subject: [PATCH 13/13] add .pre-commit-hooks.yaml --- .pre-commit-config.yaml | 12 ++------ .pre-commit-hooks.yaml | 32 ++++++++++++++++++++ dvc/command/git_hook.py | 41 ++++++++------------------ dvc/scm/git/__init__.py | 9 ++---- setup.py | 5 +++- tests/unit/scm/test_pre_commit_tool.py | 31 ------------------- 6 files changed, 55 insertions(+), 75 deletions(-) create mode 100644 .pre-commit-hooks.yaml delete mode 100644 tests/unit/scm/test_pre_commit_tool.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a4be954b0..041a99f7ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,22 +18,16 @@ repos: repo: https://github.com/lovesegfault/beautysh rev: master - hooks: - - args: - - pre-commit - id: dvc + - id: dvc-pre-commit language_version: python3 stages: - commit - - args: - - pre-push - id: dvc + - id: dvc-pre-push language_version: python3 stages: - push - always_run: true - args: - - post-checkout - id: dvc + id: dvc-post-checkout language_version: python3 stages: - post-checkout diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000000..788f023433 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,32 @@ +- args: + - git-hook + - pre-commit + entry: dvc + id: dvc-pre-commit + language: python + language_version: python3 + name: DVC pre-commit + stages: + - commit +- args: + - git-hook + - pre-push + entry: dvc + id: dvc-pre-push + language: python + language_version: python3 + name: DVC pre-push + stages: + - push +- always_run: true + args: + - git-hook + - post-checkout + entry: dvc + id: dvc-post-checkout + language: python + language_version: python3 + minimum_pre_commit_version: 2.2.0 + name: DVC post-checkout + stages: + - post-checkout diff --git a/dvc/command/git_hook.py b/dvc/command/git_hook.py index 571e17127e..6c64c582df 100644 --- a/dvc/command/git_hook.py +++ b/dvc/command/git_hook.py @@ -1,7 +1,6 @@ import logging import os -from dvc.utils import format_link from dvc.command.base import CmdBaseNoRepo, fix_subparsers from dvc.exceptions import NotDvcRepoError @@ -30,8 +29,15 @@ def _run(self): class CmdPostCheckout(CmdHookBase): def run(self): + # when we are running from pre-commit tool, it doesn't provide CLI + # flags, but instead provides respective env vars that we could use. + flag = os.environ.get("PRE_COMMIT_CHECKOUT_TYPE") + if flag is None and len(self.args.args) >= 3: + # see https://git-scm.com/docs/githooks#_post_checkout + flag = self.args.args[2] + # checking out some reference and not specific file. - if self.args.flag != "1": + if flag != "1": return 0 # make sure we are not in the middle of a rebase/merge, so we @@ -76,6 +82,9 @@ def add_parser(subparsers, parent_parser): description=PRE_COMMIT_HELP, help=PRE_COMMIT_HELP, ) + pre_commit_parser.add_argument( + "args", nargs="*", help="Arguments passed by GIT or pre-commit tool.", + ) pre_commit_parser.set_defaults(func=CmdPreCommit) POST_CHECKOUT_HELP = "Run post-checkout GIT hook." @@ -86,22 +95,7 @@ def add_parser(subparsers, parent_parser): help=POST_CHECKOUT_HELP, ) post_checkout_parser.add_argument( - "old_ref", - help="Old ref provided by GIT (see {})".format( - format_link("https://git-scm.com/docs/githooks#_post_checkout") - ), - ) - post_checkout_parser.add_argument( - "new_ref", - help="New ref provided by GIT (see {})".format( - format_link("https://git-scm.com/docs/githooks#_post_checkout") - ), - ) - post_checkout_parser.add_argument( - "flag", - help="Flag provided by GIT (see {})".format( - format_link("https://git-scm.com/docs/githooks#_post_checkout") - ), + "args", nargs="*", help="Arguments passed by GIT or pre-commit tool.", ) post_checkout_parser.set_defaults(func=CmdPostCheckout) @@ -113,15 +107,6 @@ def add_parser(subparsers, parent_parser): help=PRE_PUSH_HELP, ) pre_push_parser.add_argument( - "name", - help="Name provided by GIT (see {})".format( - format_link("https://git-scm.com/docs/githooks#_pre_push") - ), - ) - pre_push_parser.add_argument( - "location", - help="Location provided by GIT (see {})".format( - format_link("https://git-scm.com/docs/githooks#_pre_push") - ), + "args", nargs="*", help="Arguments passed by GIT or pre-commit tool.", ) pre_push_parser.set_defaults(func=CmdPrePush) diff --git a/dvc/scm/git/__init__.py b/dvc/scm/git/__init__.py index 193a04fb60..80553b3a28 100644 --- a/dvc/scm/git/__init__.py +++ b/dvc/scm/git/__init__.py @@ -281,21 +281,18 @@ def install(self, use_pre_commit_tool=False): "rev": "WIP-pre-commit-tool", "hooks": [ { - "id": "dvc", + "id": "dvc-pre-commit", "language_version": "python3", - "args": ["pre-commit"], "stages": ["commit"], }, { - "id": "dvc", + "id": "dvc-pre-push", "language_version": "python3", - "args": ["pre-push"], "stages": ["push"], }, { - "id": "dvc", + "id": "dvc-post-checkout", "language_version": "python3", - "args": ["post-checkout"], "stages": ["post-checkout"], "always_run": True, }, diff --git a/setup.py b/setup.py index 5930136a7e..16a301aa54 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,10 @@ # Prevents pkg_resources import in entry point script, # see https://github.com/ninjaaron/fast-entry_points. # This saves about 200 ms on startup time for non-wheel installs. -import fastentrypoints # noqa: F401 +try: + import fastentrypoints # noqa: F401 +except ImportError: + pass # not able to import when installing through pre-commit # Read package meta-data from version.py diff --git a/tests/unit/scm/test_pre_commit_tool.py b/tests/unit/scm/test_pre_commit_tool.py deleted file mode 100644 index f353c0c97f..0000000000 --- a/tests/unit/scm/test_pre_commit_tool.py +++ /dev/null @@ -1,31 +0,0 @@ -from dvc.scm.git.pre_commit_tool import pre_commit_tool_conf -from dvc.scm.git.pre_commit_tool import merge_pre_commit_tool_confs - -from unittest import TestCase - - -class TestPreCommitTool(TestCase): - def setUp(self): - self.conf = pre_commit_tool_conf("a", "b", "c") - - def test_merge_pre_commit_tool_confs_empty(self): - existing_conf = None - merged_conf = merge_pre_commit_tool_confs(existing_conf, self.conf) - self.assertEqual(self.conf, merged_conf) - - def test_merge_pre_commit_tool_confs_invalid_yaml(self): - existing_conf = "some invalid yaml" - merged_conf = merge_pre_commit_tool_confs(existing_conf, self.conf) - self.assertEqual(self.conf, merged_conf) - - def test_merge_pre_commit_tool_confs_no_repos(self): - existing_conf = {"foo": [1, 2, 3]} - merged_conf = merge_pre_commit_tool_confs(existing_conf, self.conf) - self.assertEqual(self.conf, merged_conf) - - def test_merge_pre_commit_tool_confs(self): - existing_conf = {"repos": [{}]} - merged_conf = merge_pre_commit_tool_confs(existing_conf, self.conf) - # Merging the new conf in should append the new repo to the end of - # the existing repos array on the existing conf. - self.assertEqual(self.conf["repos"][0], merged_conf["repos"][1])