workflow

git clone https://git.godosa.eu/workflow

master

raw ยท 6073 bytes

import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "scripts"))
import denylist_check as D  # noqa: E402

ENV = {**os.environ, "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t", "GIT_COMMITTER_NAME": "t",
       "GIT_COMMITTER_EMAIL": "t@t", "GIT_CONFIG_GLOBAL": "/dev/null", "GIT_CONFIG_NOSYSTEM": "1"}


def git(cwd, *args, check=True):
    return subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True, env=ENV, check=check)


class ParseTest(unittest.TestCase):
    def test_words_comments_exemptions(self):
        words, exempt = D.parse("# c\nSecretProj\n\n  Bob  \n!bobcat\n")
        self.assertEqual(words, [(2, "secretproj"), (4, "bob")])
        self.assertEqual(exempt, ["bobcat"])

    def test_hits_case_insensitive_and_exempt(self):
        words, exempt = [(1, "bob"), (2, "secretproj")], ["bobcat"]
        self.assertEqual(D.hits("a BOB and a Bobcat", words, exempt), [1])
        self.assertEqual(D.hits("only a bobcat here", words, exempt), [])
        self.assertEqual(D.hits("x SecretProj y", words, exempt), [2])
        self.assertEqual(D.hits("clean", words, exempt), [])


class HookTest(unittest.TestCase):
    """Real temp repo with the committed hook dir; commits through git itself."""

    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.repo = Path(self.tmp.name) / "r"
        self.repo.mkdir()
        git(self.repo, "init", "-q", "-b", "master")
        shutil.copytree(ROOT / ".githooks", self.repo / ".githooks")
        (self.repo / "scripts").mkdir()
        shutil.copy(ROOT / "scripts" / "denylist_check.py", self.repo / "scripts")
        (self.repo / ".gitignore").write_text("publish-denylist.local\n")
        git(self.repo, "config", "core.hooksPath", ".githooks")
        (self.repo / "publish-denylist.local").write_text("# private\nSecretProj\n!secretprojector\n")
        self.put("a.txt", "hello\n")
        git(self.repo, "add", ".githooks", "scripts", ".gitignore")
        r = self.commit("init")
        self.assertEqual(r.returncode, 0, r.stderr)

    def tearDown(self):
        self.tmp.cleanup()

    def put(self, path, text):
        f = self.repo / path
        f.parent.mkdir(parents=True, exist_ok=True)
        f.write_text(text)
        git(self.repo, "add", path)

    def commit(self, msg, cwd=None):
        return git(cwd or self.repo, "commit", "-q", "-m", msg, check=False)

    def head(self, cwd=None):
        return git(cwd or self.repo, "log", "--format=%s").stdout.splitlines()

    def test_content_hit_blocks_without_printing_word(self):
        self.put("b.txt", "x\nsee secretproj docs\n")
        r = self.commit("add b")
        self.assertEqual(r.returncode, 1)
        self.assertIn("b.txt:2: word #2", r.stderr)
        self.assertNotIn("secretproj", r.stderr.lower())
        self.assertEqual(self.head(), ["init"])

    def test_path_hit_blocks(self):
        self.put("docs/SECRETPROJ-notes.md", "clean\n")
        r = self.commit("add notes")
        self.assertEqual(r.returncode, 1)
        self.assertIn("path docs/***-notes.md: word #2", r.stderr)
        self.assertNotIn("secretproj", r.stderr.lower())

    def test_message_hit_blocks(self):
        self.put("b.txt", "clean\n")
        r = self.commit("port from SecretProj")
        self.assertEqual(r.returncode, 1)
        self.assertIn("commit message:1: word #2", r.stderr)
        self.assertEqual(self.head(), ["init"])

    def test_clean_and_exempt_pass(self):
        self.put("b.txt", "the secretprojector is fine\n")
        r = self.commit("clean msg")
        self.assertEqual(r.returncode, 0, r.stderr)
        self.assertEqual(self.head(), ["clean msg", "init"])

    def test_only_added_lines_count(self):
        (self.repo / "publish-denylist.local").write_text("")   # let an old hit in
        self.put("old.txt", "SecretProj legacy\n")
        self.assertEqual(self.commit("old").returncode, 0)
        (self.repo / "publish-denylist.local").write_text("SecretProj\n")
        self.put("old.txt", "SecretProj legacy\nnew clean line\n")
        r = self.commit("touch old")
        self.assertEqual(r.returncode, 0, r.stderr)

    def test_missing_denylist_warns_not_fails(self):
        (self.repo / "publish-denylist.local").unlink()
        self.put("b.txt", "SecretProj\n")
        r = self.commit("no list")
        self.assertEqual(r.returncode, 0, r.stderr)
        self.assertIn("warning: no publish-denylist.local", r.stderr)

    def test_linked_worktree_uses_main_tree_list(self):
        wt = Path(self.tmp.name) / "wt"
        git(self.repo, "worktree", "add", "-q", str(wt), "-b", "topic")
        self.assertFalse((wt / "publish-denylist.local").exists())
        (wt / "c.txt").write_text("SecretProj\n")
        git(wt, "add", "c.txt")
        r = self.commit("wt", cwd=wt)
        self.assertEqual(r.returncode, 1)
        self.assertIn("c.txt:1: word #2", r.stderr)

    def test_extra_list_from_git_config_merged(self):
        glob = Path(self.tmp.name) / "global.txt"
        glob.write_text("Alpha\n")
        git(self.repo, "config", "--add", "denylist.file", str(glob))
        self.put("b.txt", "alpha\n")
        r = self.commit("b")
        self.assertEqual(r.returncode, 1)
        self.assertIn("b.txt:1: global.txt word #1", r.stderr)
        self.assertNotIn("alpha", r.stderr.lower())

    def test_tree_mode_scans_tracked_files(self):
        (self.repo / "publish-denylist.local").write_text("")
        self.put("old.txt", "x SecretProj\n")
        self.assertEqual(self.commit("old").returncode, 0)
        (self.repo / "publish-denylist.local").write_text("SecretProj\n")
        r = subprocess.run([sys.executable, "scripts/denylist_check.py", "tree"], cwd=self.repo,
                           capture_output=True, text=True, env=ENV)
        self.assertEqual(r.returncode, 1)
        self.assertIn("old.txt:1: word #1", r.stderr)


if __name__ == "__main__":
    unittest.main()