Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions terminal/menus/mq-main-menu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -182,17 +182,6 @@ surface_compact_dual_figure_row() {
"$C_RESET"
}

# Formats git state for the compact terminal surface.
surface_git_state() {
local count
count="$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')"
if [[ -z "$count" || "$count" == "0" ]]; then
printf "Clean"
else
printf "Dirty (%s)" "$count"
fi
}

# Renders the command surface view for terminal output.
render_command_surface() {
local USER_NAME HOST_NAME TIME SURFACE_COLOR FIGURE_COLOR ALT_FIGURE_COLOR width git_state tip activity system_state
Expand Down
79 changes: 71 additions & 8 deletions terminal/menus/mq-release-menu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -243,11 +243,19 @@ show_release_status() {
row_bold "RELEASE STATUS"
empty_row

local snapshot change_count state severity next_action
snapshot="$(mq_git_status_snapshot "$RELEASE_REPO")"
change_count="$(printf '%s' "$snapshot" | cut -d'|' -f4)"
state="$(printf '%s' "$snapshot" | cut -d'|' -f5)"
severity="$(printf '%s' "$snapshot" | cut -d'|' -f6)"
next_action="$(printf '%s' "$snapshot" | cut -d'|' -f7)"

row "Repo: $RELEASE_REPO"
row "Current version: $(current_version)"
row "Latest tag: $(latest_tag || true)"
row "Release script: $RELEASE_SCRIPT"
row "Changelog: $CHANGELOG_FILE"
row "Working tree: $state ($change_count changes)"
row "Severity: $severity"
row "Next action: $next_action"

print_footer
pause_enter
Expand Down Expand Up @@ -697,19 +705,72 @@ auto_release() {
pause_enter
}

# Returns a one-line status string for the menu footer.
# Returns a compact status string for the menu footer.
release_status_line() {
local files_status
local files_status snapshot change_count
files_status="$(_release_files_status)"
case "$files_status" in
missing:*)
printf 'not initialized — missing: %s → run option 3' "${files_status#missing:}"
printf 'not initialized'
;;
not_executable)
printf 'release.sh not executable → run option 3'
printf 'release.sh not executable'
;;
*)
snapshot="$(mq_git_status_snapshot "$RELEASE_REPO")"
change_count="$(printf '%s' "$snapshot" | cut -d'|' -f4)"
if (( change_count > 0 )); then
printf 'blocked — dirty (%s)' "$change_count"
else
printf 'ready (v%s)' "$(current_version)"
fi
;;
esac
}

# Returns release status detail for the menu footer.
release_status_detail() {
local files_status missing_files snapshot change_count severity
files_status="$(_release_files_status)"
case "$files_status" in
missing:*)
missing_files="${files_status#missing:}"
missing_files="${missing_files//,/ , }"
missing_files="${missing_files// ,/,}"
printf 'Missing: %s' "$missing_files"
;;
not_executable)
printf 'Script: %s' "$RELEASE_SCRIPT"
;;
*)
snapshot="$(mq_git_status_snapshot "$RELEASE_REPO")"
change_count="$(printf '%s' "$snapshot" | cut -d'|' -f4)"
severity="$(printf '%s' "$snapshot" | cut -d'|' -f6)"
if (( change_count > 0 )); then
printf 'Severity: %s — review or stash changes before release' "$severity"
else
printf 'Latest tag: %s' "$(latest_tag || true)"
fi
;;
esac
}

# Returns the next recommended release menu action.
release_status_next() {
local files_status snapshot change_count
files_status="$(_release_files_status)"
case "$files_status" in
missing:*|not_executable)
printf 'Next: 3. Initialize files'
;;
*)
printf 'ready (v%s)' "$(current_version)"
snapshot="$(mq_git_status_snapshot "$RELEASE_REPO")"
change_count="$(printf '%s' "$snapshot" | cut -d'|' -f4)"
if (( change_count > 0 )); then
printf 'Next: 1. Review status'
else
printf 'Next: 4. Dry run release'
fi
;;
esac
}
Expand Down Expand Up @@ -757,7 +818,9 @@ print_release_menu() {
surface_split_row "7. View changelog" "8. Show latest tags" "$width" "$panel_color"
surface_split_row "9. Open changelog" "10. Open release script" "$width" "$panel_color"
surface_row "" "$width" "$panel_color"
surface_row "Status: $(release_status_line)" "$width" "$panel_color"
surface_row "STATUS" "$width" "$panel_color"
surface_split_row "Status: $(release_status_line)" "$(release_status_next)" "$width" "$panel_color"
surface_row "$(release_status_detail)" "$width" "$panel_color"
surface_bottom "$width" "$panel_color"
printf '\n'
}
Expand Down
69 changes: 69 additions & 0 deletions tests/git-status-contract-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP_REPO="$(mktemp -d)"
trap 'rm -rf "$TMP_REPO"' EXIT

fail() {
printf '[FAIL] %s\n' "$1" >&2
exit 1
}

pass() {
printf '[PASS] %s\n' "$1"
}

