Monsieur Ganesha

Pre-commit hooks for the 42 school piscine — norminette, compiler, forbidden functions, commit format, README check.

v0.1.0 · Python 3.11+ · MIT

What is it?

Monsieur Ganesha is a suite of five pre-commit hooks that run automatically before every git commit in a 42 piscine repository. The hooks catch norm violations, compile errors, forbidden function calls, malformed commit messages, and missing README structure before they reach the repository — so moulinette never has to see them first.

Hook id What it checks Tool
norminette 42 Norme compliance on .c and .h files norminette CLI
c-compiler Syntax errors in staged .c files gcc -fsyntax-only
forbidden-functions Calls to functions banned by the subject PDF Pure-Python regex
commit-message Commit subject format (Conventional Commits 1.0.0) Pure-Python regex
readme README.md structure — non-blocking advisory (+XP) Pure-Python

The characters

Bonjour, monde.
I am not here to be gentle.
I am here so she does not have to be harsh. — Monsieur Ganesha, Directeur, Conservatoire de Paris XLII

Named after Monsieur Ganesha, director of the Conservatoire de Paris XLII and husband of Mademoiselle Norminette.

Mademoiselle Norminette and her younger sister, Mademoiselle Francinette, are the daughters of the respectable Madame Moulinette. The rigor is the care. The demand is the protection. Madame Moulinette has no mercy; Mademoiselle Norminette, even less — but Ganesha at least has a reason.

Monsieur Ganesha was the first student of Paris XLII to complete every exercise list and exam with honours — using a single push per step. Madame Moulinette found this absurd. Ganesha recalls: c'était terrible.

A warning. When you and Mademoiselle Norminette fall in love — when your code is clean, your norm is perfect, and she begins to look at you the way she looks at a file with no errors — Moulinette notices. She always notices. She does not love. She evaluates. But she watches. And when she sees that Norminette has chosen you, something in her pipeline shifts. C'est terrible. There is nothing to be done. Have a good session. Close the door.
— Monsieur Ganesha, in confidence

Requirements

Installation

Automatic (recommended)

Run the setup script from inside your piscine repository:

bash /path/to/monsieur-ganesha/install.sh

The script:

  1. Sets git config --global core.editor vim
  2. Installs pre-commit via uv or pip3
  3. Creates .pre-commit-config.yaml pointing to this repository
  4. Creates a .ganesha.toml template with sensible defaults
  5. Activates the hooks with pre-commit install

Manual

Add the following to your .pre-commit-config.yaml:

repos:
  - repo: https://github.com/qlrd/monsieur-ganesha
    rev: v0.1.0
    hooks:
      - id: norminette
      - id: c-compiler
      - id: forbidden-functions
      - id: commit-message

Then activate the hooks:

pre-commit install
pre-commit install --hook-type commit-msg
Editor. The commit-message hook opens the editor when git commit is called without -m. vim is the expected editor at 42 school. Run git config --global core.editor vim to set it.

Configuration

Place .ganesha.toml at the root of your piscine repository. All sections are optional; the hooks work without a configuration file.

[project]
# Identifier for the current module (informational only).
name = "C00"

[forbidden]
# Functions disallowed by the subject PDF.
# The hook rejects any .c file that calls one of these.
functions = ["printf", "malloc", "realloc", "free", "calloc"]

[commit]
# Override the default Conventional Commits 1.0.0 pattern.
# Uncomment to use the 42-school ex00: format instead.
# pattern = "^(ex|rush|exam)\\d+: .+"

Configuration reference

KeyTypeDefaultDescription
[project] name string none Module identifier. Informational only.
[forbidden] functions string[] [] C function names to forbid. Empty = hook always passes.
[commit] pattern string (regex) CC 1.0.0 pattern Custom regex for the commit subject. Disables gamification.

Hooks

norminette

Runs the norminette CLI on every staged .c and .h file. Norminette enforces the 42 Norme — a strict coding standard covering indentation, line length, function length, variable placement, and more.

# .pre-commit-config.yaml
- id: norminette

If norminette is not installed the hook prints an installation hint and exits with code 1. Install with: pip install norminette

c-compiler

Runs gcc -Wall -Wextra -Werror -fsyntax-only on each staged .c file independently. The -fsyntax-only flag skips object-file generation, which avoids conflicts when multiple files are staged and some headers are not yet available.

# .pre-commit-config.yaml
- id: c-compiler

forbidden-functions

Scans every staged .c file for calls to functions listed in [forbidden] functions. Detection is word-boundary anchored: ft_printf is not flagged when printf is forbidden.

# .pre-commit-config.yaml
- id: forbidden-functions

Error output format:

ex00/ft_putchar.c:8: função proibida 'write'
Single-line comments (// …) are stripped before scanning. Multi-line /* */ comments are not stripped — known limitation.

commit-message

Validates the commit message subject against Conventional Commits 1.0.0. Git passes .git/COMMIT_EDITMSG to the hook; comment lines (starting with #) are stripped before validation.

# .pre-commit-config.yaml
- id: commit-message

Rejection output is intentionally terse — REJECTED. — with no format hints. Students are expected to discover the correct format themselves.

readme

Checks every staged README.md for common structural issues. This hook is non-blocking: it always exits with code 0 and never prevents a commit. Files with issues receive advisory messages; files that pass all checks earn +XP.

# .pre-commit-config.yaml
- id: readme

Advisory messages

README.md: empty file — add at least a title and a short description (+XP available).
README.md: no title found — add "# <Project Name>" as the first line (+XP available).
README.md: no file descriptor usage documented — mention which file descriptors your program reads from and writes to (0=stdin, 1=stdout, 2=stderr) (+XP available).

When fully documented

README.md: well documented — +XP.

When README exists but is not staged

README.md found but not staged — +XP for documenting.

The hook always runs (always_run: true) and checks only the top-level README.md. A level-1 ATX heading is a line beginning with # (hash followed by a space). File descriptor usage is detected by scanning for stdin, stdout, stderr, or fd in the file body.

Conventional Commits 1.0.0

The commit subject must follow the pattern:

<type>[(<scope>)][!]: <description>

The subject line must not exceed 72 characters.

Valid examples

feat: add norminette timeout option
fix(forbidden): handle empty file list
feat!: drop support for gcc older than 12
fix(compiler)!: change exit code on parse error
docs: update installation section
test: add integration test for commit-msg
init: project setup

Invalid examples

WIP                          # no type prefix
ex00: implement ft_putchar   # 42-school format, not CC 1.0.0
FEAT: something              # type must be lowercase
feat:                        # missing description
feat: <73 chars...>          # subject too long

Type reference

TypeWhen to useGamification
feat A new feature
fix A bug fix
docs Documentation only francinette approves
style Formatting, whitespace — no logic change
refactor Code restructuring — no feature, no fix
perf Performance improvement
test Adding or correcting tests ultimate type easter egg
build Build system, dependencies
ci CI/CD configuration
chore Maintenance tasks — no production code change francinette approves
revert Reverts a previous commit
init Initial commit / project bootstrap

Gamification layer

When the default Conventional Commits pattern is active, accepted commits trigger informational messages on stderr:

ConditionMessage
docs or chore commit francinette approves this docs commit.
test commit you discovered the ultimate commit type.
Breaking change (!) with body breaking change explained — +XP.
Breaking change (!) without body tip: explain why this breaks to earn +XP.

These messages are purely informational. They do not affect the hook exit code and are not shown when a custom pattern is configured.

CLI reference

After installation with uv sync or pip install -e . the ganesha command is available on $PATH. Each subcommand mirrors a hook. Exit code is 0 on success, 1 on failure, 2 on internal error.

ganesha norminette <files...>

Run norminette on the given files.

ganesha norminette ex00/ft_putchar.c include/header.h

ganesha compiler <files...>

Syntax-check the given files with gcc.

ganesha compiler ex00/ft_putchar.c ex01/ft_print_alphabet.c

ganesha forbidden <files...>

Scan the given files for forbidden function calls. Reads [forbidden] functions from .ganesha.toml in the current directory.

ganesha forbidden ex00/ft_putchar.c

ganesha commit-msg <file>

Validate the commit message stored in file. Reads the optional custom pattern from [commit] pattern in .ganesha.toml.

ganesha commit-msg .git/COMMIT_EDITMSG

ganesha readme <files...>

Check the given README.md files for structural issues. Always exits 0. Advisory messages are printed to stderr.

ganesha readme README.md

API reference

All public functions can be imported and called directly, which is useful for testing or for embedding Monsieur Ganesha checks into other tools.

ganesha.config

load_config(root: Path | None = None) → Config

Load .ganesha.toml from root (defaults to the current working directory). Returns a default Config if the file does not exist. Raises ValueError if the file exists but contains invalid TOML.

Returns: Config — fully-populated configuration dataclass with safe defaults.

from ganesha.config import load_config

cfg = load_config()
print(cfg.forbidden.functions)  # ['printf', 'malloc']
print(cfg.commit.pattern)       # None  (use default CC 1.0.0)

Dataclasses

ClassFieldsDefaults
Config project, forbidden, commit nested defaults
ProjectConfig name: str | None None
ForbiddenConfig functions: list[str] []
CommitConfig pattern: str | None None

ganesha.checks.norminette

check(files: Sequence[str]) → bool

Run norminette on all .c and .h files in files. Non-matching files are ignored. Returns True if norminette exits with code 0, False otherwise. Returns True immediately if no matching files are present.

from ganesha.checks.norminette import check
ok = check(["ex00/ft_putchar.c", "include/libft.h"])

ganesha.checks.compiler

check(files: Sequence[str]) → bool

Run gcc -Wall -Wextra -Werror -fsyntax-only on each .c file in files (one subprocess per file). All failures are collected before returning. Returns False if gcc is not installed.

from ganesha.checks.compiler import check
ok = check(["ex00/ft_putchar.c", "ex01/ft_print_alphabet.c"])

ganesha.checks.forbidden

check(files: Sequence[str], forbidden: Sequence[str]) → bool

Scan .c files for calls to any function in forbidden. Uses the regex \b(f1|f2|...)\s*\( so that ft_printf is not flagged when printf is forbidden. Returns True if forbidden is empty.

from ganesha.checks.forbidden import check
ok = check(["ex00/ft_putchar.c"], ["printf", "malloc"])

ganesha.checks.commit_msg

Constants

DEFAULT_PATTERN Conventional Commits 1.0.0 regex string
MAX_SUBJECT_LEN 72 — maximum subject line length in characters

check(file_path: str, pattern: str = DEFAULT_PATTERN) → bool

Validate the commit message at file_path. Strips # comment lines, checks subject length, then matches against pattern. When using DEFAULT_PATTERN the gamification layer is active. All rejection output goes to stderr; nothing is raised.

from ganesha.checks.commit_msg import check, DEFAULT_PATTERN

ok = check(".git/COMMIT_EDITMSG")
# or with a custom pattern:
ok = check(path, pattern=r"^(ex|rush|exam)\d+: .+")

ganesha.checks.readme

check(files: Sequence[str]) → bool

Check each README.md in files for structural issues: non-empty, level-1 ATX heading, file descriptor usage. Advisory messages are printed to stderr with / prefixes. Always returns True (non-blocking).

When called with no files (files is empty) and a top-level README.md exists on disk, prints a staging reminder and returns True.

from ganesha.checks.readme import check
ok = check(["README.md"])  # always True

Development

Setup

Python 3.11+ and uv are required.

git clone https://github.com/qlrd/monsieur-ganesha
cd monsieur-ganesha
uv sync --dev

Quality gates

All of the following must pass before committing:

black --check src/ tests/        # formatting (88-column)
isort --check-only src/ tests/   # import order
pylint src/ganesha/           # lint — 10.00/10 required
pytest                           # all 45 tests must pass

To auto-fix formatting before checking:

black src/ tests/
isort src/ tests/

Project layout

src/ganesha/
├── __init__.py        public API (re-exports checks + config)
├── __main__.py        python -m ganesha entry point
├── cli.py             CLI entry point (argparse)
├── config.py          .ganesha.toml loader (tomllib)
└── checks/
    ├── __init__.py
    ├── norminette.py  subprocess: norminette
    ├── compiler.py    subprocess: gcc -fsyntax-only
    ├── forbidden.py   pure-Python regex scan
    ├── commit_msg.py  CC 1.0.0 validator + gamification
    └── readme.py      README structure check (non-blocking)

tests/
├── fixtures/          valid.c · norm_error.c · compile_error.c · forbidden.c
├── test_forbidden.py  integration via lib API + tmp_path
├── test_commit_msg.py integration via lib API + tmp_path
└── test_cli.py        CLI integration via subprocess

Contributing

All contributions require a Developer Certificate of Origin. Add it with git commit -s.

Commit messages must follow Conventional Commits 1.0.0.

For large changes, open an issue before starting work. See CONTRIBUTING.md for the full workflow.

Adding a new check

  1. Create src/ganesha/checks/<name>.py with a public check() function and verbose docstrings.
  2. Export it in checks/__init__.py.
  3. Add a CLI subcommand in cli.py.
  4. Add an entry in .pre-commit-hooks.yaml.
  5. Write integration tests in tests/test_<name>.py.
  6. Run all quality gates and open a pull request.

Rebase note

Congratulations.
If you are reading this, you have completed at least one correct rebase — with fixup, reword, or edit — and the reflog proves it. The reflog has not been modified. It never should be. You rewrote with intention, and the reflog remembers.

git reflog does not lie. It shows every move you made. A clean rebase, visible in the reflog, is a signature. It says: I understood what I was doing.

Monsieur Ganesha has noted this. — Monsieur Ganesha, Directeur, Conservatoire de Paris XLII

The safest rebase workflow — without overwriting reflogs:

git reflog                   # read history — never modify it
git rebase -i HEAD~3         # fixup, reword, edit — with intention
git reflog                   # find the sha before the rebase
git reset --hard HEAD@{n}    # recover the exact state if needed

Never modify the reflog. Never use git push --force to overwrite a shared branch. The reflog on your machine is yours — and it is the only honest witness you have.

Public keys

All commits are signed by Monsieur Piscinette (piscinette@conservatoire42.fr) and attested by the Mahatrimurti: Madame Kali, Monsieur Shiva, and Monsieur Ganesha.

Import all keys to verify signatures locally:

gpg --import keys/*.asc
git log --show-signature
FileIdentityUsage
keys/madame-kali.ascMadame KaliRoot of trust [C]
keys/monsieur-shiva.ascMonsieur ShivaConsciousness, purge support [C]
keys/monsieur-ganesha.ascMonsieur GaneshaDirecteur du Conservatoire [C]
keys/bija-krim.ascBija Krim (Krīṃ)Seed mantra of Kali [C]
keys/monsieur-piscinette.ascMonsieur PiscinetteCommit signer [SC]
keys/qlrddev.ascqlrddev@proton.meMaintainer personal key [S]