blob: 47b2e3ad7422957eb1c0c8fa8ec8a695c4596ed2 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
#!/usr/bin/env bash
# managed-files.sh -- audit how every file under the cwd is managed.
# Run by hand from the claude-config folder (~/.config/claude) to check that the
# .gitignore and .stignore splits agree and nothing falls through. Classifies
# each file under the current directory as:
# - synced := NOT ignored by .stignore (its #include is resolved to real patterns).
# - tracked := listed by `git ls-files`.
# - local := neither (ignored by .stignore AND untracked).
# - both := tracked AND synced (double-managed; should be empty).
# Prints the four counts then the full lists. .git is pruned and shown as a single
# synthetic entry; the local/both lists are built with comm(1).
# Exits non-zero if .stignore is missing or an #include resolves to no patterns.
set -euo pipefail
ROOT=$(pwd)
TMP=$(mktemp --directory)
trap 'rm -rf "$TMP"' EXIT
die() {
printf 'error: %s\n' "$1" >&2
exit 1
}
# - Translate one .stignore glob to a fully-anchored ERE.
# - Handles the !/(?i)/(?d) prefixes, leading-/ anchoring, *, **, ?, and the
# trailing-/ directory form; a match also covers everything under a directory.
# - Prints one line: "<kind>\t<ere>" where kind is "ignore" or "include".
glob_to_ere() {
local p=$1 kind=ignore anchored=0 out="" i=0 ch
[[ $p == '!'* ]] && {
kind=include
p=${p#!}
}
while :; do
case $p in
'(?i)'*) p=${p#'(?i)'} ;; # case-insensitive prefix: ignored (patterns here are exact-case)
'(?d)'*) p=${p#'(?d)'} ;; # delete-on-conflict: irrelevant to matching
*) break ;;
esac
done
[[ $p == /* ]] && {
anchored=1
p=${p#/}
}
p=${p%/}
while ((i < ${#p})); do
ch=${p:i:1}
case $ch in
'*')
if [[ ${p:i+1:1} == '*' ]]; then
out+='.*'
((i += 2))
else
out+='[^/]*'
((i++))
fi
;;
'?')
out+='[^/]'
((i++))
;;
'[')
local j=$((i + 1)) cls
while ((j < ${#p})) && [[ ${p:j:1} != ']' ]]; do ((j++)); done
cls=${p:i+1:j-i-1}
[[ $cls == '!'* ]] && cls="^${cls#!}"
out+="[$cls]"
i=$((j + 1))
;;
'.' | '^' | '$' | '+' | '|' | '(' | ')' | '\')
out+="\\$ch"
((i++))
;;
*)
out+=$ch
((i++))
;;
esac
done
local ere
if ((anchored)); then ere="^${out}(/.*)?\$"; else ere="^(.*/)?${out}(/.*)?\$"; fi
printf '%s\t%s\n' "$kind" "$ere"
}
# - Load an ignore file, resolving #include relative to the folder ROOT.
# - Appends each pattern's ERE to $TMP/ere.txt (ignore) or $TMP/inc.txt (include).
# - Returns the number of patterns it contributed; dies on missing file.
load_patterns() {
local file=$1 added=0 line s target inc ere kind
[[ -f $file ]] || die "ignore file missing: $file"
while IFS= read -r line || [[ -n $line ]]; do
s=${line#"${line%%[![:space:]]*}"} # ltrim
s=${s%"${s##*[![:space:]]}"} # rtrim
[[ -z $s || $s == //* ]] && continue
if [[ $s == '#include'* ]]; then
target=${s#'#include'}
target=${target#"${target%%[![:space:]]*}"}
inc=$(realpath --canonicalize-missing "$ROOT/$target")
local sub
sub=$(load_patterns "$inc")
((sub == 0)) && die "#include '$target' ($inc) resolved to no patterns"
((added += sub))
continue
fi
IFS=$'\t' read -r kind ere < <(glob_to_ere "$s")
if [[ $kind == include ]]; then printf '%s\n' "$ere" >>"$TMP/inc.txt"; else printf '%s\n' "$ere" >>"$TMP/ere.txt"; fi
((added++)) || true
done <"$file"
printf '%s\n' "$added"
}
[[ -f $ROOT/.stignore ]] || die ".stignore is missing"
: >"$TMP/ere.txt"
: >"$TMP/inc.txt"
total=$(load_patterns "$ROOT/.stignore")
((total == 0)) && die ".stignore resolved to no patterns"
# all files + symlinks, pruning .git into a single synthetic entry
find . -path ./.git -prune -o \( -type f -o -type l \) -print |
sed 's,^\./,,' >"$TMP/all.tmp"
printf '.git\n' >>"$TMP/all.tmp"
sort -u "$TMP/all.tmp" >"$TMP/all.txt"
git ls-files | sort -u >"$TMP/tracked.txt"
# ignored := matches an ignore ERE but not a re-include ERE; synced := the rest
grep -E -f "$TMP/ere.txt" "$TMP/all.txt" | sort -u >"$TMP/ign_raw.txt" || true
if [[ -s $TMP/inc.txt ]]; then
grep -E -f "$TMP/inc.txt" "$TMP/all.txt" | sort -u >"$TMP/inc_match.txt" || true
else : >"$TMP/inc_match.txt"; fi
comm -23 "$TMP/ign_raw.txt" "$TMP/inc_match.txt" >"$TMP/ignored.txt"
comm -23 "$TMP/all.txt" "$TMP/ignored.txt" >"$TMP/synced.txt"
sort -u "$TMP/tracked.txt" "$TMP/synced.txt" >"$TMP/union.txt"
comm -23 "$TMP/all.txt" "$TMP/union.txt" >"$TMP/local.txt" # in all, not tracked nor synced
comm -12 "$TMP/tracked.txt" "$TMP/synced.txt" >"$TMP/both.txt" # tracked AND synced
printf 'root: %s\n' "$ROOT"
printf 'tracked: %s\n' "$(wc -l <"$TMP/tracked.txt")"
printf 'synced: %s\n' "$(wc -l <"$TMP/synced.txt")"
printf 'local: %s (ignored by .stignore AND untracked)\n' "$(wc -l <"$TMP/local.txt")"
printf 'both: %s (tracked AND synced -- should be 0)\n' "$(wc -l <"$TMP/both.txt")"
for name in tracked synced local both; do
printf '\n===== %s (%s) =====\n' "${name^^}" "$(wc -l <"$TMP/$name.txt")"
cat "$TMP/$name.txt"
done
|