mirror of
https://github.com/jlengrand/bugsink.git
synced 2026-03-10 08:01:17 +00:00
pre-commit-hook: trigger tailwind rebuild conditionally
This commit is contained in:
14
pre-commit
14
pre-commit
@@ -1,8 +1,16 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
files=`git diff --cached --name-status`
|
# this script must be placed in .git/hooks/pre-commit for it to run automatically
|
||||||
|
# it is put here in the repo so that it can be used by anyone who clones the repo
|
||||||
|
|
||||||
. bin/activate
|
. bin/activate
|
||||||
python manage.py tailwind build
|
|
||||||
|
|
||||||
git add theme/static/css/dist/styles.css
|
should_run=$(git diff --cached --name-only -z | python tools/is_tracked_by_tailwind.py)
|
||||||
|
|
||||||
|
if [ "$should_run" = "yes" ]; then
|
||||||
|
echo "Building Tailwind CSS..."
|
||||||
|
python manage.py tailwind build
|
||||||
|
git add theme/static/css/dist/styles.css
|
||||||
|
else
|
||||||
|
echo "No relevant changes; skipping Tailwind build."
|
||||||
|
fi
|
||||||
|
|||||||
138
tools/is_tracked_by_tailwind.py
Executable file
138
tools/is_tracked_by_tailwind.py
Executable file
@@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Determine whether the Tailwind build must run based on staged files.
|
||||||
|
# Reads null-separated paths from stdin, compares against globs listed in theme/static_src/tailwind.config.js
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def expand_optional_dirs(pattern):
|
||||||
|
"""
|
||||||
|
Python's Path.match() treats '**/' strictly — it requires at least one directory to match.
|
||||||
|
But in globbing systems like Tailwind or bash, '**/' can also mean 'zero directories'.
|
||||||
|
This function expands all combinations where each '**/' is either kept or removed,
|
||||||
|
so that Path.match() can simulate this more flexible behavior.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
'theme/**/templates/**/*.html' →
|
||||||
|
[
|
||||||
|
'theme/**/templates/**/*.html',
|
||||||
|
'theme/templates/**/*.html',
|
||||||
|
'theme/**/templates/*.html',
|
||||||
|
'theme/templates/*.html',
|
||||||
|
]
|
||||||
|
|
||||||
|
Use this to match paths more like Tailwind does, where '**' can mean "nothing here".
|
||||||
|
"""
|
||||||
|
parts = pattern.split("**/")
|
||||||
|
if len(parts) == 1:
|
||||||
|
return [pattern] # no '**/' present
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# There are N-1 '**/' positions
|
||||||
|
num_stars = len(parts) - 1
|
||||||
|
|
||||||
|
# For each combination of keeping or removing the '**/' parts
|
||||||
|
for bits in range(2 ** num_stars):
|
||||||
|
new_pattern = parts[0]
|
||||||
|
for i in range(num_stars):
|
||||||
|
if (bits >> i) & 1:
|
||||||
|
new_pattern += "**/"
|
||||||
|
new_pattern += parts[i + 1]
|
||||||
|
results.append(new_pattern)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def path_matches_glob(path, glob_pattern):
|
||||||
|
""" Check if the given path matches any variant of the glob pattern (including '**'-as-nothing)."""
|
||||||
|
for variant in expand_optional_dirs(glob_pattern):
|
||||||
|
if pathlib.Path(path).match(variant):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def files_from_stdin():
|
||||||
|
# Read from stdin as a single binary stream
|
||||||
|
raw = sys.stdin.buffer.read()
|
||||||
|
files = raw.decode("utf-8").split("\0")
|
||||||
|
files = [f for f in files if f] # Remove empty final item
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def parse_tailwind_config():
|
||||||
|
# tailwind.config.js is not JSON, so we can't use json.load().
|
||||||
|
# We extract the 'content: [...]' array with a regex,
|
||||||
|
# then pull all string literals using a regex.
|
||||||
|
# Any staged file under theme/static_src/ triggers a rebuild.
|
||||||
|
|
||||||
|
config_path = pathlib.Path("theme/static_src/tailwind.config.js")
|
||||||
|
text = config_path.read_text()
|
||||||
|
|
||||||
|
# Extract the 'content: [ ... ]' block.
|
||||||
|
match = re.search(r'content\s*:\s*\[(.*?)\]', text, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
raise RuntimeError("Could not find content array in Tailwind config")
|
||||||
|
raw_content_block = match.group(1)
|
||||||
|
|
||||||
|
string_literals = []
|
||||||
|
|
||||||
|
for line in raw_content_block.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or not line.startswith(("'", '"')):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Match a string at the start of the line
|
||||||
|
match = re.match(r'''(['"])(.*?)\1''', line)
|
||||||
|
if not match:
|
||||||
|
raise ValueError(f"Invalid quoted string: {line}")
|
||||||
|
|
||||||
|
string_literals.append(match.group(2))
|
||||||
|
|
||||||
|
return string_literals
|
||||||
|
|
||||||
|
|
||||||
|
def globs_from_string_literals(string_literals):
|
||||||
|
"""Globs: resolve paths relative to the repo root instead of the config file."""
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for string_literal in string_literals:
|
||||||
|
if string_literal.startswith("../../"):
|
||||||
|
result.append(string_literal[6:]) # Remove '../../' prefix
|
||||||
|
elif string_literal.startswith("../"):
|
||||||
|
result.append("theme/" + string_literal[3:])
|
||||||
|
else:
|
||||||
|
result.append(string_literal)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def any_file_matches_globs(files, globs):
|
||||||
|
for f in files:
|
||||||
|
for g in globs:
|
||||||
|
if path_matches_glob(f, g):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
files = files_from_stdin()
|
||||||
|
if not files:
|
||||||
|
print("no")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if any(f.startswith("theme/static_src/") for f in files):
|
||||||
|
print("yes")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
string_literals = parse_tailwind_config()
|
||||||
|
globs = globs_from_string_literals(string_literals)
|
||||||
|
|
||||||
|
if any_file_matches_globs(files, globs):
|
||||||
|
print("yes")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
print("no")
|
||||||
Reference in New Issue
Block a user