git -C "$TMP_REPO" init -q
git -C "$TMP_REPO" config user.name "MQ Test"
git -C "$TMP_REPO" config user.email "mq-test@example.invalid"
printf 'base\n' > "$TMP_REPO/tracked.txt"
git -C "$TMP_REPO" add tracked.txt
git -C "$TMP_REPO" commit -qm "base"

mkdir -p "$TMP_REPO/generated"
printf 'one\n' > "$TMP_REPO/generated/one.txt"
printf 'two\n' > "$TMP_REPO/generated/two.txt"
printf 'three\n' > "$TMP_REPO/generated/three.txt"
printf 'changed\n' >> "$TMP_REPO/tracked.txt"

# shellcheck disable=SC1090
source "$ROOT/ui/terminal-ui/mq-ui.sh"
snapshot="$(mq_git_status_snapshot "$TMP_REPO")"
[[ "$(printf '%s' "$snapshot" | cut -d'|' -f1)" == "0" ]] || fail "staged count"
[[ "$(printf '%s' "$snapshot" | cut -d'|' -f2)" == "1" ]] || fail "unstaged count"
[[ "$(printf '%s' "$snapshot" | cut -d'|' -f3)" == "1" ]] || fail "untracked directory count"
[[ "$(printf '%s' "$snapshot" | cut -d'|' -f4)" == "2" ]] || fail "canonical change count"
[[ "$(printf '%s' "$snapshot" | cut -d'|' -f5)" == "DIRTY" ]] || fail "dirty state"
[[ "$(printf '%s' "$snapshot" | cut -d'|' -f6)" == "LOW" ]] || fail "dirty severity"
[[ "$(surface_git_state "$TMP_REPO")" == "Dirty (2)" ]] || fail "surface state"
pass "shared Git snapshot uses porcelain entries"

zsh_snapshot="$(zsh -c 'source "$1"; mq_git_status_snapshot "$2"' _ "$ROOT/ui/terminal-ui/mq-ui.sh" "$TMP_REPO")"
[[ "$(printf '%s' "$zsh_snapshot" | cut -d'|' -f4)" == "2" ]] || fail "zsh snapshot"
pass "shared Git snapshot works in zsh"

MACOS_SCRIPTS_HOME="$ROOT"
# shellcheck disable=SC1090
source "$ROOT/terminal/menus/mq-release-menu.sh"
RELEASE_REPO="$TMP_REPO"
printf '1.0.0\n' > "$TMP_REPO/VERSION"
printf '# Changelog\n' > "$TMP_REPO/CHANGELOG.md"
printf '#!/usr/bin/env bash\n' > "$TMP_REPO/release.sh"
chmod +x "$TMP_REPO/release.sh"
refresh_release_paths

[[ "$(release_status_line)" == "blocked — dirty (5)" ]] || fail "release blocked count"
[[ "$(release_status_next)" == "Next: 1. Review status" ]] || fail "release next action"
pass "release footer blocks dirty repositories"

git -C "$TMP_REPO" add -A
git -C "$TMP_REPO" commit -qm "clean"
[[ "$(release_status_line)" == "ready (v1.0.0)" ]] || fail "clean release status"
pass "release footer reports clean repositories ready"

dashboard="$( (cd "$TMP_REPO" && MACOS_SCRIPTS_HOME="$ROOT" bash "$ROOT/ui/ascii/mqlaunch-dashboard-v7.1.sh") 2>&1)"
printf '%s\n' "$dashboard" | grep -q 'DIRTY 0 files' && fail "dashboard unexpectedly dirty"
pass "dashboard consumes shared Git snapshot"

printf 'OK: shared Git/release status contract passed\n'
1 change: 1 addition & 0 deletions tools/scripts/test-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ echo "== Running mqlaunch legacy/bridge checks =="
echo
echo "== Running mqlaunch headless checks =="
"$PROJECT_ROOT/tests/headless-smoke.sh"
"$PROJECT_ROOT/tests/git-status-contract-smoke.sh"

echo
echo "== Running HAL menu checks =="
Expand Down
29 changes: 20 additions & 9 deletions ui/ascii/mqlaunch-dashboard-v7.1.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ ACCENT_YELLOW="${C_YELLOW}"
ACCENT_RED="${C_RED}"
ACCENT_DIM="${C_DIM}"

_MQ_DASHBOARD_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
_MQ_DASHBOARD_ROOT="$(cd "$_MQ_DASHBOARD_DIR/../.." && pwd)"
_MQ_DASHBOARD_UI="$_MQ_DASHBOARD_ROOT/ui/terminal-ui/mq-ui.sh"
if [[ -f "$_MQ_DASHBOARD_UI" ]]; then
# shellcheck disable=SC1090
source "$_MQ_DASHBOARD_UI"
fi

