[Update] bulk update
This commit is contained in:
@@ -3,17 +3,19 @@
|
||||
|
||||
Reads the hook JSON from stdin, extracts the edited file path, and runs a
|
||||
language-appropriate formatter -- but, to avoid imposing a style on a project
|
||||
that never asked for it, opinionated formatters (stylua, ruff, prettier, taplo)
|
||||
only run when the project opts in via a config file discoverable by walking up
|
||||
from the edited file. Canonical single-style toolchains run on sight: gofmt and
|
||||
rustfmt on their go.mod / Cargo.toml marker; fish_indent and shfmt whenever the
|
||||
tool itself is installed (shell and fish have one de-facto style, no config).
|
||||
that never asked for it, every formatter here only runs when the project opts in
|
||||
via a config file discoverable by walking up from the edited file: stylua, ruff,
|
||||
prettier and taplo on their own configs, gofmt and rustfmt on go.mod / Cargo.toml,
|
||||
shfmt and fish_indent on .editorconfig. No marker, no formatting -- a repo that
|
||||
hand-formats its shell keeps its own layout.
|
||||
|
||||
Best-effort: always exits 0 so a format hiccup never blocks an edit. jq is not
|
||||
guaranteed on the host, hence python for the stdin JSON parse.
|
||||
"""
|
||||
import fnmatch
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -31,6 +33,47 @@ def find_up(start, names):
|
||||
d = parent
|
||||
|
||||
|
||||
def _expand_braces(pat):
|
||||
"""`a{b,c}` -> [`ab`, `ac`] (editorconfig sections use {,} alternation)."""
|
||||
m = re.search(r"\{([^{}]*)\}", pat)
|
||||
if not m:
|
||||
return [pat]
|
||||
out = []
|
||||
for alt in m.group(1).split(","):
|
||||
out.extend(_expand_braces(pat[: m.start()] + alt + pat[m.end():]))
|
||||
return out
|
||||
|
||||
|
||||
def editorconfig_covers(path):
|
||||
"""True if the nearest ancestor .editorconfig has a section matching `path`.
|
||||
|
||||
Mere existence is NOT opt-in: this repo's .editorconfig is deliberately scoped
|
||||
to `[{*.sh,dotsync,dotpub}]` with no [*] block precisely so fish files stay
|
||||
untouched, and a generic upstream .editorconfig must not switch a formatter on
|
||||
for filetypes it never mentions. Patterns without a `/` match the basename
|
||||
(the editorconfig convention); ones with a `/` match the path relative to the
|
||||
.editorconfig's directory.
|
||||
"""
|
||||
d = find_up(path, [".editorconfig"])
|
||||
if not d:
|
||||
return False
|
||||
name = os.path.basename(path)
|
||||
rel = os.path.relpath(os.path.abspath(path), d)
|
||||
try:
|
||||
with open(os.path.join(d, ".editorconfig"), errors="ignore") as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if not (line.startswith("[") and line.endswith("]")):
|
||||
continue
|
||||
for pat in _expand_braces(line[1:-1]):
|
||||
target = rel if "/" in pat else name
|
||||
if fnmatch.fnmatch(target, pat.lstrip("/")):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def has_toml_section(start, filename, section):
|
||||
"""True if the nearest ancestor `filename` contains `section`."""
|
||||
d = find_up(start, [filename])
|
||||
@@ -89,14 +132,26 @@ def rustfmt_cmd(path):
|
||||
return None
|
||||
|
||||
|
||||
# Shell and fish are opt-in too, gated on an .editorconfig SECTION that matches the file
|
||||
# (shfmt reads .editorconfig natively for its settings, so the marker and the tool agree).
|
||||
# "One canonical style" turned out not to mean "the style the project uses": 3 of ~/.dots's
|
||||
# 25 fish files are not fish_indent-clean, so running these on sight rewrote whole files and
|
||||
# buried a 40-line change in a 200-line diff. ~/.dots opts its shell in via
|
||||
# `[{*.sh,dotsync,dotpub}]` while its fish files stay hand-formatted — existence of the
|
||||
# .editorconfig alone must not turn either formatter on.
|
||||
# Neovim agrees: conform.nvim there lists lua/css/html/ts/js only — never sh or fish.
|
||||
def fish_cmd(path):
|
||||
exe = shutil.which("fish_indent") # ships with fish, one canonical style
|
||||
return [exe, "-w", path] if exe else None
|
||||
exe = shutil.which("fish_indent")
|
||||
if exe and editorconfig_covers(path):
|
||||
return [exe, "-w", path]
|
||||
return None
|
||||
|
||||
|
||||
def shfmt_cmd(path):
|
||||
exe = shutil.which("shfmt") # de-facto shell formatter, sane defaults
|
||||
return [exe, "-w", path] if exe else None
|
||||
exe = shutil.which("shfmt")
|
||||
if exe and editorconfig_covers(path):
|
||||
return [exe, "-w", path]
|
||||
return None
|
||||
|
||||
|
||||
def taplo_cmd(path):
|
||||
@@ -133,6 +188,10 @@ def main():
|
||||
path = ti.get("file_path") or tr.get("filePath") or ""
|
||||
if not path or not os.path.isfile(path):
|
||||
return
|
||||
# Resolve symlinks before walking for markers: deployed dotfiles get edited via
|
||||
# their ~/.config/... links, but the opt-in configs live beside the real file in
|
||||
# the repo — a lexical walk from the link's path would never find them.
|
||||
path = os.path.realpath(path)
|
||||
handler = HANDLERS.get(os.path.splitext(path)[1].lower())
|
||||
if not handler:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user