302 lines
10 KiB
Bash
Executable File
302 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
#MISE description="Summarise codebase shape"
|
|
|
|
set -euo pipefail
|
|
|
|
source "$(git rev-parse --show-toplevel)/scripts/_helpers.sh"
|
|
|
|
ensure_working_directory!
|
|
|
|
# Thin contexts have no sub-directory but are contexts per docs/architecture.md.
|
|
THIN_CONTEXTS=(
|
|
"lib/music_library/collection.ex"
|
|
"lib/music_library/wishlist.ex"
|
|
"lib/music_library/search.ex"
|
|
"lib/music_library/scrobble_activity.ex"
|
|
"lib/music_library/maintenance.ex"
|
|
)
|
|
|
|
# Namespace modules whose matching sibling dir is structural, not a context.
|
|
CONTEXT_EXCLUSIONS=(
|
|
"lib/music_library.ex"
|
|
"lib/music_library_web.ex"
|
|
)
|
|
|
|
BAR_WIDTH=33
|
|
INNER_WIDTH=62 # dashes between box corners
|
|
BOX_WIDTH=$((INNER_WIDTH + 2))
|
|
|
|
if [[ -n "${NO_COLOR:-}" ]] || ! command -v tput >/dev/null 2>&1; then
|
|
COLOR=0
|
|
BLOCK_FILL="#"
|
|
BLOCK_EMPTY="-"
|
|
BOX_TL="+"
|
|
BOX_TR="+"
|
|
BOX_BL="+"
|
|
BOX_BR="+"
|
|
BOX_H="-"
|
|
BOX_V="|"
|
|
RULE="-"
|
|
reset=""
|
|
dim=""
|
|
bold=""
|
|
declare -a PALETTE=("" "" "" "" "" "")
|
|
else
|
|
COLOR=1
|
|
BLOCK_FILL="█"
|
|
BLOCK_EMPTY="░"
|
|
BOX_TL="╭"
|
|
BOX_TR="╮"
|
|
BOX_BL="╰"
|
|
BOX_BR="╯"
|
|
BOX_H="─"
|
|
BOX_V="│"
|
|
RULE="─"
|
|
reset="$(tput sgr0)"
|
|
dim="$(tput setaf 8 2>/dev/null || tput setaf 0)"
|
|
bold="$(tput bold)"
|
|
declare -a PALETTE=(
|
|
"$(tput setaf 6)" # cyan
|
|
"$(tput setaf 5)" # magenta
|
|
"$(tput setaf 3)" # yellow
|
|
"$(tput setaf 2)" # green
|
|
"$(tput setaf 4)" # blue
|
|
"$(tput setaf 7)" # white
|
|
)
|
|
fi
|
|
|
|
workdir="$(mktemp -d)"
|
|
trap 'rm -rf "$workdir"' EXIT
|
|
|
|
# Build "all lib .ex files" (sorted, unique) as the candidate pool.
|
|
all_lib="$workdir/all_lib"
|
|
fd --type file --extension ex . lib | sort -u > "$all_lib"
|
|
|
|
# Helper: subtract a classified list from the candidate pool in place.
|
|
function consume {
|
|
local classified="$1"
|
|
local remaining="$workdir/remaining"
|
|
comm -23 "$all_lib" <(sort -u "$classified") > "$remaining"
|
|
mv "$remaining" "$all_lib"
|
|
}
|
|
|
|
# 1. Schemas — any .ex with `use Ecto.Schema`.
|
|
schemas="$workdir/schemas"
|
|
grep -rlE '^\s*use\s+Ecto\.Schema' lib | sort -u > "$schemas" || true
|
|
# Intersect with the candidate pool so we only count pool members.
|
|
comm -12 "$all_lib" "$schemas" > "$workdir/schemas.scoped"
|
|
mv "$workdir/schemas.scoped" "$schemas"
|
|
consume "$schemas"
|
|
|
|
# 2. Controllers.
|
|
controllers="$workdir/controllers"
|
|
fd --type file --extension ex . lib/music_library_web/controllers | sort -u > "$controllers"
|
|
comm -12 "$all_lib" "$controllers" > "$workdir/controllers.scoped"
|
|
mv "$workdir/controllers.scoped" "$controllers"
|
|
consume "$controllers"
|
|
|
|
# 3. Components.
|
|
components="$workdir/components"
|
|
fd --type file --extension ex . lib/music_library_web/components | sort -u > "$components"
|
|
comm -12 "$all_lib" "$components" > "$workdir/components.scoped"
|
|
mv "$workdir/components.scoped" "$components"
|
|
consume "$components"
|
|
|
|
# 4. Live Views.
|
|
liveviews="$workdir/liveviews"
|
|
fd --type file --extension ex . lib/music_library_web/live | sort -u > "$liveviews"
|
|
comm -12 "$all_lib" "$liveviews" > "$workdir/liveviews.scoped"
|
|
mv "$workdir/liveviews.scoped" "$liveviews"
|
|
consume "$liveviews"
|
|
|
|
# 5. Contexts — depth-1 .ex with sibling directory OR in THIN_CONTEXTS allowlist.
|
|
contexts="$workdir/contexts"
|
|
: > "$contexts"
|
|
for f in lib/*.ex lib/music_library/*.ex; do
|
|
[[ -f "$f" ]] || continue
|
|
dir="${f%.ex}"
|
|
if [[ -d "$dir" ]]; then
|
|
echo "$f"
|
|
fi
|
|
done >> "$contexts"
|
|
for f in "${THIN_CONTEXTS[@]}"; do
|
|
[[ -f "$f" ]] && echo "$f" >> "$contexts"
|
|
done
|
|
sort -u "$contexts" -o "$contexts"
|
|
excluded="$workdir/context_exclusions"
|
|
printf "%s\n" "${CONTEXT_EXCLUSIONS[@]}" | sort -u > "$excluded"
|
|
comm -23 "$contexts" "$excluded" > "$workdir/contexts.filtered"
|
|
mv "$workdir/contexts.filtered" "$contexts"
|
|
comm -12 "$all_lib" "$contexts" > "$workdir/contexts.scoped"
|
|
mv "$workdir/contexts.scoped" "$contexts"
|
|
consume "$contexts"
|
|
|
|
# 6. Logic — everything else.
|
|
logic="$workdir/logic"
|
|
cp "$all_lib" "$logic"
|
|
|
|
lib_liveviews=$(wc -l < "$liveviews" | tr -d ' ')
|
|
lib_components=$(wc -l < "$components" | tr -d ' ')
|
|
lib_controllers=$(wc -l < "$controllers" | tr -d ' ')
|
|
lib_contexts=$(wc -l < "$contexts" | tr -d ' ')
|
|
lib_schemas=$(wc -l < "$schemas" | tr -d ' ')
|
|
lib_logic=$(wc -l < "$logic" | tr -d ' ')
|
|
lib_total=$((lib_liveviews + lib_components + lib_controllers + lib_contexts + lib_schemas + lib_logic))
|
|
|
|
# test/ counts.
|
|
test_files=$(fd --type file --full-path '_test\.exs$' test | wc -l | tr -d ' ')
|
|
test_support=$(
|
|
fd --type file --extension ex --extension exs . test \
|
|
| grep -cvE '(_test\.exs|/test_helper\.exs)$'
|
|
)
|
|
test_hurl=$(fd --type file --extension hurl . test | wc -l | tr -d ' ')
|
|
test_total=$((test_files + test_support + test_hurl))
|
|
|
|
# Stack versions and prod-dep count.
|
|
elixir_ver=$(sed -nE 's/^elixir[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' mise.toml)
|
|
elixir_ver="${elixir_ver%-otp-*}"
|
|
otp_ver=$(sed -nE 's/^erlang[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' mise.toml)
|
|
phoenix_ver=$(sed -nE 's/.*"phoenix":[[:space:]]*\{:hex, :phoenix, "([^"]+)".*/\1/p' mix.lock)
|
|
liveview_ver=$(sed -nE 's/.*"phoenix_live_view":[[:space:]]*\{:hex, :phoenix_live_view, "([^"]+)".*/\1/p' mix.lock)
|
|
|
|
# Prod deps: lines starting with `{:name` in mix.exs minus those tagged only: :dev/:test.
|
|
total_deps=$(grep -cE '^[[:space:]]*\{:[a-z_]+' mix.exs)
|
|
nonprod_deps=$(grep -cE 'only:[[:space:]]*(:dev|:test|\[:dev|\[:test)' mix.exs)
|
|
prod_deps=$((total_deps - nonprod_deps))
|
|
|
|
function max_of {
|
|
local max=0
|
|
for v in "$@"; do
|
|
(( v > max )) && max=$v
|
|
done
|
|
echo "$max"
|
|
}
|
|
|
|
function repeat {
|
|
local s="$1" n="$2"
|
|
local out=""
|
|
for ((i = 0; i < n; i++)); do out+="$s"; done
|
|
printf "%s" "$out"
|
|
}
|
|
|
|
function render_bar {
|
|
local count="$1" max="$2" color="$3"
|
|
local filled=0
|
|
if (( max > 0 )); then
|
|
filled=$(( count * BAR_WIDTH / max ))
|
|
fi
|
|
local empty=$(( BAR_WIDTH - filled ))
|
|
if (( COLOR )); then
|
|
printf "%s%s%s%s%s%s" \
|
|
"$color" "$(repeat "$BLOCK_FILL" "$filled")" "$reset" \
|
|
"$dim" "$(repeat "$BLOCK_EMPTY" "$empty")" "$reset"
|
|
else
|
|
printf "%s%s" \
|
|
"$(repeat "$BLOCK_FILL" "$filled")" \
|
|
"$(repeat "$BLOCK_EMPTY" "$empty")"
|
|
fi
|
|
}
|
|
|
|
function render_row {
|
|
local label="$1" count="$2" max="$3" total="$4" color="$5"
|
|
local pct="0.0"
|
|
if (( total > 0 )); then
|
|
pct=$(awk -v c="$count" -v t="$total" 'BEGIN { printf "%.1f", (c*100)/t }')
|
|
fi
|
|
printf "%-15s " "$label"
|
|
render_bar "$count" "$max" "$color"
|
|
printf " %4d (%5s%%)\n" "$count" "$pct"
|
|
}
|
|
|
|
function render_fact {
|
|
local label="$1" value="$2" color="$3"
|
|
if (( COLOR )); then
|
|
printf "%-15s %s%s%s\n" "$label" "$color" "$value" "$reset"
|
|
else
|
|
printf "%-15s %s\n" "$label" "$value"
|
|
fi
|
|
}
|
|
|
|
function render_header {
|
|
local title="$1"
|
|
local pad=$(( INNER_WIDTH - ${#title} - 2 ))
|
|
(( pad < 0 )) && pad=0
|
|
printf "%s%s%s\n" "$BOX_TL" "$(repeat "$BOX_H" "$INNER_WIDTH")" "$BOX_TR"
|
|
if (( COLOR )); then
|
|
printf "%s %s%s%s%s%s\n" "$BOX_V" "$bold" "$title" "$reset" "$(repeat " " "$pad")" "$BOX_V"
|
|
else
|
|
printf "%s %s%s%s\n" "$BOX_V" "$title" "$(repeat " " "$pad")" "$BOX_V"
|
|
fi
|
|
printf "%s%s%s\n" "$BOX_BL" "$(repeat "$BOX_H" "$INNER_WIDTH")" "$BOX_BR"
|
|
}
|
|
|
|
function render_section_header {
|
|
local label="$1" total="${2:-}" unit="${3:-}"
|
|
local rule
|
|
rule="$(repeat "$RULE" "$BOX_WIDTH")"
|
|
if (( COLOR )); then
|
|
if [[ -n "$total" ]]; then
|
|
printf "\n%s%s%s %s%s%s %s\n%s%s%s\n" \
|
|
"$bold" "$label" "$reset" \
|
|
"$bold" "$total" "$reset" "$unit" \
|
|
"$dim" "$rule" "$reset"
|
|
else
|
|
printf "\n%s%s%s\n%s%s%s\n" \
|
|
"$bold" "$label" "$reset" \
|
|
"$dim" "$rule" "$reset"
|
|
fi
|
|
else
|
|
if [[ -n "$total" ]]; then
|
|
printf "\n%s %s %s\n%s\n" "$label" "$total" "$unit" "$rule"
|
|
else
|
|
printf "\n%s\n%s\n" "$label" "$rule"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
render_header "music_library · codebase summary"
|
|
|
|
render_section_header "stack" "" ""
|
|
render_fact "Elixir" "$elixir_ver" "${PALETTE[0]}"
|
|
render_fact "OTP" "$otp_ver" "${PALETTE[1]}"
|
|
render_fact "Phoenix" "$phoenix_ver" "${PALETTE[2]}"
|
|
render_fact "LiveView" "$liveview_ver" "${PALETTE[3]}"
|
|
render_fact "Prod deps" "$prod_deps" "${PALETTE[4]}"
|
|
|
|
lib_max=$(max_of "$lib_liveviews" "$lib_components" "$lib_controllers" "$lib_contexts" "$lib_schemas" "$lib_logic")
|
|
render_section_header "lib/" "$lib_total" "modules"
|
|
render_row "Live Views" "$lib_liveviews" "$lib_max" "$lib_total" "${PALETTE[0]}"
|
|
render_row "Components" "$lib_components" "$lib_max" "$lib_total" "${PALETTE[1]}"
|
|
render_row "Controllers" "$lib_controllers" "$lib_max" "$lib_total" "${PALETTE[2]}"
|
|
render_row "Contexts" "$lib_contexts" "$lib_max" "$lib_total" "${PALETTE[3]}"
|
|
render_row "Schemas" "$lib_schemas" "$lib_max" "$lib_total" "${PALETTE[4]}"
|
|
render_row "Logic" "$lib_logic" "$lib_max" "$lib_total" "${PALETTE[5]}"
|
|
|
|
test_max=$(max_of "$test_files" "$test_support" "$test_hurl")
|
|
render_section_header "test/" "$test_total" "files"
|
|
render_row "Test files" "$test_files" "$test_max" "$test_total" "${PALETTE[0]}"
|
|
render_row "Support" "$test_support" "$test_max" "$test_total" "${PALETTE[1]}"
|
|
render_row "HTTP (.hurl)" "$test_hurl" "$test_max" "$test_total" "${PALETTE[2]}"
|
|
|
|
if command -v backlog >/dev/null 2>&1; then
|
|
backlog_raw=$(backlog task list --plain 2>/dev/null)
|
|
backlog_todo=$(printf "%s\n" "$backlog_raw" | awk 'BEGIN{p=0} /^To Do:/{p=1;next} /^[A-Z][A-Za-z ]*:$/{p=0} p && /^ \[/{c++} END{print c+0}')
|
|
backlog_in_progress=$(printf "%s\n" "$backlog_raw" | awk 'BEGIN{p=0} /^In Progress:/{p=1;next} /^[A-Z][A-Za-z ]*:$/{p=0} p && /^ \[/{c++} END{print c+0}')
|
|
backlog_done=$(printf "%s\n" "$backlog_raw" | awk 'BEGIN{p=0} /^Done:/{p=1;next} /^[A-Z][A-Za-z ]*:$/{p=0} p && /^ \[/{c++} END{print c+0}')
|
|
backlog_total=$((backlog_todo + backlog_in_progress + backlog_done))
|
|
|
|
backlog_max=$(max_of "$backlog_todo" "$backlog_in_progress" "$backlog_done")
|
|
render_section_header "backlog/" "$backlog_total" "tasks"
|
|
render_row "To Do" "$backlog_todo" "$backlog_max" "$backlog_total" "${PALETTE[2]}"
|
|
render_row "In Progress" "$backlog_in_progress" "$backlog_max" "$backlog_total" "${PALETTE[0]}"
|
|
render_row "Done" "$backlog_done" "$backlog_max" "$backlog_total" "${PALETTE[3]}"
|
|
fi
|
|
|
|
if (( COLOR )); then
|
|
printf "\n%sgenerated %s · source: fd + grep + backlog%s\n" \
|
|
"$dim" "$(date +%Y-%m-%d)" "$reset"
|
|
else
|
|
printf "\ngenerated %s · source: fd + grep + backlog\n" "$(date +%Y-%m-%d)"
|
|
fi
|