# Handles mq strip ansi.
mq_strip_ansi() {
printf '%s' "$1" | perl -pe 's/\e\[[0-9;]*m//g'
Expand Down Expand Up @@ -394,7 +402,7 @@ mqlaunch_dashboard_v71() {
local width compact
local mode_color state_color severity severity_color
local user host now shell_name os_name cwd repo branch dirty counts staged unstaged untracked ahead_behind
local total_changes next_action workspace_summary
local snapshot total_changes next_action workspace_summary
local mem_widget batt_widget bar_max

width="$(mq_term_width)"
Expand All @@ -409,11 +417,14 @@ mqlaunch_dashboard_v71() {
cwd="$(mq_cwd)"
repo="$(mq_git_repo)"
branch="$(mq_git_branch)"
dirty="$(mq_git_dirty_state)"
counts="$(mq_git_counts)"
staged="$(printf '%s' "$counts" | cut -d'|' -f1)"
unstaged="$(printf '%s' "$counts" | cut -d'|' -f2)"
untracked="$(printf '%s' "$counts" | cut -d'|' -f3)"
snapshot="$(mq_git_status_snapshot "$cwd")"
staged="$(printf '%s' "$snapshot" | cut -d'|' -f1)"
unstaged="$(printf '%s' "$snapshot" | cut -d'|' -f2)"
untracked="$(printf '%s' "$snapshot" | cut -d'|' -f3)"
total_changes="$(printf '%s' "$snapshot" | cut -d'|' -f4)"
dirty="$(printf '%s' "$snapshot" | cut -d'|' -f5)"
severity="$(printf '%s' "$snapshot" | cut -d'|' -f6)"
next_action="$(printf '%s' "$snapshot" | cut -d'|' -f7)"
ahead_behind="$(mq_git_ahead_behind)"
mem_widget="$(mq_memory_widget)"
batt_widget="$(mq_battery_widget)"
Expand All @@ -424,11 +435,11 @@ mqlaunch_dashboard_v71() {

bar_max=$(( staged + unstaged + untracked ))
(( bar_max < 5 )) && bar_max=5
total_changes=$(( staged + unstaged + untracked ))

severity="$(mq_dirty_severity "$staged" "$unstaged" "$untracked")"
severity_color="$(mq_dirty_severity_color "$severity")"
next_action="$(mq_git_next_action "$staged" "$unstaged" "$untracked" "$ahead_behind")"
if (( total_changes == 0 )); then
next_action="$(mq_git_next_action "$staged" "$unstaged" "$untracked" "$ahead_behind")"
fi

mode_color="$(mq_mode_color "$mode")"
state_color="$(mq_state_color "$dirty")"
Expand Down
57 changes: 54 additions & 3 deletions ui/terminal-ui/mq-ui.sh
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,64 @@ surface_split_row() {
"$C_RESET"
}

# Returns the canonical Git status snapshot used by mqlaunch surfaces.
# Format: staged|unstaged|untracked|changes|state|severity|next_action
mq_git_status_snapshot() {
local repo="${1:-.}"
local porcelain staged unstaged untracked changes state severity next_action

if ! git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
printf '0|0|0|0|NO_REPO|UNKNOWN|Open a Git repository'
return 0
fi

porcelain="$(git -C "$repo" status --porcelain=v1 --untracked-files=normal 2>/dev/null || true)"
staged="$(printf '%s\n' "$porcelain" | awk 'length($0) >= 2 && substr($0, 1, 1) != " " && substr($0, 1, 1) != "?" {count++} END {print count + 0}')"
unstaged="$(printf '%s\n' "$porcelain" | awk 'length($0) >= 2 && substr($0, 1, 2) != "??" && substr($0, 2, 1) != " " {count++} END {print count + 0}')"
untracked="$(printf '%s\n' "$porcelain" | awk 'substr($0, 1, 2) == "??" {count++} END {print count + 0}')"
changes="$(printf '%s\n' "$porcelain" | awk 'length($0) >= 2 {count++} END {print count + 0}')"

if (( changes == 0 )); then
state="CLEAN"
severity="STABLE"
next_action="Nothing to commit"
else
state="DIRTY"
if (( changes <= 2 )); then
severity="LOW"
elif (( changes <= 6 )); then
severity="MEDIUM"
elif (( changes <= 12 )); then
severity="HIGH"
else
severity="CRITICAL"
fi

if (( unstaged > 0 || untracked > 0 )); then
next_action="Review diff, then stage selected files"
elif (( staged > 0 )); then
next_action="Commit staged changes"
else
next_action="Review git status"
fi
fi

printf '%s|%s|%s|%s|%s|%s|%s' \
"$staged" "$unstaged" "$untracked" "$changes" "$state" "$severity" "$next_action"
}

# Handles surface git state.
surface_git_state() {
local count
count="$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')"
local repo="${1:-.}"
local snapshot count state
snapshot="$(mq_git_status_snapshot "$repo")"
count="$(printf '%s' "$snapshot" | cut -d'|' -f4)"
state="$(printf '%s' "$snapshot" | cut -d'|' -f5)"

if [[ -z "$count" || "$count" == "0" ]]; then
if [[ "$state" == "CLEAN" ]]; then
printf "Clean"
elif [[ "$state" == "NO_REPO" ]]; then
printf "No Git"
else
printf "Dirty (%s)" "$count"
fi
Expand Down
Loading