From abf486aaf56e5a14b57cd14765816db89f2a4b28 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Tue, 8 Aug 2023 11:22:29 +0100 Subject: [PATCH 01/44] =?UTF-8?q?perf:=20=F0=9F=9A=80=20Remove=20unnecessa?= =?UTF-8?q?ry=20directory=20path=20variable.=20Disable=20internal=20aliase?= =?UTF-8?q?s=20and=20set=20owner=20of=20packages.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Salvydas Lukosius --- zi.zsh | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/zi.zsh b/zi.zsh index 200a7492..a1cc7b13 100755 --- a/zi.zsh +++ b/zi.zsh @@ -113,8 +113,6 @@ fi : ${ZI[ZMODULES_DIR]:=${ZI[HOME_DIR]}/zmodules} : ${ZI[ZCOMPDUMP_PATH]:=${ZI[CACHE_DIR]}/.zcompdump} : ${ZI[COMPLETIONS_DIR]:=${ZI[HOME_DIR]}/completions} -# Additional/Optional directories for software/data files. -: ${ZI[NODE_PATH_DIR]:=${ZPFX}/lib/node_modules} # Base Directory Specification (XDG) # https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html @@ -122,8 +120,13 @@ fi : ${XDG_ZI_CACHE:=${ZI[CACHE_DIR]}} : ${XDG_ZI_CONFIG:=${ZI[CONFIG_DIR]}} -# Options Specification +# Disable aliases. : ${ZI[ALIASES_OPT]:=${${options[aliases]:#off}:+1}} + +# Disable Zi's internal aliases. +: ${ZI[INTERNAL_ALIASES]:=0} + +# Set owner of the Zi's packages. It allows to use the packages from specific user instead of the z-shell organization. : ${ZI[PKG_OWNER]:=z-shell} ZI[HOME_DIR]=${~ZI[HOME_DIR]} @@ -140,7 +143,6 @@ ZI[SERVICES_DIR]=${~ZI[SERVICES_DIR]} ZI[ZMODULES_DIR]=${~ZI[ZMODULES_DIR]} ZI[ZCOMPDUMP_PATH]=${~ZI[ZCOMPDUMP_PATH]} ZI[COMPLETIONS_DIR]=${~ZI[COMPLETIONS_DIR]} -ZI[NODE_PATH_DIR]=${~ZI[NODE_PATH_DIR]} ZPFX=${~ZPFX} XDG_ZI_HOME=${~ZI[HOME_DIR]} @@ -170,7 +172,7 @@ fi if [[ -z ${path[(re)${ZPFX}/sbin]} ]] && [[ -d ${ZPFX}/sbin ]]; then path=( "${ZPFX}/sbin" "${path[@]}" ) - typeset -gxU path PATH + sudo typeset -gxU path PATH fi if [[ -z ${fpath[(re)${ZI[COMPLETIONS_DIR]}]} ]]; then @@ -185,12 +187,6 @@ if [[ -z ${logpath[(re)${ZI[LOG_DIR]}]} ]] && [[ -d ${ZI[LOG_DIR]} ]]; then typeset -gxTU LOG_PATH logpath fi -if [[ -z ${nodepath[(re)${ZI[NODE_PATH_DIR]}]} ]] && [[ -d ${ZI[NODE_PATH_DIR]} ]]; then - nodepath=( "${ZI[NODE_PATH_DIR]}" "${nodepath[@]}" ) - typeset -gxU nodepath NODE_PATH - typeset -gxTU NODE_PATH nodepath -fi - ZI[UPAR]=";:^[[A;:^[OA;:\\e[A;:\\eOA;:${termcap[ku]/$'\e'/^\[};:${terminfo[kcuu1]/$'\e'/^\[};:" ZI[DOWNAR]=";:^[[B;:^[OB;:\\e[B;:\\eOB;:${termcap[kd]/$'\e'/^\[};:${terminfo[kcud1]/$'\e'/^\[};:" ZI[RIGHTAR]=";:^[[C;:^[OC;:\\e[C;:\\eOC;:${termcap[kr]/$'\e'/^\[};:${terminfo[kcuf1]/$'\e'/^\[};:" @@ -1199,7 +1195,7 @@ builtin setopt noaliases if [[ ! -d ${ZI[SNIPPETS_DIR]} ]]; then command mkdir -p "${ZI[SNIPPETS_DIR]}/OMZ::plugins" command chmod go-w "${ZI[SNIPPETS_DIR]}" - ( builtin cd ${ZI[SNIPPETS_DIR]}; command ln -s OMZ::plugins plugins; ) + ( builtin cd -q ${ZI[SNIPPETS_DIR]}; command ln -s OMZ::plugins plugins; ) command mkdir -p "${ZI[SERVICES_DIR]}" command chmod go-w "${ZI[SERVICES_DIR]}" fi @@ -2912,7 +2908,9 @@ zmodload zsh/zpty zsh/system 2>/dev/null zmodload -F zsh/stat b:zstat 2>/dev/null && ZI[HAVE_ZSTAT]=1 # code. [[[ -builtin alias zini=zi zinit=zi zplugin=zi + +# Internal aliases for compatibility with deprecated names. Can be enabled with ZI[INTERNAL_ALIASES]=1. +(( ZI[INTERNAL_ALIASES] )) && builtin alias zinit=zi zplugin=zi .zi-prepare-home From 88a3d20746b327d3ee9761e040178eb5e49e4a70 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sun, 15 Jun 2025 12:54:15 +0100 Subject: [PATCH 02/44] fix: Correctly declare 'sigsize' as local in rpm2cpio.zsh and remove sudo from path declaration in zi.zsh Signed-off-by: Salvydas Lukosius --- lib/zsh/rpm2cpio.zsh | 2 +- zi.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/zsh/rpm2cpio.zsh b/lib/zsh/rpm2cpio.zsh index 2ea78587..26d12d58 100755 --- a/lib/zsh/rpm2cpio.zsh +++ b/lib/zsh/rpm2cpio.zsh @@ -15,7 +15,7 @@ set -- ${(s: :)$(od -j $o -N 8 -t u1 $pkg)} local i=$(( 256 * ( 256 * ( 256 * $2 + $3 ) + $4 ) + $5 )) local d=$(( 256 * ( 256 * ( 256 * $6 + $7 ) + $8 ) + $9 )) -sigsize=$(( 8 + 16 * $i + $d )) +local sigsize=$(( 8 + 16 * $i + $d )) o=$(( $o + $sigsize + ( 8 - ( $sigsize % 8 ) ) % 8 + 8 )) set -- ${(s: :)$(od -j $o -N 8 -t u1 $pkg)} i=$(( 256 * ( 256 * ( 256 * $2 + $3 ) + $4 ) + $5 )) diff --git a/zi.zsh b/zi.zsh index a1cc7b13..95474344 100755 --- a/zi.zsh +++ b/zi.zsh @@ -172,7 +172,7 @@ fi if [[ -z ${path[(re)${ZPFX}/sbin]} ]] && [[ -d ${ZPFX}/sbin ]]; then path=( "${ZPFX}/sbin" "${path[@]}" ) - sudo typeset -gxU path PATH + typeset -gxU path PATH fi if [[ -z ${fpath[(re)${ZI[COMPLETIONS_DIR]}]} ]]; then From b8248aa165cc13f0050e29cf8c02b67eefae7c2a Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sun, 15 Jun 2025 16:36:46 +0100 Subject: [PATCH 03/44] feat: Add version command to display Zi version Signed-off-by: Salvydas Lukosius --- lib/_zi | 3 ++- zi.zsh | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/_zi b/lib/_zi index 0ea5fa94..5ce82ea6 100755 --- a/lib/_zi +++ b/lib/_zi @@ -1,6 +1,6 @@ #compdef zi -setopt local_options warn_create_global type_set_silent +builtin setopt local_options warn_create_global type_set_silent local curcontext="$curcontext" state state_descr line ret=1 local -a expl @@ -61,6 +61,7 @@ commands=( run:"Execute code inside plugin's folder" env-whitelist:"Allows to specify names (also patterns) of variables left unchanged during an unload. -v - verbose" add-fpath:"Add plugin folder to \$fpath" + version:"Show Zi version" ) _arguments -C \ diff --git a/zi.zsh b/zi.zsh index 95474344..1f7152a0 100755 --- a/zi.zsh +++ b/zi.zsh @@ -27,6 +27,13 @@ multisrc|compile|nocompile|nocompletions|reset-prompt|wrap|reset|sh|\!sh|bash|\! countdown|ps-on-unload|ps-on-update|trigger-load|light-mode|is-snippet|atdelete|pack|git|verbose|on-update-of|\ subscribe|extract|param|opts|autoload|subst|install|pullopts|debug|null|binary" +# Determine ZI[VERSION] +if [[ -d "${ZI[BIN_DIR]:-$PWD}/.git" || -e "${ZI[BIN_DIR]:-$PWD}/.git" ]]; then + ZI[VERSION]=$(git -C "${ZI[BIN_DIR]:-$PWD}" describe --tags --exact-match 2>/dev/null) || ZI[VERSION]=$(git -C "${ZI[BIN_DIR]:-$PWD}" rev-parse --short HEAD 2>/dev/null) || ZI[VERSION]="unknown" +else + ZI[VERSION]="unknown" +fi + # In no value, i.e. not designed to hold value. ZI[nval-ice-list]="blockf|silent|lucid|trackbinds|cloneonly|nocd|run-atpull|nocompletions|sh|\!sh|bash|\!bash|\ ksh|\!ksh|csh|\!csh|aliases|countdown|light-mode|is-snippet|git|verbose|cloneopts|pullopts|debug|null|binary|make|\ @@ -36,7 +43,7 @@ nocompile|reset" ZI[cmd-list]="-h|--help|help|subcmds|icemods|analytics|man|self-update|times|zstatus|load|light|unload|\ snippet|ls|ice|update|status|report|delete|loaded|list|cd|create|edit|glance|stress|changes|recently|clist|completions|\ cclear|cdisable|cenable|creinstall|cuninstall|csearch|compinit|dtrace|dstart|dstop|dunload|dreport|dclear|compile|\ -uncompile|compiled|cdlist|cdreplay|cdclear|srv|recall|env-whitelist|bindkeys|module|add-fpath|run" +uncompile|compiled|cdlist|cdreplay|cdclear|srv|recall|env-whitelist|bindkeys|module|add-fpath|run|version" # Establish ZI[BIN_DIR] [[ ! -e ${ZI[BIN_DIR]}/zi.zsh ]] && ZI[BIN_DIR]= @@ -1567,6 +1574,7 @@ builtin setopt noaliases [[ -n ${reply[1-correct]} && ! -x ${reply[1-correct]} ]] && command chmod a+x ${reply[@]} [[ ${ICE[atinit]} = '!'* || -n ${ICE[src]} || -n ${ICE[multisrc]} || ${ICE[atload][1]} = "!" ]] && { if [[ ${ZI[TMP_SUBST]} = inactive ]]; then + # Temporary substituting of functions code is inlined from .zi-tmp-subst-on. (( ${+functions[compdef]} )) && ZI[bkp-compdef]="${functions[compdef]}" || builtin unset "ZI[bkp-compdef]" functions[compdef]=':zi-tmp-subst-compdef "$@";' ZI[TMP_SUBST]=1 @@ -1938,7 +1946,7 @@ builtin setopt noaliases shift 2 # No – an error message: +zi-message "{b}{u-warn}ERROR{b-warn}:{rst}{msg2} Incorrect options given{ehi}:" "${(Mpj:$sep:)@:#-*}{rst}{msg2}. Allowed for the subcommand{ehi}:{rst}" \ - "{apo}\`{cmd}$cmd{apo}\`{msg2} are{ehi}:{rst}" "{nl}{mmdsh} {opt}${allowed//\|/$sep2}{msg2}." "{nl}{…} Aborting.{rst}" + "{apo}\`{cmd}$cmd{apo}\`{msg2} are{ehi}:{rst}" "{nl}{mmdsh} {opt}${allowed//\|/$sep2}{msg2}." "{nl}(To see the general help message, type: {cmd}zi help{nl})" "{…} Aborting.{rst}" } else { local -a cmds cmds=( load snippet update delete ) @@ -2619,6 +2627,9 @@ zi() { .zi-unload "${2%%(///|//|/)}" "${${3:#-q}%%(///|//|/)}" "${${(M)4:#-q}:-${(M)3:#-q}}"; ___retval=$? fi ;; + (version) + +zi-message "Zi version: ${ZI[VERSION]}" + ;; (bindkeys) .zi-list-bindkeys ;; From 3425c1c04e5a4becb120bf1e47a04b45625f6cad Mon Sep 17 00:00:00 2001 From: Sall <59910950+ss-o@users.noreply.github.com> Date: Mon, 16 Jun 2025 08:37:19 +0100 Subject: [PATCH 04/44] (misc): improved functionality, formatting and usability. (#327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [refactor] Update codebase (#264) * [feat]: ZI_USR hash * [fix]: updates/deprecates/adds subcommand options and other minor helpers. * [refactor]: ⚡️ rename zi-show-times > zi-times + minor fixes --------- * message/output (#265) [feat]: improve/rewrite/silence message. * updates (#266) refactor: ⚡️ improves updates * zpmod/build (#267) * [build]: ⚡️ consolidate zpmod --------- * chore: 🚀 * style: 🎨 add space in output of 'zi times' * [fix]: .zi-prepare-home use builtin cd -q * fix: 🐛 register subcommand: report * improve .zi-module build process * perf: ⚰️ deduplicate compinit calls --------- Signed-off-by: Salvydas Lukosius Co-authored-by: Salvydas Lukosius --- .github/workflows/zunit.yml | 10 +- docs/README.md | 88 ++--- lib/_zi | 11 +- lib/zsh/autoload.zsh | 660 +++++++++++++++++---------------- lib/zsh/git-process-output.zsh | 13 +- lib/zsh/install.zsh | 41 +- lib/zsh/side.zsh | 31 +- zi.zsh | 267 ++++++------- 8 files changed, 537 insertions(+), 584 deletions(-) mode change 100755 => 100644 zi.zsh diff --git a/.github/workflows/zunit.yml b/.github/workflows/zunit.yml index aff0fd88..13c28d4d 100644 --- a/.github/workflows/zunit.yml +++ b/.github/workflows/zunit.yml @@ -8,13 +8,11 @@ on: paths: - "lib/zsh/install.zsh" - "lib/zsh/autoload.zsh" - pull_request: - branches: [main] - paths: - - "lib/zsh/install.zsh" - - "lib/zsh/autoload.zsh" workflow_dispatch: {} +env: + branch_ref: $GITHUB_REF_NAME + jobs: zunit-matrix: runs-on: ubuntu-latest @@ -50,7 +48,7 @@ jobs: with: repository: "z-shell/zd" token: ${{ secrets.GITHUB_TOKEN }} - - run: command git clone --branch main --depth 1 -- https://github.com/z-shell/zi.git zi + - run: command git clone --branch ${{ env.branch_ref }} --depth 1 -- https://github.com/z-shell/zi.git zi - name: "⚡ Install dependencies" run: | sudo apt-get update && sudo apt-get install -yq zsh diff --git a/docs/README.md b/docs/README.md index d8a81958..5d9d6d42 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,66 +1,46 @@ - +
@@ -74,10 +54,8 @@ @@ -194,21 +172,15 @@

⭐ Credits

diff --git a/lib/_zi b/lib/_zi index 5ce82ea6..b6933ca1 100755 --- a/lib/_zi +++ b/lib/_zi @@ -157,14 +157,15 @@ case $state in && ret=0 ;; unload|report) - typeset -a plugins absolute normal + typeset -a plugins absolute1 absolute2 absolute3 normal plugins=( "${ZI_REGISTERED_PLUGINS[@]:#_local/zi}" ) normal=( "${plugins[@]:#%*}" ) - absolute=( "${(M)plugins[@]:#%*}" ) - absolute=( "${absolute[@]/\%\/\//%/}" ) + absolute1=( "${(M)plugins[@]:#%*}" ) + absolute1=( "${absolute1[@]/\%\/\//%/}" ) local hm="${HOME%/}" - absolute=( "${absolute[@]/$hm/HOME}" ) - plugins=( $normal $absolute ) + absolute2=( "${absolute1[@]/$hm/HOME}" ) + absolute3=( "${absolute1[@]/\%/}" ) + plugins=( $absolute1 $absolute2 $absolute3 $normal ) _wanted plugins expl "-- Plugin --" \ compadd "$@" -a - plugins \ diff --git a/lib/zsh/autoload.zsh b/lib/zsh/autoload.zsh index db32118e..7dc24388 100755 --- a/lib/zsh/autoload.zsh +++ b/lib/zsh/autoload.zsh @@ -491,7 +491,7 @@ ZI[EXTENDED_GLOB]="" error=1 fi # Tell user that he can manually modify but should do it right - (( error )) && builtin print "${ZI[col-error]}Manual edit of ${ZI[COMPLETIONS_DIR]} occured?${ZI[col-rst]}" + (( error )) && +zi-message "{error}Manual edit of ${ZI[COMPLETIONS_DIR]} occurred?{rst}" } # ]]] # FUNCTION: .zi-check-which-completions-are-installed [[[ # For each argument that each should be a path to completion @@ -540,7 +540,7 @@ ZI[EXTENDED_GLOB]="" # $2 - plugin (only when $1 - i.e. user - given) .zi-uninstall-completions() { builtin emulate -LR zsh ${=${options[xtrace]:#off}:+-o xtrace} - builtin setopt nullglob extendedglob warncreateglobal typesetsilent noshortloops + builtin setopt null_glob extended_glob warn_create_global typeset_silent no_short_loops typeset -a completions symlinked backup_comps local c cfile bkpfile integer action global_action=0 @@ -557,8 +557,9 @@ ZI[EXTENDED_GLOB]="" # Delete completions if they are really there, either as completions (_fname) or backups (fname) for c in ${completions[@]}; do action=0 - cfile=${c:t} - bkpfile=${cfile#_} + cfile="${c:t}" + cfile="_${cfile#_}" + bkpfile="${cfile#_}" # Remove symlink to completion if [[ -n ${symlinked[(r)*/$cfile]} ]]; then command rm -f ${ZI[COMPLETIONS_DIR]}/$cfile @@ -581,7 +582,11 @@ ZI[EXTENDED_GLOB]="" if (( global_action > 0 )); then +zi-message "{msg}Uninstalled {num}$global_action{rst} completions" fi - .zi-compinit >/dev/null + + # Workaround for a nasty trick in _vim + (( ${+functions[_vim_files]} )) && unfunction _vim_files + + .zi-compinit 1 1 &>/dev/null } # ]]] # @@ -590,7 +595,7 @@ ZI[EXTENDED_GLOB]="" # FUNCTION: .zi-pager [[[ .zi-pager() { - builtin setopt LOCAL_OPTIONS EQUALS + builtin setopt extended_glob no_short_loops local_options equals # Quiet mode ? → no pager. if (( OPTS[opt_-n,--no-pager] )) { cat @@ -623,61 +628,97 @@ ZI[EXTENDED_GLOB]="" return 0 fi } # ]]] +# FUNCTION: .zi-auto-reload [[[ +# Function checks sum of all files that are used to calculate the mtime +# and if it is different from the stored one, then recompile, and reload. +.zi-auto-reload() { + builtin emulate -L zsh ${=${options[xtrace]:#off}:+-o xtrace} + builtin setopt extended_glob warn_create_global + + [[ $1 == (-q|--quiet) ]] && { local quiet=1; } || { local quiet=0; }; + + # The sum of all files that are used to calculate the mtime + local file rm_file comp_file src_file + integer sum ela elb + .zi-get-mtime-into "${ZI[BIN_DIR]}/zi.zsh" ela; (( sum += ela )) + for file ( side install autoload additional ) { + .zi-get-mtime-into "${ZI[BIN_DIR]}/lib/zsh/${file}.zsh" elb; (( sum += elb )) + } + + # If the sum of all mtime is different from the stored one, + # then remove all compiled files then recompile new ones and reload. + if (( ZI[mtime] + ZI[mtime-side] + ZI[mtime-install] + ZI[mtime-autoload] + ZI[mtime-additional] != sum )); then + + # Remove all compiled files + for rm_file ( ${ZI[BIN_DIR]}/**/*.zwc(DN) ) { + command rm -f -- ${rm_compiled} > /dev/null 2>&1 + } + + # Recompile new ones + (( quiet )) || +zi-message -n "{mmdsh}{happy} Zi{rst} » {profile}recompiling codebase{rst}{…}" + for comp_file ( ${ZI[BIN_DIR]}/**/*.zsh*~*.zwc ) { + zcompile -U -- ${comp_file} > /dev/null 2>&1 + } && { (( quiet )) || +zi-message " {term}✔{rst}"; } + + # Reload Zi + (( quiet )) || +zi-message -n "{mmdsh}{happy} Zi{rst} » {profile}reloading{rst}{…}" + for src_file ( zi side install autoload additional ) { + builtin source ${ZI[BIN_DIR]}/**/${src_file}.zsh > /dev/null 2>&1 + } && { (( quiet )) || +zi-message " {term}✔{rst}"; } + + # Update the stored mtime + .zi-set-mtime && return 0 + fi +} # ]]] # FUNCTION: .zi-self-update [[[ -# Updates ❮ Zi ❯ code (does a git pull). +# Function manages self-update of Zi codebase. +# +# It will fetch the latest changes from the upstream repository and, +# display changelog if there are any, then update the codebase and, +# reload Zi if there are any changes. # # User-action entry point. +# +# $1 - user given options for self-update (optional) .zi-self-update() { builtin emulate -LR zsh ${=${options[xtrace]:#off}:+-o xtrace} - builtin setopt extendedglob typesetsilent warncreateglobal - [[ $1 != -q ]] && +zi-message "{profile}Updating »»»»{rst} ❮ {happy}Zi{rst} ❯ {…}{rst}" - local nl=$'\n' escape=$'\x1b[' current_branch=$(builtin cd -q "$ZI[BIN_DIR]" && command git rev-parse --abbrev-ref HEAD 2>/dev/null) + builtin setopt extended_glob warn_create_global typeset_silent \ + no_short_loops rc_quotes no_auto_pushd + + (( OPTS[opt_-D,--dry-run] )) && { local dry_run=1; } || { local dry_run=0; } + (( OPTS[opt_-q,--quiet] )) && { local quiet=1 opt='--quiet'; } || { local quiet=0; } + + [[ -z $origin_url ]] && local origin_url="$(builtin cd -q $ZI[BIN_DIR] && command git remote get-url origin)" + local -a lines - ( builtin cd -q "$ZI[BIN_DIR]" && command git checkout $current_branch &>/dev/null && command git fetch --quiet && \ - lines=( ${(f)"$(command git log --color --abbrev-commit --date=short --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset || %b' ..FETCH_HEAD)"} ) - if (( ${#lines} > 0 )); then - # Remove the (origin/master ...) segments, to expect only tags to appear - lines=( "${(S)lines[@]//\(([,[:blank:]]#(origin|HEAD|master|main)[^a-zA-Z]##(HEAD|origin|master|main)[,[:blank:]]#)#\)/}" ) - # Remove " ||" if it ends the line (i.e. no additional text from the body) - lines=( "${lines[@]/ \|\|[[:blank:]]#(#e)/}" ) - # If there's no ref-name, 2 consecutive spaces occur - fix this - lines=( "${lines[@]/(#b)[[:space:]]#\|\|[[:space:]]#(*)(#e)/|| ${match[1]}}" ) - lines=( "${lines[@]/(#b)$escape([0-9]##)m[[:space:]]##${escape}m/$escape${match[1]}m${escape}m}" ) - # Replace what follows "|| ..." with the same thing but with no newlines, - # and also only first 10 words (the (w)-flag enables word-indexing) - lines=( "${lines[@]/(#b)[[:blank:]]#\|\|(*)(#e)/| ${${match[1]//$nl/ }[(w)1,(w)10]}}" ) - builtin print -rl -- "${lines[@]}" | .zi-pager - builtin print - fi - if [[ $1 != -q ]] { - command git pull --no-stat --ff-only origin $current_branch - } else { - command git pull --no-stat --quiet --ff-only origin $current_branch - } + local nl=$'\n' escape=$'\x1b[' cores=$(nproc 2>/dev/null || \ + sysctl -n hw.ncpu 2>/dev/null || command getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) + + ( + # Fetch the latest changes from the upstream repository + (( quiet )) || +zi-message "{mmdsh}{happy} Zi{rst} » {profile}fetching updates{rst}{…}" + builtin cd -q $ZI[BIN_DIR] && command git fetch $opt --jobs $cores --tags --prune --force $origin_url refs/heads/main && \ + lines=( ${(f)"$(command git log --color --abbrev-commit --decorate --date=short --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cd) %C(bold blue)<%an>%Creset' ..FETCH_HEAD)"} ) + + # If there are any changes then update the codebase and reload Zi + if (( ${#lines} > 0 )); then + lines=( "${(S)lines[@]//\(([,[:blank:]]#(origin|HEAD|master|main)[^a-zA-Z]##(HEAD|origin|master|main)[,[:blank:]]#)#\)/}" ) + lines=( "${lines[@]/ \|\|[[:blank:]]#(#e)/}" ) + lines=( "${lines[@]/(#b)[[:space:]]#\|\|[[:space:]]#(*)(#e)/|| ${match[1]}}" ) + lines=( "${lines[@]/(#b)$escape([0-9]##)m[[:space:]]##${escape}m/$escape${match[1]}m${escape}m}" ) + lines=( "${lines[@]/(#b)[[:blank:]]#\|\|(*)(#e)/| ${${match[1]//$nl/ }[(w)1,(w)10]}}" ) + (( quiet )) || { builtin print -rl -- "${lines[@]}" | .zi-pager; } + + if (( ! dry_run )); then + # Update the codebase + command git merge -n $opt --autostash --ff-only FETCH_HEAD || \ + { (( quiet )) || +zi-message "{mmdsh}{happy} Zi{rst} » {profile}update failed {quos}✘{rst}"; return 1 } + .zi-auto-reload $opt + fi + else + (( quiet )) || +zi-message "{mmdsh}{happy} Zi{rst} » {profile}up-to-date {term}✔{rst}" + fi ) - if [[ $1 != -q ]] { - +zi-message "{profile}Compiling »»»{rst} ❮ {happy}Zi{rst} ❯ {…}{rst}" - } - command rm -f ${ZI[BIN_DIR]}/*.zwc(DN) - command rm -f ${ZI[BIN_DIR]}/lib/zsh/*.zwc(DN) - zcompile -U ${ZI[BIN_DIR]}/zi.zsh - zcompile -U ${ZI[BIN_DIR]}/lib/zsh/side.zsh - zcompile -U ${ZI[BIN_DIR]}/lib/zsh/install.zsh - zcompile -U ${ZI[BIN_DIR]}/lib/zsh/autoload.zsh - zcompile -U ${ZI[BIN_DIR]}/lib/zsh/additional.zsh - zcompile -U ${ZI[BIN_DIR]}/lib/zsh/git-process-output.zsh - # Load for the current session - [[ $1 != -q ]] && +zi-message "{profile}Reloading »»»{rst} ❮ {happy}Zi{rst} ❯ {…}{rst}" - source ${ZI[BIN_DIR]}/zi.zsh - source ${ZI[BIN_DIR]}/lib/zsh/side.zsh - source ${ZI[BIN_DIR]}/lib/zsh/install.zsh - source ${ZI[BIN_DIR]}/lib/zsh/autoload.zsh - # Read and remember the new modification timestamps - local file - .zi-get-mtime-into "${ZI[BIN_DIR]}/zi.zsh" "ZI[mtime]" - for file ( side install autoload ) { - .zi-get-mtime-into "${ZI[BIN_DIR]}/lib/zsh/${file}.zsh" "ZI[mtime-${file}]" - } } # ]]] # FUNCTION: .zi-show-registered-plugins [[[ # Lists loaded plugins (subcommands list, loaded). @@ -709,7 +750,7 @@ ZI[EXTENDED_GLOB]="" # 0. Call the Zsh Plugin's Standard *_plugin_unload function # 0. Call the code provided by the Zsh Plugin's Standard @zsh-plugin-run-at-update # 1. Delete bindkeys (...) -# 2. Delete Zstyles +# 2. Delete zstyles # 3. Restore options # 4. Remove aliases # 5. Restore Zle state @@ -1199,7 +1240,7 @@ ZI[EXTENDED_GLOB]="" fi # Print title builtin printf "${ZI[col-title]}Report for${ZI[col-rst]} %s%s plugin\n"\ - "${user:+${ZI[col-uname]}$user${ZI[col-rst]}}${${user:#(%|/)*}:+/}"\ + "${user:+${ZI[col-uname]}$user${ZI[col-rst]}}${${user:#(%|/)*}:+/}"\ "${ZI[col-pname]}$plugin${ZI[col-rst]}" # Print "----------" local msg="Report for $user${${user:#(%|/)*}:+/}$plugin plugin" @@ -1418,7 +1459,7 @@ ZI[EXTENDED_GLOB]="" # Effectively return the last != 0 rc [[ "$hook_rc" -ne 0 ]] && { retval="$hook_rc" - builtin print -Pr -- "${ZI[col-warn]}Warning:%f%b ${ZI[col-obj]}${arr[5]}${ZI[col-warn]} hook returned with ${ZI[col-obj]}${hook_rc}${ZI[col-rst]}" + +zi-message "{warn}Warning{ehi}:{rst} {b}{var}${arr[5]}{rst}{msg} hook returned {b}{num}$hook_rc{rst}" } done if (( ZI[annex-multi-flag:pull-active] >= 2 )) { @@ -1432,73 +1473,75 @@ ZI[EXTENDED_GLOB]="" ICE=() } } - if [[ -d $local_dir/.git ]] && ( builtin cd -q $local_dir ; git show-ref --verify --quiet refs/heads/main ); then + if [[ -d $local_dir/.git ]] && ( builtin cd -q $local_dir ; command git show-ref --verify --quiet refs/heads/main ); then local main_branch=main - else + elif [[ -d $local_dir/.git ]] && ( builtin cd -q $local_dir ; command git show-ref --verify --quiet refs/heads/master ); then local main_branch=master + else + local main_branch=$(builtin cd -q $local_dir ; command git rev-parse --abbrev-ref HEAD 2>/dev/null) fi if (( ! is_release )) { ( builtin cd -q "$local_dir" || return 1 - integer had_output=0 - local IFS=$'\n' - command git fetch --quiet && \ - declare -a line; line=( ${(f)"$(command git log --color --abbrev-commit --date=short --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cd) %C(bold blue)<%an>%Creset' ..FETCH_HEAD)"} ) - if (( ${#line} > 0 )); then - [[ $had_output -eq 0 ]] && { - had_output=1 - if (( OPTS[opt_-q,--quiet] && !PUPDATE )) { - .zi-any-colorify-as-uspl2 "$id_as" - (( ZI[first-plugin-mark] )) && { - ZI[first-plugin-mark]=0 - } || +zi-message "{nl}Updating{ehi}:{rst} $REPLY{rst}" - } - } - +zi-message "$line" - fi | command tee .zi_lastupd | .zi-pager & - integer pager_pid=$! - { sleep 20 && kill -9 $pager_pid 2>/dev/null 1>&2; } &! - { wait $pager_pid; } > /dev/null 2>&1 - local -a log - { log=( ${(@f)"$(<$local_dir/.zi_lastupd)"} ); } 2>/dev/null - command rm -f $local_dir/.zi_lastupd - if [[ ${#log} -gt 0 ]] { - ZI[annex-multi-flag:pull-active]=2 - } else { - if (( ${+ice[run-atpull]} || OPTS[opt_-u,--urge] )) { - ZI[annex-multi-flag:pull-active]=1 - # Handle the snippet/plugin boundary in the messages - if (( OPTS[opt_-q,--quiet] && !PUPDATE )) { - .zi-any-colorify-as-uspl2 "$id_as" - (( ZI[first-plugin-mark] )) && { - ZI[first-plugin-mark]=0 - } || +zi-message "{nl}Updating{ehi}:{rst} $REPLY{rst}" + integer had_output=0 + local IFS=$'\n' + local cores=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || command getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) + command git fetch --quiet --jobs $cores && \ + local -a line; line=( ${(f)"$(command git log --color --abbrev-commit --date=short --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cd) %C(bold blue)<%an>%Creset' ..FETCH_HEAD)"} ) + if (( ${#line} > 0 )); then + [[ $had_output -eq 0 ]] && { + had_output=1 + if (( OPTS[opt_-q,--quiet] && !PUPDATE )) { + .zi-any-colorify-as-uspl2 "$id_as" + (( ZI[first-plugin-mark] )) && { + ZI[first-plugin-mark]=0 + } || +zi-message "{nl}Updating{ehi}:{rst} $REPLY{rst}" + } } - } else { - ZI[annex-multi-flag:pull-active]=0 + +zi-message "$line" | command tee .zi_lastupd | .zi-pager & + integer pager_pid=$! + { sleep 20 && kill -9 $pager_pid 2>/dev/null 1>&2; } &! + { wait $pager_pid; } > /dev/null 2>&1 + local -a log + { log=( ${(@f)"$(<$local_dir/.zi_lastupd)"} ); } 2>/dev/null + command rm -f $local_dir/.zi_lastupd + fi + if [[ ${#log} -gt 0 ]]; then + ZI[annex-multi-flag:pull-active]=2 + else + if (( ${+ice[run-atpull]} || OPTS[opt_-u,--urge] )); then + ZI[annex-multi-flag:pull-active]=1 + # Handle the snippet/plugin boundary in the messages + if (( OPTS[opt_-q,--quiet] && !PUPDATE )); then + .zi-any-colorify-as-uspl2 "$id_as" + (( ZI[first-plugin-mark] )) && \ + { ZI[first-plugin-mark]=0; } || +zi-message "{nl}Updating{ehi}:{rst} $REPLY{rst}" + fi + else + ZI[annex-multi-flag:pull-active]=0 + fi + fi + if (( ZI[annex-multi-flag:pull-active] >= 1 )) { + ICE=( "${(kv)ice[@]}" ) + # Run annexes' atpull hooks (the before atpull-ice ones). + # The regular Git-plugins block. + reply=( + ${(on)ZI_EXTS2[(I)zi hook:e-\!atpull-pre <->]} + ${${(M)ICE[atpull]#\!}:+${(on)ZI_EXTS[(I)z-annex hook:\!atpull-<-> <->]}} + ${(on)ZI_EXTS2[(I)zi hook:e-\!atpull-post <->]} + ) + for key in "${reply[@]}"; do + arr=( "${(Q)${(z@)ZI_EXTS[$key]:-$ZI_EXTS2[$key]}[@]}" ) + "${arr[5]}" plugin "$user" "$plugin" "$id_as" "$local_dir" "${${key##(zi|z-annex) hook:}%% <->}" update:git + hook_rc=$? + # Effectively return the last != 0 rc + [[ "$hook_rc" -ne 0 ]] && { + retval="$hook_rc" + +zi-message "{warn}Warning{ehi}:{rst} {b}{var}${arr[5]}{rst}{msg} hook returned {b}{num}$hook_rc{rst}" + } + done + ICE=() + (( ZI[annex-multi-flag:pull-active] >= 2 )) && command git pull -n ${=ice[pullopts]:---ff-only} origin ${ice[ver]:-$main_branch} |& command grep -E -v '(FETCH_HEAD|up.to.date\.|From.*://)' } - } - if (( ZI[annex-multi-flag:pull-active] >= 1 )) { - ICE=( "${(kv)ice[@]}" ) - # Run annexes' atpull hooks (the before atpull-ice ones). - # The regular Git-plugins block. - reply=( - ${(on)ZI_EXTS2[(I)zi hook:e-\!atpull-pre <->]} - ${${(M)ICE[atpull]#\!}:+${(on)ZI_EXTS[(I)z-annex hook:\!atpull-<-> <->]}} - ${(on)ZI_EXTS2[(I)zi hook:e-\!atpull-post <->]} - ) - for key in "${reply[@]}"; do - arr=( "${(Q)${(z@)ZI_EXTS[$key]:-$ZI_EXTS2[$key]}[@]}" ) - "${arr[5]}" plugin "$user" "$plugin" "$id_as" "$local_dir" "${${key##(zi|z-annex) hook:}%% <->}" update:git - hook_rc=$? - # Effectively return the last != 0 rc - [[ "$hook_rc" -ne 0 ]] && { - retval="$hook_rc" - builtin print -Pr -- "${ZI[col-warn]}Warning:%f%b ${ZI[col-obj]}${arr[5]}${ZI[col-warn]} hook returned with ${ZI[col-obj]}${hook_rc}${ZI[col-rst]}" - } - done - ICE=() - (( ZI[annex-multi-flag:pull-active] >= 2 )) && command git pull --no-stat ${=ice[pullopts]:---ff-only} origin ${ice[ver]:-$main_branch} |& command grep -E -v '(FETCH_HEAD|up.to.date\.|From.*://)' - } return ${ZI[annex-multi-flag:pull-active]} ) ZI[annex-multi-flag:pull-active]=$? @@ -1538,7 +1581,7 @@ ZI[EXTENDED_GLOB]="" # Effectively return the last != 0 rc [[ "$hook_rc" -ne 0 ]] && { retval="$hook_rc" - builtin print -Pr -- "${ZI[col-warn]}Warning:%f%b ${ZI[col-obj]}${arr[5]}${ZI[col-warn]} hook returned with ${ZI[col-obj]}${hook_rc}${ZI[col-rst]}" + +zi-message "{warn}Warning{ehi}:{rst} {b}{var}${arr[5]}{rst}{msg} hook returned {b}{num}$hook_rc{rst}" } done # Run annexes' atpull hooks (the after atpull-ice ones). @@ -1555,7 +1598,7 @@ ZI[EXTENDED_GLOB]="" # Effectively return the last != 0 rc [[ "$hook_rc" -ne 0 ]] && { retval="$hook_rc" - builtin print -Pr -- "${ZI[col-warn]}Warning:%f%b ${ZI[col-obj]}${arr[5]}${ZI[col-warn]} hook returned with ${ZI[col-obj]}${hook_rc}${ZI[col-rst]}" + +zi-message "{warn}Warning{ehi}:{rst} {b}{var}${arr[5]}{rst}{msg} hook returned {b}{num}$hook_rc{rst}" } done ICE=() @@ -1578,7 +1621,7 @@ ZI[EXTENDED_GLOB]="" # Effectively return the last != 0 rc [[ "$hook_rc" -ne 0 ]] && { retval="$hook_rc" - builtin print -Pr -- "${ZI[col-warn]}Warning:%f%b ${ZI[col-obj]}${arr[5]}${ZI[col-warn]} hook returned with ${ZI[col-obj]}${hook_rc}${ZI[col-rst]}" + +zi-message "{warn}Warning{ehi}:{rst} {b}{var}${arr[5]}{rst}{msg} hook returned {b}{num}$hook_rc{rst}" } done ICE=() @@ -1644,49 +1687,22 @@ ZI[EXTENDED_GLOB]="" builtin emulate -LR zsh ${=${options[xtrace]:#off}:+-o xtrace} builtin setopt extendedglob nullglob warncreateglobal typesetsilent noshortloops local -F2 SECONDS=0 - .zi-self-update -q - [[ $2 = restart ]] && +zi-message "{msg2}Restarting the update with the new codebase loaded.{rst}"$'\n' - local file - integer sum ela elb update_rc - .zi-get-mtime-into "${ZI[BIN_DIR]}/zi.zsh" ela; (( sum += ela )) - for file ( side install autoload ) { - .zi-get-mtime-into "${ZI[BIN_DIR]}/lib/zsh/${file}.zsh" elb; (( sum += elb )) - } - # Reload ZI? - if [[ $2 != restart ]] && (( ZI[mtime] + ZI[mtime-side] + ZI[mtime-install] + ZI[mtime-autoload] != sum )) { - +zi-message "{info2}Detected {rst}❮ {happy}Zi{rst} ❯ {info2}update in another session -" "{pre}reloading {rst}{…}" - source ${ZI[BIN_DIR]}/zi.zsh - source ${ZI[BIN_DIR]}/lib/zsh/side.zsh - source ${ZI[BIN_DIR]}/lib/zsh/install.zsh - source ${ZI[BIN_DIR]}/lib/zsh/autoload.zsh - .zi-get-mtime-into "${ZI[BIN_DIR]}/zi.zsh" "ZI[mtime]" - for file ( side install autoload ) { - .zi-get-mtime-into "${ZI[BIN_DIR]}/lib/zsh/${file}.zsh" "ZI[mtime-${file}]" - } - +zi-message "{pname}Done.{rst}"$'\n' - .zi-update-or-status-all "$1" restart - return $? - } integer retval if (( OPTS[opt_-p,--parallel] )) && [[ $1 = update ]] { - (( !OPTS[opt_-q,--quiet] )) && \ - +zi-message '{info}Initiating parallel update {rst}{…}' - .zi-update-all-parallel - retval=$? - .zi-compinit 1 1 &>/dev/null - rehash - if (( !OPTS[opt_-q,--quiet] )) { - +zi-message "{info}The update took {num}${SECONDS}{info} seconds{rst}" - } + (( OPTS[opt_-q,--quiet] )) || +zi-message "{mmdsh}{happy} Zi{rst} » {profile}parallelizing update{rst}{…}" + .zi-update-all-parallel; retval=$? + .zi-compinit 1 1 &>/dev/null; rehash + (( OPTS[opt_-q,--quiet] )) || \ + +zi-message "{mmdsh}{happy} Zi{rst} » {info3}update took {num}$SECONDS{info3} seconds{rst}{…}" return $retval } - local st=$1 id_as repo snip pd user plugin - integer PUPDATE=0 + local st=$1 id_as repo snip pd user plugin; integer PUPDATE=0 local -A ICE if (( OPTS[opt_-s,--snippets] || !OPTS[opt_-l,--plugins] )) { local -a snipps snipps=( ${ZI[SNIPPETS_DIR]}/**/(._zi|._zinit|._zplugin)(ND) ) - [[ $st != status && ${OPTS[opt_-q,--quiet]} != 1 && -n $snipps ]] && +zi-message "{note}Note:{rst} update includes unloaded snippets" + [[ $st != status && ${OPTS[opt_-q,--quiet]} != 1 && -n $snipps ]] && \ + +zi-message "{mmdsh}{happy} Zi{rst} » {faint}updating snippets{rst}{…}" for snip ( ${ZI[SNIPPETS_DIR]}/**/(._zi|._zinit|._zplugin)/mode(D) ) { [[ ! -f ${snip:h}/url ]] && continue [[ -f ${snip:h}/id-as ]] && id_as="$(<${snip:h}/id-as)" || id_as= @@ -1696,13 +1712,11 @@ ZI[EXTENDED_GLOB]="" [[ -n $snipps ]] && builtin print } ICE=() - if (( OPTS[opt_-s,--snippets] && !OPTS[opt_-l,--plugins] )) { - return - } + if (( OPTS[opt_-s,--snippets] && !OPTS[opt_-l,--plugins] )) { return; } if [[ $st = status ]]; then - (( !OPTS[opt_-q,--quiet] )) && +zi-message "{note}Note:{rst} status includes unloaded plugins" + (( OPTS[opt_-q,--quiet] )) || +zi-message "{mmdsh}{happy} Zi{rst} » {faint}status for all installed plugins{rst}{…}" else - (( !OPTS[opt_-q,--quiet] )) && +zi-message "{note}Note:{rst} update includes unloaded plugins" + (( OPTS[opt_-q,--quiet] )) || +zi-message "{mmdsh}{happy} Zi{rst} » {faint}updating plugins{rst}{…}{nl}" fi ZI[first-plugin-mark]=init for repo in ${ZI[PLUGINS_DIR]}/*; do @@ -1726,27 +1740,25 @@ ZI[EXTENDED_GLOB]="" local user=${reply[-2]} plugin=${reply[-1]} # Must be a git repository or a binary release if [[ ! -d $repo/.git && ! -f $repo/._zi/is_release ]]; then - (( !OPTS[opt_-q,--quiet] )) && \ - +zi-message "$REPLY: not a git repository" + (( OPTS[opt_-q,--quiet] )) || +zi-message "$REPLY: not a git repository" continue fi if [[ $st = status ]]; then builtin print "\nStatus for plugin $REPLY" ( builtin cd -q "$repo"; command git status ) else - (( !OPTS[opt_-q,--quiet] )) && +zi-message "Updating{ehi}:{rst} $REPLY{rst}" || builtin print -n . + (( OPTS[opt_-q,--quiet] )) || +zi-message "Updating{ehi}:{rst} $REPLY{rst}" || builtin print -n . .zi-update-or-status update "$user" "$plugin" - update_rc=$? + local update_rc=$? [[ $update_rc -ne 0 ]] && { - +zi-message "{warn}Warning: {pid}${user}/${plugin} {warn}update returned{ehi}:{rst} {num}${update_rc}" + +zi-message "{warn}Warning{ehi}:{rst} {pid}${user}/${plugin} {warn}update returned{ehi}:{rst} {num}$update_rc{rst}" retval=$? } fi done - .zi-compinit 1 1 &>/dev/null - if (( !OPTS[opt_-q,--quiet] )) { - +zi-message "{ok}The update took {num}${SECONDS}{ok} seconds{rst}" - } + .zi-compinit 1 1 &>/dev/null; rehash + (( OPTS[opt_-q,--quiet] )) || \ + +zi-message "{mmdsh}{happy} Zi{rst} » {info3}update took {num}$SECONDS{info3} seconds{rst}{…}" return "$retval" } # ]]] # FUNCTION: .zi-update-in-parallel [[[ @@ -1840,7 +1852,7 @@ ZI[EXTENDED_GLOB]="" counter=0 PUAssocArray=() } elif (( counter == 1 && !OPTS[opt_-q,--quiet] )) { - +zi-message "{info3}Spawning the next{opt} ${OPTS[value]}{info3} concurrent update jobs{ehi}:{rst} {var}${tpe}{rst} {…}" + +zi-message "{info3}Spawning {opt}${OPTS[value]}{info3} concurrent {var}${tpe}{info3} updates{rst}{…}" } } # ]]] # FUNCTION: .zi-show-zstatus [[[ @@ -1908,21 +1920,22 @@ ZI[EXTENDED_GLOB]="" [[ -z $sni ]] && builtin print -n " " builtin print '\b\b ' } # ]]] -# FUNCTION: .zi-show-times [[[ +# FUNCTION: .zi-times [[[ # Shows loading times of all loaded plugins. # # User-action entry point. -.zi-show-times() { - builtin emulate -LR zsh ${=${options[xtrace]:#off}:+-o xtrace} - builtin setopt extendedglob warncreateglobal noshortloops +.zi-times() { +builtin emulate -LR zsh ${=${options[xtrace]:#off}:+-o xtrace} +builtin setopt extended_glob warn_create_global typeset_silent \ + no_short_loops rc_quotes no_auto_pushd no_bang_hist local opt="$1 $2 $3" entry entry2 entry3 user plugin float -F 3 sum=0.0 local -A sice local -a tmp [[ "$opt" = *-[a-z]#m[a-z]#* ]] && \ - { builtin print "Plugin loading moments (relative to the first prompt):"; ((1)); } || \ - builtin print "Plugin loading times:" + { +zi-message "{info2}Plugin loading moments (relative to the first prompt){ehi}:{rst}"; ((1)); } || \ + +zi-message "{info2}Plugin loading times{ehi}:{rst}" for entry in "${(@on)ZI[(I)TIME_[0-9]##_*]}"; do entry2="${entry#TIME_[0-9]##_}" entry3="AT_$entry" @@ -1941,8 +1954,8 @@ ZI[EXTENDED_GLOB]="" fi local attime=$(( ZI[$entry3] - ZI[START_TIME] )) if [[ "$opt" = *-[a-z]#s[a-z]#* ]]; then - local time="$ZI[$entry] sec" - attime="${(M)attime#*.???} sec" + local time="$ZI[$entry] s" + attime="${(M)attime#*.???} s" else local time="${(l:5:: :)$(( ZI[$entry] * 1000 ))%%[,.]*} ms" attime="${(l:5:: :)$(( attime * 1000 ))%%[,.]*} ms" @@ -1954,20 +1967,20 @@ ZI[EXTENDED_GLOB]="" elif [[ "$opt" = *-[a-z]#a[a-z]#* ]]; then line="$attime $line" fi - line="$line - $REPLY" + line="$line {mdsh} $REPLY {rst}" if [[ ${sice[as]} == "command" ]]; then - line="$line (command)" - elif [[ -n ${sice[sbin]+abc} ]]; then - line="$line (sbin command)" - elif [[ -n ${sice[fbin]+abc} ]]; then - line="$line (fbin command)" + line="$line {mdsh} {faint} ({cmd}command{rst}{faint}){rst}" + elif [[ -n ${sice[sbin]+abc} || -n ${sice[sbin]} ]]; then + line="$line {mdsh} {faint} ({ice}sbin{rst} {cmd}command{rst}{faint}){rst}" + elif [[ -n ${sice[fbin]+abc} || -n ${sice[fbin]} ]]; then + line="$line {mdsh} {faint} ({ice}fbin{rst} {cmd}command{rst}{faint}){rst}" elif [[ ( ${sice[pick]} = /dev/null || ${sice[as]} = null ) && ${+sice[make]} = 1 ]]; then - line="$line (/dev/null make plugin)" + line="$line {mdsh} {faint} ({dir}/dev/null{rst} {cmd}make{rst} {p}plugin{rst}{faint}){rst}" fi - builtin print "$line" + +zi-message "$line" (( sum += ZI[$entry] )) done - builtin print "Total: $sum sec" + +zi-message "{mmdsh}{rst} {b}Total{ehi}:{rst} {auto}${sum}s" } # ]]] # FUNCTION: .zi-list-bindkeys [[[ .zi-list-bindkeys() { @@ -2546,10 +2559,9 @@ builtin print -Pr \"\$ZI[col-obj]Done (with the exit code: \$_retval).%f%b\"" if (( OPTS[opt_-y,--yes] )); then integer retval eval "$2"; retval=$? - (( OPTS[opt_-q,--quiet] )) || builtin print "\nDone (action executed, exit code: $retval)" + (( OPTS[opt_-q,--quiet] )) || +zi-message "{nl}Done! (exit code{ehi}:{rst} {num}$retval{rst})" else - builtin print -Pr -- "$1" - builtin print "[yY/n…]" + builtin print -Pr -- "$1 - [y/N]?" local ans if [[ -t 0 ]] { read -q ans @@ -2558,9 +2570,9 @@ builtin print -Pr \"\$ZI[col-obj]Done (with the exit code: \$_retval).%f%b\"" } if [[ "$ans" = "y" ]] { eval "$2" - builtin print "\nDone (action executed, exit code: $?)" + +zi-message "{nl}Done! (exit code{ehi}:{rst} {num}$?{rst})" } else { - builtin print "\nBreak, no action" + +zi-message "{nl}Break, no action…" return 1 } fi @@ -2603,7 +2615,7 @@ builtin print -Pr \"\$ZI[col-obj]Done (with the exit code: \$_retval).%f%b\"" [[ $uspl1 = custom || $uspl1 = _local---zi ]] && continue pushd "$p" >/dev/null || continue if [[ -d .git ]]; then - gitout=`command git log --all --max-count=1 --since=$timespec 2>/dev/null` + gitout=$(command git log --all --max-count=1 --since=$timespec 2>/dev/null) if [[ -n $gitout ]]; then .zi-any-colorify-as-uspl2 "$uspl1" builtin print -r -- "$REPLY" @@ -2868,9 +2880,9 @@ EOF elif (( ${+commands[eza]} )); then ZI[TREE]="${commands[eza]} --color=always -T -l -L3" else - builtin print "${ZI[col-error]}No \`tree' program, it is required by the subcommand \`ls\'${ZI[col-rst]}" - builtin print "Download from: http://mama.indstate.edu/users/ice/tree/" - builtin print "It is also available probably in all distributions and Homebrew, as package \`tree'" + +zi-message "{auto}Program \`tree\` not found, it is required by the subcommand \`ls\`" + +zi-message "Download from{ehi}:{rst} {auto}http://mama.indstate.edu/users/ice/tree/" + +zi-message "{auto}It is also available probably in all distributions and Homebrew, as package \`tree\`." fi ( builtin cd -q "${ZI[SNIPPETS_DIR]}" @@ -2942,87 +2954,93 @@ EOF } || builtin print -r -- "No such plugin or snippet" } # ]]] # FUNCTION: .zi-module [[[ -# Function that has sub-commands passed as long-options (with two dashes, --). -# It's an attempt to plugin only this one function into `zi' function -# defined in zi.zsh, to not make this file longer than it's needed. +# Builds a module from its git repository and displays information how to load the module in .zshrc, +# performs checks, verify hard stored values, and rebuilds the module if requested. .zi-module() { - if [[ "$1" = "build" ]]; then + if [[ "$1" = (-r|--reset) ]]; then + [[ -e ${ZI[ZMODULES_DIR]}/zpmod/COMPILED_AT ]] && \ + local compiled_at_ts="$(<${ZI[ZMODULES_DIR]}/zpmod/COMPILED_AT)" + [[ -e ${ZI[ZMODULES_DIR]}/zpmod/RECOMPILE_REQUEST ]] && \ + local recompile_request_ts="$(<${ZI[ZMODULES_DIR]}/zpmod/RECOMPILE_REQUEST)" + if [[ ${recompile_request_ts:-1} -gt ${compiled_at_ts:-0} ]]; then + +zi-message "{warn}Warning{warn}{ehi}:{rst} {lhi}recompilation{rst} of the {bcmd}zpmod{rst} module has been requested…" + (( ${+functions[.zi-confirm]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/autoload.zsh" || return 1 + .zi-confirm "Do you want to proceed?" || return 1 + command make -C "${ZI[ZMODULES_DIR]}/zpmod" distclean &>/dev/null + .zi-module --build + fi + elif [[ "$1" = (-B|--build|build) ]]; then builtin autoload -Uz is-at-least if is-at-least 5.8.1; then - .zi-build-module "${@[2,-1]}" + if command git -C "${${ZI[ZMODULES_DIR]}}/zpmod" rev-parse 2>/dev/null; then + command git -C "${${ZI[ZMODULES_DIR]}}/zpmod" clean -d -f -x + command git -C "${${ZI[ZMODULES_DIR]}}/zpmod" reset --hard HEAD >/dev/null + command git -C "${${ZI[ZMODULES_DIR]}}/zpmod" pull --rebase --progress || { + +zi-message "{error}-- Failed to update module repository --{rst}"; return 1 + } + if [[ "$2" = "--clean" ]]; then + +zi-message "{p}-- Module {ok}source is clean{p}, {cmd}make distclean{p} not required --{rst}" + fi + else + if ! test -d "${${ZI[ZMODULES_DIR]}}/zpmod"; then + command mkdir -p "${${ZI[ZMODULES_DIR]}}/zpmod"; command chmod g-rwX "${${ZI[ZMODULES_DIR]}}/zpmod" + fi + command git clone --verbose --progress "https://github.com/z-shell/zpmod.git" "${${ZI[ZMODULES_DIR]}}/zpmod" \ + |& { command ${ZI[BIN_DIR]}/lib/zsh/git-process-output.zsh || cat; } || { + +zi-message "{error}-- Failed to clone module repository --{rst}"; return 1 + } + fi + ( builtin cd -q "${ZI[ZMODULES_DIR]}/zpmod" + +zi-message "{p}-- The module sources are located at{ehi}:{rst} {dir}"${ZI[ZMODULES_DIR]}/zpmod"{p} --{rst}" + if [[ -f Makefile ]]; then + if [[ "$2" = "--clean" ]]; then + +zi-message "{p}-- Building module {bcmd}zi/zpmod{p}, running: {cmd}make distclean{p}, then {cmd}./configure{p} and then {cmd}make{p} --{rst}" + +zi-message "{p}-- make distclean --{rst}" + command make distclean + ((1)) + else + +zi-message "{p}-- Building module {bcmd}zi/zpmod{p}, running: {cmd}make clean{p}, then {cmd}./configure{p} and then {cmd}make{p} --{rst}" + +zi-message "{p}-- make clean --{rst}" + command make clean + fi + fi + +zi-message "{p}-- ./configure --{rst}" + ./configure --enable-cflags='-g -Wall -Wextra -O3' --disable-gdbm --without-tcsetpgrp --quiet + +zi-message "{p}-- make --{rst}" + local cores=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || command getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) + local build_log="${ZI[LOG_DIR]}/zpmod/${EPOCHSECONDS}-build.log" + command mkdir -p "${ZI[LOG_DIR]}/zpmod" 2>/dev/null + command make --jobs=$cores -C "${ZI[ZMODULES_DIR]}/zpmod" | command tee "$build_log" >/dev/null + if command make -C "${ZI[ZMODULES_DIR]}/zpmod" &>/dev/null; then + [[ -f Src/zi/zpmod.so ]] && command cp -vf Src/zi/zpmod.{so,bundle} + builtin print "$EPOCHSECONDS" >! "${ZI[ZMODULES_DIR]}/zpmod/COMPILED_AT" + +zi-message "{ok}-- Build successful! --{rst}" + +zi-message "{faint}-- Build log{ehi}:{rst} {file}$build_log{faint} --{rst}" + .zi-module --info + return 0 + else + +zi-message "{error}Build failed{rst}…" \ + "To report an issue at GitHub, please provide error messages from{ehi}:{rst}{nl}" \ + "{ndsh} Build log{rst} {dir}${build_log}{rst} if exists{nl}" \ + "{ndsh} Run '{cmd}make -C ${ZI[ZMODULES_DIR]}/zpmod{rst}' to reproduce{rst}{nl}" \ + "{ndsh}{rst} {var}URL{rst} to submit{rst} {url}https://github.com/z-shell/zpmod/issues{rst}" + return 1 + fi + ) else +zi-message "{warn}Zsh version{rst} {obj}5.8.1{warn} or higher required{rst}" return 1 fi - elif [[ "$1" = "info" ]]; then - if [[ "$2" = "--link" ]]; then - +zi-message "Please submit any issues at the address below{ehi}:{rst} " \ - "{nl}{mmdsh}{url} https://github.com/z-shell/zpmod/issues{rst}" - else - +zi-message "To load the module, add following 2 lines to .zshrc, at top:{nl}" \ - "{nl}" \ - "{p} module_path+=( \"${ZI[ZMODULES_DIR]}/zpmod/Src\" ){rst}{nl}" \ - "{p} zmodload zi/zpmod{rst}{nl}" \ - "{nl}" \ - "After loading, use command \`zpmod' to communicate with the module.{nl}" \ - "{info2}See \`zpmod -h' for more information.{rst}" - fi - elif [[ "$1" = (help|usage) ]]; then - +zi-message "{info2}Usage{rst}{obj}:{rst}{nl}" \ - "{p}zi module{rst} {info}{build|info|help}{rst} {p}[options]{rst}{nl}" \ - "{p}zi module{rst} {info}build{rst} {p}[--clean]{rst}{nl}" \ - "{p}zi module{rst} {info}info{rst} {p}[--link]{rst}{nl}" \ + elif [[ "$1" = (-I|--info|info) ]]; then + +zi-message "{nl}To load the module, add following {info}2 lines{rst} at the top of {file}.zshrc{ehi}:{nl}" \ "{nl}" \ - "To start using the zpmod module run{rst}{obj}:{rst}{nl}" \ - "{p}zi module{rst} {info}build{rst}{nl}" \ - "Append {p}--clean{rst} to run {cmd}make distclean{rst}{nl}" \ - "To display the instructions on loading the module, run{rst}{obj}:{rst}{nl}" \ - "{p}zi module info{rst}." - fi -} # ]]] -# FUNCTION: .zi-build-module [[[ -# Performs ./configure && make on the module and displays information how to load the module in .zshrc. -.zi-build-module() { - if command git -C "${${ZI[ZMODULES_DIR]}}/zpmod" rev-parse 2>/dev/null; then - command git -C "${${ZI[ZMODULES_DIR]}}/zpmod" clean -d -f -f - command git -C "${${ZI[ZMODULES_DIR]}}/zpmod" reset --hard HEAD - command git -C "${${ZI[ZMODULES_DIR]}}/zpmod" pull - else - if ! test -d "${${ZI[ZMODULES_DIR]}}/zpmod"; then - mkdir -p "${${ZI[ZMODULES_DIR]}}/zpmod" - chmod g-rwX "${${ZI[ZMODULES_DIR]}}/zpmod" - fi - command git clone --progress "https://github.com/z-shell/zpmod.git" "${${ZI[ZMODULES_DIR]}}/zpmod" || { - +zi-message "{error}Failed to clone module repository{rst}" - return 1 - } + "{b}{p} module_path+=( \"${ZI[ZMODULES_DIR]}/zpmod/Src\" ){rst}{nl}" \ + "{b}{p} zmodload zi/zpmod{rst}{nl}" \ + "{nl}Reload shell and use command{ehi}:{rst}" \ + "{nl}{mmdsh}{cmd} zpmod{opt} -h{rst} for more information.{rst}{nl}" \ + "{nl}Please submit any issues at the address below{ehi}:{rst}" \ + "{nl}{mmdsh}{url} https://github.com/z-shell/zpmod/issues{rst}" fi - ( builtin cd -q "${ZI[ZMODULES_DIR]}/zpmod" - +zi-message "{pname}== Building module zi/zpmod, running: make clean, then ./configure and then make =={rst}" - +zi-message "{pname}== The module sources are located at: "${ZI[ZMODULES_DIR]}/zpmod" =={rst}" - if [[ -f Makefile ]]; then - if [[ "$1" = "--clean" ]]; then - noglob +zi-message {p}-- make distclean --{rst} - make distclean - ((1)) - else - noglob +zi-message {p}-- make clean --{rst} - make clean - fi - fi - noglob +zi-message {p}-- ./configure --{rst} - CPPFLAGS=-I/usr/local/include CFLAGS="-g -Wall -O3" LDFLAGS=-L/usr/local/lib ./configure --disable-gdbm --without-tcsetpgrp - noglob +zi-message {p}-- make --{rst} - if command make -s; then - [[ -f Src/zi/zpmod.so ]] && cp -vf Src/zi/zpmod.{so,bundle} - noglob +zi-message "{info}Module has been built correctly.{rst}" - .zi-module info - else - noglob +zi-message "{error}Module didn't build.{rst}" - .zi-module info --link - fi - builtin print $EPOCHSECONDS >! "${ZI[ZMODULES_DIR]}/zpmod/COMPILED_AT" - ) } # ]]] # FUNCTION: .zi-help [[[ # Shows usage information. @@ -3031,35 +3049,35 @@ EOF .zi-help() { # +zi-message "{hi}Welcome ${(%):-%n}" if (( $+commands[clear] )) { clear; } -sleep 0.03 && +zi-message "{mmdsh}{rst} ❮ {happy}Zi{rst} ❯ {mmdsh}{info} Usage{ehi}:{rst}" -sleep 0.03 && +zi-message "❯ analytics {mdsh}{rst} {auto}Statistics, benchmarks, and information" -sleep 0.03 && +zi-message "❯ subcmds {mdsh}{rst} {auto}Show subcommands registered by the annexes" -sleep 0.03 && +zi-message "❯ icemods {mdsh}{rst} {auto}Show all registered ice-modifiers" -sleep 0.03 && +zi-message "❯ self-update {mdsh}{rst} {auto}Self-update and compile" -sleep 0.04 && +zi-message "❯ compinit {mdsh}{rst} {auto}Refresh completions" -sleep 0.04 && +zi-message "❯ cclear {mdsh}{rst} {auto}Clear stray and improper completions" -sleep 0.04 && +zi-message "❯ cdreplay {opt}[-q]{rst} {mdsh}{rst} {auto}Replay compdefs (run after compinit)" -sleep 0.04 && +zi-message "❯ cdclear {opt}[-q]{rst} {mdsh}{rst} {auto}Clear compdef replay list" -sleep 0.04 && +zi-message "❯ env-whitelist {opt}[-v][-h]{rst} {mdsh}{rst} {auto}Specify names or patterns of variables left unchanged during an{rst} unload" -sleep 0.05 && +zi-message "❯ snippet {opt}[-f]{p} [snippet]{rst}|{url}[url]{rst} {mdsh}{rst} {auto}Source local or remote file" -sleep 0.05 && +zi-message "❯ delete {opt}[--all][--clean]{p} [plugin]{rst}|{url}[url]{rst} {mdsh}{rst} {auto}Remove 'plugin/snippet' from the disk" -sleep 0.05 && +zi-message "❯ update {opt}[-L][-s][-v][-q][-r][-p]{p} [plugin]{rst}|{url}[url]{rst} {mdsh}{rst} {auto}Git update plugins or snippets" -sleep 0.05 && +zi-message "❯ load {opt}[-b]{p} [plugin]{rst} {mdsh}{rst} {auto}Load plugin or absolute local path" -sleep 0.06 && +zi-message "❯ unload {opt}[-q]{p} [plugin]{rst} {mdsh}{rst} {auto}Unload plugin" -sleep 0.06 && +zi-message "❯ light {opt}[-b]{p} [plugin]{rst} {mdsh}{rst} {auto}Load plugins without 'reporting/tracking'" -sleep 0.06 && +zi-message "❯ add-fpath {opt}[-f]{p} [plugin]{rst}|{dir}[dir]{rst} {mdsh}{rst} {auto}Append directory to{var} \$fpath{rst}, use -f to prepend instead" -sleep 0.06 && +zi-message "❯ run {opt}[-l]{p} [plugin]{rst}|{cmd}[cmd]{rst} {mdsh}{rst} {auto}Runs a command in the given plugin's directory" -sleep 0.07 && +zi-message "❯ compile {opt}[--all]{p} [plugin]{rst} {mdsh}{rst} Compile plugins" -sleep 0.07 && +zi-message "❯ uncompile {opt}[--all]{p} [plugin]{rst} {mdsh}{rst} Remove compiled plugins" -sleep 0.07 && +zi-message "❯ cdisable {p}[name]{rst} {mdsh}{rst} {auto}Disable completion" -sleep 0.07 && +zi-message "❯ cenable {p}[name]{rst} {mdsh}{rst} {auto}Enable completion" -sleep 0.08 && +zi-message "❯ creinstall {p}[plugin]{rst} {mdsh}{rst} {auto}Install completions for the plugin, can also receive absolute local path" -sleep 0.08 && +zi-message "❯ cuninstall {p}[plugin]{rst} {mdsh}{rst} {auto}Uninstall completions for plugin" -sleep 0.08 && +zi-message "❯ recall {p}[plugin]{rst}|{url}[url]{rst} {mdsh}{rst} {auto}Fetch saved ice-modifiers and construct the command" -sleep 0.08 && +zi-message "❯ srv {p}[service]{rst}|{cmd}[cmd]{rst} {mdsh}{rst} Control a service{ehi}:{rst} {auto}'stop,start,restart,next,quit'" -sleep 0.09 && +zi-message "❯ create {p}[plugin]{rst} {mdsh}{rst} {auto}Create a plugin" -sleep 0.09 && +zi-message "❯ edit {p}[plugin]{rst} {mdsh}{rst} Edit plugin's file with{var} \$EDITOR{rst}{nl}" -sleep 0.09 && +zi-message "{mmdsh}{rst} ❮ {happy}Zi{rst} ❯ {mmdsh}{info} Wiki{ehi}:{rst} {url}https://wiki.zshell.dev{rst}{nl}" +sleep 0.01 && +zi-message "{mmdsh}{rst} ❮ {happy}Zi{rst} ❯ {mmdsh}{info} Usage{ehi}:{rst}{nl}" +sleep 0.01 && +zi-message "❯ analytics {mdsh}{rst} {auto}Statistics, benchmarks, and information" +sleep 0.01 && +zi-message "❯ subcmds {mdsh}{rst} {auto}Show subcommands registered by the annexes" +sleep 0.01 && +zi-message "❯ icemods {mdsh}{rst} {auto}Show all registered ice-modifiers" +sleep 0.01 && +zi-message "❯ compinit {mdsh}{rst} {auto}Refresh completions" +sleep 0.01 && +zi-message "❯ cclear {mdsh}{rst} {auto}Clear stray and improper completions" +sleep 0.02 && +zi-message "❯ cdreplay {opt}[-q]{rst} {mdsh}{rst} {auto}Replay compdefs (run after compinit)" +sleep 0.02 && +zi-message "❯ cdclear {opt}[-q]{rst} {mdsh}{rst} {auto}Clear compdef replay list" +sleep 0.01 && +zi-message "❯ self-update {opt}[-q][-D]{rst} {mdsh}{rst} {auto}Self-update and compile codebase" +sleep 0.02 && +zi-message "❯ env-whitelist {opt}[-v][-h]{rst} {mdsh}{rst} {auto}Specify names or patterns of variables left unchanged during an{rst} unload" +sleep 0.02 && +zi-message "❯ snippet {opt}[-f]{p} [snippet]{rst}|{url}[url]{rst} {mdsh}{rst} {auto}Source local or remote file" +sleep 0.02 && +zi-message "❯ delete {opt}[-a][-c][-y][-q]{p} [plugin]{rst}|{url}[url]{rst} {mdsh}{rst} {auto}Remove 'plugin' or 'snippet' from the disk" +sleep 0.02 && +zi-message "❯ update {opt}[-L][-s][-v][-q][-r][-p]{p} [plugin]{rst}|{url}[url]{rst} {mdsh}{rst} {auto}Git update plugins or snippets" +sleep 0.02 && +zi-message "❯ load {opt}[-b]{p} [plugin]{rst} {mdsh}{rst} {auto}Load plugin or absolute local path" +sleep 0.02 && +zi-message "❯ unload {opt}[-q]{p} [plugin]{rst} {mdsh}{rst} {auto}Unload plugin" +sleep 0.02 && +zi-message "❯ light {opt}[-b]{p} [plugin]{rst} {mdsh}{rst} {auto}Load plugins without 'reporting' or 'tracking'" +sleep 0.02 && +zi-message "❯ add-fpath {opt}[-f]{p} [plugin]{rst}|{dir}[dir]{rst} {mdsh}{rst} {auto}Append directory to{var} \$fpath{rst}, use {opt}-f{rst} {auto} to prepend instead" +sleep 0.02 && +zi-message "❯ run {opt}[-l]{p} [plugin]{rst}|{cmd}[cmd]{rst} {mdsh}{rst} {auto}Runs a command in the given plugin's directory" +sleep 0.02 && +zi-message "❯ compile {opt}[-a]{p} [plugin]{rst} {mdsh}{rst} Compile plugins" +sleep 0.02 && +zi-message "❯ uncompile {opt}[-a]{p} [plugin]{rst} {mdsh}{rst} Remove compiled plugins" +sleep 0.02 && +zi-message "❯ cdisable {p}[name]{rst} {mdsh}{rst} {auto}Disable completion" +sleep 0.03 && +zi-message "❯ cenable {p}[name]{rst} {mdsh}{rst} {auto}Enable completion" +sleep 0.03 && +zi-message "❯ creinstall {p}[plugin]{rst} {mdsh}{rst} {auto}Install completions for the plugin, can also receive absolute local path" +sleep 0.03 && +zi-message "❯ cuninstall {p}[plugin]{rst} {mdsh}{rst} {auto}Uninstall completions for plugin" +sleep 0.03 && +zi-message "❯ recall {p}[plugin]{rst}|{url}[url]{rst} {mdsh}{rst} {auto}Fetch saved ice-modifiers and construct the command" +sleep 0.03 && +zi-message "❯ srv {p}[service]{rst}|{cmd}[cmd]{rst} {mdsh}{rst} Control a service{ehi}:{rst} {auto}'stop,start,restart,next,quit'" +sleep 0.03 && +zi-message "❯ create {p}[plugin]{rst} {mdsh}{rst} {auto}Create a plugin" +sleep 0.03 && +zi-message "❯ edit {p}[plugin]{rst} {mdsh}{rst} Edit plugin's file with {var}\$EDITOR{rst}{nl}" +sleep 0.04 && +zi-message "{mmdsh}{info} Wiki{ehi}:{rst} {url}https://wiki.zshell.dev{rst}{nl}" } # ]]] # FUNCTION: .zi-analytics-menu [[[ # Statistics, benchmarks and information. @@ -3067,30 +3085,30 @@ sleep 0.09 && +zi-message "{mmdsh}{rst} ❮ {happy}Zi{rst} ❯ {mmdsh}{info} Wik # User-action entry point. .zi-analytics-menu() { if (( $+commands[clear] )) { clear; } -sleep 0.03 && +zi-message "{mmdsh}{rst} ❮ {happy}Zi{rst} ❯ {mmdsh}{info} Analytics{ehi}:{rst}" -sleep 0.03 && +zi-message "❯ compiled {mdsh}{rst} {auto}List plugins that are compiled" -sleep 0.03 && +zi-message "❯ zstatus {mdsh}{rst} {auto}Overall status" -sleep 0.03 && +zi-message "❯ module help {mdsh}{rst} {auto}Manage zpmod" -sleep 0.03 && +zi-message "❯ dtrace|dstart {mdsh}{rst} {auto}Start tracking what's going on in session" -sleep 0.04 && +zi-message "❯ dstop {mdsh}{rst} {auto}Stop tracking what's going on in session" -sleep 0.04 && +zi-message "❯ dreport {mdsh}{rst} {auto}Report what was going on in session" -sleep 0.04 && +zi-message "❯ dunload {mdsh}{rst} {auto}Revert changes recorded between dstart and dstop" -sleep 0.04 && +zi-message "❯ dclear {mdsh}{rst} {auto}Clear report of what was going on in session" -sleep 0.05 && +zi-message "❯ bindkeys {mdsh}{rst} {auto}List bindkeys" -sleep 0.05 && +zi-message "❯ clist|completions {mdsh}{rst} {auto}List completions in use" -sleep 0.05 && +zi-message "❯ cdlist {mdsh}{rst} {auto}Show compdef replay list" -sleep 0.05 && +zi-message "❯ csearch {mdsh}{rst} {auto}Search for available completions from any plugin" -sleep 0.06 && +zi-message "❯ man {mdsh}{rst} {auto}Show manual" -sleep 0.06 && +zi-message "❯ ls {mdsh}{rst} {auto}List snippets in formatted and colorized manner" -sleep 0.06 && +zi-message "❯ status {opt}[--all]{p} [plugin]{rst}|{url}[url]{rst} {mdsh}{rst} {auto}Git status for plugin or svn status for snippet" -sleep 0.06 && +zi-message "❯ report {opt}[--all]{p} [plugin]{rst} {mdsh}{rst} {auto}Show reports" -sleep 0.07 && +zi-message "❯ times {opt}[-s][-m][-a]{rst} {mdsh}{rst} {auto}Statistics on plugin load times, sorted in order of loading" -sleep 0.07 && +zi-message "❯ glance {p}[plugin]{rst} {mdsh}{rst} {auto}Look at plugin's source" -sleep 0.07 && +zi-message "❯ stress {p}[plugin]{rst} {mdsh}{rst} {auto}Test plugin for compatibility with set of options" -sleep 0.07 && +zi-message "❯ changes {p}[plugin]{rst} {mdsh}{rst} {auto}View plugin's git log" -sleep 0.08 && +zi-message "❯ recently {p}[time]{rst} {mdsh}{rst} {auto}Show plugins that changed recently (e.g.: 1 month 2 days)" -sleep 0.08 && +zi-message "❯ cd {p}[plugin]{rst} {mdsh}{rst} {auto}Enter plugin's directory; also support snippets, if feed with URL" -sleep 0.08 && +zi-message "❯ loaded|lists {p}[keyword]{rst} {mdsh}{rst} {auto}Show what plugins are loaded (filter: keyword)" +sleep 0.02 && +zi-message "{mmdsh}{rst} ❮ {happy}Zi{rst} ❯ {mmdsh}{info} Analytics{ehi}:{rst}{nl}" +sleep 0.02 && +zi-message "❯ compiled {mdsh}{rst} {auto}List plugins that are compiled" +sleep 0.02 && +zi-message "❯ zstatus {mdsh}{rst} {auto}Overall status" +sleep 0.02 && +zi-message "❯ dtrace|dstart {mdsh}{rst} {auto}Start tracking what's going on in session" +sleep 0.02 && +zi-message "❯ dstop {mdsh}{rst} {auto}Stop tracking what's going on in session" +sleep 0.02 && +zi-message "❯ dreport {mdsh}{rst} {auto}Report what was going on in session" +sleep 0.02 && +zi-message "❯ dunload {mdsh}{rst} {auto}Revert changes recorded between dstart and dstop" +sleep 0.02 && +zi-message "❯ dclear {mdsh}{rst} {auto}Clear report of what was going on in session" +sleep 0.03 && +zi-message "❯ bindkeys {mdsh}{rst} {auto}List bindkeys" +sleep 0.03 && +zi-message "❯ clist|completions {mdsh}{rst} {auto}List completions in use" +sleep 0.03 && +zi-message "❯ cdlist {mdsh}{rst} {auto}Show compdef replay list" +sleep 0.03 && +zi-message "❯ csearch {mdsh}{rst} {auto}Search for available completions from any plugin" +sleep 0.03 && +zi-message "❯ man {mdsh}{rst} {auto}Show manual" +sleep 0.03 && +zi-message "❯ ls {mdsh}{rst} {auto}List snippets in formatted and colorized manner" +sleep 0.03 && +zi-message "❯ status {opt}[--all]{p} [plugin]{rst}|{url}[url]{rst} {mdsh}{rst} {auto}Git status for plugin or svn status for snippet" +sleep 0.03 && +zi-message "❯ report {opt}[--all]{p} [plugin]{rst} {mdsh}{rst} {auto}Show reports" +sleep 0.04 && +zi-message "❯ module {opt}[-B][-I][-r]{rst} {mdsh}{rst} {auto}Manage zpmod" +sleep 0.04 && +zi-message "❯ times {opt}[-s][-m][-a]{rst} {mdsh}{rst} {auto}Statistics on plugin load times, sorted in order of loading" +sleep 0.04 && +zi-message "❯ glance {p}[plugin]{rst} {mdsh}{rst} {auto}Look at plugin's source" +sleep 0.04 && +zi-message "❯ stress {p}[plugin]{rst} {mdsh}{rst} {auto}Test plugin for compatibility with set of options" +sleep 0.04 && +zi-message "❯ changes {p}[plugin]{rst} {mdsh}{rst} {auto}View plugin's git log" +sleep 0.04 && +zi-message "❯ recently {p}[time]{rst} {mdsh}{rst} {auto}Show plugins that changed recently (e.g.: 1 month 2 days)" +sleep 0.04 && +zi-message "❯ cd {p}[plugin]{rst} {mdsh}{rst} {auto}Enter plugin's directory; also support snippets, if feed with URL" +sleep 0.04 && +zi-message "❯ loaded|lists {p}[keyword]{rst} {mdsh}{rst} {auto}Show what plugins are loaded (filter: keyword)" } # ]]] # FUNCTION: .zi-registered-subcommands [[[ # Shows subcommands registered by annex. diff --git a/lib/zsh/git-process-output.zsh b/lib/zsh/git-process-output.zsh index a0b95294..7222c0ac 100755 --- a/lib/zsh/git-process-output.zsh +++ b/lib/zsh/git-process-output.zsh @@ -3,6 +3,9 @@ builtin emulate -LR zsh builtin setopt extended_glob warn_create_global typeset_silent +typeset -Ag ZI_USR +[[ -z $ZI_USR[GP_COL] ]] && ZI_USR[GP_COL]=46 + { typeset -g COLS="$(tput cols)" } 2>/dev/null if (( COLS < 10 )) { COLS=40 @@ -41,7 +44,7 @@ local first=1 timeline() { local sp='▚▞'; sp="${sp:$2%2:1}" # Maximal width is 24 characters - local bar="$(builtin print -f "%.$2s█%0$(($3-$2-1))s" "░▒▓█████████████████████" "")" + local bar="$(builtin print -f "%.$2s█%0$(($3-$2-1))s" "░▒▓████████████████████" "")" local -a frames_splitted frames_splitted=( ${(@zQ)progress_frames[progress_style]} ) if (( SECONDS - last_time >= frames_splitted[1] )) { @@ -50,7 +53,7 @@ timeline() { last_time=$SECONDS } builtin print -nr -- ${frames_splitted[cur_frame+1]}" " - builtin print -nPr "%F{201}" + builtin print -nPr "%F{$ZI_USR[GP_COL]}" builtin print -f "%s %s" "${bar// /░}" "" builtin print -nPr "%f" } @@ -98,10 +101,10 @@ IFS='' [[ $+ZI_CIVIS == 1 && -n $TERM ]] && eval $ZI_CIVIS if [[ -n $TERM ]] { +[[ $COLORTERM == (24bit|truecolor) || ${terminfo[colors]} -eq 16777216 ]] || \ + zmodload zsh/nearcolor &>/dev/null { command perl -pe 'BEGIN { $|++; $/ = \1 }; tr/\r/\n/' || \ - gstdbuf -o0 gtr '\r' '\n' || \ - cat } |& \ -while read -r line; do + gstdbuf -o0 gtr '\r' '\n' || cat; } |& while read -r line; do (( ++ loop_count )) if [[ "$line" = "Cloning into"* ]]; then builtin print $line diff --git a/lib/zsh/install.zsh b/lib/zsh/install.zsh index 8df2c07c..104a416a 100755 --- a/lib/zsh/install.zsh +++ b/lib/zsh/install.zsh @@ -325,7 +325,7 @@ builtin source "${ZI[BIN_DIR]}/lib/zsh/side.zsh" || { builtin print -P "${ZI[col fi command rm -f ${TMPDIR:-/tmp}/zi-execs.$$.lst ${TMPDIR:-/tmp}/zi.installed_comps.$$.lst \ - ${TMPDIR:-/tmp}/zi.skipped_comps.$$.lst ${TMPDIR:-/tmp}/zi.compiled.$$.lst + ${TMPDIR:-/tmp}/zi.skipped_comps.$$.lst ${TMPDIR:-/tmp}/zi.compiled.$$.lst if [[ $tpe != tarball ]] { if [[ -z $update ]] { @@ -511,7 +511,7 @@ builtin source "${ZI[BIN_DIR]}/lib/zsh/side.zsh" || { builtin print -P "${ZI[col # $3 - if 1, then reinstall, otherwise only install completions that aren't there .zi-install-completions() { builtin emulate -LR zsh ${=${options[xtrace]:#off}:+-o xtrace} - builtin setopt nullglob extendedglob warncreateglobal typesetsilent noshortloops + builtin setopt null_glob extended_glob warn_create_global typeset_silent no_short_loops local id_as=$1${2:+${${${(M)1:#%}:+$2}:-/$2}} local reinstall=${3:-0} quiet=${${4:+1}:-0} @@ -549,6 +549,7 @@ builtin source "${ZI[BIN_DIR]}/lib/zsh/side.zsh" || { builtin print -P "${ZI[col ]]; then if [[ $reinstall = 1 ]]; then # Remove old files + command rm -f "${ZI[ZCOMPDUMP_PATH]}" command rm -f "${ZI[COMPLETIONS_DIR]}/${cfile}" "${ZI[COMPLETIONS_DIR]}/${bkpfile}" fi INSTALLED_COMPS+=( $cfile ) @@ -577,43 +578,17 @@ builtin source "${ZI[BIN_DIR]}/lib/zsh/side.zsh" || { builtin print -P "${ZI[col builtin print -rl -- $SKIPPED_COMPS >! ${TMPDIR:-/tmp}/zi.skipped_comps.$$.lst } - .zi-compinit 1 1 &>/dev/null + .zi-compinit 1 1 &>/dev/null; rehash } # ]]] # FUNCTION: .zi-compinit [[[ -# User-exposed `compinit' frontend which first ensures that all completions managed by ❮ Zi ❯ are forgotten by Z-shell. -# After that it runs normal `compinit', which should more easily detect ❮ Zi ❯ completions. -# -# No arguments. .zi-compinit() { [[ -n ${OPTS[opt_-p,--parallel]} && $1 != 1 ]] && return builtin emulate -LR zsh ${=${options[xtrace]:#off}:+-o xtrace} - builtin setopt nullglob extendedglob warncreateglobal typesetsilent + builtin setopt null_glob extended_glob warn_create_global typeset_silent integer use_C=$2 - typeset -a symlinked backup_comps - local c cfile bkpfile action - - symlinked=( "${ZI[COMPLETIONS_DIR]}"/_[^_.]*~*.zwc ) - backup_comps=( "${ZI[COMPLETIONS_DIR]}"/[^_.]*~*.zwc ) - - # Delete completions if they are really there, - # either as completions (_fname) or backups (fname) - for c in "${symlinked[@]}" "${backup_comps[@]}"; do - action=0 - cfile="${c:t}" - cfile="_${cfile#_}" - bkpfile="${cfile#_}" - - #print -Pr "${ZI[col-info]}Processing completion $cfile%f%b" - .zi-forget-completion "$cfile" - done - - +zi-message "Initializing completion ({func}compinit{rst}){…}" - command rm -f "${ZI[ZCOMPDUMP_PATH]}" - - # Workaround for a nasty trick in _vim - (( ${+functions[_vim_files]} )) && unfunction _vim_files + +zi-message "{mmdsh}{happy} Zi{rst} » {faint}initializing {func}compinit{rst}{…}" builtin autoload -Uz compinit compinit ${${(M)use_C:#1}:+-C} -d "${ZI[ZCOMPDUMP_PATH]}" "${(Q@)${(z@)ZI[COMPINIT_OPTS]}}" @@ -773,6 +748,7 @@ builtin source "${ZI[BIN_DIR]}/lib/zsh/side.zsh" || { builtin print -P "${ZI[col (( quiet || first )) || builtin print unfunction -- 2>/dev/null "$f" + .zi-compinit 1 1 &>/dev/null; rehash } # ]]] # FUNCTION: .zi-compile-plugin [[[ # Compiles given plugin (its main source file, and also an additional "....zsh" file if it exists). @@ -1790,9 +1766,6 @@ ziextract() { ) } # ]]] -# FUNCTION: zpextract [[[ -zpextract() { ziextract "$@"; } -# ]]] # FUNCTION: .zi-at-eval [[[ .zi-at-eval() { local atpull="$1" atclone="$2" diff --git a/lib/zsh/side.zsh b/lib/zsh/side.zsh index 3017c217..abb6935c 100755 --- a/lib/zsh/side.zsh +++ b/lib/zsh/side.zsh @@ -229,7 +229,7 @@ ( builtin print -Pr -- "${ZI[col-pre]}UPGRADING THE DIRECTORY STRUCTURE" "FOR THE ZPLUGIN -> ZI RENAME…%f" builtin cd -q ${ZI[PLUGINS_DIR]} || return 1 - autoload -Uz zmv + builtin autoload -Uz zmv ( zmv -W '**/._zplugin' '**/._zi' ) &>/dev/null builtin cd -q ${ZI[SNIPPETS_DIR]} || return 1 ( zmv -W '**/._zplugin' '**/._zi' ) &>/dev/null @@ -242,7 +242,7 @@ ( builtin print -Pr -- "${ZI[col-pre]}UPGRADING THE DIRECTORY STRUCTURE" "FOR THE ZINIT -> ZI RENAME…%f" builtin cd -q ${ZI[PLUGINS_DIR]} || return 1 - autoload -Uz zmv + builtin autoload -Uz zmv ( zmv -W '**/.zinit' '**/._zi' ) &>/dev/null builtin cd -q ${ZI[SNIPPETS_DIR]} || return 1 ( zmv -W '**/._zinit' '**/._zi' ) &>/dev/null @@ -341,7 +341,7 @@ } # ]]] # FUNCTION: .zi-countdown [[[ # Displays a countdown 5...4... etc. and returns 0 if it -# sucessfully reaches 0, or 1 if Ctrl-C will be pressed. +# successfully reaches 0, or 1 if Ctrl-C will be pressed. .zi-countdown() { (( !${+ICE[countdown]} )) && return 0 builtin emulate -L zsh -o extendedglob ${=${options[xtrace]:#off}:+-o xtrace} @@ -358,28 +358,3 @@ +zi-message -r -- "{b}{error}0 {rst}{…}" return 0 } # ]]] -# FUNCTION: .zi-check-module. [[[ -# Check module. Don't trust access times and verify hard stored values. -.zi-check-module() { - [[ -e ${ZI[ZMODULES_DIR]}/zpmod/COMPILED_AT ]] && \ - local compiled_at_ts="$(<${ZI[ZMODULES_DIR]}/zpmod/COMPILED_AT)" - [[ -e ${ZI[ZMODULES_DIR]}/zpmod/RECOMPILE_REQUEST ]] && \ - local recompile_request_ts="$(<${ZI[ZMODULES_DIR]}/zpmod/RECOMPILE_REQUEST)" - if [[ ${recompile_request_ts:-1} -gt ${compiled_at_ts:-0} ]]; then - +zi-message "{u-warn}WARNING{b-warn}:{rst}{msg} A {lhi}recompilation{rst}" \ - "of the zpmod module has been requested… {hi}Building{rst}…" - (( ${+functions[.zi-confirm]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/autoload.zsh" || return 1 - command make -C "${ZI[ZMODULES_DIR]}/zpmod" distclean &>/dev/null - .zi-module build &>/dev/null - if command make -C "${ZI[ZMODULES_DIR]}/zpmod" &>/dev/null; then - +zi-message "{ok}Build successful!{rst}" - return 0 - else - builtin print -r -- "${ZI[col-error]}Compilation failed.${ZI[col-rst]}" \ - "${ZI[col-pre]}You can enter the following command:${ZI[col-rst]}" \ - 'make -C ${ZI[ZMODULES_DIR]}/zpmod' "${ZI[col-pre]}to see the error messages and e.g.: report an issue" \ - "at GitHub${ZI[col-rst]}" - return 1 - fi - fi -} # ]]] diff --git a/zi.zsh b/zi.zsh old mode 100755 new mode 100644 index 1f7152a0..c423f8d3 --- a/zi.zsh +++ b/zi.zsh @@ -40,7 +40,7 @@ ksh|\!ksh|csh|\!csh|aliases|countdown|light-mode|is-snippet|git|verbose|cloneopt nocompile|reset" # Subcommands list -ZI[cmd-list]="-h|--help|help|subcmds|icemods|analytics|man|self-update|times|zstatus|load|light|unload|\ +ZI[cmd-list]="-V|--version|version|-h|--help|help|subcmds|icemods|analytics|man|self-update|times|zstatus|load|light|unload|\ snippet|ls|ice|update|status|report|delete|loaded|list|cd|create|edit|glance|stress|changes|recently|clist|completions|\ cclear|cdisable|cenable|creinstall|cuninstall|csearch|compinit|dtrace|dstart|dstop|dunload|dreport|dclear|compile|\ uncompile|compiled|cdlist|cdreplay|cdclear|srv|recall|env-whitelist|bindkeys|module|add-fpath|run|version" @@ -157,40 +157,40 @@ XDG_ZI_CACHE=${~ZI[CACHE_DIR]} XDG_ZI_CONFIG=${~ZI[CONFIG_DIR]} if [[ -z ${manpath[(re)${ZI[MAN_DIR]}]} ]] && [[ -d ${ZI[MAN_DIR]} ]]; then - manpath=( "${ZI[MAN_DIR]}" "${manpath[@]}" ) typeset -gxU manpath + manpath=( "${ZI[MAN_DIR]}" "${manpath[@]}" ) fi if [[ -z ${cdpath[(re)${ZI[CDPATH_DIR]}]} ]] && [[ -d ${ZI[CDPATH_DIR]} ]]; then builtin setopt auto_cd - cdpath=( "${ZI[CDPATH_DIR]}" "${cdpath[@]}" ) typeset -gxU cdpath CDPATH + cdpath=( "${ZI[CDPATH_DIR]}" "${cdpath[@]}" ) fi if [[ -z ${mailpath[(re)${ZI[MAIL_DIR]}]} ]]; then - mailpath=( "${ZI[MAIL_DIR]}" "${mailpath[@]}" ) typeset -gxU mailpath MAILPATH + mailpath=( "${ZI[MAIL_DIR]}" "${mailpath[@]}" ) fi if [[ -z ${path[(re)${ZPFX}/bin]} ]] && [[ -d ${ZPFX}/bin ]]; then - path=( "${ZPFX}/bin" "${path[@]}" ) typeset -gxU path PATH + path=( "${ZPFX}/bin" "${path[@]}" ) fi if [[ -z ${path[(re)${ZPFX}/sbin]} ]] && [[ -d ${ZPFX}/sbin ]]; then - path=( "${ZPFX}/sbin" "${path[@]}" ) typeset -gxU path PATH + path=( "${ZPFX}/sbin" "${path[@]}" ) fi if [[ -z ${fpath[(re)${ZI[COMPLETIONS_DIR]}]} ]]; then - fpath=( "${ZI[COMPLETIONS_DIR]}" "${fpath[@]}" ) typeset -gxU fpath FPATH + fpath=( "${ZI[COMPLETIONS_DIR]}" "${fpath[@]}" ) fi # Export/assign/tie new paths. if [[ -z ${logpath[(re)${ZI[LOG_DIR]}]} ]] && [[ -d ${ZI[LOG_DIR]} ]]; then - logpath=( "${ZI[LOG_DIR]}" "${logpath[@]}" ) typeset -gxU logpath LOG_PATH + logpath=( "${ZI[LOG_DIR]}" "${logpath[@]}" ) typeset -gxTU LOG_PATH logpath fi @@ -1150,9 +1150,6 @@ builtin setopt noaliases if [[ ! -d ${ZPFX}/bin ]]; then command mkdir -p "${ZPFX}/bin" fi - if [[ ! -d ${ZPFX}/sbin ]]; then - command mkdir -p "${ZPFX}/sbin" - fi if [[ ! -d ${ZPFX}/lib ]]; then command mkdir -p "${ZPFX}/lib" command chmod go-w "${ZPFX}/lib" @@ -1189,7 +1186,7 @@ builtin setopt noaliases (( ${+functions[.zi-setup-plugin-dir]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/install.zsh" || return 1 (( ${+functions[.zi-confirm]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/autoload.zsh" || return 1 .zi-clear-completions &>/dev/null - .zi-compinit &>/dev/null + .zi-compinit 1 1 &>/dev/null fi if [[ ! -d ${ZI[COMPLETIONS_DIR]} ]]; then command mkdir "${ZI[COMPLETIONS_DIR]}" @@ -1197,7 +1194,7 @@ builtin setopt noaliases # Symlink _zi completion into _local---zi directory. command ln -s "${ZI[PLUGINS_DIR]}/_local---zi/_zi" "${ZI[COMPLETIONS_DIR]}" (( ${+functions[.zi-setup-plugin-dir]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/install.zsh" || return 1 - .zi-compinit &>/dev/null + .zi-compinit 1 1 &>/dev/null fi if [[ ! -d ${ZI[SNIPPETS_DIR]} ]]; then command mkdir -p "${ZI[SNIPPETS_DIR]}/OMZ::plugins" @@ -1336,8 +1333,8 @@ builtin setopt noaliases if [[ -d $local_dir/$dirname/functions ]] { [[ -z ${fpath[(r)$local_dir/$dirname/functions]} ]] && fpath+=( "$local_dir/$dirname/functions" ) () { - builtin setopt localoptions extendedglob - autoload $local_dir/$dirname/functions/^([_.]*|prompt_*_setup|README*)(D-.N:t) + builtin setopt local_options extended_glob + builtin autoload $local_dir/$dirname/functions/^([_.]*|prompt_*_setup|README*)(D-.N:t) } } # Source. @@ -1760,17 +1757,19 @@ builtin setopt noaliases # formats, and colorizes the following pieces of text: # [URLs], [plugin IDs (user/repo) if exists on the disk], [numbers], [time], [single-word id-as plugins], # [single word commands], [single word functions], [zi commands], [ice modifiers (e.g: mv'a -> b')], -# [single-char bits and quoted strings (`...', ,'...', "...")]. +# [single-char bits and quoted strings (`...`, ,'...', "...")]. .zi-formatter-auto() { - builtin emulate -L zsh ${=${options[xtrace]:#off}:+-o xtrace} - builtin setopt extendedglob warncreateglobal typesetsilent - local out in=$1 i rwmsg match spaces rest - integer mbegin mend - local -a ice_order ecmds + builtin emulate -LR zsh ${=${options[xtrace]:#off}:+-o xtrace} + builtin setopt extended_glob warn_create_global typeset_silent no_short_loops rc_quotes no_auto_pushd + + local MATCH in=$1 out rwmsg spaces rest; integer MBEGIN MEND + local -a match mbegin mend ice_order ecmds + ice_order=( ${(As:|:)ZI[ice-list]} ${(@)${(@Akons:|:u)${ZI_EXTS[ice-mods]//\'\'/}}/(#s)<->-/} ) ecmds=( ${ZI_EXTS[(I)z-annex subcommand:*]#z-annex subcommand:} ) in=${(j: :)${${(Z+Cn+)in}//[$'\t ']/$'\u00a0'}} rwmsg=$in + while [[ $in == (#b)([[:space:]]#)([^[:space:]]##)(*) ]]; do spaces=$match[1] rest=$match[3] @@ -1779,31 +1778,30 @@ builtin setopt noaliases if [[ $rwmsg == ([[:space:]]##|(#s))[0-9.]##([[:space:]]##|(#e)) && \ $rest == ([[:space:]]#|(#s))[sm]([[:space:]]##*|(#e)) || $rwmsg == ([[:space:]]##|(#s))[0-9.]##[sm]([[:space:]]##|(#e)) ]]; then REPLY=${ZI[col-time]}$rwmsg${ZI[col-rst]} - if [[ $rwmsg != *[sm]* ]]; then - rest=$ZI[col-time]${(M)rest##[[:space:]]#[sm]}$ZI[col-rst]${rest##[[:space:]]#[sm]} - fi + [[ $rwmsg != *[sm]* ]] && rest=$ZI[col-time]${(M)rest##[[:space:]]#[sm]}$ZI[col-rst]${rest##[[:space:]]#[sm]} elif [[ $rwmsg == ([[:space:]]##|(#s))[0-9.]##([[:space:]]##|(#e)) ]]; then REPLY=${ZI[col-num]}$rwmsg${ZI[col-rst]} - elif [[ $rwmsg == (#b)((http(s|)|ftp(s|)|rsync|ssh|scp|ntp|file)://[[:alnum:].:+/]##) ]]; then - .zi-formatter-url $rwmsg - elif [[ $rwmsg == (--|)(${(~j:|:)ice_order})[:=\"\'\!a-zA-Z0-9-]* ]]; then + elif [[ $rwmsg == (#b)(--|)(${(~j:|:)ice_order})([:=\"\'\!a-zA-Z0-9-][^\"\']##[^a-zA-Z0-9]##) ]]; then REPLY=${ZI[col-ice]}$rwmsg${ZI[col-rst]} - elif [[ $rwmsg == (OMZ|PZT|PZTM|OMZP|OMZT|OMZL)::* || $rwmsg == [^/]##/[^/]## || -d ${ZI[PLUGINS_DIR]}/${rwmsg//\//---} ]]; then - .zi-formatter-pid $rwmsg elif [[ $rwmsg == (${~ZI[cmd-list]}|${(~j:|:)ecmds}) ]]; then REPLY=${ZI[col-cmd]}$rwmsg${ZI[col-rst]} - elif (( $+commands[$1] )); then + elif (( $+commands[$rwmsg] )); then REPLY=${ZI[col-bcmd]}$rwmsg${ZI[col-rst]} - elif (( $+functions[$1] )); then + elif (( $+functions[$rwmsg] )); then REPLY=${ZI[col-func]}$rwmsg${ZI[col-rst]} - elif [[ $rwmsg == (#b)(*)('<->'|'<–>'|'<—>')(*) || $rwmsg == (#b)(*)(…|–|—|↔|...)(*) ]]; then - local -A map=( … … - dsh – ndsh — mdsh '<->' ↔ '<–>' ↔ '<—>' ↔ ↔ ↔ ... …) - REPLY=$match[1]$ZI[col-$map[$rwmsg]]$match[3] + elif [[ $rwmsg == (#b)((http(s|)|ftp(s|)|rsync|ssh|scp|ntp|file)://[[:alnum:].:+/]##) ]]; then + .zi-formatter-url $rwmsg + elif [[ $rwmsg == (OMZ|PZT|PZTM|OMZP|OMZT|OMZL)::* || $rwmsg == [^/]##/[^/]## || -d ${ZI[PLUGINS_DIR]}/${rwmsg//\//---} ]]; then + .zi-formatter-pid $rwmsg + elif [[ $rwmsg == (#b)(*)(...|'<->'|'<–>'|'<—>')(*) || $rwmsg == (#b)(*)(…|–|—|――|↔|...)(*) ]]; then + local -A map=( … … - dsh – ndsh — mdsh ―― mmdsh '<->' ↔ '<–>' ↔ '<—>' ↔ ↔ ↔ ... … ) + local openq=$match[2] str=$match[3] closeq=$match[4] RST=$ZI[col-rst] + REPLY=$match[1]$ZI[col-${map[$openq]}]$str$RST$ZI[col-${map[$closeq]}]$match[5]$ZI[col-rst] # \1 - preceding \2 - open, \3 - string, \4 - close, \5 - following elif [[ $rwmsg == (#b)(*)([\'\`\"])([^\'\`\"]##)([\'\`\"])(*) ]]; then local -A map=( \` bapo \' apo \" quo x\` baps x\' aps x\" quos ) local openq=$match[2] str=$match[3] closeq=$match[4] RST=$ZI[col-rst] - REPLY=$match[1]$ZI[col-$map[$openq]]$openq$RST$ZI[col-$map[x$openq]]$str$RST$ZI[col-$map[$closeq]]$closeq$RST$match[5] + REPLY=$match[1]$ZI[col-${map[$openq]}]$openq$RST$ZI[col-${map[x$openq]}]$str$RST$ZI[col-${map[$closeq]}]$closeq$RST$match[5]$ZI[col-rst] fi in=$rest out+=${spaces//$'\n'/$'\013\015'}$REPLY @@ -1899,8 +1897,12 @@ builtin setopt noaliases } # ]]] # FUNCTION: +zi-message. [[[ +zi-message() { - builtin emulate -LR zsh -o extendedglob ${=${options[xtrace]:#off}:+-o xtrace} - local opt msg + builtin emulate -LR zsh ${=${options[xtrace]:#off}:+-o xtrace} + builtin setopt extended_glob warn_create_global typeset_silent no_short_loops rc_quotes no_auto_pushd + + local MATCH opt msg; integer MBEGIN MEND + local -a match mbegin mend + [[ $1 = -* ]] && { local opt=$1; shift; } ZI[__last-formatter-code]= @@ -1908,8 +1910,7 @@ builtin setopt noaliases # First try a dedicated formatter, marking its empty output with ←→, then # the general formatter and in the end filter-out the ←→ from the message. - msg=${${msg//(#b)(([\\]|(%F))([\{]([^\}]##)[\}])|([\{]([^\}]##)[\}])([^\%\{\\]#))/${match[4]:+${${match[3]:-$ZI[col-${ZI[__last-formatter-code]}]}:#%F}}$match[3]$match[4]${${functions[.zi-formatter-$match[7]]:+${$(.zi-formatter-$match[7] "$match[8]"; builtin print -rn -- $REPLY):-←→}}:-$(.zi-main-message-formatter "$match[6]" "$match[7]" "$match[8]"; builtin print -rn -- "$REPLY" - )${${ZI[__last-formatter-code]::=${${${match[7]:#(…|ndsh|mdsh|mmdsh|-…|lr)}:+$match[7]}:-${ZI[__last-formatter-code]}}}:+}}}//←→} + msg=${${msg//(#b)(([\\]|(%F))([\{]([^\}]##)[\}])|([\{]([^\}]##)[\}])([^\%\{\\]#))/${match[4]:+${${match[3]:-$ZI[col-${ZI[__last-formatter-code]}]}:#%F}}$match[3]$match[4]${${functions[.zi-formatter-$match[7]]:+${$(.zi-formatter-$match[7] "$match[8]"; builtin print -rn -- $REPLY):-←→}}:-$(.zi-main-message-formatter "$match[6]" "$match[7]" "$match[8]"; builtin print -rn -- "$REPLY")${${ZI[__last-formatter-code]::=${${${match[7]:#(…|ndsh|mdsh|mmdsh|-…|lr)}:+$match[7]}:-${ZI[__last-formatter-code]}}}:+}}}//←→} # Reset color attributes at the end of the message. msg=$msg$ZI[col-rst] # Output the processed message: @@ -1923,14 +1924,15 @@ builtin setopt noaliases # FUNCTION: +zi-prehelp-usage-message. [[[ # Prints the usage message. +zi-prehelp-usage-message() { - builtin emulate -LR zsh -o extendedglob ${=${options[xtrace]:#off}:+-o xtrace} + builtin emulate -LR zsh ${=${options[xtrace]:#off}:+-o xtrace} + builtin setopt extended_glob warn_create_global typeset_silent no_short_loops rc_quotes no_auto_pushd local cmd=$1 allowed=$2 sep="$ZI[col-msg2], $ZI[col-ehi]" sep2="$ZI[col-msg2], $ZI[col-opt]" bcol # -h/--help given? if (( OPTS[opt_-h,--help] )) { # Yes – a help message: -+zi-message "{lhi}HELP FOR {apo}\`{cmd}$cmd{apo}\`{lhi} subcommand {mdsh}" "the available {b-lhi}options{ehi}:{rst}" + +zi-message "Available options for {apo}\`{cmd}$cmd{apo}\`{rst} subcommand{ehi}:{rst}" local opt for opt ( ${(kos:|:)allowed} ) { [[ $opt == --* ]] && continue @@ -1944,9 +1946,7 @@ builtin setopt noaliases } } elif [[ -n $allowed ]] { shift 2 - # No – an error message: - +zi-message "{b}{u-warn}ERROR{b-warn}:{rst}{msg2} Incorrect options given{ehi}:" "${(Mpj:$sep:)@:#-*}{rst}{msg2}. Allowed for the subcommand{ehi}:{rst}" \ - "{apo}\`{cmd}$cmd{apo}\`{msg2} are{ehi}:{rst}" "{nl}{mmdsh} {opt}${allowed//\|/$sep2}{msg2}." "{nl}(To see the general help message, type: {cmd}zi help{nl})" "{…} Aborting.{rst}" + +zi-message "{error}Error{ehi}:{rst} Incorrect options given{ehi}:{rst} ${(Mpj:$sep:)@:#-*}{rst}. Allowed for the subcommand{ehi}:{rst} {apo}\`{cmd}$cmd{apo}\`{rst} are{ehi}:{rst} {nl}{mmdsh}{opt} ${allowed//\|/$sep2}{rst}{nl}{nl}To see the general help message, type: {cmd}zi help" } else { local -a cmds cmds=( load snippet update delete ) @@ -1961,11 +1961,11 @@ builtin setopt noaliases "See \`{cmd}help$bcol\` for a more detailed usage information and" \ "the list of the {cmd}subcommands$bcol.{rst}" } -} -# ]]] +} # ]]] # FUNCTION: +zi-parse-opts. [[[ .zi-parse-opts() { - builtin emulate -LR zsh -o extendedglob ${=${options[xtrace]:#off}:+-o xtrace} + builtin emulate -LR zsh ${=${options[xtrace]:#off}:+-o xtrace} + builtin setopt extended_glob typeset_silent no_short_loops rc_quotes no_auto_pushd reply=( "${(@)${@[2,-1]//([  $'\t']##|(#s))(#b)(${(~j.|.)${(@s.|.)___opt_map[$1]}})(#B)([  $'\t']##|(#e))/${OPTS[${___opt_map[${match[1]}]%%:*}]::=1}ß←↓→}:#1ß←↓→}" ) } # ]]] @@ -2149,7 +2149,6 @@ return retval # (delay), i.e. "burst" allows to run package installations from # script, not from prompt. # - @zi-scheduler() { integer ___ret="${${ZI[lro-data]%:*}##*:}" # lro stands for lastarg-retval-option. @@ -2248,6 +2247,17 @@ return retval [[ ${ZI[lro-data]##*:} = on ]] && return 0 || return ___ret } # ]]] +# FUNCTION: .zi-set-mtime. [[[ +# Stores mtime of zi.zsh and its lib files into ZI[mtime] and ZI[mtime-{side,install,autoload,additional}]. +# This is used to determine if the files were changed and need to be reloaded. +.zi-set-mtime() { + local set_mtime + .zi-get-mtime-into "${ZI[BIN_DIR]}/zi.zsh" "ZI[mtime]" + for set_mtime ( side install autoload additional ) { + .zi-get-mtime-into "${ZI[BIN_DIR]}/lib/zsh/${set_mtime}.zsh" "ZI[mtime-${set_mtime}]" + } +} + # # Exposed functions. # @@ -2278,21 +2288,29 @@ zi() { --quiet opt_-q,--quiet -v opt_-v,--verbose:"Turn on more messages from the operation." --verbose opt_-v,--verbose - -r opt_-r,--reset:"Reset the repository before updating (or remove the files for single-file snippets and gh-r plugins)." + -d opt_-d,--debug:"Turn on debug messages from the operation." + --debug opt_-d,--debug + -D opt_-D,--dry-run:"Turn on dry-run mode, which means that no changes will be made to the filesystem." + --dry-run opt_-D,--dry-run + -r opt_-r,--reset:"update:[Reset/clean the repository before updating.] module:[Check and rebuild the module if needed.]" --reset opt_-r,--reset - -a opt_-a,--all:"delete:[Delete {hi}all{rst} plugins and snippets.] update:[Update {b-lhi}all{rst} plugins and snippets.]" + -a opt_-a,--all:"delete:[Delete {hi}all{rst} plugins and snippets.] update:[Update {b-lhi}all{rst} plugins and snippets.] report:[Show report of {b-lhi}all{rst} plugins and snippets.] compile:[Compile {b-lhi}all{rst} plugins and snippets] uncompile:[Uncompile {b-lhi}all{rst} plugins and snippets] times:[Show loading times and moments.]" --all opt_-a,--all + -B opt_-B,--build:"Build the module, append {p}--clean{rst} to run distclean." + --build opt_-B,--build -c opt_-c,--clean:"Delete {b-lhi}only{rst} the {b-lhi}currently-not loaded{rst} plugins and snippets." --clean opt_-c,--clean + -I opt_-I,--info:"Display additional information." + --info opt_-I,--info -y opt_-y,--yes:"Automatically confirm any yes/no prompts." --yes opt_-y,--yes -f opt_-f,--force:"Force new download of the snippet file." --force opt_-f,--force -p opt_-p,--parallel:"Turn on concurrent, multi-thread update (of all objects)." --parallel opt_-p,--parallel - -s opt_-s,--snippets:"snippets:[Update only snippets (i.e.: skip updating plugins).] times:[Show times in seconds instead of milliseconds.]" + -s opt_-s,--snippets:"Update only snippets (i.e.: skip updating plugins)." --snippets opt_-s,--snippets - -L opt_-l,--plugins:"Update only plugins (i.e.: skip updating snippets)." + -l opt_-l,--plugins:"Update only plugins (i.e.: skip updating snippets)." --plugins opt_-l,--plugins -h opt_-h,--help:"Show this help message." --help opt_-h,--help @@ -2300,25 +2318,32 @@ zi() { --urge opt_-u,--urge -n opt_-n,--no-pager:"Disable the use of the pager." --no-pager opt_-n,--no-pager - -m opt_-m,--moments:"Show the {apo}*{b-lhi}moments{apo}*{rst} of object (i.e.: a plugin or snippet) loading time." + -m opt_-m,--moments:"Show the loading {apo}*{b-lhi}moments{apo}*{rst} of of the objects relative to the first prompt." --moments opt_-m,--moments + -S opt_-S,--seconds:"Show times in seconds." + --seconds opt_-S,--seconds -b opt_-b,--bindkeys:"Load in light mode, however do still track {cmd}bindkey{rst} calls (to allow remapping the keys bound)." --bindkeys opt_-b,--bindkeys -x opt_-x,--command:"Load the snippet as a {cmd}command{rst}, i.e.: add it to {var}\$PATH{rst} and set {b-lhi}+x{rst} on it." --command opt_-x,--command env-whitelist "-h|--help|-v|--verbose" - update "-L|--plugins|-s|--snippets|-p|--parallel|-a|--all|-q|--quiet|-r|--reset|-u|--urge|-n|--no-pager|-v|--verbose|-h|--help" - delete "-a|--all|-c|--clean|-y|--yes|-q|--quiet|-h|--help" + update "-h|--help|-l|--plugins|-s|--snippets|-p|--parallel|-a|--all|-q|--quiet|-r|--reset|-u|--urge|-n|--no-pager|-v|--verbose" + self-update "-h|--help|-q|--quiet|-D|--dry-run" + compile "-h|--help|-a|--all|-q|--quiet" + uncompile "-h|--help|-a|--all|-q|--quiet" + delete "-h|--help|-a|--all|-c|--clean|-y|--yes|-q|--quiet" unload "-h|--help|-q|--quiet" cdclear "-h|--help|-q|--quiet" cdreplay "-h|--help|-q|--quiet" - times "-h|--help|-m|-s|-a" - light "-h|--help|-b" + module "-h|--help|-B|--build|-I|--info|-r|--reset" + times "-h|--help|-m|--moments|-S|--seconds|-a|--all" + light "-h|--help|-b|--bindkeys" + report "-h|--help|-a|--all" snippet "-h|--help|-f|--force|--command|-x" ) cmd="$1" - if [[ $cmd == (times|unload|env-whitelist|update|snippet|load|light|cdreplay|cdclear|delete) ]]; then + if [[ $cmd == (times|unload|env-whitelist|update|self-update|compile|uncompile|snippet|load|light|report|cdreplay|module|cdclear|delete) ]]; then if (( $@[(I)-*] || OPTS[opt_-h,--help] )); then .zi-parse-opts "$cmd" "$@" if (( OPTS[opt_-h,--help] )); then @@ -2348,17 +2373,14 @@ zi() { ICE=() ZI_ICE=() 1="${1:+@}${1#@}${2:+/$2}" (( $# > 1 )) && { shift -p $(( $# - 1 )); } - [[ -z $1 ]] && { - +zi-message "Argument needed, try: {cmd}help." - return 1 - } + [[ -z $1 ]] && { +zi-message "{mmdsh}{rst} {auto}Argument is missing. See \`zi help\`"; return 1; } } else { .zi-ice "$@" ___retval2=$? local ___last_ice=${@[___retval2]} shift ___retval2 if [[ $# -gt 0 && $1 != for ]] { - +zi-message -n "{b}{u-warn}ERROR{b-warn}:{rst} Unknown subcommand{ehi}:" "{apo}\`{cmd}$1{apo}\`{rst} " + +zi-message -n "{error}Error{ehi}:{rst} Unknown subcommand{ehi}:{rst} {apo}\`{cmd}$1{apo}\`{rst} " +zi-prehelp-usage-message rst return 1 } elif (( $# == 0 )) { @@ -2384,7 +2406,7 @@ zi() { ICE=( "${___ices[@]}" "${(kv)ZI_ICES[@]}" ) ZI_ICE=( "${(kv)ICE[@]}" ) ZI_ICES=() integer ___msgs=${+ICE[debug]} - (( ___msgs )) && +zi-message "{profile}zi-main{ehi}:{faint} Processing {pname}$1{faint}{…}{rst}" + (( ___msgs )) && +zi-message "{mmdsh}{happy} Zi{rst} » {faint}processing {pname}$1{rst}{…}" # Delete up to the final space to get the previously-processed ID. ZI[annex-exposed-processed-IDs]+="${___id:+ $___id}" # Strip the ID-qualifier (`@') and GitHub domain from the ID. @@ -2611,16 +2633,10 @@ zi() { (zstatus) .zi-show-zstatus ;; - (times) - .zi-show-times "${@[2-correct,-1]}" - ;; - (self-update) - .zi-self-update "$2" - ;; (unload) (( ${+functions[.zi-unload]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/autoload.zsh" || return 1 if [[ -z $2 && -z $3 ]]; then - builtin print "Argument needed, try: help"; ___retval=1 + +zi-message "{mmdsh}{rst} {auto}Argument is missing. See \`zi help\`"; ___retval=1 else [[ $2 = -q ]] && { 5=-q; shift; } # Unload given plugin. Cloned directory remains intact so as are completions. @@ -2644,7 +2660,7 @@ zi() { .zi-parse-opts update "$@" builtin set -- "${reply[@]}" if [[ ${OPTS[opt_-a,--all]} -eq 1 || ${OPTS[opt_-p,--parallel]} -eq 1 || ${OPTS[opt_-s,--snippets]} -eq 1 || ${OPTS[opt_-l,--plugins]} -eq 1 || -z $1$2${ICE[teleid]}${ICE[id-as]} ]]; then - [[ -z $1$2 && $(( OPTS[opt_-a,--all] + OPTS[opt_-p,--parallel] + OPTS[opt_-s,--snippets] + OPTS[opt_-l,--plugins] )) -eq 0 ]] && { builtin print -r -- "Assuming --all is passed"; sleep 3; } + [[ -z $1$2 && $(( OPTS[opt_-a,--all] + OPTS[opt_-p,--parallel] + OPTS[opt_-s,--snippets] + OPTS[opt_-l,--plugins] )) -eq 0 ]] && { +zi-message "{mmdsh}{happy} Zi{rst} » {faint}initializing{rst}{…}"; sleep 2; } (( OPTS[opt_-p,--parallel] )) && OPTS[value]=${1:-15} .zi-update-or-status-all update; ___retval=$? else @@ -2655,7 +2671,7 @@ zi() { ;; (status) if [[ $2 = --all || ( -z $2 && -z $3 ) ]]; then - [[ -z $2 ]] && { builtin print -r -- "Assuming --all is passed"; sleep 3; } + [[ -z $2 ]] && { +zi-message "{mmdsh}{happy} Zi{rst} » {faint}initializing status{rst}{…}"; sleep 2; } .zi-update-or-status-all status; ___retval=$? else .zi-update-or-status status "${2%%(///|//|/)}" "${3%%(///|//|/)}"; ___retval=$? @@ -2663,8 +2679,8 @@ zi() { ;; (report) if [[ $2 = --all || ( -z $2 && -z $3 ) ]]; then - [[ -z $2 ]] && { builtin print -r -- "Assuming --all is passed"; sleep 4; } - .zi-show-all-reports + [[ -z $2 ]] && { +zi-message "{mmdsh}{happy} Zi{rst} » {faint}initializing report{rst}{…}"; sleep 2; } + .zi-show-all-reports else .zi-show-report "${2%%(///|//|/)}" "${3%%(///|//|/)}"; ___retval=$? fi @@ -2684,16 +2700,14 @@ zi() { ;; (cdisable) if [[ -z $2 ]]; then - builtin print "Argument needed, try: help"; ___retval=1 + +zi-message "{mmdsh}{rst} {auto}Argument is missing. See \`zi help\`"; ___retval=1 else local ___f="_${2#_}" # Disable completion given by completion function name with or without leading _, e.g. cp, _cp. if .zi-cdisable "$___f"; then (( ${+functions[.zi-forget-completion]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/install.zsh" || return 1 .zi-forget-completion "$___f" - +zi-message "Initializing completion system ({func}compinit{rst}){…}" - builtin autoload -Uz compinit - compinit -d "${ZI[ZCOMPDUMP_PATH]}" "${(Q@)${(z@)ZI[COMPINIT_OPTS]}}" + .zi-compinit 1 1 &>/dev/null else ___retval=1 fi @@ -2701,17 +2715,14 @@ zi() { ;; (cenable) if [[ -z $2 ]]; then - builtin print "Argument needed, try: help"; ___retval=1 + +zi-message "{mmdsh}{rst} {auto}Argument is missing. See \`zi help\`"; ___retval=1 else local ___f="_${2#_}" - # Enable completion given by completion function name - # with or without leading _, e.g. cp, _cp. + # Enable completion given by completion function name with or without leading _, e.g. cp, _cp. if .zi-cenable "$___f"; then (( ${+functions[.zi-forget-completion]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/install.zsh" || return 1 .zi-forget-completion "$___f" - +zi-message "Initializing completion system ({func}compinit{rst}){…}" - builtin autoload -Uz compinit - compinit -d "${ZI[ZCOMPDUMP_PATH]}" "${(Q@)${(z@)ZI[COMPINIT_OPTS]}}" + .zi-compinit 1 1 &>/dev/null else ___retval=1 fi @@ -2719,31 +2730,25 @@ zi() { ;; (creinstall) (( ${+functions[.zi-install-completions]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/install.zsh" || return 1 - # Installs completions for plugin. Enables them all. It is a - # reinstallation, thus every obstacle gets overwritten or removed. + # Installs completions for plugin. Enables them all. + # It is a reinstallation, thus every obstacle gets overwritten or removed. [[ $2 = -[qQ] ]] && { 5=$2; shift; } .zi-install-completions "${2%%(///|//|/)}" "${3%%(///|//|/)}" 1 "${(M)4:#-[qQ]}"; ___retval=$? - [[ -z ${(M)4:#-[qQ]} ]] && +zi-message "Initializing completion ({func}compinit{rst}){…}" - builtin autoload -Uz compinit - compinit -d "${ZI[ZCOMPDUMP_PATH]}" "${(Q@)${(z@)ZI[COMPINIT_OPTS]}}" ;; (cuninstall) if [[ -z $2 && -z $3 ]]; then - builtin print "Argument needed, try: help"; ___retval=1 + +zi-message "{mmdsh}{rst} {auto}Argument is missing. See \`zi help\`"; ___retval=1 else - (( ${+functions[.zi-forget-completion]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/install.zsh" || return 1 - # Uninstalls completions for plugin. + (( ${+functions[.zi-uninstall-completions]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/autoload.zsh" || return 1 + # Uninstalls completions for plugin. Disables them all. .zi-uninstall-completions "${2%%(///|//|/)}" "${3%%(///|//|/)}"; ___retval=$? - +zi-message "Initializing completion ({func}compinit{rst}){…}" - builtin autoload -Uz compinit - compinit -d "${ZI[ZCOMPDUMP_PATH]}" "${(Q@)${(z@)ZI[COMPINIT_OPTS]}}" fi ;; (csearch) .zi-search-completions ;; (compinit) - (( ${+functions[.zi-forget-completion]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/install.zsh" || return 1 + (( ${+functions[.zi-compinit]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/install.zsh" || return 1 .zi-compinit; ___retval=$? ;; (dreport) @@ -2758,18 +2763,30 @@ zi() { .zi-debug-unload ;; (compile) + .zi-parse-opts compile "$@" + builtin set -- "${reply[@]}" (( ${+functions[.zi-compile-plugin]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/install.zsh" || return 1 - if [[ $2 = --all || ( -z $2 && -z $3 ) ]]; then - [[ -z $2 ]] && { builtin print -r -- "Assuming --all is passed"; sleep 3; } - .zi-compile-uncompile-all 1; ___retval=$? + if (( OPTS[opt_-a,--all] )) || [[ -z $2 && -z $3 ]]; then + if (( OPTS[opt_-q,--quiet] )); then + .zi-compile-uncompile-all 1 >/dev/null; ___retval=$? + else + +zi-message "{mmdsh}{happy} Zi{rst} » {faint}compiling{rst}{…}"; sleep 2 + .zi-compile-uncompile-all 1; ___retval=$? + fi else .zi-compile-plugin "${2%%(///|//|/)}" "${3%%(///|//|/)}"; ___retval=$? fi ;; (uncompile) - if [[ $2 = --all || ( -z $2 && -z $3 ) ]]; then - [[ -z $2 ]] && { builtin print -r -- "Assuming --all is passed"; sleep 3; } - .zi-compile-uncompile-all 0; ___retval=$? + .zi-parse-opts uncompile "$@" + builtin set -- "${reply[@]}" + if (( OPTS[opt_-a,--all] )) || [[ -z $2 && -z $3 ]]; then + if (( OPTS[opt_-q,--quiet] )); then + .zi-compile-uncompile-all 0 >/dev/null; ___retval=$? + else + +zi-message "{mmdsh}{happy} Zi{rst} » {faint}uncompiling{rst}{…}"; sleep 2 + .zi-compile-uncompile-all 0; ___retval=$? + fi else .zi-uncompile-plugin "${2%%(///|//|/)}" "${3%%(///|//|/)}"; ___retval=$? fi @@ -2780,7 +2797,7 @@ zi() { (cdlist) .zi-list-compdef-replay ;; - (cd|delete|recall|edit|glance|changes|create|stress) + (cd|delete|recall|edit|glance|changes|create|stress|self-update|times) .zi-"$1" "${@[2-correct,-1]%%(///|//|/)}"; ___retval=$? ;; (recently) @@ -2815,14 +2832,18 @@ zi() { } "$@" ;; (module) - .zi-module "${@[2-correct,-1]}"; ___retval=$? + if [[ -z $2 ]]; then + +zi-message "{b}{error}Error{ehi}:{rst} Argument required, try{ehi}:{rst} {cmd}zi module {opt}-h{rst}"; ___retval=1 + else + .zi-module "$2" "${@[3-correct,-1]}"; ___retval=$? + fi ;; (*) if [[ -z $1 ]] { - +zi-message -n "{b}{u-warn}ERROR{b-warn}:{rst} Missing a {cmd}subcommand " + +zi-message -n "{error}Error{ehi}:{rst} Missing a {cmd}subcommand " +zi-prehelp-usage-message rst } else { - +zi-message -n "{b}{u-warn}ERROR{b-warn}:{rst} Unknown subcommand{ehi}:{rst}" "{apo}\`{error}$1{apo}\`{rst} " + +zi-message -n "{error}Error{ehi}:{rst} Unknown subcommand{ehi}:{rst} {apo}\`{error}$1{apo}\`{rst} " +zi-prehelp-usage-message rst } ___retval=1 @@ -2846,7 +2867,7 @@ zicdclear() { .zi-compdef-clear -q; } # A function that can be invoked from within `atinit', `atload', etc. ice-mod. # It runs `autoload compinit; compinit' and respects # ZI[ZCOMPDUMP_PATH] and ZI[COMPINIT_OPTS]. -zicompinit() { autoload -Uz compinit; compinit -d "${ZI[ZCOMPDUMP_PATH]}" "${(Q@)${(z@)ZI[COMPINIT_OPTS]}}"; } +zicompinit() { builtin autoload -Uz compinit; compinit -d "${ZI[ZCOMPDUMP_PATH]}" "${(Q@)${(z@)ZI[COMPINIT_OPTS]}}"; } # ]]] # FUNCTION: zicompinit_fast. [[[ # Checking the cached .zcompdump file to see if it must be regenerated adds a noticable delay to zsh startup. @@ -2854,7 +2875,7 @@ zicompinit() { autoload -Uz compinit; compinit -d "${ZI[ZCOMPDUMP_PATH]}" "${(Q@ # modify the compdump and compiles mapped to share (total mem reduction) run in background in multiple shells. # A function that can be invoked from within `atinit', `atload' zicompinit_fast() { - autoload -Uz compinit + builtin autoload -Uz compinit local zcompf="${ZI[ZCOMPDUMP_PATH]}" #local check_ub="$(awk -F= '/^NAME/{print $2}' /etc/os-release | grep 'Ubuntu')" local zcompf_a="${zcompf}.augur" @@ -2895,12 +2916,6 @@ zicompdef() { ZI_COMPDEF_REPLAY+=( "${(j: :)${(q)@}}" ); } # Default ices depth'3' and lucid, allowed values [0-9][a-c]. zi-turbo() { zi depth'3' lucid ${1/#[0-9][a-c]/wait"${1}"} "${@:2}"; } # ]]] -# Compatibility functions. [[[ -❮▼❯() { zi "$@"; } -zpcdreplay() { .zi-compdef-replay -q; } -zpcdclear() { .zi-compdef-clear -q; } -zpcompinit() { autoload -Uz compinit; compinit -d "${ZI[ZCOMPDUMP_PATH]}" "${(Q@)${(z@)ZI[COMPINIT_OPTS]}}"; } -zpcompdef() { ZI_COMPDEF_REPLAY+=( "${(j: :)${(q)@}}" ); } # # Source-executed code. @@ -2909,28 +2924,24 @@ zpcompdef() { ZI_COMPDEF_REPLAY+=( "${(j: :)${(q)@}}" ); } (( ZI[ALIASES_OPT] )) && builtin setopt aliases (( ZI[SOURCED] ++ )) && return -autoload add-zsh-hook +builtin autoload add-zsh-hook + if { zmodload zsh/datetime } { add-zsh-hook -- precmd @zi-scheduler # zsh/datetime required for wait/load/unload ice-mods ZI[HAVE_SCHEDULER]=1 } + functions -M -- zi_scheduler_add 1 1 -zi_scheduler_add_sh 2>/dev/null + zmodload zsh/zpty zsh/system 2>/dev/null zmodload -F zsh/stat b:zstat 2>/dev/null && ZI[HAVE_ZSTAT]=1 # code. [[[ - # Internal aliases for compatibility with deprecated names. Can be enabled with ZI[INTERNAL_ALIASES]=1. (( ZI[INTERNAL_ALIASES] )) && builtin alias zinit=zi zplugin=zi .zi-prepare-home - -# Remember source's timestamps for the automatic-reload feature. -typeset -g ZI_TMP -.zi-get-mtime-into "${ZI[BIN_DIR]}/zi.zsh" "ZI[mtime]" -for ZI_TMP ( side install autoload ) { - .zi-get-mtime-into "${ZI[BIN_DIR]}/lib/zsh/${ZI_TMP}.zsh" "ZI[mtime-${ZI_TMP}]" -} +.zi-set-mtime # Simulate existence of _local/zi plugin. This will allow to cuninstall of its completion ZI_REGISTERED_PLUGINS=( _local/zi "${(u)ZI_REGISTERED_PLUGINS[@]:#_local/zi}" ) @@ -2944,12 +2955,14 @@ zstyle ':completion:*:zi:argument-rest:plugins' list-colors '=(#b)(*)/(*)==1;34= zstyle ':completion:*:zi:argument-rest:plugins' matcher 'r:|=** l:|=*' zstyle ':completion:*:*:zi:*' group-name "" # ]]] -# Check module built / compile status [[[ + +# Check the module compiled timestamps and recompile if needed, +# if no action is required, then no message will be printed. [[[ if [[ -e "${ZI[ZMODULES_DIR]}/zpmod/Src/zi/zpmod.so" ]]; then if [[ ! -f ${ZI[ZMODULES_DIR]}/zpmod/COMPILED_AT || \ ( ${ZI[ZMODULES_DIR]}/zpmod/COMPILED_AT -ot ${ZI[ZMODULES_DIR]}/zpmod/RECOMPILE_REQUEST ) ]]; then - (( ${+functions[.zi-check-module]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/side.zsh" || return 1 - .zi-check-module + (( ${+functions[.zi-module]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/autoload.zsh" || return 1 + .zi-module --reset fi fi # ]]] From ee0e06cfcd9e6b7bee56fba2f7b80e8f66f7925b Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Mon, 16 Jun 2025 09:05:07 +0100 Subject: [PATCH 05/44] fix: Update labeler.yml to standardize changed-files formatting Signed-off-by: Salvydas Lukosius --- .github/labeler.yml | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 94059087..deee3c15 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,14 +1,22 @@ documentation 📝: - - "docs/*.md" - - "docs/man/*" + - changed-files: + - any-glob-to-any-file: + - "docs/*.md" + - "docs/man/*" enhancement ✨: - - "*.zsh" - - "lib/**" + - changed-files: + - any-glob-to-any-file: + - "*.zsh" + - "lib/**" maintenance 📈: - - ".github/CODEOWNERS" - - ".github/*.yml" - - ".github/*.json" - - ".vscode/*" - - ".trunk/**" + - changed-files: + - any-glob-to-any-file: + - ".github/CODEOWNERS" + - ".github/*.yml" + - ".github/*.json" + - ".vscode/*" + - ".trunk/**" ci 🤖: - - ".github/workflows/*.yml" + - changed-files: + - any-glob-to-any-file: + - ".github/workflows/*.yml" From 6a6a35a192eed0ed50511e5a1d06c4de876fd1a5 Mon Sep 17 00:00:00 2001 From: Sall <59910950+ss-o@users.noreply.github.com> Date: Mon, 16 Jun 2025 10:06:39 +0100 Subject: [PATCH 06/44] improve performance and maintainability (#328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit perf: ⚡️ Checks the cached .zcompdump file to see if it must be regenerated adds a noticable delay to zsh startup. This restricts checking it once a day, determines when to regenerate compinit (it doesn't need to modify the compdump everytime) and compiles mapped to share (total mem reduction) run in background in multiple shells. A function that can be invoked from within `atinit', `atload', e.g: zi ice atinit'zicompinit_fast'; zi load user/repository. Signed-off-by: Salvydas Lukosius Co-authored-by: Salvydas Lukosius --- zi.zsh | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/zi.zsh b/zi.zsh index c423f8d3..4563e8a8 100644 --- a/zi.zsh +++ b/zi.zsh @@ -2870,34 +2870,34 @@ zicdclear() { .zi-compdef-clear -q; } zicompinit() { builtin autoload -Uz compinit; compinit -d "${ZI[ZCOMPDUMP_PATH]}" "${(Q@)${(z@)ZI[COMPINIT_OPTS]}}"; } # ]]] # FUNCTION: zicompinit_fast. [[[ -# Checking the cached .zcompdump file to see if it must be regenerated adds a noticable delay to zsh startup. +# Checks the cached .zcompdump file to see if it must be regenerated adds a noticable delay to zsh startup. # This restricts checking it once a day, determines when to regenerate, as compinit doesn't always need to # modify the compdump and compiles mapped to share (total mem reduction) run in background in multiple shells. -# A function that can be invoked from within `atinit', `atload' +# A function that can be invoked from within `atinit', `atload', e.g: zi ice atinit'zicompinit_fast'. zicompinit_fast() { - builtin autoload -Uz compinit - local zcompf="${ZI[ZCOMPDUMP_PATH]}" - #local check_ub="$(awk -F= '/^NAME/{print $2}' /etc/os-release | grep 'Ubuntu')" - local zcompf_a="${zcompf}.augur" - #[[ $check_ub ]] && export skip_global_compinit=1 + builtin autoload -Uz compinit zrecompile + + if grep -q '^ID.*=.*ubuntu' /etc/os-release 2>/dev/null; then + typeset -gx skip_global_compinit=1 + fi # Globbing (#qN.mh+24): # - '#q' is an explicit glob qualifier that makes globbing work within zsh's [[ ]] construct. # - 'N' makes the glob pattern evaluate to nothing when it doesn't match (rather than throw a globbing error) # - '.' matches "regular files" # - 'mh+24' matches files, directories and etc., that are older than 24 hours. - if [[ -e "$zcompf_a" && -f "$zcompf_a"(#qN.mh+24) ]]; then - compinit -d "$zcompf" - command touch "$zcompf_a" + + # If the .lock file exists and is older than 24 hours, then regenerate the zcompdump + if [[ -e "${ZI[ZCOMPDUMP_PATH]}.lock" && -f "${ZI[ZCOMPDUMP_PATH]}.lock"(#qN.mh+24) ]]; then + compinit -d "$ZI[ZCOMPDUMP_PATH]" + command touch "${ZI[ZCOMPDUMP_PATH]}.lock" else - compinit -C -d "$zcompf" + compinit -C -d "$ZI[ZCOMPDUMP_PATH]" fi - # if .zcompdump exists (and is non-zero), and is older than the .zwc file, then regenerate - if [[ -s "$zcompf" && (! -s "${zcompf}.zwc" || "$zcompf" -nt "${zcompf}.zwc") ]]; then - # since file is mapped, it might be mapped right now (current shells), so rename it then make a new one - [[ -e "$zcompf.zwc" ]] && command mv -f "$zcompf.zwc" "$zcompf.zwc.old" - # compile it mapped, so multiple shells can share it (total mem reduction) run in background - { zcompile -M "$zcompf" && command rm -f "$zcompf.zwc.old" }&! + + # If zcompdump exists (and is non-zero), and is older than the .zwc file, then regenerate in the background + if [[ -s "$ZI[ZCOMPDUMP_PATH]" && (! -s "${ZI[ZCOMPDUMP_PATH]}.zwc" || "$ZI[ZCOMPDUMP_PATH]" -nt "${ZI[ZCOMPDUMP_PATH]}.zwc") ]]; then + { zrecompile -q -p "$ZI[ZCOMPDUMP_PATH]" && command rm -f "$ZI[ZCOMPDUMP_PATH].zwc.old" } &! fi } # ]]] From d092066be26d4ec5aedc7a19ed3d85e03a343b35 Mon Sep 17 00:00:00 2001 From: Sall <59910950+ss-o@users.noreply.github.com> Date: Sat, 16 May 2026 14:16:16 +0100 Subject: [PATCH 07/44] ci: modernize workflows, governance, and CI quality gates Remove deprecated bot workflows (labeler, lock, pr-labels, rebase, stale, legacy zunit). Add CodeQL v4, ZD integration test, commit-lint. SHA-pin all actions. Restructure CODEOWNERS with granular per-area ownership. --- .github/CODEOWNERS | 33 ++++++++- .github/PULL_REQUEST_TEMPLATE.md | 65 +++++++++-------- .github/copilot-instructions.md | 70 +++++++++++++++++++ .github/dependabot.yml | 3 +- .github/labeler.yml | 22 ------ .github/workflows/codeql.yml | 45 ++++++++++++ .github/workflows/commit-lint.yml | 101 +++++++++++++++++++++++++++ .github/workflows/labeler.yml | 17 ----- .github/workflows/lock.yml | 37 ---------- .github/workflows/pr-labels.yml | 24 ------- .github/workflows/rebase.yml | 27 ------- .github/workflows/stale.yml | 38 ---------- .github/workflows/trunk-check.yml | 4 +- .github/workflows/zd-integration.yml | 28 ++++++++ .github/workflows/zsh-n.yml | 11 ++- .github/workflows/zunit.yml | 71 ------------------- docs/CONTRIBUTING.md | 62 ++++++++++++++-- docs/README.md | 8 +-- lib/_zi | 2 +- 19 files changed, 385 insertions(+), 283 deletions(-) create mode 100644 .github/copilot-instructions.md delete mode 100644 .github/labeler.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/commit-lint.yml delete mode 100644 .github/workflows/labeler.yml delete mode 100644 .github/workflows/lock.yml delete mode 100644 .github/workflows/pr-labels.yml delete mode 100644 .github/workflows/rebase.yml delete mode 100644 .github/workflows/stale.yml create mode 100644 .github/workflows/zd-integration.yml delete mode 100644 .github/workflows/zunit.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4936cf1d..0a9a5b36 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,32 @@ -* @z-shell/tsc +# CODEOWNERS — zi +# +# GitHub evaluates rules top-to-bottom; the LAST matching rule wins. +# Sections are ordered from broadest to most specific so that narrower +# rules can override the default without removing coverage elsewhere. +# +# Team: @z-shell/tsc — Technical Steering Committee (final sign-off) +# Owner: @ss-o — Primary maintainer (day-to-day reviews) + +# ── Default ────────────────────────────────────────────────────────────────── +# Catch-all: everything not matched by a more specific rule below. +* @ss-o + +# ── Core Zsh runtime ───────────────────────────────────────────────────────── +# Changes here affect every Zi user; require TSC review. +/zi.zsh @z-shell/tsc +/lib/ @z-shell/tsc + +# ── CI / Workflows ─────────────────────────────────────────────────────────── +# Workflow changes reviewed by primary maintainer; no TSC sign-off required. +/.github/workflows/ @ss-o +/.github/dependabot.yml @ss-o + +# ── Documentation & templates ──────────────────────────────────────────────── +/docs/ @ss-o +/.github/ISSUE_TEMPLATE/ @ss-o +/.github/PULL_REQUEST_TEMPLATE.md @ss-o +/.github/copilot-instructions.md @ss-o + +# ── Governance (must be last — highest specificity) ─────────────────────────── +# CODEOWNERS itself requires TSC approval to change. +/.github/CODEOWNERS @z-shell/tsc diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index dc09aad9..85c9019f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,43 +1,46 @@ - + - -## Motivation and Context + Branch model: + • Create your branch FROM next: git checkout -b fix/my-fix next + • Open this PR TARGETING next — never target main directly + • main is only updated from next via a release PR - - + Commit messages must follow Conventional Commits: + type(scope): short description (≤72 chars, imperative mood) + Types: feat fix perf refactor docs test ci chore revert + Example: fix(self-update): correctly propagate exit code on failure -## How Has This Been Tested? + See .github/copilot-instructions.md for full guidelines. +--> - - - - -## Screenshots (if appropriate): +## Description -## Types of changes + - +## Related issues -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to change) -- [ ] Documentation update (if none of the other choices apply) -- [ ] Chore (general maintenance, refactoring, or code style update) + -## Checklist: +## Type of change - - + -- [ ] My code follows the code style of this project. -- [ ] I have read the **CONTRIBUTING** document. -- [ ] I have added tests to cover my changes. -- [ ] All new and existing tests passed. -- [ ] I have updated the documentation accordingly. +- [ ] `fix` — bug fix (non-breaking) +- [ ] `feat` — new feature (non-breaking) +- [ ] `feat!` / `fix!` — breaking change +- [ ] `perf` — performance improvement +- [ ] `refactor` — code change with no functional impact +- [ ] `docs` — documentation only +- [ ] `ci` — CI/workflow change +- [ ] `chore` — maintenance / dependency bump -## Other information (if applicable) +## Checklist - +- [ ] My branch was created from `next` (not `main`) +- [ ] This PR targets the `next` branch +- [ ] Commit messages follow Conventional Commits format +- [ ] No AI co-author trailers in commit messages +- [ ] I have read [CONTRIBUTING.md](docs/CONTRIBUTING.md) +- [ ] Existing tests pass (`zsh -n zi.zsh` / Trunk checks) +- [ ] Documentation updated if needed diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..557fad46 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,70 @@ +# Project Context + +## Architecture + + + +## Conventions + + + +## Key Commands + + + +## Out of Scope + + + +## Commit messages + +- Use **Conventional Commits** format: `type(scope): short description` +- Allowed types: `feat`, `fix`, `perf`, `refactor`, `docs`, `test`, `ci`, `chore`, `revert` +- Subject line: imperative mood, ≤72 characters, no trailing period +- Body: wrap at 72 characters, explain _what_ and _why_ (not _how_) +- **Never add AI co-author trailers** — do not append `Co-authored-by: Copilot`, `Co-authored-by: Claude`, `Co-authored-by: GitHub Copilot`, or any AI-generated attribution to commit messages +- Breaking changes: add `!` after the type (`feat!:`) and include `BREAKING CHANGE:` in the footer + +Examples: + +````text +feat(loader): add lazy-loading for completions + +Defer completion setup until first TAB press to cut startup time +for users with large fpath trees. + +BREAKING CHANGE: ZI_COMPLETION_LAZY must be set before zi is sourced. +```text + +```text +fix(self-update): correctly set exit code on network failure +```text + +## Branch model + +```text +main ← production releases (tags only, squash-merged from next) +next ← integration branch (PR target for all work) + └─ feat/ feature branches + └─ fix/ bug-fix branches + └─ perf/ performance improvements + └─ docs/ documentation updates + └─ ci/ CI/workflow changes + └─ chore/ maintenance, refactors +```text + +- **All branches must be created from `next`**, not `main` +- Open PRs **targeting `next`**, never `main` directly +- `next` → `main` is the only path to production; it requires all CI checks to pass + +## AI-generated files + +- Do **not** suggest adding `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.cursorrules`, `.aider*`, or similar AI-specific config files to this repository +- Copilot configuration lives **only** in `.github/copilot-instructions.md` + +## Code style + +- Zsh files: 2-space indent, `# vim: ft=zsh sw=2 ts=2 et` modeline, LF endings +- Every plugin entry file must resolve `$0` via the ZERO pattern +- Follow the [Z-Shell Plugin Standard](https://wiki.zshell.dev/community/zsh_plugin_standard) +```` diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 914813e4..09179bc1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,4 +5,5 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "weekly" + target-branch: "next" diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index deee3c15..00000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,22 +0,0 @@ -documentation 📝: - - changed-files: - - any-glob-to-any-file: - - "docs/*.md" - - "docs/man/*" -enhancement ✨: - - changed-files: - - any-glob-to-any-file: - - "*.zsh" - - "lib/**" -maintenance 📈: - - changed-files: - - any-glob-to-any-file: - - ".github/CODEOWNERS" - - ".github/*.yml" - - ".github/*.json" - - ".vscode/*" - - ".trunk/**" -ci 🤖: - - changed-files: - - any-glob-to-any-file: - - ".github/workflows/*.yml" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..66f4e3df --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,45 @@ +--- +name: CodeQL + +on: + push: + branches: [main, next] + pull_request: + branches: [main, next] + schedule: + - cron: "28 15 * * 5" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + permissions: + security-events: write + packages: read + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml new file mode 100644 index 00000000..efd2e49e --- /dev/null +++ b/.github/workflows/commit-lint.yml @@ -0,0 +1,101 @@ +--- +name: "📝 Commit Lint" + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + branches: + - main + - next + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + pull-requests: read + contents: read + +jobs: + commit-lint: + name: "📝 Validate Commits" + runs-on: ubuntu-latest + steps: + - name: "⤵️ Check out code from GitHub" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: "📝 Lint commit messages" + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + DISALLOWED_TRAILER_PATTERN: ${{ secrets.DISALLOWED_TRAILER_PATTERN }} + run: | + set -euo pipefail + + CONVENTIONAL_PATTERN='^(feat|fix|perf|refactor|docs|test|ci|chore|revert)(\([^)]+\))?!?: .{1,72}$' + + errors=0 + checked=0 + + while IFS= read -r sha; do + parent_count=$(git cat-file -p "$sha" | grep -c '^parent' || true) + [ "$parent_count" -gt 1 ] && continue + + subject=$(git show -s --format='%s' "$sha") + checked=$((checked + 1)) + + if ! echo "$subject" | grep -qE "$CONVENTIONAL_PATTERN"; then + echo "❌ Invalid commit message (${sha:0:7}): expected type(scope): description ≤72 chars" + errors=$((errors + 1)) + fi + + if [ -n "${DISALLOWED_TRAILER_PATTERN:-}" ]; then + if git show -s --format='%B' "$sha" | grep -qE "$DISALLOWED_TRAILER_PATTERN"; then + echo "❌ Disallowed trailer found (${sha:0:7}): remove before merging" + errors=$((errors + 1)) + fi + fi + done < <(git log --format='%H' "${BASE_SHA}..${HEAD_SHA}") + + if [ "$errors" -gt 0 ]; then + echo "::error::$errors commit(s) failed validation — run: git rebase -i ${BASE_SHA}" + exit 1 + fi + + echo "✅ $checked commit(s) validated" + + pr-title: + name: "📋 Validate PR Title" + runs-on: ubuntu-latest + steps: + - name: "📋 Check PR title follows Conventional Commits" + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + PATTERN='^(feat|fix|perf|refactor|docs|test|ci|chore|revert)(\([^)]+\))?!?: .{1,72}$' + if ! echo "$PR_TITLE" | grep -qE "$PATTERN"; then + echo "::error::PR title must follow Conventional Commits: type(scope): description" + exit 1 + fi + echo "✅ PR title valid" + + branch-naming: + name: "🌿 Validate Branch Name" + runs-on: ubuntu-latest + steps: + - name: "🌿 Check branch naming convention" + env: + BRANCH: ${{ github.head_ref }} + run: | + PATTERN='^(feat|fix|perf|refactor|docs|test|ci|chore|revert)/[a-z0-9][a-z0-9-]*[a-z0-9]$' + if echo "$BRANCH" | grep -q '^dependabot/'; then + echo "✅ OK" + exit 0 + fi + if ! echo "$BRANCH" | grep -qE "$PATTERN"; then + echo "::error::Branch name must follow type/short-description (e.g. feat/my-feature)" + exit 1 + fi + echo "✅ Branch name valid" diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml deleted file mode 100644 index 24151b81..00000000 --- a/.github/workflows/labeler.yml +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: 🔖 Pull Request Labeler -on: - pull_request_target: - -permissions: - contents: read - pull-requests: write - -jobs: - triage: - runs-on: ubuntu-latest - steps: - - uses: actions/labeler@v5 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - sync-labels: false diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml deleted file mode 100644 index 0ca1c103..00000000 --- a/.github/workflows/lock.yml +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: 🔒 Lock closed issues and PRs - -on: - schedule: - - cron: "30 2 * * *" - -permissions: - issues: write - pull-requests: write - -concurrency: - group: lock - -jobs: - lock: - name: 🔐 Lock closed issues and PRs - runs-on: ubuntu-latest - steps: - - uses: dessant/lock-threads@v4 - with: - github-token: ${{ github.token }} - issue-inactive-days: "30" - issue-lock-reason: "" - issue-comment: > - Issue closed and locked due to lack of activity. - - If you encounter this same issue, please open a new issue and refer to this closed one. - - pr-inactive-days: "7" - pr-lock-reason: "" - pr-comment: > - Pull Request closed and locked due to lack of activity. - - If you'd like to build on this closed PR, you can clone it using this method: https://stackoverflow.com/a/14969986 - - Then open a new PR, referencing this closed PR in your message. diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml deleted file mode 100644 index 1f30bbc6..00000000 --- a/.github/workflows/pr-labels.yml +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: 🏷️ Verify PR Labels - -on: - workflow_dispatch: - pull_request_target: - types: ["opened", "labeled", "unlabeled", "synchronize"] - -jobs: - pr_labels: - name: 🏭 Verify PR Labels - runs-on: ubuntu-latest - steps: - - name: 🏷 Verify PR has a valid label - uses: jesusvasquez333/verify-pr-label-action@v1.4.0 - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - pull-request-number: "${{ github.event.pull_request.number }}" - valid-labels: > - "breaking-change 💥, bug 🐞, i18n 🌐, l10n 🗣, documentation 📝, enhancement ✨, security 🛡️, refactor ♻️, performance 🚀, new-feature 🎉, triage 📑, maintenance 📈, ci 🤖, fix ⚡, - dependencies 📦, submodules ⚙️, annex 🌀, plugin ⚙️, package 📦" - invalid-labels: > - "Q&A ✍️, stale 👻, no-stale 🔒, locked ‼️, resolved ☑️, feature-request 💡, help-wanted, beginner-friendly 💕, priority-low 🔖, invalid ⚠️" - disable-reviews: true diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml deleted file mode 100644 index ad7b8652..00000000 --- a/.github/workflows/rebase.yml +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: "🔁 Rebase" -on: - issue_comment: - types: [created] -jobs: - rebase: - runs-on: ubuntu-latest - name: 🔁 Rebase - # Automate with comments: /autosquash, /rebase - if: >- - github.event.issue.pull_request != '' && ( - contains(github.event.comment.body, '/rebase') || - contains(github.event.comment.body, '/autosquash') - ) - steps: - - name: ⤵️ Check out code from GitHub - uses: actions/checkout@v3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 # otherwise, you will fail to push refs to dest repo - - name: 🔁 Rebase - uses: z-shell/.github/actions/rebase@main - with: - autosquash: ${{ contains(github.event.comment.body, '/autosquash') || contains(github.event.comment.body, '/rebase-autosquash') }} - env: - GITHUB_TOKEN: ${{ secrets.GH_PAT }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index 91f6468f..00000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: 🧹 Clean up stale issues and PRs - -on: - schedule: - - cron: "0 8 * * *" - workflow_dispatch: - -jobs: - stale: - name: 🧹 Clean up stale issues and PRs - runs-on: ubuntu-latest - steps: - - name: 🚀 Run stale - uses: actions/stale@v7 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - days-before-stale: 30 - days-before-close: 7 - remove-stale-when-updated: true - stale-issue-label: "stale 👻" - exempt-issue-labels: "no-stale 🔒,help-wanted 👥" - stale-issue-message: > - There hasn't been any activity on this issue recently, and in order to prioritize active issues, it will be marked as stale. - - Please make sure to update to the latest version and check if that solves the issue. Let us know if that works for you by leaving a 👍 - - Because this issue is marked as stale, it will be closed and locked in 7 days if no further activity occurs. - - Thank you for your contributions! - stale-pr-label: "stale 👻" - exempt-pr-labels: "no-stale 🔒" - stale-pr-message: > - There hasn't been any activity on this pull request recently, and in order to prioritize active work, it has been marked as stale. - - This PR will be closed and locked in 7 days if no further activity occurs. - - Thank you for your contributions! diff --git a/.github/workflows/trunk-check.yml b/.github/workflows/trunk-check.yml index 4bf797d1..69b74740 100644 --- a/.github/workflows/trunk-check.yml +++ b/.github/workflows/trunk-check.yml @@ -2,7 +2,7 @@ name: Trunk Code Quality on: push: - branches: [main] + branches: [main, next] tags: ["v*.*.*"] pull_request: types: [opened, synchronize] @@ -22,6 +22,6 @@ jobs: contents: read # For repo checkout steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Trunk Code Quality uses: trunk-io/trunk-action@75699af9e26881e564e9d832ef7dc3af25ec031b diff --git a/.github/workflows/zd-integration.yml b/.github/workflows/zd-integration.yml new file mode 100644 index 00000000..1af598a1 --- /dev/null +++ b/.github/workflows/zd-integration.yml @@ -0,0 +1,28 @@ +--- +name: "🧪 ZD Integration" + +on: + push: + branches: [main, next] + paths: + - "zi.zsh" + - "lib/**" + pull_request: + paths: + - "zi.zsh" + - "lib/**" + workflow_dispatch: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + zd-test: + uses: z-shell/zd/.github/workflows/test-native.yml@cbb1211f6a39f9d89a600297883b3766d4fa8ab9 # main + with: + zi_repo: ${{ github.repository }} + zi_ref: ${{ github.head_ref || github.ref_name }} diff --git a/.github/workflows/zsh-n.yml b/.github/workflows/zsh-n.yml index d3ae47b7..1b0ec5d5 100644 --- a/.github/workflows/zsh-n.yml +++ b/.github/workflows/zsh-n.yml @@ -21,6 +21,13 @@ on: branches: [main] workflow_dispatch: {} +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + jobs: zsh-matrix: runs-on: ubuntu-latest @@ -28,7 +35,7 @@ jobs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - name: ⤵️ Check out code from GitHub - uses: actions/checkout@v3 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: "Set matrix output" id: set-matrix run: | @@ -44,7 +51,7 @@ jobs: matrix: ${{ fromJSON(needs.zsh-matrix.outputs.matrix) }} steps: - name: ⤵️ Check out code from GitHub - uses: actions/checkout@v3 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: "⚡ Install dependencies" run: sudo apt update && sudo apt-get install -yq zsh - name: "⚡ zsh -n: ${{ matrix.file }}" diff --git a/.github/workflows/zunit.yml b/.github/workflows/zunit.yml deleted file mode 100644 index 13c28d4d..00000000 --- a/.github/workflows/zunit.yml +++ /dev/null @@ -1,71 +0,0 @@ ---- -name: "♾️ ZUnit" - -on: - push: - branches: [main] - tags: ["v*.*.*"] - paths: - - "lib/zsh/install.zsh" - - "lib/zsh/autoload.zsh" - workflow_dispatch: {} - -env: - branch_ref: $GITHUB_REF_NAME - -jobs: - zunit-matrix: - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - steps: - - uses: actions/checkout@v3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - repository: "z-shell/zd" - - name: "Set matrix output" - id: set-matrix - run: | - builtin cd docker/tests - MATRIX="$(ls -1 *.zunit | sed 's/.zunit$//' | jq -ncR '{"include": [{"file": inputs}]}')" - echo "MATRIX=${MATRIX}" >&2 - echo "matrix=${MATRIX}" >> $GITHUB_OUTPUT - - zunit: - runs-on: ubuntu-latest - needs: zunit-matrix - strategy: - fail-fast: false - matrix: ${{ fromJSON(needs.zunit-matrix.outputs.matrix) }} - steps: - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@v3 - with: - repository: "z-shell/zd" - token: ${{ secrets.GITHUB_TOKEN }} - - run: command git clone --branch ${{ env.branch_ref }} --depth 1 -- https://github.com/z-shell/zi.git zi - - name: "⚡ Install dependencies" - run: | - sudo apt-get update && sudo apt-get install -yq zsh - mkdir bin - curl -fsSL https://raw.githubusercontent.com/zdharma/revolver/v0.2.4/revolver > bin/revolver - curl -fsSL https://raw.githubusercontent.com/zdharma/color/d8f91ab5fcfceb623ae45d3333ad0e543775549c/color.zsh > bin/color - git clone https://github.com/zdharma/zunit.git zunit.git - cd zunit.git - ./build.zsh - cd .. - mv ./zunit.git/zunit bin - chmod u+x bin/{color,revolver,zunit} - - name: "⚡ ZUnit: ${{ matrix.file }}" - env: - ZUNIT_TEST: ${{ matrix.file }} - run: | - echo "⚡ $ZUNIT_TEST" >&2 - export PATH="$PWD/bin:$PATH" - export TERM=xterm-256color - zunit --tap --verbose "docker/tests/${ZUNIT_TEST}.zunit" diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 37779e87..d55e8ebd 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,7 +1,59 @@ -# Contributing Guidelines +# Contributing to zi -When contributing, please first [discuss](https://github.com/z-shell/zi/issues/new/choose) the change you wish. The -[contributing guidelines](https://github.com/z-shell/community/blob/main/docs/CONTRIBUTING_GUIDELINES.md) and other relevant documents can be found in the -[community](https://github.com/z-shell/community) repository. +Thank you for contributing! Please follow the guidelines below to keep the project history clean and easy to navigate. -Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. +## Branch model + +````text +main ←─── production (tagged releases only) + ↑ +next ←─── integration branch ← open all PRs here + ↑ + ├── feat/ new features + ├── fix/ bug fixes + ├── perf/ performance improvements + ├── refactor/ code refactors + ├── docs/ documentation updates + └── ci/ CI / workflow changes +```text + +1. **Always branch from `next`**: `git checkout -b fix/my-issue next` +2. **Open PRs targeting `next`** — never target `main` directly +3. `next` → `main` happens via a release PR once `next` is stable + +## Commit message format + +All commits must follow [Conventional Commits](https://www.conventionalcommits.org/): + +```text +type(scope): short description + +Optional body — explain what and why, not how. +Wrap at 72 characters. + +Optional footer(s): +Fixes #123 +BREAKING CHANGE: description of what breaks +```text + +**Allowed types:** `feat` `fix` `perf` `refactor` `docs` `test` `ci` `chore` `revert` + +**Rules:** + +- Subject line: imperative mood, ≤72 characters, no trailing period +- Breaking changes: use `!` suffix (`feat!:`) and add `BREAKING CHANGE:` footer +- **No AI co-author trailers** — do not add `Co-authored-by: Copilot` or similar + +To clean up commits before opening a PR: `git rebase -i $(git merge-base HEAD next)` + +## What not to add + +- `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.cursorrules`, or any AI-specific config files +- Secrets, credentials, or tokens of any kind + +## Discussion and issues + +Before starting significant work, [open an issue](https://github.com/z-shell/zi/issues/new/choose) to discuss the change. + +See also the [community contributing guidelines](https://github.com/z-shell/community/blob/main/docs/CONTRIBUTING_GUIDELINES.md) and the [Code of Conduct](CODE_OF_CONDUCT.md). +```` diff --git a/docs/README.md b/docs/README.md index 5d9d6d42..f2713ca8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,7 +3,7 @@ diff --git a/lib/_zi b/lib/_zi index b6933ca1..b1a759a0 100755 --- a/lib/_zi +++ b/lib/_zi @@ -1,6 +1,6 @@ #compdef zi -builtin setopt local_options warn_create_global type_set_silent +builtin setopt local_options warn_create_global typeset_silent local curcontext="$curcontext" state state_descr line ret=1 local -a expl From 351a7d481c5b01e6a6f4ac4a5f879e661915da87 Mon Sep 17 00:00:00 2001 From: Sall <59910950+ss-o@users.noreply.github.com> Date: Sat, 16 May 2026 14:17:42 +0100 Subject: [PATCH 08/44] fix(autoload): correct bugs in auto-reload, zpmod build, and zi.zsh cleanup Fix loop variable rm_file in zi-auto-reload, drop invalid --autostash from git merge, fix zpmod build to run make once with pipestatus check, make cp explicit, guard recompile message with RECOMPILE_REQUEST check. Clean up zi.zsh: remove duplicate version entry, fix spelling. --- lib/zsh/autoload.zsh | 13 +++++++------ zi.zsh | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/zsh/autoload.zsh b/lib/zsh/autoload.zsh index 7dc24388..f95e9740 100755 --- a/lib/zsh/autoload.zsh +++ b/lib/zsh/autoload.zsh @@ -651,7 +651,7 @@ ZI[EXTENDED_GLOB]="" # Remove all compiled files for rm_file ( ${ZI[BIN_DIR]}/**/*.zwc(DN) ) { - command rm -f -- ${rm_compiled} > /dev/null 2>&1 + command rm -f -- "${rm_file}" > /dev/null 2>&1 } # Recompile new ones @@ -711,7 +711,7 @@ ZI[EXTENDED_GLOB]="" if (( ! dry_run )); then # Update the codebase - command git merge -n $opt --autostash --ff-only FETCH_HEAD || \ + command git merge -n $opt --ff-only FETCH_HEAD || \ { (( quiet )) || +zi-message "{mmdsh}{happy} Zi{rst} » {profile}update failed {quos}✘{rst}"; return 1 } .zi-auto-reload $opt fi @@ -2962,7 +2962,8 @@ EOF local compiled_at_ts="$(<${ZI[ZMODULES_DIR]}/zpmod/COMPILED_AT)" [[ -e ${ZI[ZMODULES_DIR]}/zpmod/RECOMPILE_REQUEST ]] && \ local recompile_request_ts="$(<${ZI[ZMODULES_DIR]}/zpmod/RECOMPILE_REQUEST)" - if [[ ${recompile_request_ts:-1} -gt ${compiled_at_ts:-0} ]]; then + if [[ -e ${ZI[ZMODULES_DIR]}/zpmod/RECOMPILE_REQUEST ]] && \ + [[ ${recompile_request_ts:-1} -gt ${compiled_at_ts:-0} ]]; then +zi-message "{warn}Warning{warn}{ehi}:{rst} {lhi}recompilation{rst} of the {bcmd}zpmod{rst} module has been requested…" (( ${+functions[.zi-confirm]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/autoload.zsh" || return 1 .zi-confirm "Do you want to proceed?" || return 1 @@ -3010,9 +3011,9 @@ EOF local cores=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || command getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) local build_log="${ZI[LOG_DIR]}/zpmod/${EPOCHSECONDS}-build.log" command mkdir -p "${ZI[LOG_DIR]}/zpmod" 2>/dev/null - command make --jobs=$cores -C "${ZI[ZMODULES_DIR]}/zpmod" | command tee "$build_log" >/dev/null - if command make -C "${ZI[ZMODULES_DIR]}/zpmod" &>/dev/null; then - [[ -f Src/zi/zpmod.so ]] && command cp -vf Src/zi/zpmod.{so,bundle} + command make --jobs=$cores -C "${ZI[ZMODULES_DIR]}/zpmod" 2>&1 | command tee "$build_log" >/dev/null + if (( pipestatus[1] == 0 )); then + [[ -f Src/zi/zpmod.so ]] && command cp -vf Src/zi/zpmod.so Src/zi/zpmod.bundle builtin print "$EPOCHSECONDS" >! "${ZI[ZMODULES_DIR]}/zpmod/COMPILED_AT" +zi-message "{ok}-- Build successful! --{rst}" +zi-message "{faint}-- Build log{ehi}:{rst} {file}$build_log{faint} --{rst}" diff --git a/zi.zsh b/zi.zsh index 4563e8a8..5993d968 100644 --- a/zi.zsh +++ b/zi.zsh @@ -43,7 +43,7 @@ nocompile|reset" ZI[cmd-list]="-V|--version|version|-h|--help|help|subcmds|icemods|analytics|man|self-update|times|zstatus|load|light|unload|\ snippet|ls|ice|update|status|report|delete|loaded|list|cd|create|edit|glance|stress|changes|recently|clist|completions|\ cclear|cdisable|cenable|creinstall|cuninstall|csearch|compinit|dtrace|dstart|dstop|dunload|dreport|dclear|compile|\ -uncompile|compiled|cdlist|cdreplay|cdclear|srv|recall|env-whitelist|bindkeys|module|add-fpath|run|version" +uncompile|compiled|cdlist|cdreplay|cdclear|srv|recall|env-whitelist|bindkeys|module|add-fpath|run" # Establish ZI[BIN_DIR] [[ ! -e ${ZI[BIN_DIR]}/zi.zsh ]] && ZI[BIN_DIR]= @@ -2870,7 +2870,7 @@ zicdclear() { .zi-compdef-clear -q; } zicompinit() { builtin autoload -Uz compinit; compinit -d "${ZI[ZCOMPDUMP_PATH]}" "${(Q@)${(z@)ZI[COMPINIT_OPTS]}}"; } # ]]] # FUNCTION: zicompinit_fast. [[[ -# Checks the cached .zcompdump file to see if it must be regenerated adds a noticable delay to zsh startup. +# Checks the cached .zcompdump file to see if it must be regenerated; adds a noticeable delay to zsh startup. # This restricts checking it once a day, determines when to regenerate, as compinit doesn't always need to # modify the compdump and compiles mapped to share (total mem reduction) run in background in multiple shells. # A function that can be invoked from within `atinit', `atload', e.g: zi ice atinit'zicompinit_fast'. From 72e40e104a97fc1366934f55b57568008be6e10b Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Wed, 20 May 2026 07:43:25 +0100 Subject: [PATCH 09/44] chore(meta): secure workflows and unify agent instructions --- AGENTS.md | 7 +++++++ CLAUDE.md | 1 + GEMINI.md | 1 + lib/zsh/autoload.zsh | 6 +++--- lib/zsh/install.zsh | 4 ++-- zi.zsh | 8 ++++---- 6 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 AGENTS.md create mode 120000 CLAUDE.md create mode 120000 GEMINI.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..1d299df8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,7 @@ +# Project Guidelines — zi + +This project follows the organization-wide [Z-Shell Organization Guidelines](https://github.com/z-shell/.github/blob/main/AGENTS.md). + +## Project-Specific Details + +This repository contains the Zi plugin manager documentation pages. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/lib/zsh/autoload.zsh b/lib/zsh/autoload.zsh index f95e9740..4e116db9 100755 --- a/lib/zsh/autoload.zsh +++ b/lib/zsh/autoload.zsh @@ -586,7 +586,7 @@ ZI[EXTENDED_GLOB]="" # Workaround for a nasty trick in _vim (( ${+functions[_vim_files]} )) && unfunction _vim_files - .zi-compinit 1 1 &>/dev/null + .zi-compinit 1 &>/dev/null } # ]]] # @@ -1691,7 +1691,7 @@ ZI[EXTENDED_GLOB]="" if (( OPTS[opt_-p,--parallel] )) && [[ $1 = update ]] { (( OPTS[opt_-q,--quiet] )) || +zi-message "{mmdsh}{happy} Zi{rst} » {profile}parallelizing update{rst}{…}" .zi-update-all-parallel; retval=$? - .zi-compinit 1 1 &>/dev/null; rehash + .zi-compinit 1 &>/dev/null; rehash (( OPTS[opt_-q,--quiet] )) || \ +zi-message "{mmdsh}{happy} Zi{rst} » {info3}update took {num}$SECONDS{info3} seconds{rst}{…}" return $retval @@ -1756,7 +1756,7 @@ ZI[EXTENDED_GLOB]="" } fi done - .zi-compinit 1 1 &>/dev/null; rehash + .zi-compinit 1 &>/dev/null; rehash (( OPTS[opt_-q,--quiet] )) || \ +zi-message "{mmdsh}{happy} Zi{rst} » {info3}update took {num}$SECONDS{info3} seconds{rst}{…}" return "$retval" diff --git a/lib/zsh/install.zsh b/lib/zsh/install.zsh index 104a416a..8f81f3ed 100755 --- a/lib/zsh/install.zsh +++ b/lib/zsh/install.zsh @@ -578,7 +578,7 @@ builtin source "${ZI[BIN_DIR]}/lib/zsh/side.zsh" || { builtin print -P "${ZI[col builtin print -rl -- $SKIPPED_COMPS >! ${TMPDIR:-/tmp}/zi.skipped_comps.$$.lst } - .zi-compinit 1 1 &>/dev/null; rehash + .zi-compinit 1 &>/dev/null; rehash } # ]]] # FUNCTION: .zi-compinit [[[ .zi-compinit() { @@ -748,7 +748,7 @@ builtin source "${ZI[BIN_DIR]}/lib/zsh/side.zsh" || { builtin print -P "${ZI[col (( quiet || first )) || builtin print unfunction -- 2>/dev/null "$f" - .zi-compinit 1 1 &>/dev/null; rehash + .zi-compinit 1 &>/dev/null; rehash } # ]]] # FUNCTION: .zi-compile-plugin [[[ # Compiles given plugin (its main source file, and also an additional "....zsh" file if it exists). diff --git a/zi.zsh b/zi.zsh index 5993d968..47aec3d4 100644 --- a/zi.zsh +++ b/zi.zsh @@ -1186,7 +1186,7 @@ builtin setopt noaliases (( ${+functions[.zi-setup-plugin-dir]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/install.zsh" || return 1 (( ${+functions[.zi-confirm]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/autoload.zsh" || return 1 .zi-clear-completions &>/dev/null - .zi-compinit 1 1 &>/dev/null + .zi-compinit 1 &>/dev/null fi if [[ ! -d ${ZI[COMPLETIONS_DIR]} ]]; then command mkdir "${ZI[COMPLETIONS_DIR]}" @@ -1194,7 +1194,7 @@ builtin setopt noaliases # Symlink _zi completion into _local---zi directory. command ln -s "${ZI[PLUGINS_DIR]}/_local---zi/_zi" "${ZI[COMPLETIONS_DIR]}" (( ${+functions[.zi-setup-plugin-dir]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/install.zsh" || return 1 - .zi-compinit 1 1 &>/dev/null + .zi-compinit 1 &>/dev/null fi if [[ ! -d ${ZI[SNIPPETS_DIR]} ]]; then command mkdir -p "${ZI[SNIPPETS_DIR]}/OMZ::plugins" @@ -2707,7 +2707,7 @@ zi() { if .zi-cdisable "$___f"; then (( ${+functions[.zi-forget-completion]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/install.zsh" || return 1 .zi-forget-completion "$___f" - .zi-compinit 1 1 &>/dev/null + .zi-compinit 1 &>/dev/null else ___retval=1 fi @@ -2722,7 +2722,7 @@ zi() { if .zi-cenable "$___f"; then (( ${+functions[.zi-forget-completion]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/install.zsh" || return 1 .zi-forget-completion "$___f" - .zi-compinit 1 1 &>/dev/null + .zi-compinit 1 &>/dev/null else ___retval=1 fi From e3ff8e3bc50fbd695b6fb22b4b9fda50dbf25b2c Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Thu, 21 May 2026 08:23:12 +0100 Subject: [PATCH 10/44] chore(ci): remove emoji prefixes from workflow and job names --- .github/workflows/commit-lint.yml | 8 ++++---- .github/workflows/zd-integration.yml | 2 +- .github/workflows/zsh-n.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml index efd2e49e..12db0f19 100644 --- a/.github/workflows/commit-lint.yml +++ b/.github/workflows/commit-lint.yml @@ -1,5 +1,5 @@ --- -name: "📝 Commit Lint" +name: Commit Lint on: pull_request: @@ -18,7 +18,7 @@ permissions: jobs: commit-lint: - name: "📝 Validate Commits" + name: Validate Commits runs-on: ubuntu-latest steps: - name: "⤵️ Check out code from GitHub" @@ -67,7 +67,7 @@ jobs: echo "✅ $checked commit(s) validated" pr-title: - name: "📋 Validate PR Title" + name: Validate PR Title runs-on: ubuntu-latest steps: - name: "📋 Check PR title follows Conventional Commits" @@ -82,7 +82,7 @@ jobs: echo "✅ PR title valid" branch-naming: - name: "🌿 Validate Branch Name" + name: Validate Branch Name runs-on: ubuntu-latest steps: - name: "🌿 Check branch naming convention" diff --git a/.github/workflows/zd-integration.yml b/.github/workflows/zd-integration.yml index 1af598a1..b629cc50 100644 --- a/.github/workflows/zd-integration.yml +++ b/.github/workflows/zd-integration.yml @@ -1,5 +1,5 @@ --- -name: "🧪 ZD Integration" +name: ZD Integration on: push: diff --git a/.github/workflows/zsh-n.yml b/.github/workflows/zsh-n.yml index 1b0ec5d5..dd1420b7 100644 --- a/.github/workflows/zsh-n.yml +++ b/.github/workflows/zsh-n.yml @@ -1,5 +1,5 @@ --- -name: "✅ Zsh" +name: Zsh on: push: tags: ["v*.*.*"] From 81a1cf5e7b9d719410777830a3c4c62c2842602e Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Mon, 1 Jun 2026 02:42:21 +0100 Subject: [PATCH 11/44] chore(zsh): apply standard modelines across plugins and libraries --- lib/zsh/git-process-output.zsh | 2 ++ lib/zsh/rpm2cpio.zsh | 2 ++ lib/zsh/single-line.zsh | 2 ++ 3 files changed, 6 insertions(+) diff --git a/lib/zsh/git-process-output.zsh b/lib/zsh/git-process-output.zsh index 7222c0ac..aec3cd16 100755 --- a/lib/zsh/git-process-output.zsh +++ b/lib/zsh/git-process-output.zsh @@ -1,4 +1,6 @@ #!/usr/bin/env zsh +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et builtin emulate -LR zsh builtin setopt extended_glob warn_create_global typeset_silent diff --git a/lib/zsh/rpm2cpio.zsh b/lib/zsh/rpm2cpio.zsh index 26d12d58..3a570d4a 100755 --- a/lib/zsh/rpm2cpio.zsh +++ b/lib/zsh/rpm2cpio.zsh @@ -1,4 +1,6 @@ #!/usr/bin/env zsh +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et builtin emulate -R zsh ${=${options[xtrace]:#off}:+-o xtrace} builtin setopt extended_glob diff --git a/lib/zsh/single-line.zsh b/lib/zsh/single-line.zsh index 7658f020..970d07f1 100755 --- a/lib/zsh/single-line.zsh +++ b/lib/zsh/single-line.zsh @@ -1,4 +1,6 @@ #!/usr/bin/env zsh +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et builtin emulate -R zsh ${=${options[xtrace]:#off}:+-o xtrace} builtin setopt extended_glob warn_create_global typeset_silent no_short_loops rc_quotes From 0b7e79aa4bf058058814fadfc12a824d6e794525 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 15 Aug 2026 10:23:11 +0100 Subject: [PATCH 12/44] ci: align branch validation with org policy --- .github/workflows/commit-lint.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml index 12db0f19..9e924963 100644 --- a/.github/workflows/commit-lint.yml +++ b/.github/workflows/commit-lint.yml @@ -89,13 +89,14 @@ jobs: env: BRANCH: ${{ github.head_ref }} run: | - PATTERN='^(feat|fix|perf|refactor|docs|test|ci|chore|revert)/[a-z0-9][a-z0-9-]*[a-z0-9]$' - if echo "$BRANCH" | grep -q '^dependabot/'; then + PATTERN='^(feature|bug|hotfix)-[1-9][0-9]*$' + if echo "$BRANCH" | grep -qE '^(dependabot|renovate)/' || \ + [ "$BRANCH" = "next" ]; then echo "✅ OK" exit 0 fi if ! echo "$BRANCH" | grep -qE "$PATTERN"; then - echo "::error::Branch name must follow type/short-description (e.g. feat/my-feature)" + echo "::error::Branch name must be feature-, bug-, or hotfix-" exit 1 fi echo "✅ Branch name valid" From 39607be7773f57c462614ce545b1d7b6a49e9b7b Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 15 Aug 2026 10:29:09 +0100 Subject: [PATCH 13/44] fix(autoload): preserve self-update reload state --- lib/zsh/autoload.zsh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/zsh/autoload.zsh b/lib/zsh/autoload.zsh index 0e2c6bb1..ac161785 100755 --- a/lib/zsh/autoload.zsh +++ b/lib/zsh/autoload.zsh @@ -712,13 +712,19 @@ ZI[EXTENDED_GLOB]="" if (( ! dry_run )); then # Update the codebase command git merge -n $opt --ff-only FETCH_HEAD || \ - { (( quiet )) || +zi-message "{mmdsh}{happy} Zi{rst} » {profile}update failed {quos}✘{rst}"; return 1 } - .zi-auto-reload $opt + { (( quiet )) || +zi-message "{mmdsh}{happy} Zi{rst} » {profile}update failed {quos}✘{rst}"; exit 1 } + exit 10 fi else (( quiet )) || +zi-message "{mmdsh}{happy} Zi{rst} » {profile}up-to-date {term}✔{rst}" fi ) + local update_status=$? + if (( update_status == 10 )); then + .zi-auto-reload $opt + return $? + fi + return $update_status } # ]]] # FUNCTION: .zi-show-registered-plugins [[[ # Lists loaded plugins (subcommands list, loaded). @@ -1953,7 +1959,7 @@ builtin setopt extended_glob warn_create_global typeset_silent \ (( ${#tmp} > 1 && ${#tmp} % 2 == 0 )) && sice=( "${(Q)tmp[@]}" ) || sice=() fi local attime=$(( ZI[$entry3] - ZI[START_TIME] )) - if [[ "$opt" = *-[a-z]#s[a-z]#* ]]; then + if (( OPTS[opt_-S,--seconds] )); then local time="$ZI[$entry] s" attime="${(M)attime#*.???} s" else From 47d02ae4382d1f67dcd2d7e336eafc3f4166ca54 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 15 Aug 2026 10:29:59 +0100 Subject: [PATCH 14/44] fix(ci): remove next promotion blockers --- .github/PULL_REQUEST_TEMPLATE.md | 6 +++--- .github/workflows/commit-lint.yml | 10 ++++------ .github/workflows/zd-integration.yml | 2 +- docs/CONTRIBUTING.md | 27 +++++++++++++-------------- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 85c9019f..b288c97d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,9 +2,9 @@ Thanks for contributing to zi! Please read the checklist below. Branch model: - • Create your branch FROM next: git checkout -b fix/my-fix next - • Open this PR TARGETING next — never target main directly - • main is only updated from next via a release PR + - Ordinary work: create feature- or bug- from next and target next + - Urgent production fixes: create hotfix- from main and target main + - Promote next to main only after integration checks pass Commit messages must follow Conventional Commits: type(scope): short description (≤72 chars, imperative mood) diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml index 9e924963..d9a29034 100644 --- a/.github/workflows/commit-lint.yml +++ b/.github/workflows/commit-lint.yml @@ -30,7 +30,7 @@ jobs: env: BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} - DISALLOWED_TRAILER_PATTERN: ${{ secrets.DISALLOWED_TRAILER_PATTERN }} + DISALLOWED_TRAILER_PATTERN: "^[[:space:]]*Co-authored-by:" run: | set -euo pipefail @@ -51,11 +51,9 @@ jobs: errors=$((errors + 1)) fi - if [ -n "${DISALLOWED_TRAILER_PATTERN:-}" ]; then - if git show -s --format='%B' "$sha" | grep -qE "$DISALLOWED_TRAILER_PATTERN"; then - echo "❌ Disallowed trailer found (${sha:0:7}): remove before merging" - errors=$((errors + 1)) - fi + if git show -s --format='%B' "$sha" | grep -qiE "$DISALLOWED_TRAILER_PATTERN"; then + echo "❌ Disallowed trailer found (${sha:0:7}): remove before merging" + errors=$((errors + 1)) fi done < <(git log --format='%H' "${BASE_SHA}..${HEAD_SHA}") diff --git a/.github/workflows/zd-integration.yml b/.github/workflows/zd-integration.yml index b629cc50..721ba1b6 100644 --- a/.github/workflows/zd-integration.yml +++ b/.github/workflows/zd-integration.yml @@ -24,5 +24,5 @@ jobs: zd-test: uses: z-shell/zd/.github/workflows/test-native.yml@cbb1211f6a39f9d89a600297883b3766d4fa8ab9 # main with: - zi_repo: ${{ github.repository }} + zi_repo: ${{ github.event.pull_request.head.repo.full_name || github.repository }} zi_ref: ${{ github.head_ref || github.ref_name }} diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index d55e8ebd..1b9f4838 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -5,21 +5,19 @@ Thank you for contributing! Please follow the guidelines below to keep the proje ## Branch model ````text -main ←─── production (tagged releases only) - ↑ -next ←─── integration branch ← open all PRs here - ↑ - ├── feat/ new features - ├── fix/ bug fixes - ├── perf/ performance improvements - ├── refactor/ code refactors - ├── docs/ documentation updates - └── ci/ CI / workflow changes +main production and consumable ref + ^ +next integration branch; ordinary PRs target here + ^ + |-- feature- new features + |-- bug- bug fixes + `-- hotfix- urgent fixes that may target main ```text -1. **Always branch from `next`**: `git checkout -b fix/my-issue next` -2. **Open PRs targeting `next`** — never target `main` directly -3. `next` → `main` happens via a release PR once `next` is stable +1. Branch ordinary work from `next`: `git switch -c bug-123 next`. +2. Target `next` from `feature-` and `bug-` branches. +3. Use `hotfix-` only for urgent fixes branched from and targeting `main`. +4. Promote `next` to `main` once the integration branch is stable. ## Commit message format @@ -48,7 +46,8 @@ To clean up commits before opening a PR: `git rebase -i $(git merge-base HEAD ne ## What not to add -- `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.cursorrules`, or any AI-specific config files +- Root `CLAUDE.md`, `GEMINI.md`, `.cursorrules`, or duplicate agent policy files. + Extend the repository's `AGENTS.md` instead. - Secrets, credentials, or tokens of any kind ## Discussion and issues From e5c97fe625d2ec2c890ef1f98b9dbc9062dcb2db Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 15 Aug 2026 10:37:36 +0100 Subject: [PATCH 15/44] fix(ci): close promotion review gaps --- .github/PULL_REQUEST_TEMPLATE.md | 3 +-- .github/workflows/commit-lint.yml | 10 +++++----- docs/CONTRIBUTING.md | 7 ++++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b288c97d..207f1b80 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -37,8 +37,7 @@ ## Checklist -- [ ] My branch was created from `next` (not `main`) -- [ ] This PR targets the `next` branch +- [ ] Ordinary work branches from and targets `next`; hotfixes branch from and target `main` - [ ] Commit messages follow Conventional Commits format - [ ] No AI co-author trailers in commit messages - [ ] I have read [CONTRIBUTING.md](docs/CONTRIBUTING.md) diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml index d9a29034..8b1ec8b8 100644 --- a/.github/workflows/commit-lint.yml +++ b/.github/workflows/commit-lint.yml @@ -40,6 +40,11 @@ jobs: checked=0 while IFS= read -r sha; do + if git show -s --format='%B' "$sha" | grep -qiE "$DISALLOWED_TRAILER_PATTERN"; then + echo "❌ Disallowed trailer found (${sha:0:7}): remove before merging" + errors=$((errors + 1)) + fi + parent_count=$(git cat-file -p "$sha" | grep -c '^parent' || true) [ "$parent_count" -gt 1 ] && continue @@ -50,11 +55,6 @@ jobs: echo "❌ Invalid commit message (${sha:0:7}): expected type(scope): description ≤72 chars" errors=$((errors + 1)) fi - - if git show -s --format='%B' "$sha" | grep -qiE "$DISALLOWED_TRAILER_PATTERN"; then - echo "❌ Disallowed trailer found (${sha:0:7}): remove before merging" - errors=$((errors + 1)) - fi done < <(git log --format='%H' "${BASE_SHA}..${HEAD_SHA}") if [ "$errors" -gt 0 ]; then diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 1b9f4838..0c817338 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -6,12 +6,12 @@ Thank you for contributing! Please follow the guidelines below to keep the proje ````text main production and consumable ref + |-- hotfix- urgent fixes that may target main ^ next integration branch; ordinary PRs target here ^ |-- feature- new features - |-- bug- bug fixes - `-- hotfix- urgent fixes that may target main + `-- bug- bug fixes ```text 1. Branch ordinary work from `next`: `git switch -c bug-123 next`. @@ -42,7 +42,8 @@ BREAKING CHANGE: description of what breaks - Breaking changes: use `!` suffix (`feat!:`) and add `BREAKING CHANGE:` footer - **No AI co-author trailers** — do not add `Co-authored-by: Copilot` or similar -To clean up commits before opening a PR: `git rebase -i $(git merge-base HEAD next)` +To clean up commits before opening a PR, rebase against the intended target: +`next` for ordinary work and `main` for hotfixes. ## What not to add From 46dfe970469a4be6d66d5f4d4e0d92b3fdc4b65b Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 15 Aug 2026 10:38:31 +0100 Subject: [PATCH 16/44] docs: clarify same-repository hotfix flow --- .github/PULL_REQUEST_TEMPLATE.md | 4 ++-- docs/CONTRIBUTING.md | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 207f1b80..fb621f95 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,7 +3,7 @@ Branch model: - Ordinary work: create feature- or bug- from next and target next - - Urgent production fixes: create hotfix- from main and target main + - Urgent production fixes: use a same-repository hotfix- from main to main - Promote next to main only after integration checks pass Commit messages must follow Conventional Commits: @@ -37,7 +37,7 @@ ## Checklist -- [ ] Ordinary work branches from and targets `next`; hotfixes branch from and target `main` +- [ ] Ordinary work targets `next`; only same-repository hotfixes target `main` - [ ] Commit messages follow Conventional Commits format - [ ] No AI co-author trailers in commit messages - [ ] I have read [CONTRIBUTING.md](docs/CONTRIBUTING.md) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 0c817338..c81a1afd 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -16,7 +16,9 @@ next integration branch; ordinary PRs target here 1. Branch ordinary work from `next`: `git switch -c bug-123 next`. 2. Target `next` from `feature-` and `bug-` branches. -3. Use `hotfix-` only for urgent fixes branched from and targeting `main`. +3. Use `hotfix-` only for urgent fixes created in this repository, + branched from `main`, and targeting `main`. Fork pull requests must target + `next`. 4. Promote `next` to `main` once the integration branch is stable. ## Commit message format From a95c591f30e2fd6037da0f64b904c2514016b0a7 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 15 Aug 2026 10:44:46 +0100 Subject: [PATCH 17/44] fix(runtime): preserve promotion compatibility --- zi.zsh | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/zi.zsh b/zi.zsh index 4360b56f..eb2750c6 100644 --- a/zi.zsh +++ b/zi.zsh @@ -27,13 +27,6 @@ multisrc|compile|nocompile|nocompletions|reset-prompt|wrap|reset|sh|\!sh|bash|\! countdown|ps-on-unload|ps-on-update|trigger-load|light-mode|is-snippet|atdelete|pack|git|verbose|on-update-of|\ subscribe|extract|param|opts|autoload|subst|install|pullopts|debug|null|binary" -# Determine ZI[VERSION] -if [[ -d "${ZI[BIN_DIR]:-$PWD}/.git" || -e "${ZI[BIN_DIR]:-$PWD}/.git" ]]; then - ZI[VERSION]=$(git -C "${ZI[BIN_DIR]:-$PWD}" describe --tags --exact-match 2>/dev/null) || ZI[VERSION]=$(git -C "${ZI[BIN_DIR]:-$PWD}" rev-parse --short HEAD 2>/dev/null) || ZI[VERSION]="unknown" -else - ZI[VERSION]="unknown" -fi - # In no value, i.e. not designed to hold value. ZI[nval-ice-list]="blockf|silent|lucid|trackbinds|cloneonly|nocd|run-atpull|nocompletions|sh|\!sh|bash|\!bash|\ ksh|\!ksh|csh|\!csh|aliases|countdown|light-mode|is-snippet|git|verbose|cloneopts|pullopts|debug|null|binary|make|\ @@ -61,6 +54,15 @@ if [[ ! -e ${ZI[BIN_DIR]}/zi.zsh ]]; then return 1 fi +# Determine ZI[VERSION] after establishing the repository path. +if [[ -d ${ZI[BIN_DIR]}/.git || -e ${ZI[BIN_DIR]}/.git ]]; then + ZI[VERSION]=$(git -C "${ZI[BIN_DIR]}" describe --tags --exact-match 2>/dev/null) || \ + ZI[VERSION]=$(git -C "${ZI[BIN_DIR]}" rev-parse --short HEAD 2>/dev/null) || \ + ZI[VERSION]="unknown" +else + ZI[VERSION]="unknown" +fi + # Zi home path for creating working directories. if [[ -z $ZI[HOME_DIR] ]]; then if [[ -d $HOME ]]; then @@ -130,8 +132,8 @@ fi # Disable aliases. : ${ZI[ALIASES_OPT]:=${${options[aliases]:#off}:+1}} -# Disable Zi's internal aliases. -: ${ZI[INTERNAL_ALIASES]:=0} +# Configure Zi's deprecated command aliases. +: ${ZI[INTERNAL_ALIASES]:=1} # Set owner of the Zi's packages. It allows to use the packages from specific user instead of the z-shell organization. : ${ZI[PKG_OWNER]:=z-shell} @@ -2310,7 +2312,8 @@ zi() { --parallel opt_-p,--parallel -s opt_-s,--snippets:"Update only snippets (i.e.: skip updating plugins)." --snippets opt_-s,--snippets - -l opt_-l,--plugins:"Update only plugins (i.e.: skip updating snippets)." + -L opt_-l,--plugins:"Update only plugins (i.e.: skip updating snippets)." + -l opt_-l,--plugins --plugins opt_-l,--plugins -h opt_-h,--help:"Show this help message." --help opt_-h,--help @@ -2327,7 +2330,7 @@ zi() { -x opt_-x,--command:"Load the snippet as a {cmd}command{rst}, i.e.: add it to {var}\$PATH{rst} and set {b-lhi}+x{rst} on it." --command opt_-x,--command env-whitelist "-h|--help|-v|--verbose" - update "-h|--help|-l|--plugins|-s|--snippets|-p|--parallel|-a|--all|-q|--quiet|-r|--reset|-u|--urge|-n|--no-pager|-v|--verbose" + update "-h|--help|-L|-l|--plugins|-s|--snippets|-p|--parallel|-a|--all|-q|--quiet|-r|--reset|-u|--urge|-n|--no-pager|-v|--verbose" self-update "-h|--help|-q|--quiet|-D|--dry-run" compile "-h|--help|-a|--all|-q|--quiet" uncompile "-h|--help|-a|--all|-q|--quiet" @@ -2937,8 +2940,8 @@ zmodload zsh/zpty zsh/system 2>/dev/null zmodload -F zsh/stat b:zstat 2>/dev/null && ZI[HAVE_ZSTAT]=1 # code. [[[ -# Internal aliases for compatibility with deprecated names. Can be enabled with ZI[INTERNAL_ALIASES]=1. -(( ZI[INTERNAL_ALIASES] )) && builtin alias zinit=zi zplugin=zi +# Internal aliases preserve compatibility with deprecated command names. +(( ZI[INTERNAL_ALIASES] )) && builtin alias zini=zi zinit=zi zplugin=zi .zi-prepare-home .zi-set-mtime From 847b7c5dd67ba5cde0d81acd19c12e9202ce6499 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 15 Aug 2026 10:52:34 +0100 Subject: [PATCH 18/44] fix(runtime): restore documented compatibility helpers --- lib/zsh/install.zsh | 3 +++ zi.zsh | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/zsh/install.zsh b/lib/zsh/install.zsh index 8f81f3ed..d454b586 100755 --- a/lib/zsh/install.zsh +++ b/lib/zsh/install.zsh @@ -1740,6 +1740,9 @@ ziextract() { } return 0 } # ]]] +# FUNCTION: zpextract [[[ +zpextract() { ziextract "$@"; } +# ]]] # FUNCTION: .zi-extract() [[[ .zi-extract() { builtin emulate -LR zsh ${=${options[xtrace]:#off}:+-o xtrace} diff --git a/zi.zsh b/zi.zsh index eb2750c6..b89263a0 100644 --- a/zi.zsh +++ b/zi.zsh @@ -2646,7 +2646,7 @@ zi() { .zi-unload "${2%%(///|//|/)}" "${${3:#-q}%%(///|//|/)}" "${${(M)4:#-q}:-${(M)3:#-q}}"; ___retval=$? fi ;; - (version) + (-V|--version|version) +zi-message "Zi version: ${ZI[VERSION]}" ;; (bindkeys) @@ -2909,6 +2909,13 @@ zicompinit_fast() { # An utility function of an undefined use case. zicompdef() { ZI_COMPDEF_REPLAY+=( "${(j: :)${(q)@}}" ); } # ]]] +# Compatibility functions. [[[ +❮▼❯() { zi "$@"; } +zpcdreplay() { zicdreplay "$@"; } +zpcdclear() { zicdclear "$@"; } +zpcompinit() { zicompinit "$@"; } +zpcompdef() { zicompdef "$@"; } +# ]]] # FUNCTION: @autoload. [[[ @autoload() { :zi-tmp-subst-autoload -Uz ${(s: :)${${(j: :)${@#\!}}//(#b)((*)(->|=>|→)(*)|(*))/${match[2]:+$match[2] -S $match[4]}${match[5]:+${match[5]} -S ${match[5]}}}} ${${${(@M)${@#\!}:#*(->|=>|→)*}}:+-C} ${${@#\!}:+-C} From eaa3a8d5bd23e168386d9c87f0aa412f04cdb9e8 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 15 Aug 2026 10:54:14 +0100 Subject: [PATCH 19/44] fix(runtime): load zpextract compatibility lazily --- lib/zsh/install.zsh | 3 --- zi.zsh | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/zsh/install.zsh b/lib/zsh/install.zsh index d454b586..8f81f3ed 100755 --- a/lib/zsh/install.zsh +++ b/lib/zsh/install.zsh @@ -1740,9 +1740,6 @@ ziextract() { } return 0 } # ]]] -# FUNCTION: zpextract [[[ -zpextract() { ziextract "$@"; } -# ]]] # FUNCTION: .zi-extract() [[[ .zi-extract() { builtin emulate -LR zsh ${=${options[xtrace]:#off}:+-o xtrace} diff --git a/zi.zsh b/zi.zsh index b89263a0..3f3aa9ed 100644 --- a/zi.zsh +++ b/zi.zsh @@ -2915,6 +2915,10 @@ zpcdreplay() { zicdreplay "$@"; } zpcdclear() { zicdclear "$@"; } zpcompinit() { zicompinit "$@"; } zpcompdef() { zicompdef "$@"; } +zpextract() { + (( ${+functions[ziextract]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/install.zsh" || return 1 + ziextract "$@" +} # ]]] # FUNCTION: @autoload. [[[ @autoload() { From d45fe2468a6650b7e06df7dd631903a5952d48c2 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 15 Aug 2026 10:59:54 +0100 Subject: [PATCH 20/44] fix(times): preserve lowercase seconds flag --- lib/zsh/autoload.zsh | 2 +- zi.zsh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/zsh/autoload.zsh b/lib/zsh/autoload.zsh index ac161785..0927e812 100755 --- a/lib/zsh/autoload.zsh +++ b/lib/zsh/autoload.zsh @@ -1959,7 +1959,7 @@ builtin setopt extended_glob warn_create_global typeset_silent \ (( ${#tmp} > 1 && ${#tmp} % 2 == 0 )) && sice=( "${(Q)tmp[@]}" ) || sice=() fi local attime=$(( ZI[$entry3] - ZI[START_TIME] )) - if (( OPTS[opt_-S,--seconds] )); then + if (( OPTS[opt_-S,--seconds] || OPTS[opt_-s,--snippets] )); then local time="$ZI[$entry] s" attime="${(M)attime#*.???} s" else diff --git a/zi.zsh b/zi.zsh index 3f3aa9ed..e6f7e23c 100644 --- a/zi.zsh +++ b/zi.zsh @@ -2310,7 +2310,7 @@ zi() { --force opt_-f,--force -p opt_-p,--parallel:"Turn on concurrent, multi-thread update (of all objects)." --parallel opt_-p,--parallel - -s opt_-s,--snippets:"Update only snippets (i.e.: skip updating plugins)." + -s opt_-s,--snippets:"update:[Update only snippets (i.e.: skip updating plugins).] times:[Show times in seconds.]" --snippets opt_-s,--snippets -L opt_-l,--plugins:"Update only plugins (i.e.: skip updating snippets)." -l opt_-l,--plugins @@ -2339,7 +2339,7 @@ zi() { cdclear "-h|--help|-q|--quiet" cdreplay "-h|--help|-q|--quiet" module "-h|--help|-B|--build|-I|--info|-r|--reset" - times "-h|--help|-m|--moments|-S|--seconds|-a|--all" + times "-h|--help|-m|--moments|-s|-S|--seconds|-a|--all" light "-h|--help|-b|--bindkeys" report "-h|--help|-a|--all" snippet "-h|--help|-f|--force|--command|-x" From e402c4b20651d055c7eebab3ae05625e230ba478 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 15 Aug 2026 12:11:29 +0100 Subject: [PATCH 21/44] ci: add promotion readiness gate --- .github/PULL_REQUEST_TEMPLATE.md | 2 + .github/PULL_REQUEST_TEMPLATE/promotion.md | 101 ++++++++++++ .github/workflows/promotion-readiness.yml | 174 +++++++++++++++++++++ .github/workflows/zd-integration.yml | 4 +- 4 files changed, 279 insertions(+), 2 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE/promotion.md create mode 100644 .github/workflows/promotion-readiness.yml diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fb621f95..b9e139e1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,6 +5,8 @@ - Ordinary work: create feature- or bug- from next and target next - Urgent production fixes: use a same-repository hotfix- from main to main - Promote next to main only after integration checks pass + - For next-to-main promotions, use: + ?template=promotion.md Commit messages must follow Conventional Commits: type(scope): short description (≤72 chars, imperative mood) diff --git a/.github/PULL_REQUEST_TEMPLATE/promotion.md b/.github/PULL_REQUEST_TEMPLATE/promotion.md new file mode 100644 index 00000000..5a95226f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/promotion.md @@ -0,0 +1,101 @@ +# Promotion readiness record + +Use this template only for a same-repository `next` to `main` promotion. +Ordinary work continues to target `next`; urgent production fixes use a +same-repository `hotfix-*` branch. + +## Candidate identity + +- Prior `main` SHA: `____________________________` +- Candidate `next` SHA: `____________________________` +- Complete compare: + [`main...next`](https://github.com/z-shell/zi/compare/BASE_SHA...HEAD_SHA) + +Replace `BASE_SHA` and `HEAD_SHA` in the compare link with the exact SHAs above. +The PR head SHA must still equal the recorded candidate SHA before merge. + +## Required validation + +The `Promotion gate` check is the ruleset context. It directly depends on every +constituent below and fails if any constituent fails, is cancelled, or is +skipped. + +| Validation | Stable job name | Result | +| ----------------------------------------- | --------------------------- | ------- | +| Zsh syntax and compile | `Zsh syntax and compile` | Pending | +| ZD ZUnit integration | `ZD integration` | Pending | +| Trunk | `Trunk` | Pending | +| CodeQL | `CodeQL` | Pending | +| Exact-candidate clean install and startup | `Clean install and startup` | Pending | +| Aggregate | `Promotion gate` | Pending | + +- [ ] `z-shell/zd#95` is merged and the reusable workflow pin represents its + immutable Zi-SHA contract. +- [ ] `z-shell/zd#96` is merged and the reusable workflow pin includes its + focused compatibility tests. +- [ ] The candidate SHA has not changed since all required checks completed. + +## Readiness review + +### Unresolved issues + +List each unresolved issue and its disposition. Write `None` only after checking +the promotion candidate and linked release work. + +### User-facing and migration notes + +Summarize behavioral changes. If no migration is required, state why. + +### Public-contract follow-ups + +Link documentation and consumer follow-ups identified by the +[#376 public-contract impact review](https://github.com/z-shell/zi/issues/376). +Write `None` only with a rationale. + +## Rollback readiness + +- Rollback owner: `@________________` +- Observable rollback criteria: + - `____________________________________________________________` +- [ ] The prior `main` SHA above is confirmed immediately before merge. +- [ ] The owner can open a same-repository `hotfix-*` PR that reverts the + promotion commit through protected `main`. +- [ ] The revert PR will run `Guard main branch source` and `Promotion gate`. +- [ ] After a revert, clean install and startup will be confirmed at the new + `main` head. + +Never force-push `main`. Roll back with a reviewed revert commit through the +protected-branch hotfix path so the incident and recovery remain auditable. + +## Publication boundary + +Merging this promotion updates the Git-consumed `main` branch. It does not +create a semantic tag or GitHub release. Tagging remains a separately approved +publication action under [#346](https://github.com/z-shell/zi/issues/346), with +its own release notes and green validation. + +## Bootstrap and post-merge ruleset change + +Do not change repository settings from the implementation PR. GitHub evaluates +`pull_request` workflow definitions from the default branch, so merging this +file only into `next` cannot activate it for a `next` to `main` PR. + +After the implementation and both ZD dependencies are merged: + +1. Create `hotfix-377-promotion-bootstrap` from `main`. +2. Copy the reviewed promotion workflow and templates from `next` without other + pending `next` changes. +3. Open the same-repository hotfix PR into protected `main`. The existing exact + context `Guard main branch source` must pass. Do not bypass or force-push. +4. Merge the bootstrap PR, then open the `next` to `main` promotion PR. Confirm + that all constituent jobs and the exact context `Promotion gate` run. +5. Update only the active `main` ruleset's required status checks: keep + `Guard main branch source` and add `Promotion gate`. +6. Keep strict required-status-check policy disabled unless maintainers approve + a separate policy change. +7. Do not add constituent contexts; `Promotion gate` owns their dependency + semantics, and the ZD reusable workflow expands to matrix checks. + +Leave the `next` ruleset unchanged. Promotion validation runs only for PRs into +`main`, so ordinary PRs into `next` remain lightweight. After the promotion, +reconcile `main` back into `next` through the protected pull-request process. diff --git a/.github/workflows/promotion-readiness.yml b/.github/workflows/promotion-readiness.yml new file mode 100644 index 00000000..522f2aca --- /dev/null +++ b/.github/workflows/promotion-readiness.yml @@ -0,0 +1,174 @@ +--- +name: Promotion Readiness + +on: + pull_request: + branches: [main] + types: [opened, reopened, synchronize, ready_for_review] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + zsh: + name: Zsh syntax and compile + runs-on: ubuntu-latest + steps: + - name: Check out the candidate + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Install Zsh + run: sudo apt-get update && sudo apt-get install -yq zsh + + - name: Check and compile Zsh sources + shell: bash + run: | + set -euo pipefail + checked=0 + while IFS= read -r -d '' file; do + zsh -n "${file}" + zsh -fc 'zcompile "$1"' _ "${file}" + rm -f "${file}.zwc" + checked=$((checked + 1)) + done < <( + find . -type d -name doc -prune -o \ + -type f \( -name '*.zsh' -o -name '_zi' \) -print0 + ) + if ((checked == 0)); then + echo "::error::No Zsh sources were found." + exit 1 + fi + + zd: + name: ZD integration + uses: z-shell/zd/.github/workflows/test-native.yml@da915fd260112430aa34981aed3d4e0262f33b87 # z-shell/zd#95 + with: + zi_repo: ${{ github.event.pull_request.head.repo.full_name }} + zi_ref: ${{ github.event.pull_request.head.sha }} + + trunk: + name: Trunk + runs-on: ubuntu-latest + permissions: + checks: write + contents: read + steps: + - name: Check out the candidate + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Run Trunk + uses: trunk-io/trunk-action@75699af9e26881e564e9d832ef7dc3af25ec031b + + codeql: + name: CodeQL + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + security-events: write + steps: + - name: Check out the candidate + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Initialize CodeQL + uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + with: + build-mode: none + languages: actions + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + with: + category: /language:actions + + clean-install: + name: Clean install and startup + runs-on: ubuntu-latest + steps: + - name: Install Zsh + run: sudo apt-get update && sudo apt-get install -yq zsh + + - name: Install and start the exact candidate + shell: bash + env: + CANDIDATE_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + CANDIDATE_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + checkout="${RUNNER_TEMP}/zi-candidate" + data_root="${RUNNER_TEMP}/zi-fresh-data" + + test ! -e "${checkout}" + test ! -e "${data_root}" + + git init "${checkout}" + git -C "${checkout}" remote add origin \ + "https://github.com/${CANDIDATE_REPOSITORY}.git" + git -C "${checkout}" fetch --no-tags --depth 1 origin -- \ + "${CANDIDATE_SHA}" + git -C "${checkout}" checkout --detach FETCH_HEAD + test "$(git -C "${checkout}" rev-parse HEAD)" = "${CANDIDATE_SHA}" + + mkdir -p \ + "${data_root}/home" \ + "${data_root}/cache" \ + "${data_root}/config" + env \ + CANDIDATE_SHA="${CANDIDATE_SHA}" \ + HOME="${data_root}/home" \ + XDG_CACHE_HOME="${data_root}/cache" \ + XDG_CONFIG_HOME="${data_root}/config" \ + XDG_DATA_HOME="${data_root}/data" \ + ZDOTDIR="${data_root}/zdotdir" \ + ZI_CHECKOUT="${checkout}" \ + zsh -f <<'ZSH' + builtin source "${ZI_CHECKOUT}/zi.zsh" || exit 1 + [[ ${ZI[BIN_DIR]} = ${ZI_CHECKOUT} ]] || exit 1 + [[ "$(git -C "${ZI[BIN_DIR]}" rev-parse HEAD)" = ${CANDIDATE_SHA} ]] || + exit 1 + (( ${+functions[zi]} )) || exit 1 + .zi-prepare-home || exit 1 + [[ -d ${ZI[HOME_DIR]} ]] || exit 1 + zi -V >/dev/null || exit 1 + ZSH + + gate: + name: Promotion gate + if: ${{ always() }} + needs: [zsh, zd, trunk, codeql, clean-install] + runs-on: ubuntu-latest + permissions: {} + steps: + - name: Require every promotion validation + env: + ZSH_RESULT: ${{ needs.zsh.result }} + ZD_RESULT: ${{ needs.zd.result }} + TRUNK_RESULT: ${{ needs.trunk.result }} + CODEQL_RESULT: ${{ needs.codeql.result }} + CLEAN_INSTALL_RESULT: ${{ needs.clean-install.result }} + run: | + failed=0 + for result_name in \ + ZSH_RESULT \ + ZD_RESULT \ + TRUNK_RESULT \ + CODEQL_RESULT \ + CLEAN_INSTALL_RESULT + do + result="${!result_name}" + if [[ "${result}" != success ]]; then + echo "::error::${result_name} was '${result}', not 'success'." + failed=1 + fi + done + exit "${failed}" diff --git a/.github/workflows/zd-integration.yml b/.github/workflows/zd-integration.yml index 721ba1b6..ad066c4b 100644 --- a/.github/workflows/zd-integration.yml +++ b/.github/workflows/zd-integration.yml @@ -22,7 +22,7 @@ permissions: jobs: zd-test: - uses: z-shell/zd/.github/workflows/test-native.yml@cbb1211f6a39f9d89a600297883b3766d4fa8ab9 # main + uses: z-shell/zd/.github/workflows/test-native.yml@da915fd260112430aa34981aed3d4e0262f33b87 # z-shell/zd#95 with: zi_repo: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - zi_ref: ${{ github.head_ref || github.ref_name }} + zi_ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} From 507a143bad05c73cc3d52a04c6adb6810daf83b6 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 15 Aug 2026 12:13:40 +0100 Subject: [PATCH 22/44] chore(meta): trim repository metadata --- .editorconfig | 351 +----------------- .gitattributes | 89 +---- .github/ISSUE_TEMPLATE/01_bug_report.yml | 98 +---- .github/ISSUE_TEMPLATE/02_feature_request.yml | 56 +-- .../03_codebase_improvement.yml | 49 +-- .github/ISSUE_TEMPLATE/04_documentation.yml | 74 ++-- .github/PULL_REQUEST_TEMPLATE.md | 12 +- .github/workflows/codeql.yml | 8 +- .github/workflows/commit-lint.yml | 6 +- .github/workflows/trunk-check.yml | 16 +- .github/workflows/zsh-n.yml | 14 +- .gitignore | 129 +------ .trunk/config/.markdownlint.yaml | 12 +- .trunk/config/.yamllint.yaml | 7 - .trunk/trunk.yaml | 23 +- .vscode/settings.json | 86 ----- docs/README.md | 8 +- 17 files changed, 151 insertions(+), 887 deletions(-) delete mode 100644 .trunk/config/.yamllint.yaml delete mode 100644 .vscode/settings.json diff --git a/.editorconfig b/.editorconfig index 3928e572..8c52ff93 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,359 +1,12 @@ -# Space or Tabs? -# https://stackoverflow.com/questions/35649847/objective-reasons-for-using-spaces-instead-of-tabs-for-indentation -# https://stackoverflow.com/questions/12093748/how-to-use-tabs-instead-of-spaces-in-a-shell-script -# https://github.com/editorconfig/editorconfig-defaults/blob/master/editorconfig-defaults.json -# -# 1. What happens when I press the Tab key in my text editor? -# 2. What happens when I request my editor to indent one or more lines? -# 3. What happens when I view a file containing U+0009 HORIZONTAL TAB characters? -# -# Answers: -# -# 1. Pressing the Tab key should indent the current line (or selected lines) one additional level. -# 2. As a secondary alternative, I can also tolerate an editor that, -# like Emacs, uses this key for a context-sensitive fix-my-indentation command. -# 3. Indenting one or more lines should follow the reigning convention, if consensus is sufficiently strong; otherwise, -# I greatly prefer 2-space indentation at each level. U+0009 characters should shift subsequent characters to the next tab stop. -# -# Note: VIM users should use alternate marks [[[ and ]]] as the original ones can confuse nested substitutions, e.g.: ${${${VAR}}} -# -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et - root = true [*] charset = utf-8 +end_of_line = lf indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true -[*.sln] -indent_style = tab - -[*.{md,mdx,rst}] +[*.md] trim_trailing_whitespace = false - -[*.{cmd,bat}] -end_of_line = crlf - -[*za-*] -end_of_line = lf - -[*.{sh,bash,zsh,fish}] -end_of_line = lf - -[Makefile] -indent_style = tab -indent_size = 4 - -[*.{py,rb}] -indent_size = 4 - -[*.{go,java,scala,groovy,kotlin}] -indent_style = tab -indent_size = 4 - -[*.{cs,csx,cake,vb,vbx}] -# Default Severity for all .NET Code Style rules below -dotnet_analyzer_diagnostic.severity = warning - -# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#net-style-rules -[*.{cs,csx,cake,vb,vbx}] -# "this." and "Me." qualifiers -dotnet_style_qualification_for_field = true:warning -dotnet_style_qualification_for_property = true:warning -dotnet_style_qualification_for_method = true:warning -dotnet_style_qualification_for_event = true:warning -# Language keywords instead of framework type names for type references -dotnet_style_predefined_type_for_locals_parameters_members = true:warning -dotnet_style_predefined_type_for_member_access = true:warning -# Modifier preferences -dotnet_style_require_accessibility_modifiers = always:warning -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning -visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:warning -dotnet_style_readonly_field = true:warning -# Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning -# Expression-level preferences -dotnet_style_object_initializer = true:warning -dotnet_style_collection_initializer = true:warning -dotnet_style_explicit_tuple_names = true:warning -dotnet_style_prefer_inferred_tuple_names = true:warning -dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning -dotnet_style_prefer_auto_properties = true:warning -dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion -dotnet_diagnostic.IDE0045.severity = suggestion -dotnet_style_prefer_conditional_expression_over_return = false:suggestion -dotnet_diagnostic.IDE0046.severity = suggestion -dotnet_style_prefer_compound_assignment = true:warning -dotnet_style_prefer_simplified_interpolation = true:warning -dotnet_style_prefer_simplified_boolean_expressions = true:warning -# Null-checking preferences -dotnet_style_coalesce_expression = true:warning -dotnet_style_null_propagation = true:warning -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning -# File header preferences -# file_header_template = \n© PROJECT-AUTHOR\n -# If you use StyleCop, you'll need to disable SA1636: File header copyright text should match. -# dotnet_diagnostic.SA1636.severity = none -# Undocumented -dotnet_style_operator_placement_when_wrapping = end_of_line:warning -csharp_style_prefer_null_check_over_type_check = true:warning - -# C# Style Rules -# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#c-style-rules -[*.{cs,csx,cake}] -# 'var' preferences -csharp_style_var_for_built_in_types = true:warning -csharp_style_var_when_type_is_apparent = true:warning -csharp_style_var_elsewhere = true:warning -# Expression-bodied members -csharp_style_expression_bodied_methods = true:warning -csharp_style_expression_bodied_constructors = true:warning -csharp_style_expression_bodied_operators = true:warning -csharp_style_expression_bodied_properties = true:warning -csharp_style_expression_bodied_indexers = true:warning -csharp_style_expression_bodied_accessors = true:warning -csharp_style_expression_bodied_lambdas = true:warning -csharp_style_expression_bodied_local_functions = true:warning -# Pattern matching preferences -csharp_style_pattern_matching_over_is_with_cast_check = true:warning -csharp_style_pattern_matching_over_as_with_null_check = true:warning -csharp_style_prefer_switch_expression = true:warning -csharp_style_prefer_pattern_matching = true:warning -csharp_style_prefer_not_pattern = true:warning -# Expression-level preferences -csharp_style_inlined_variable_declaration = true:warning -csharp_prefer_simple_default_expression = true:warning -csharp_style_pattern_local_over_anonymous_function = true:warning -csharp_style_deconstructed_variable_declaration = true:warning -csharp_style_prefer_index_operator = true:warning -csharp_style_prefer_range_operator = true:warning -csharp_style_implicit_object_creation_when_type_is_apparent = true:warning -# "Null" checking preferences -csharp_style_throw_expression = true:warning -csharp_style_conditional_delegate_call = true:warning -# Code block preferences -csharp_prefer_braces = true:warning -csharp_prefer_simple_using_statement = true:suggestion -dotnet_diagnostic.IDE0063.severity = suggestion -# 'using' directive preferences -csharp_using_directive_placement = inside_namespace:warning -# Modifier preferences -csharp_prefer_static_local_function = true:warning - -# .NET Unnecessary code rules -[*.{cs,csx,cake,vb,vbx}] -dotnet_code_quality_unused_parameters = all:warning -dotnet_remove_unnecessary_suppression_exclusions = none:warning - -# C# Unnecessary code rules -[*.{cs,csx,cake}] -csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion -dotnet_diagnostic.IDE0058.severity = suggestion -csharp_style_unused_value_assignment_preference = discard_variable:suggestion -dotnet_diagnostic.IDE0059.severity = suggestion - -# .NET formatting rules -# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#net-formatting-rules -[*.{cs,csx,cake,vb,vbx}] -# Organize using directives -dotnet_sort_system_directives_first = true -dotnet_separate_import_directive_groups = false -# Dotnet namespace options -dotnet_style_namespace_match_folder = true:suggestion -dotnet_diagnostic.IDE0130.severity = suggestion - -# C# formatting rules -# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#c-formatting-rules -[*.{cs,csx,cake}] -# Newline options -# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#new-line-options -csharp_new_line_before_open_brace = all -csharp_new_line_before_else = true -csharp_new_line_before_catch = true -csharp_new_line_before_finally = true -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_between_query_expression_clauses = true -# Indentation options -# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#indentation-options -csharp_indent_case_contents = true -csharp_indent_switch_labels = true -csharp_indent_labels = no_change -csharp_indent_block_contents = true -csharp_indent_braces = false -csharp_indent_case_contents_when_block = false -# Spacing options -# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#spacing-options -csharp_space_after_cast = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_between_parentheses = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_around_binary_operators = before_and_after -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_declaration_name_and_open_parenthesis = false -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_after_comma = true -csharp_space_before_comma = false -csharp_space_after_dot = false -csharp_space_before_dot = false -csharp_space_after_semicolon_in_for_statement = true -csharp_space_before_semicolon_in_for_statement = false -csharp_space_around_declaration_statements = false -csharp_space_before_open_square_brackets = false -csharp_space_between_empty_square_brackets = false -csharp_space_between_square_brackets = false -# Wrap options -# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#wrap-options -csharp_preserve_single_line_statements = false -csharp_preserve_single_line_blocks = true -# Namespace options -# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#namespace-options -csharp_style_namespace_declarations = file_scoped:warning - -[*.{cs,csx,cake,vb,vbx}] - -# camel_case_style - Define the camelCase style -dotnet_naming_style.camel_case_style.capitalization = camel_case -# pascal_case_style - Define the PascalCase style -dotnet_naming_style.pascal_case_style.capitalization = pascal_case -# first_upper_style - The first character must start with an upper-case character -dotnet_naming_style.first_upper_style.capitalization = first_word_upper -# prefix_interface_with_i_style - Interfaces must be PascalCase and the first character of an interface must be an 'I' -dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case -dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I -# prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and the first character must be a 'T' -dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case -dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T -# disallowed_style - Anything that has this style applied is marked as disallowed -dotnet_naming_style.disallowed_style.capitalization = pascal_case -dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____ -dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____ -# internal_error_style - This style should never occur... if it does, it indicates a bug in file or in the parser using the file -dotnet_naming_style.internal_error_style.capitalization = pascal_case -dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____ -dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR____ - -# All public/protected/protected_internal constant fields must be PascalCase -# https://docs.microsoft.com/dotnet/standard/design-guidelines/field -dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal -dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const -dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field -dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group -dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = pascal_case_style -dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = warning - -# All public/protected/protected_internal static readonly fields must be PascalCase -# https://docs.microsoft.com/dotnet/standard/design-guidelines/field -dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal -dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly -dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field -dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group -dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style -dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = warning - -# No other public/protected/protected_internal fields are allowed -# https://docs.microsoft.com/dotnet/standard/design-guidelines/field -dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal -dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field -dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group -dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style -dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error - -# All constant fields must be PascalCase -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md -dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private -dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const -dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field -dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group -dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = pascal_case_style -dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning - -# All static readonly fields must be PascalCase -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md -dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private -dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly -dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field -dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group -dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style -dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning - -# No non-private instance fields are allowed -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md -dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected -dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field -dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group -dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style -dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error - -# Private fields must be camelCase -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md -dotnet_naming_symbols.stylecop_private_fields_group.applicable_accessibilities = private -dotnet_naming_symbols.stylecop_private_fields_group.applicable_kinds = field -dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_private_fields_group -dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = camel_case_style -dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = warning - -# Local variables must be camelCase -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md -dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local -dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local -dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group -dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style -dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent - -# This rule should never fire. However, it's included for at least two purposes: -# First, it helps to understand, reason about, and root-case certain types of issues, such as bugs in .editorconfig parsers. -# Second, it helps to raise immediate awareness if a new field type is added (as occurred recently in C#). -dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = * -dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field -dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group -dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style -dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error - -# All of the following must be PascalCase: -# - Namespaces -# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-namespaces -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md -# - Classes and Enumerations -# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md -# - Delegates -# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces#names-of-common-types -# - Constructors, Properties, Events, Methods -# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-type-members -dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property -dotnet_naming_rule.element_rule.symbols = element_group -dotnet_naming_rule.element_rule.style = pascal_case_style -dotnet_naming_rule.element_rule.severity = warning - -# Interfaces use PascalCase and are prefixed with uppercase 'I' -# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces -dotnet_naming_symbols.interface_group.applicable_kinds = interface -dotnet_naming_rule.interface_rule.symbols = interface_group -dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style -dotnet_naming_rule.interface_rule.severity = warning - -# Generics Type Parameters use PascalCase and are prefixed with uppercase 'T' -# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces -dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter -dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group -dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style -dotnet_naming_rule.type_parameter_rule.severity = warning - -# Function parameters use camelCase -# https://docs.microsoft.com/dotnet/standard/design-guidelines/naming-parameters -dotnet_naming_symbols.parameters_group.applicable_kinds = parameter -dotnet_naming_rule.parameters_rule.symbols = parameters_group -dotnet_naming_rule.parameters_rule.style = camel_case_style -dotnet_naming_rule.parameters_rule.severity = warning diff --git a/.gitattributes b/.gitattributes index e215d30b..5a53b71a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,82 +1,11 @@ -# Auto detect text files and perform LF normalization -* text=auto +* text=auto -# -# The above will handle all files NOT found below -# +*.sh text eol=lf +*.zsh text eol=lf +*.zunit text eol=lf +_zi text eol=lf +lib/zcompile text eol=lf +lib/templates/example-script text eol=lf -# Documents -*.bibtex text diff=bibtex -*.doc diff=astextplain -*.DOC diff=astextplain -*.docx diff=astextplain -*.DOCX diff=astextplain -*.dot diff=astextplain -*.DOT diff=astextplain -*.pdf diff=astextplain -*.PDF diff=astextplain -*.rtf diff=astextplain -*.RTF diff=astextplain -*.md text diff=markdown -*.mdx text diff=markdown -*.tex text diff=tex -*.adoc text -*.textile text -*.mustache text -*.csv text -*.tab text -*.tsv text -*.txt text -*.sql text -*.epub diff=astextplain - -# Graphics -*.png binary -*.jpg binary -*.jpeg binary -*.gif binary -*.tif binary -*.tiff binary -*.ico binary -# SVG treated as text by default. -*.svg text -# If you want to treat it as binary, -# use the following line instead. -# *.svg binary -*.eps binary - -# Scripts -*.bash text eol=lf -*.fish text eol=lf -*.sh text eol=lf -*.zsh text eol=lf -*.zunit text eol=lf -# These are explicitly windows files and should use crlf -*.bat text eol=crlf -*.cmd text eol=crlf -*.ps1 text eol=crlf - -# Serialisation -*.json text -*.toml text -*.xml text -*.yaml text -*.yml text - -# Archives -*.7z binary -*.gz binary -*.tar binary -*.tgz binary -*.zip binary - -# Text files where line endings should be preserved -*.patch -text - -# -# Exclude files from exporting -# - -.gitattributes export-ignore -.gitignore export-ignore -.gitkeep export-ignore +*.gz binary +*.png binary diff --git a/.github/ISSUE_TEMPLATE/01_bug_report.yml b/.github/ISSUE_TEMPLATE/01_bug_report.yml index e789112a..a35e694b 100644 --- a/.github/ISSUE_TEMPLATE/01_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/01_bug_report.yml @@ -1,110 +1,50 @@ name: "🐞 Bug report" -description: File a bug report +description: Report reproducible incorrect Zi behavior title: "[bug]: " -labels: ["bug 🐞", "triage 📑"] +labels: ["type:bug", "status:triage"] assignees: ["ss-o"] body: - - type: markdown - attributes: - value: | - First off, thanks for taking the time to contribute! Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make will benefit everybody else and are greatly appreciated. - type: input id: environment attributes: label: Environment - description: > - Please describe your environment in as much detail as possible. Otherwise, we may not be able to reproduce the issue. - placeholder: > - Example: - print "Device: $VENDOR | $OSTYPE | $CPUTYPE" - print "Shell: $SHELL | $ZSH_ARGZERO | $ZSH_PATCHLEVEL" - print "Zi: $(git -C $ZI[BIN_DIR] rev-parse HEAD)" - validations: - required: true - - type: textarea - id: repro - attributes: - label: Reproduction steps - description: "How do you trigger this bug? Please walk us through it step by step." - value: | - 1. - 2. - 3. - 4. - 5. - ... - render: bash + description: Include your OS, Zsh version, and Zi commit. + placeholder: zsh --version; git -C "${ZI[BIN_DIR]}" rev-parse HEAD validations: required: true - type: textarea - id: expected + id: reproduction attributes: - label: Expected behavior - description: "Please describe the expected behavior" - value: | - 1. - 2. - 3. - 4. - 5. - ... - render: bash + label: Minimal reproduction + description: Provide the smallest Zsh commands or configuration that reproduce the problem. + render: zsh validations: required: true - type: textarea - id: current + id: actual attributes: - label: Current behavior - description: "Please describe how the bug manifests" - value: | - 1. - 2. - 3. - 4. - 5. - ... - render: bash + label: Actual behavior + description: Include complete error output or logs where relevant. validations: required: true - type: textarea - id: code-snippet - attributes: - label: Code snippet - description: "Please insert your zshrc or just a short code snippet in concern." - validations: - required: true - - type: textarea - id: additional + id: expected attributes: - label: Additional information - description: "List any other information that is relevant to your issue. For reports and stats run `zi analytics`" + label: Expected behavior validations: required: true - type: checkboxes + id: self-service attributes: label: Self-service - description: | - If you feel like you could contribute to this issue, please check the box below. This would tell us and other people looking for contributions that someone's working on it. - If you do check this box, please send a pull request within 7 days so we can still delegate this to someone else. options: - - label: I'd be willing to address this documentation request myself. + - label: I would be willing to submit a fix for this bug. - type: checkboxes + id: policies attributes: - label: Have you read the Contributing Guidelines? + label: Project policies options: - - label: I have read the [Contributing Guidelines](https://github.com/z-shell/community/blob/main/docs/CONTRIBUTING_GUIDELINES.md). + - label: I have read the [contribution guidelines](https://github.com/z-shell/.github/blob/main/.github/CONTRIBUTING.md). required: true - - type: checkboxes - attributes: - label: Are you familiar with the Contributor Covenant Code of Conduct? - options: - - label: I have read the [Contributor Covenant Code of Conduct](https://github.com/z-shell/zi/blob/main/docs/CODE_OF_CONDUCT.md). + - label: I agree to follow the [Code of Conduct](https://github.com/z-shell/.github/blob/main/.github/CODE_OF_CONDUCT.md). required: true - - type: input - id: contact - attributes: - label: Contact Details - description: How can we get in touch with you if we need more info? - placeholder: ex. email@example.com - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/02_feature_request.yml b/.github/ISSUE_TEMPLATE/02_feature_request.yml index 84d3586e..aeca2c63 100644 --- a/.github/ISSUE_TEMPLATE/02_feature_request.yml +++ b/.github/ISSUE_TEMPLATE/02_feature_request.yml @@ -1,61 +1,41 @@ name: "💡 Feature request" -description: Suggest an idea for this project +description: Propose a new Zi capability title: "[feat]: " -labels: ["feature-request 💡"] +labels: ["type:feature", "status:triage"] assignees: ["ss-o"] body: - - type: markdown - attributes: - value: | - First off, thanks for taking the time to contribute! Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make will benefit everybody else and are greatly appreciated. - - type: input - id: description + - type: textarea + id: problem attributes: - label: Feature description - description: A clear and concise description of what the feature request is. Please include if your feature request is related to a problem + label: Problem + description: What Zi use case is difficult or impossible today? validations: required: true - type: textarea - id: code-snippet + id: proposal attributes: - label: Related Code - description: "If you can illustrate the bug or feature request with an example, please provide it here" + label: Proposed solution + description: Describe the behavior and show an example Zi command or configuration. validations: - required: false + required: true - type: textarea - id: additional + id: alternatives attributes: - label: Additional Context - description: - "List any other information that is relevant to your issue. Stack traces, related issues, suggestions on how to add, use case, Stack Overflow links, forum links, screenshots, OS if applicable, - etc." + label: Alternatives considered validations: required: false - type: checkboxes + id: self-service attributes: label: Self-service - description: | - If you feel like you could contribute to this issue, please check the box below. This would tell us and other people looking for contributions that someone's working on it. - If you do check this box, please send a pull request within 7 days so we can still delegate this to someone else. options: - - label: I'd be willing to address this documentation request myself. + - label: I would be willing to implement this feature. - type: checkboxes + id: policies attributes: - label: Have you read the Contributing Guidelines? + label: Project policies options: - - label: I have read the [Contributing Guidelines](https://github.com/z-shell/community/blob/main/docs/CONTRIBUTING_GUIDELINES.md). + - label: I have read the [contribution guidelines](https://github.com/z-shell/.github/blob/main/.github/CONTRIBUTING.md). required: true - - type: checkboxes - attributes: - label: Are you familiar with the Contributor Covenant Code of Conduct? - options: - - label: I have read the [Contributor Covenant Code of Conduct](https://github.com/z-shell/zi/blob/main/docs/CODE_OF_CONDUCT.md). + - label: I agree to follow the [Code of Conduct](https://github.com/z-shell/.github/blob/main/.github/CODE_OF_CONDUCT.md). required: true - - type: input - id: contact - attributes: - label: Contact Details - description: How can we get in touch with you if we need more info? - placeholder: ex. email@example.com - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/03_codebase_improvement.yml b/.github/ISSUE_TEMPLATE/03_codebase_improvement.yml index 21d854c1..bf6a21e5 100644 --- a/.github/ISSUE_TEMPLATE/03_codebase_improvement.yml +++ b/.github/ISSUE_TEMPLATE/03_codebase_improvement.yml @@ -1,46 +1,35 @@ name: "✨ Codebase improvement" -description: Suggest a better solution for algorithms, annexes, packages, development tools, etc. -title: "[dev]: " -labels: ["enhancement ✨"] +description: Propose maintenance or an improvement to existing Zi code +title: "[chore]: " +labels: ["type:maintenance", "status:triage"] assignees: ["ss-o"] body: - type: textarea - id: enhancement + id: limitation attributes: - label: "Your feedback for the existing codebase" + label: Current limitation + description: Identify the code, tooling, or workflow that should be improved. validations: - required: false - - type: markdown + required: true + - type: textarea + id: proposal attributes: - value: | - First off, thanks for taking the time to contribute! - Contributions are what make the open-source community such an amazing place to learn, inspire, and create. - Any contributions you make will benefit everybody else and are greatly appreciated. + label: Proposed improvement + description: Explain the change and its expected benefit or trade-offs. + validations: + required: true - type: checkboxes + id: self-service attributes: label: Self-service - description: | - If you feel like you could contribute to this issue, please check the box below. This would tell us and other people looking for contributions that someone's working on it. - If you do check this box, please send a pull request within 7 days so we can still delegate this to someone else. options: - - label: I'd be willing to address this documentation request myself. + - label: I would be willing to implement this improvement. - type: checkboxes + id: policies attributes: - label: Have you read the Contributing Guidelines? + label: Project policies options: - - label: I have read the [Contributing Guidelines](https://github.com/z-shell/community/blob/main/docs/CONTRIBUTING_GUIDELINES.md). + - label: I have read the [contribution guidelines](https://github.com/z-shell/.github/blob/main/.github/CONTRIBUTING.md). required: true - - type: checkboxes - attributes: - label: Are you familiar with the Contributor Covenant Code of Conduct? - options: - - label: I have read the [Contributor Covenant Code of Conduct](https://github.com/z-shell/zi/blob/main/docs/CODE_OF_CONDUCT.md). + - label: I agree to follow the [Code of Conduct](https://github.com/z-shell/.github/blob/main/.github/CODE_OF_CONDUCT.md). required: true - - type: input - id: contact - attributes: - label: Contact Details - description: How can we get in touch with you if we need more info? - placeholder: ex. email@example.com - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/04_documentation.yml b/.github/ISSUE_TEMPLATE/04_documentation.yml index ad9382a5..c4a04596 100644 --- a/.github/ISSUE_TEMPLATE/04_documentation.yml +++ b/.github/ISSUE_TEMPLATE/04_documentation.yml @@ -1,66 +1,48 @@ name: "📚 Documentation" -description: The issue request for changes in documentation +description: Request a correction or improvement to Zi documentation title: "[docs]: " -labels: ["documentation 📝"] +labels: ["type:docs", "area:docs"] assignees: ["ss-o"] body: - - type: markdown - attributes: - value: | - This template is strictly used for documentation requests, including: - - - Elaborating on a particular topic; - - Updating external links; - - Anything else that doesn't require touching the codebase itself. - - If you followed the documentation but things don't work, take some time to consider if it's the documentation or the code that's wrong. In the latter, prefer using the "bug" template. - - You may proceed directly to sending a pull request without filing this issue, and we can improve your work. - If you think some of the requirements above are not met, or if you are not able to contribute yourself, the issue is still welcomed. - - - type: textarea - id: request-info - attributes: - label: Description - description: Tell us, what can be done better or a clear and concise description of what the issue is. - validations: - required: true - type: dropdown - id: ecosystem + id: area attributes: - label: Select the area that is associated with this issue. - multiple: true + label: Documentation area options: - - ZI (this repository) + - Zi repository + - Wiki - Annexes - Plugins - Packages - Other + validations: + required: true + - type: input + id: location + attributes: + label: Location + description: Link to the page or identify the file and section. + validations: + required: true + - type: textarea + id: request + attributes: + label: Requested change + description: Explain what is missing, unclear, or incorrect and suggest replacement wording if possible. + validations: + required: true - type: checkboxes + id: self-service attributes: label: Self-service - description: | - If you feel like you could contribute to this issue, please check the box below. This would tell us and other people looking for contributions that someone's working on it. - If you do check this box, please send a pull request within 7 days so we can still delegate this to someone else. options: - - label: I'd be willing to address this documentation request myself. + - label: I would be willing to update this documentation. - type: checkboxes + id: policies attributes: - label: Have you read the Contributing Guidelines? + label: Project policies options: - - label: I have read the [Contributing Guidelines](https://github.com/z-shell/community/blob/main/docs/CONTRIBUTING_GUIDELINES.md). + - label: I have read the [contribution guidelines](https://github.com/z-shell/.github/blob/main/.github/CONTRIBUTING.md). required: true - - type: checkboxes - attributes: - label: Are you familiar with the Contributor Covenant Code of Conduct? - options: - - label: I have read the [Contributor Covenant Code of Conduct](https://github.com/z-shell/zi/blob/main/docs/CODE_OF_CONDUCT.md). + - label: I agree to follow the [Code of Conduct](https://github.com/z-shell/.github/blob/main/.github/CODE_OF_CONDUCT.md). required: true - - type: input - id: contact - attributes: - label: Contact Details - description: How can we get in touch with you if we need more info? - placeholder: ex. email@example.com - validations: - required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fb621f95..46259f7f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,10 +8,10 @@ Commit messages must follow Conventional Commits: type(scope): short description (≤72 chars, imperative mood) - Types: feat fix perf refactor docs test ci chore revert + Types: feat fix perf refactor docs test build ci style chore revert Example: fix(self-update): correctly propagate exit code on failure - See .github/copilot-instructions.md for full guidelines. + See AGENTS.md for the repository contribution guidelines. --> ## Description @@ -32,14 +32,18 @@ - [ ] `perf` — performance improvement - [ ] `refactor` — code change with no functional impact - [ ] `docs` — documentation only +- [ ] `test` — test addition or correction +- [ ] `build` — build system or dependency change - [ ] `ci` — CI/workflow change +- [ ] `style` — formatting with no behavior change - [ ] `chore` — maintenance / dependency bump +- [ ] `revert` — revert of an earlier change ## Checklist - [ ] Ordinary work targets `next`; only same-repository hotfixes target `main` - [ ] Commit messages follow Conventional Commits format -- [ ] No AI co-author trailers in commit messages -- [ ] I have read [CONTRIBUTING.md](docs/CONTRIBUTING.md) +- [ ] No `Co-authored-by` trailers in commit messages +- [ ] I have read the [contribution guidelines](https://github.com/z-shell/.github/blob/main/.github/CONTRIBUTING.md) - [ ] Existing tests pass (`zsh -n zi.zsh` / Trunk checks) - [ ] Documentation updated if needed diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 66f4e3df..20b0f37e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,8 +21,8 @@ jobs: name: Analyze (${{ matrix.language }}) runs-on: ubuntu-latest permissions: + contents: read security-events: write - packages: read strategy: fail-fast: false matrix: @@ -31,15 +31,15 @@ jobs: build-mode: none steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Initialize CodeQL - uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml index 8b1ec8b8..1ff1d2d5 100644 --- a/.github/workflows/commit-lint.yml +++ b/.github/workflows/commit-lint.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "⤵️ Check out code from GitHub" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 @@ -34,7 +34,7 @@ jobs: run: | set -euo pipefail - CONVENTIONAL_PATTERN='^(feat|fix|perf|refactor|docs|test|ci|chore|revert)(\([^)]+\))?!?: .{1,72}$' + CONVENTIONAL_PATTERN='^(feat|fix|perf|refactor|docs|test|build|ci|style|chore|revert)(\([^)]+\))?!?: .{1,72}$' errors=0 checked=0 @@ -72,7 +72,7 @@ jobs: env: PR_TITLE: ${{ github.event.pull_request.title }} run: | - PATTERN='^(feat|fix|perf|refactor|docs|test|ci|chore|revert)(\([^)]+\))?!?: .{1,72}$' + PATTERN='^(feat|fix|perf|refactor|docs|test|build|ci|style|chore|revert)(\([^)]+\))?!?: .{1,72}$' if ! echo "$PR_TITLE" | grep -qE "$PATTERN"; then echo "::error::PR title must follow Conventional Commits: type(scope): description" exit 1 diff --git a/.github/workflows/trunk-check.yml b/.github/workflows/trunk-check.yml index 69b74740..13bac2b3 100644 --- a/.github/workflows/trunk-check.yml +++ b/.github/workflows/trunk-check.yml @@ -5,14 +5,14 @@ on: branches: [main, next] tags: ["v*.*.*"] pull_request: - types: [opened, synchronize] + types: [opened, reopened, synchronize] workflow_dispatch: {} concurrency: - group: ${{ github.head_ref || github.run_id }} + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} cancel-in-progress: true -permissions: read-all +permissions: {} jobs: check: @@ -21,7 +21,9 @@ jobs: checks: write # For trunk to post annotations contents: read # For repo checkout steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Trunk Code Quality - uses: trunk-io/trunk-action@75699af9e26881e564e9d832ef7dc3af25ec031b + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Run Trunk checks + uses: trunk-io/trunk-action@04ba50e7658c81db7356da96657e6e77f220bfa3 # v1.3.1 + with: + check-mode: all diff --git a/.github/workflows/zsh-n.yml b/.github/workflows/zsh-n.yml index dd1420b7..92c87373 100644 --- a/.github/workflows/zsh-n.yml +++ b/.github/workflows/zsh-n.yml @@ -13,12 +13,6 @@ on: paths: - "zi.zsh" - "lib/**" - workflow_run: - workflows: - - "⭕ Trunk" - types: - - completed - branches: [main] workflow_dispatch: {} concurrency: @@ -35,13 +29,13 @@ jobs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - name: ⤵️ Check out code from GitHub - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: "Set matrix output" id: set-matrix run: | - MATRIX="$(find . -type d -name 'doc' -prune -o -type f \( -iname '*.zsh' -o -iname '_zi' \) -print | jq -ncR '{"include": [{"file": inputs}]}')" + MATRIX="$(find . -type d -name 'doc' -prune -o -type f \( -iname '*.zsh' -o -iname '_zi' -o -path './lib/zcompile' -o -path './lib/templates/example-script' \) -print | jq -ncR '{"include": [{"file": inputs}]}')" echo "MATRIX=${MATRIX}" >&2 - echo "matrix=${MATRIX}" >> $GITHUB_OUTPUT + echo "matrix=${MATRIX}" >> "$GITHUB_OUTPUT" zsh-n: runs-on: ubuntu-latest @@ -51,7 +45,7 @@ jobs: matrix: ${{ fromJSON(needs.zsh-matrix.outputs.matrix) }} steps: - name: ⤵️ Check out code from GitHub - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: "⚡ Install dependencies" run: sudo apt update && sudo apt-get install -yq zsh - name: "⚡ zsh -n: ${{ matrix.file }}" diff --git a/.gitignore b/.gitignore index f18a949a..afcfe656 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,21 @@ -# VSCODE workspace -.vscode/* -.vscode/settings.json -.vscode/tasks.json -.vscode/launch.json -.vscode/extensions.json +# Local editor files +.vscode/ *.code-workspace +*~ +.*.sw? +.#* -# Exclude for security reasons +# Local configuration and history +.env .history/ .dccache -.env +.directory_history +.zcalc_history -# Zsh compiled script + zrecompile backup +# Zi and Zsh generated files *.zwc *.zwc.old - -# Zsh completion-optimization dumpfile *zcompdump* - -# Zsh zcalc history -.zcalc_history - -# A popular plugin manager's files ._zi ._zinit ._zplugin @@ -29,102 +23,7 @@ .zinit_lastupd .zplugin_lstupd -# z-shell/zshelldoc tool's files -zsdoc/data -docs/zsdoc/data - -# ohmyzsh/ohmyzsh/plugins/per-directory-history plugin's files -# (when set-up to store the history in the local directory) -.directory_history - -# MichaelAquilina/zsh-autoswitch-virtualenv plugin's files -# (for Zsh plugins using Python) -.venv - -# Zunit tests' output -/tests/_output/* -!/tests/_output/.gitkeep - -### C -# Prerequisites -*.d - -# Object files -*.o -*.ko -*.obj -*.elf - -# Linker output -*.ilk -*.map -*.exp - -# Precompiled Headers -*.gch -*.pch - -# Libraries -*.lib -*.a -*.la -*.lo - -# Shared objects (inc. Windows DLLs) -*.dll -*.so -*.so.* -*.dylib - -# Executables -*.exe -*.out -*.app -*.i*86 -*.x86_64 -*.hex - -# Debug files -*.dSYM/ -*.su -*.idb -*.pdb - -# Kernel Module Compile Results -*.mod* -*.cmd -.tmp_versions/ -modules.order -Module.symvers -Mkfile.old -dkms.conf - -# Repository specific files -test/ -txt/ -*.txt -*.zwc -*.zini -*lib/zsh/*zwc -.ycm_extra_conf.py -*deploy*key* -*.bundle -site*/ -other -TODO* -tags -TAGS -*.o -*.o.c -*.orig -*.a -*.so -*.dll -*~ -.*.sw? -\#* - -CVS -.#* - -.trunk/out* \ No newline at end of file +# Development tool output +/tests/_output/ +zsdoc/data/ +docs/zsdoc/data/ diff --git a/.trunk/config/.markdownlint.yaml b/.trunk/config/.markdownlint.yaml index 80110bca..c59dc43e 100644 --- a/.trunk/config/.markdownlint.yaml +++ b/.trunk/config/.markdownlint.yaml @@ -1,12 +1,4 @@ -# Autoformatter friendly markdownlint config (all formatting rules disabled) default: true -blank_lines: false -bullet: false -html: false -indentation: false -line_length: false -spaces: false -url: false -whitespace: false +MD013: false +MD033: false MD041: false -MD026: false diff --git a/.trunk/config/.yamllint.yaml b/.trunk/config/.yamllint.yaml deleted file mode 100644 index 184e251f..00000000 --- a/.trunk/config/.yamllint.yaml +++ /dev/null @@ -1,7 +0,0 @@ -rules: - quoted-strings: - required: only-when-needed - extra-allowed: ["{|}"] - key-duplicates: {} - octal-values: - forbid-implicit-octal: true diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index ac0395b4..f9db7284 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -1,34 +1,27 @@ version: 0.1 cli: - version: 1.24.0 + version: 1.25.0 plugins: sources: - id: trunk - ref: v1.7.0 + ref: v1.11.0 uri: https://github.com/trunk-io/plugins lint: - disabled: - - yamllint enabled: - - checkov@3.2.441 - oxipng@9.1.5 - - trufflehog@3.89.1 - - actionlint@1.7.7 - - svgo@3.3.2 - - gitleaks@8.27.2 + - trufflehog@3.90.13 + - actionlint@1.7.8 + - svgo@4.0.0 - markdownlint@0.45.0 - git-diff-check - - prettier@3.5.3 - - shellcheck@0.10.0 - - shfmt@3.7.0 + - prettier@3.6.2 + - shellcheck@0.11.0 + - shfmt@3.13.1 runtimes: enabled: - python@3.10.8 - - go@1.21.0 - node@22.16.0 actions: enabled: - - trunk-announce - trunk-check-pre-push - trunk-fmt-pre-commit - - trunk-upgrade-available diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index c11e4b06..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "conventionalCommits.scopes": [ - "maintenance", - "library", - "feature", - "refactor", - "style", - "chore", - "revert", - "docs", - "deps", - "misc", - "build", - "perf", - "test", - "fix", - "ci" - ], - "shellformat.useEditorConfig": true, - "todo-tree.general.tags": [ - "BUG", - "HACK", - "FIXME", - "TODO", - "NOTE", - "[ ]", - "[x]" - ], - "todo-tree.highlights.customHighlight": { - "TODO": { - "icon": "tools", - "type": "line", - "fontWeight": "bold", - "iconColour": "#2fca2f", - "background": "#00ff00", - "opacity": 0.1 - }, - "BUG": { - "icon": "bug", - "type": "line", - "fontWeight": "bold", - "iconColour": "#ff0000", - "background": "#ff0000", - "opacity": 0.2 - }, - "HACK": { - "icon": "tools" - }, - "FIXME": { - "icon": "flame", - "type": "line", - "fontWeight": "bold", - "iconColour": "#ffff00", - "background": "#ffff00", - "opacity": 0.1 - }, - "NOTE": { - "icon": "info", - "type": "line", - "fontWeight": "bold", - "iconColour": "#0077ff", - "background": "#0077ff", - "opacity": 0.1 - } - }, - "files.associations": { - "*za-*": "shellscript" - }, - "shellcheck.ignorePatterns": { - "**/*.xonshrc": true, - "**/*.xsh": true, - "**/*.zsh": true, - "**/*.zshrc": true, - "**/zshrc": true, - "**/*.zprofile": true, - "**/zprofile": true, - "**/*.zlogin": true, - "**/zlogin": true, - "**/*.zlogout": true, - "**/zlogout": true, - "**/*.zshenv": true, - "**/zshenv": true, - "**/*.zsh-theme": true, - "**/*za-*": true - } -} diff --git a/docs/README.md b/docs/README.md index f2713ca8..15af91fe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,7 +34,7 @@

Zi Matrix Zi Gitter Matrix - + Crowdin localization status Version Project License VIM @@ -47,15 +47,15 @@

From 795cc9f43d22b76f3e564d7f451271383fd2b4e3 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 15 Aug 2026 12:13:59 +0100 Subject: [PATCH 23/44] docs: correct promotion bootstrap branch --- .github/PULL_REQUEST_TEMPLATE/promotion.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE/promotion.md b/.github/PULL_REQUEST_TEMPLATE/promotion.md index 5a95226f..e6b92ac9 100644 --- a/.github/PULL_REQUEST_TEMPLATE/promotion.md +++ b/.github/PULL_REQUEST_TEMPLATE/promotion.md @@ -82,7 +82,7 @@ file only into `next` cannot activate it for a `next` to `main` PR. After the implementation and both ZD dependencies are merged: -1. Create `hotfix-377-promotion-bootstrap` from `main`. +1. Create `hotfix-377` from `main`. 2. Copy the reviewed promotion workflow and templates from `next` without other pending `next` changes. 3. Open the same-repository hotfix PR into protected `main`. The existing exact From 5069bfad5c2c9d0e10d36d7050ddb0319581dd5e Mon Sep 17 00:00:00 2001 From: Sal <59910950+ss-o@users.noreply.github.com> Date: Mon, 17 Aug 2026 13:15:23 +0100 Subject: [PATCH 24/44] fix(module): support CMake build in .zi-module with autotools fallback (#389) Closes #385 ## Summary - Add CMake build detection and support in `.zi-module()` (`lib/zsh/autoload.zsh`) - Prefer `scripts/cmake.configure.zsh` if present, falling back to standard `cmake` invocations - Use nullglob qualifiers `(N)` when discovering built shared objects to prevent unbound glob errors - Maintain legacy `./configure && make` fallback for classic autotools repositories - Clean CMake caches on `.zi-module -r` / `--reset` - Align DTRACE report append fallback condition in `zi.zsh` ## Verification - `zsh -n zi.zsh` and `zsh -n lib/zsh/autoload.zsh` pass syntax verification - End-to-end `.zi-module --build` verified against both `zpmod:main` (legacy autotools) and `zpmod:develop` (modern CMake) - Built module successfully loaded via `zmodload zi/zpmod` Co-authored-by: Sal --- lib/zsh/autoload.zsh | 106 +++++++++++++++++++++++++++++++------------ zi.zsh | 2 +- 2 files changed, 78 insertions(+), 30 deletions(-) diff --git a/lib/zsh/autoload.zsh b/lib/zsh/autoload.zsh index 0927e812..76928ffe 100755 --- a/lib/zsh/autoload.zsh +++ b/lib/zsh/autoload.zsh @@ -2973,8 +2973,11 @@ EOF +zi-message "{warn}Warning{warn}{ehi}:{rst} {lhi}recompilation{rst} of the {bcmd}zpmod{rst} module has been requested…" (( ${+functions[.zi-confirm]} )) || builtin source "${ZI[BIN_DIR]}/lib/zsh/autoload.zsh" || return 1 .zi-confirm "Do you want to proceed?" || return 1 - command make -C "${ZI[ZMODULES_DIR]}/zpmod" distclean &>/dev/null - .zi-module --build + if [[ -f "${ZI[ZMODULES_DIR]}/zpmod/Makefile" ]]; then + command make -C "${ZI[ZMODULES_DIR]}/zpmod" distclean &>/dev/null + fi + command rm -rf "${ZI[ZMODULES_DIR]}/zpmod/build-cmake" "${ZI[ZMODULES_DIR]}/zpmod/build" 2>/dev/null + .zi-module --build --clean fi elif [[ "$1" = (-B|--build|build) ]]; then builtin autoload -Uz is-at-least @@ -2986,7 +2989,7 @@ EOF +zi-message "{error}-- Failed to update module repository --{rst}"; return 1 } if [[ "$2" = "--clean" ]]; then - +zi-message "{p}-- Module {ok}source is clean{p}, {cmd}make distclean{p} not required --{rst}" + +zi-message "{p}-- Module {ok}source is clean{p}, {cmd}clean{p} not required --{rst}" fi else if ! test -d "${${ZI[ZMODULES_DIR]}}/zpmod"; then @@ -2999,39 +3002,84 @@ EOF fi ( builtin cd -q "${ZI[ZMODULES_DIR]}/zpmod" +zi-message "{p}-- The module sources are located at{ehi}:{rst} {dir}"${ZI[ZMODULES_DIR]}/zpmod"{p} --{rst}" - if [[ -f Makefile ]]; then - if [[ "$2" = "--clean" ]]; then - +zi-message "{p}-- Building module {bcmd}zi/zpmod{p}, running: {cmd}make distclean{p}, then {cmd}./configure{p} and then {cmd}make{p} --{rst}" - +zi-message "{p}-- make distclean --{rst}" - command make distclean - ((1)) - else - +zi-message "{p}-- Building module {bcmd}zi/zpmod{p}, running: {cmd}make clean{p}, then {cmd}./configure{p} and then {cmd}make{p} --{rst}" - +zi-message "{p}-- make clean --{rst}" - command make clean - fi - fi - +zi-message "{p}-- ./configure --{rst}" - ./configure --enable-cflags='-g -Wall -Wextra -O3' --disable-gdbm --without-tcsetpgrp --quiet - +zi-message "{p}-- make --{rst}" - local cores=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || command getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) local build_log="${ZI[LOG_DIR]}/zpmod/${EPOCHSECONDS}-build.log" command mkdir -p "${ZI[LOG_DIR]}/zpmod" 2>/dev/null - command make --jobs=$cores -C "${ZI[ZMODULES_DIR]}/zpmod" 2>&1 | command tee "$build_log" >/dev/null - if (( pipestatus[1] == 0 )); then - [[ -f Src/zi/zpmod.so ]] && command cp -vf Src/zi/zpmod.so Src/zi/zpmod.bundle - builtin print "$EPOCHSECONDS" >! "${ZI[ZMODULES_DIR]}/zpmod/COMPILED_AT" - +zi-message "{ok}-- Build successful! --{rst}" - +zi-message "{faint}-- Build log{ehi}:{rst} {file}$build_log{faint} --{rst}" - .zi-module --info - return 0 - else + local cores=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || command getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) + + if [[ -f CMakeLists.txt ]]; then + +zi-message "{p}-- Building module {bcmd}zi/zpmod{p} with {cmd}CMake{p} --{rst}" + local build_rc=0 + if [[ -x scripts/cmake.configure.zsh ]]; then + local -a cmake_opts + cmake_opts=( --generator make --build-type Release -j "$cores" ) + [[ "$2" = "--clean" ]] && cmake_opts+=( --clean ) + ./scripts/cmake.configure.zsh "${cmake_opts[@]}" 2>&1 | command tee "$build_log" >/dev/null + build_rc=${pipestatus[1]} + else + [[ "$2" = "--clean" ]] && command rm -rf build-cmake + command cmake -S . -B build-cmake -DCMAKE_BUILD_TYPE=Release 2>&1 | command tee "$build_log" >/dev/null && \ + command cmake --build build-cmake -j "$cores" 2>&1 | command tee -a "$build_log" >/dev/null && \ + command cmake --install build-cmake --prefix build-cmake/stage 2>&1 | command tee -a "$build_log" >/dev/null + build_rc=${pipestatus[1]} + fi + if (( build_rc == 0 )); then + local f="" staged_file="" + for f in build-cmake/stage/lib/zsh/site-modules/zpmod.*(N) build-cmake/out/lib/zpmod.*(N) build/out/lib/zpmod.*(N) Src/zi/zpmod.so(N); do + [[ -f $f ]] && { staged_file="$f"; break; } + done + if [[ -n $staged_file ]]; then + command mkdir -p Src/zi 2>/dev/null + command cp -vf "$staged_file" Src/zi/zpmod.so + command cp -vf "$staged_file" Src/zi/zpmod.bundle + command cp -vf "$staged_file" zpmod.so + builtin print "$EPOCHSECONDS" >! "${ZI[ZMODULES_DIR]}/zpmod/COMPILED_AT" + +zi-message "{ok}-- Build successful! --{rst}" + +zi-message "{faint}-- Build log{ehi}:{rst} {file}$build_log{faint} --{rst}" + .zi-module --info + return 0 + fi + fi +zi-message "{error}Build failed{rst}…" \ "To report an issue at GitHub, please provide error messages from{ehi}:{rst}{nl}" \ "{ndsh} Build log{rst} {dir}${build_log}{rst} if exists{nl}" \ - "{ndsh} Run '{cmd}make -C ${ZI[ZMODULES_DIR]}/zpmod{rst}' to reproduce{rst}{nl}" \ + "{ndsh} Run '{cmd}cmake --build build-cmake{rst}' to reproduce{rst}{nl}" \ "{ndsh}{rst} {var}URL{rst} to submit{rst} {url}https://github.com/z-shell/zpmod/issues{rst}" return 1 + elif [[ -f Makefile || -f configure || -f configure.ac ]]; then + if [[ -f Makefile ]]; then + if [[ "$2" = "--clean" ]]; then + +zi-message "{p}-- Building module {bcmd}zi/zpmod{p}, running: {cmd}make distclean{p}, then {cmd}./configure{p} and then {cmd}make{p} --{rst}" + +zi-message "{p}-- make distclean --{rst}" + command make distclean + ((1)) + else + +zi-message "{p}-- Building module {bcmd}zi/zpmod{p}, running: {cmd}make clean{p}, then {cmd}./configure{p} and then {cmd}make{p} --{rst}" + +zi-message "{p}-- make clean --{rst}" + command make clean + fi + fi + +zi-message "{p}-- ./configure --{rst}" + ./configure --enable-cflags='-g -Wall -Wextra -O3' --disable-gdbm --without-tcsetpgrp --quiet + +zi-message "{p}-- make --{rst}" + command make --jobs=$cores -C "${ZI[ZMODULES_DIR]}/zpmod" 2>&1 | command tee "$build_log" >/dev/null + if (( pipestatus[1] == 0 )); then + [[ -f Src/zi/zpmod.so ]] && command cp -vf Src/zi/zpmod.so Src/zi/zpmod.bundle + builtin print "$EPOCHSECONDS" >! "${ZI[ZMODULES_DIR]}/zpmod/COMPILED_AT" + +zi-message "{ok}-- Build successful! --{rst}" + +zi-message "{faint}-- Build log{ehi}:{rst} {file}$build_log{faint} --{rst}" + .zi-module --info + return 0 + else + +zi-message "{error}Build failed{rst}…" \ + "To report an issue at GitHub, please provide error messages from{ehi}:{rst}{nl}" \ + "{ndsh} Build log{rst} {dir}${build_log}{rst} if exists{nl}" \ + "{ndsh} Run '{cmd}make -C ${ZI[ZMODULES_DIR]}/zpmod{rst}' to reproduce{rst}{nl}" \ + "{ndsh}{rst} {var}URL{rst} to submit{rst} {url}https://github.com/z-shell/zpmod/issues{rst}" + return 1 + fi + else + +zi-message "{error}No supported build system found in ${ZI[ZMODULES_DIR]}/zpmod (expected CMakeLists.txt or configure){rst}" + return 1 fi ) else diff --git a/zi.zsh b/zi.zsh index e6f7e23c..acfd50f7 100644 --- a/zi.zsh +++ b/zi.zsh @@ -1690,7 +1690,7 @@ builtin setopt noaliases .zi-add-report() { # Use zi binary module if available. [[ -n $1 ]] && { (( ${+builtins[zpmod]} && 0 )) && zpmod report-append "$1" "$2"$'\n' || ZI_REPORTS[$1]+="$2"$'\n'; } - [[ ${ZI[DTRACE]} = 1 ]] && { (( ${+builtins[zpmod]} )) && zpmod report-append _dtrace/_dtrace "$2"$'\n' || ZI_REPORTS[_dtrace/_dtrace]+="$2"$'\n'; } + [[ ${ZI[DTRACE]} = 1 ]] && { (( ${+builtins[zpmod]} && 0 )) && zpmod report-append _dtrace/_dtrace "$2"$'\n' || ZI_REPORTS[_dtrace/_dtrace]+="$2"$'\n'; } return 0 } # ]]] # FUNCTION: .zi-add-fpath. [[[ From 4814669d67efc033f320f6c2c272506563dcbd35 Mon Sep 17 00:00:00 2001 From: Sal <59910950+ss-o@users.noreply.github.com> Date: Mon, 17 Aug 2026 13:41:55 +0100 Subject: [PATCH 25/44] feat(module): fetch prebuilt release binaries before building from source (#390) ## Summary - Adds prebuilt release binary package downloading to `.zi-module()` (`lib/zsh/autoload.zsh`) - Checks GitHub releases for CPack `.tar.gz` packages matching current OS and machine architecture - Unpacks and stages prebuilt binary module directly into `Src/zi/zpmod.so` - Falls back seamlessly to CMake/autotools build when no prebuilt package is found - Supports `--from-source` flag to bypass prebuilt binary checking ## Verification - `zsh -n lib/zsh/autoload.zsh` passes syntax checks - Verified fallback paths and prebuilt extraction flow Co-authored-by: Sal --- lib/zsh/autoload.zsh | 56 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/lib/zsh/autoload.zsh b/lib/zsh/autoload.zsh index 76928ffe..d95b95df 100755 --- a/lib/zsh/autoload.zsh +++ b/lib/zsh/autoload.zsh @@ -3006,6 +3006,62 @@ EOF command mkdir -p "${ZI[LOG_DIR]}/zpmod" 2>/dev/null local cores=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || command getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) + local fetch_prebuilt=1 + [[ "$2" = "--from-source" || "$2" = "--source" || "$3" = "--from-source" || "$3" = "--source" ]] && fetch_prebuilt=0 + + if (( fetch_prebuilt )); then + local os_name="$(uname -s)" + local arch_name="$(uname -m)" + local dl_url="" + if (( ${+commands[curl]} || ${+commands[wget]} )); then + local release_json="" + if (( ${+commands[curl]} )); then + release_json="$(command curl -fsSL -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/z-shell/zpmod/releases/latest" 2>/dev/null)" + elif (( ${+commands[wget]} )); then + release_json="$(command wget -qO- "https://api.github.com/repos/z-shell/zpmod/releases/latest" 2>/dev/null)" + fi + if [[ -n "$release_json" ]]; then + local match_pattern="browser_download_url\": \"([^\"]*zpmod[^\"]*${os_name}[^\"]*${arch_name}[^\"]*\\.tar\\.gz)\"" + if [[ "$release_json" =~ $match_pattern ]]; then + dl_url="${match[1]}" + fi + fi + + if [[ -n "$dl_url" ]]; then + +zi-message "{p}-- Downloading prebuilt package{ehi}:{rst} {url}$dl_url{rst}" + local tmp_pkg="${ZI[LOG_DIR]}/zpmod/prebuilt-${EPOCHSECONDS}.tar.gz" + local dl_rc=1 + if (( ${+commands[curl]} )); then + command curl -fsSL "$dl_url" -o "$tmp_pkg" 2>/dev/null && dl_rc=0 + elif (( ${+commands[wget]} )); then + command wget -q "$dl_url" -O "$tmp_pkg" 2>/dev/null && dl_rc=0 + fi + if (( dl_rc == 0 )) && [[ -f "$tmp_pkg" ]]; then + local tmp_extract="${ZI[LOG_DIR]}/zpmod/extract-${EPOCHSECONDS}" + command mkdir -p "$tmp_extract" 2>/dev/null + if command tar -xzf "$tmp_pkg" -C "$tmp_extract" 2>/dev/null; then + local f="" staged_file="" + for f in "$tmp_extract"/lib/zsh/site-modules/zpmod.*(N) "$tmp_extract"/**/zpmod.so(N) "$tmp_extract"/**/zpmod.bundle(N); do + [[ -f $f ]] && { staged_file="$f"; break; } + done + if [[ -n "$staged_file" ]]; then + command mkdir -p Src/zi 2>/dev/null + command cp -vf "$staged_file" Src/zi/zpmod.so + command cp -vf "$staged_file" Src/zi/zpmod.bundle + command cp -vf "$staged_file" zpmod.so + builtin print "$EPOCHSECONDS" >! "${ZI[ZMODULES_DIR]}/zpmod/COMPILED_AT" + command rm -rf "$tmp_extract" "$tmp_pkg" 2>/dev/null || true + +zi-message "{ok}-- Prebuilt binary installed successfully! --{rst}" + .zi-module --info + return 0 + fi + fi + command rm -rf "$tmp_extract" "$tmp_pkg" 2>/dev/null || true + fi + fi + fi + fi + if [[ -f CMakeLists.txt ]]; then +zi-message "{p}-- Building module {bcmd}zi/zpmod{p} with {cmd}CMake{p} --{rst}" local build_rc=0 From 7a0376378686a547b7bb8ee481ab4a3d681ff8f7 Mon Sep 17 00:00:00 2001 From: Sal <59910950+ss-o@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:04:44 +0100 Subject: [PATCH 26/44] fix(module): query recent releases to discover release candidate assets (#391) ## Summary - Updates GitHub releases endpoint query to fetch `/releases?per_page=5` with fallback to `/releases/latest` - Enables discovering release candidate assets (e.g. `v2.0.1-rc3`) as well as final releases ## Verification - Verified end-to-end download, extraction, and staging of live `v2.0.1-rc3` prebuilt binary package on Linux Co-authored-by: Sal --- lib/zsh/autoload.zsh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/zsh/autoload.zsh b/lib/zsh/autoload.zsh index d95b95df..e562b53d 100755 --- a/lib/zsh/autoload.zsh +++ b/lib/zsh/autoload.zsh @@ -3016,9 +3016,11 @@ EOF if (( ${+commands[curl]} || ${+commands[wget]} )); then local release_json="" if (( ${+commands[curl]} )); then - release_json="$(command curl -fsSL -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/z-shell/zpmod/releases/latest" 2>/dev/null)" + release_json="$(command curl -fsSL -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/z-shell/zpmod/releases?per_page=5" 2>/dev/null)" + [[ -z "$release_json" ]] && release_json="$(command curl -fsSL -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/z-shell/zpmod/releases/latest" 2>/dev/null)" elif (( ${+commands[wget]} )); then - release_json="$(command wget -qO- "https://api.github.com/repos/z-shell/zpmod/releases/latest" 2>/dev/null)" + release_json="$(command wget -qO- "https://api.github.com/repos/z-shell/zpmod/releases?per_page=5" 2>/dev/null)" + [[ -z "$release_json" ]] && release_json="$(command wget -qO- "https://api.github.com/repos/z-shell/zpmod/releases/latest" 2>/dev/null)" fi if [[ -n "$release_json" ]]; then local match_pattern="browser_download_url\": \"([^\"]*zpmod[^\"]*${os_name}[^\"]*${arch_name}[^\"]*\\.tar\\.gz)\"" From 5dabe4633667c45615776c0022c83f19fa92bf8b Mon Sep 17 00:00:00 2001 From: Sal <59910950+ss-o@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:13:28 +0100 Subject: [PATCH 27/44] fix(pmspec): drop the unallocated X capability letter (#394) Co-authored-by: Sal --- zi.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zi.zsh b/zi.zsh index acfd50f7..093e787a 100644 --- a/zi.zsh +++ b/zi.zsh @@ -13,7 +13,7 @@ typeset -gAH ZI ZI_SNIPPETS ZI_REPORTS ZI_ICES ZI_SICE ZI_CUR_BIND_MAP ZI_EXTS Z typeset -gaH ZI_COMPDEF_REPLAY # The specified names are marked for automatic export to the environment of subsequently executed commands. -typeset -gx ZPFX XDG_ZI_HOME XDG_ZI_CACHE XDG_ZI_CONFIG PMSPEC=0fuUpiPsX +typeset -gx ZPFX XDG_ZI_HOME XDG_ZI_CACHE XDG_ZI_CONFIG PMSPEC=0fuUpiPs # Compatibility for previous versions. typeset -gAH ZINIT From 3e9f6c361e3f99cbb334ae2707d1318b839fe133 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 22 Aug 2026 12:57:05 +0100 Subject: [PATCH 28/44] ci: pin promotion validation to compatibility suite --- .github/workflows/promotion-readiness.yml | 2 +- .github/workflows/zd-integration.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/promotion-readiness.yml b/.github/workflows/promotion-readiness.yml index 522f2aca..ffdb9e73 100644 --- a/.github/workflows/promotion-readiness.yml +++ b/.github/workflows/promotion-readiness.yml @@ -47,7 +47,7 @@ jobs: zd: name: ZD integration - uses: z-shell/zd/.github/workflows/test-native.yml@da915fd260112430aa34981aed3d4e0262f33b87 # z-shell/zd#95 + uses: z-shell/zd/.github/workflows/test-native.yml@191642eaf9587e64f910c79aaf750d2cfe237241 # z-shell/zd#95, #96 with: zi_repo: ${{ github.event.pull_request.head.repo.full_name }} zi_ref: ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/zd-integration.yml b/.github/workflows/zd-integration.yml index ad066c4b..1685af9a 100644 --- a/.github/workflows/zd-integration.yml +++ b/.github/workflows/zd-integration.yml @@ -22,7 +22,7 @@ permissions: jobs: zd-test: - uses: z-shell/zd/.github/workflows/test-native.yml@da915fd260112430aa34981aed3d4e0262f33b87 # z-shell/zd#95 + uses: z-shell/zd/.github/workflows/test-native.yml@191642eaf9587e64f910c79aaf750d2cfe237241 # z-shell/zd#95, #96 with: zi_repo: ${{ github.event.pull_request.head.repo.full_name || github.repository }} zi_ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} From f175851027b687135aba7d7083158aad8b7401de Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 22 Aug 2026 13:09:02 +0100 Subject: [PATCH 29/44] fix(ci): harden live promotion gate --- .github/workflows/promotion-readiness.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/promotion-readiness.yml b/.github/workflows/promotion-readiness.yml index ffdb9e73..39c18336 100644 --- a/.github/workflows/promotion-readiness.yml +++ b/.github/workflows/promotion-readiness.yml @@ -47,10 +47,11 @@ jobs: zd: name: ZD integration - uses: z-shell/zd/.github/workflows/test-native.yml@191642eaf9587e64f910c79aaf750d2cfe237241 # z-shell/zd#95, #96 + uses: z-shell/zd/.github/workflows/test-native.yml@fd0995d6f7958fea6c4abbc4fff81a6da2277b66 # z-shell/zd#95, #96, #107 with: zi_repo: ${{ github.event.pull_request.head.repo.full_name }} zi_ref: ${{ github.event.pull_request.head.sha }} + include_compat: ${{ github.head_ref == 'next' }} trunk: name: Trunk @@ -62,7 +63,7 @@ jobs: - name: Check out the candidate uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: - ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 - name: Run Trunk uses: trunk-io/trunk-action@75699af9e26881e564e9d832ef7dc3af25ec031b @@ -139,7 +140,6 @@ jobs: (( ${+functions[zi]} )) || exit 1 .zi-prepare-home || exit 1 [[ -d ${ZI[HOME_DIR]} ]] || exit 1 - zi -V >/dev/null || exit 1 ZSH gate: From 00fe70cb0fadee8ba74a76cdac0aadf5ce34d3da Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 15 Aug 2026 12:30:10 +0100 Subject: [PATCH 30/44] ci(contract): add public impact gate --- .github/CODEOWNERS | 2 + .github/PULL_REQUEST_TEMPLATE.md | 12 + .github/workflows/public-contract-impact.yml | 54 ++ contracts/public-contract-v1.json | 204 ++++++ scripts/public-contract-impact.zsh | 588 ++++++++++++++++++ .../fixtures/public-contract/addition/zi.zsh | 26 + .../base/lib/zsh/git-process-output.zsh | 3 + tests/fixtures/public-contract/base/zi.zsh | 25 + .../fixtures/public-contract/breaking/zi.zsh | 24 + .../fixtures/public-contract/refactor/zi.zsh | 37 ++ tests/public-contract-impact.zsh | 179 ++++++ 11 files changed, 1154 insertions(+) create mode 100644 .github/workflows/public-contract-impact.yml create mode 100644 contracts/public-contract-v1.json create mode 100644 scripts/public-contract-impact.zsh create mode 100644 tests/fixtures/public-contract/addition/zi.zsh create mode 100644 tests/fixtures/public-contract/base/lib/zsh/git-process-output.zsh create mode 100644 tests/fixtures/public-contract/base/zi.zsh create mode 100644 tests/fixtures/public-contract/breaking/zi.zsh create mode 100644 tests/fixtures/public-contract/refactor/zi.zsh create mode 100644 tests/public-contract-impact.zsh diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0a9a5b36..83720e37 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,6 +15,8 @@ # Changes here affect every Zi user; require TSC review. /zi.zsh @z-shell/tsc /lib/ @z-shell/tsc +/contracts/ @z-shell/tsc +/scripts/public-contract-impact.zsh @z-shell/tsc # ── CI / Workflows ─────────────────────────────────────────────────────────── # Workflow changes reviewed by primary maintainer; no TSC sign-off required. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b50f2234..630ce1de 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -49,3 +49,15 @@ - [ ] I have read the [contribution guidelines](https://github.com/z-shell/.github/blob/main/.github/CONTRIBUTING.md) - [ ] Existing tests pass (`zsh -n zi.zsh` / Trunk checks) - [ ] Documentation updated if needed + +## Migration plan + + diff --git a/.github/workflows/public-contract-impact.yml b/.github/workflows/public-contract-impact.yml new file mode 100644 index 00000000..e4c19e8b --- /dev/null +++ b/.github/workflows/public-contract-impact.yml @@ -0,0 +1,54 @@ +--- +name: Public Contract Impact + +on: + pull_request: + types: [opened, synchronize, reopened, edited, labeled, unlabeled] + +concurrency: + group: public-contract-impact-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + contract-impact: + name: Public contract impact + runs-on: ubuntu-latest + steps: + - name: Check out pull request head + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha }} + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -yq jq zsh + + - name: Test public contract detector + run: zsh tests/public-contract-impact.zsh + + - name: Prepare trusted detector + id: detector + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + if git cat-file -e "${BASE_SHA}:scripts/public-contract-impact.zsh" 2>/dev/null; then + git show "${BASE_SHA}:scripts/public-contract-impact.zsh" > "$RUNNER_TEMP/public-contract-impact.zsh" + echo "policy_arg=" >> "$GITHUB_OUTPUT" + else + cp scripts/public-contract-impact.zsh "$RUNNER_TEMP/public-contract-impact.zsh" + echo "policy_arg=--no-policy" >> "$GITHUB_OUTPUT" + echo "::notice title=Public contract bootstrap::The base branch has no trusted detector; this introducing run is informational-only." + fi + + - name: Compare public contract + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + POLICY_ARG: ${{ steps.detector.outputs.policy_arg }} + PR_BODY: ${{ github.event.pull_request.body }} + PR_LABELS_JSON: ${{ toJSON(github.event.pull_request.labels.*.name) }} + run: zsh "$RUNNER_TEMP/public-contract-impact.zsh" --base "$BASE_SHA" --head "$HEAD_SHA" ${POLICY_ARG:+"$POLICY_ARG"} diff --git a/contracts/public-contract-v1.json b/contracts/public-contract-v1.json new file mode 100644 index 00000000..d0e09536 --- /dev/null +++ b/contracts/public-contract-v1.json @@ -0,0 +1,204 @@ +{ + "schema_version": 1, + "contract_version": 1, + "evidence_reviewed": "2026-08-15", + "renames": [], + "surfaces": [ + { + "id": "commands", + "kind": "token-set", + "file": "zi.zsh", + "assignment": "ZI[cmd-list]", + "description": "Tokens accepted as Zi subcommands" + }, + { + "id": "ices", + "kind": "token-set", + "file": "zi.zsh", + "assignment": "ZI[ice-list]", + "description": "Built-in ice modifier tokens" + }, + { + "id": "annex-registration", + "kind": "function", + "file": "zi.zsh", + "symbol": "@zi-register-annex", + "description": "Annex registration API positional signature" + }, + { + "id": "hook-registration", + "kind": "function", + "file": "zi.zsh", + "symbol": "@zi-register-hook", + "description": "Hook registration API positional signature" + }, + { + "id": "compatibility-functions", + "kind": "function-region", + "file": "zi.zsh", + "start_marker": "# Compatibility functions. [[[", + "end_marker": "# ]]]", + "description": "Named compatibility wrapper functions" + }, + { + "id": "prezto-compatibility", + "kind": "function", + "file": "zi.zsh", + "symbol": "pmodload", + "presence_only": true, + "description": "Prezto compatibility function" + }, + { + "id": "compatibility-aliases", + "kind": "aliases", + "file": "zi.zsh", + "target": "zi", + "description": "Deprecated command-name aliases targeting zi (currently zini, zinit, and zplugin)" + }, + { + "id": "installer-git-output", + "kind": "path", + "path": "lib/zsh/git-process-output.zsh", + "description": "Installer-facing progress filter consumed by z-shell/src" + } + ], + "consumers": [ + { + "repository": "z-shell/wiki", + "path": "ecosystem/annexes/0_overview.mdx", + "surfaces": ["annex-registration", "hook-registration"], + "evidence": "https://github.com/z-shell/wiki/blob/7fca4272b6fbb40afeaa2c25288c1089d887c7d4/ecosystem/annexes/0_overview.mdx" + }, + { + "repository": "z-shell/docs", + "path": "code/zsdoc/asciidoc/zi.zsh.adoc", + "surfaces": [ + "commands", + "ices", + "annex-registration", + "hook-registration", + "compatibility-functions", + "prezto-compatibility", + "compatibility-aliases" + ], + "evidence": "https://github.com/z-shell/docs/blob/77f0d7d30eb090ce335fcb6fe21fd09c55dc6bd7/code/zsdoc/asciidoc/zi.zsh.adoc" + }, + { + "repository": "z-shell/src", + "path": "public/sh/install.sh", + "surfaces": ["installer-git-output"], + "evidence": "https://github.com/z-shell/src/blob/5e301f2bb9d6e70c5aebb30b83217ac07b794025/public/sh/install.sh" + }, + { + "repository": "z-shell/src", + "path": "public/zsh/init.zsh", + "surfaces": ["installer-git-output"], + "evidence": "https://github.com/z-shell/src/blob/5e301f2bb9d6e70c5aebb30b83217ac07b794025/public/zsh/init.zsh" + }, + { + "repository": "z-shell/zd", + "path": "tests/helpers.zsh", + "surfaces": [ + "commands", + "ices", + "annex-registration", + "hook-registration", + "compatibility-functions", + "prezto-compatibility", + "compatibility-aliases", + "installer-git-output" + ], + "evidence": "https://github.com/z-shell/zd/blob/3380ddef839309c5886ee0addb4fcb114639d7c4/tests/helpers.zsh" + }, + { + "repository": "z-shell/z-a-rust", + "path": "z-a-rust.plugin.zsh", + "surfaces": ["annex-registration"], + "evidence": "https://github.com/z-shell/z-a-rust/blob/c896c792baf4bc5d24468e0109534a174316afb9/z-a-rust.plugin.zsh" + }, + { + "repository": "z-shell/z-a-patch-dl", + "path": "z-a-patch-dl.plugin.zsh", + "surfaces": ["annex-registration"], + "evidence": "https://github.com/z-shell/z-a-patch-dl/blob/abd2e081725ccc4d50924a88ad6214178abe138c/z-a-patch-dl.plugin.zsh" + }, + { + "repository": "z-shell/z-a-submods", + "path": "z-a-submods.plugin.zsh", + "surfaces": ["annex-registration"], + "evidence": "https://github.com/z-shell/z-a-submods/blob/a2fe5a60260879470dc7d4dc2c2ca6c05e145623/z-a-submods.plugin.zsh" + }, + { + "repository": "z-shell/z-a-readurl", + "path": "z-a-readurl.plugin.zsh", + "surfaces": ["annex-registration"], + "evidence": "https://github.com/z-shell/z-a-readurl/blob/60a2dfdfcc7f9fd416c894b2f875c582d2b45c64/z-a-readurl.plugin.zsh" + }, + { + "repository": "z-shell/z-a-bin-gem-node", + "path": "z-a-bin-gem-node.plugin.zsh", + "surfaces": ["annex-registration"], + "evidence": "https://github.com/z-shell/z-a-bin-gem-node/blob/5584f8a451b175c466a2d3f1b620b7187868226f/z-a-bin-gem-node.plugin.zsh" + }, + { + "repository": "z-shell/z-a-default-ice", + "path": "z-a-default-ice.plugin.zsh", + "surfaces": ["annex-registration"], + "evidence": "https://github.com/z-shell/z-a-default-ice/blob/9a5eda0d6eedc8a6c3af1fbbda1c444d47062fe1/z-a-default-ice.plugin.zsh" + }, + { + "repository": "z-shell/z-a-linkman", + "path": "z-a-linkman.plugin.zsh", + "surfaces": ["annex-registration"], + "evidence": "https://github.com/z-shell/z-a-linkman/blob/81e91309ab490c4e6167e7eeab20d1e925a7eb30/z-a-linkman.plugin.zsh" + }, + { + "repository": "z-shell/z-a-unscope", + "path": "z-a-unscope.plugin.zsh", + "surfaces": ["annex-registration"], + "evidence": "https://github.com/z-shell/z-a-unscope/blob/137dad6ff2fa552015d274e23f91beabe93b0d1c/z-a-unscope.plugin.zsh" + }, + { + "repository": "z-shell/z-a-man", + "path": "z-a-man.plugin.zsh", + "surfaces": ["annex-registration"], + "evidence": "https://github.com/z-shell/z-a-man/blob/d8ab407a445d394a7738296daf2fcb7951d9cc93/z-a-man.plugin.zsh" + }, + { + "repository": "z-shell/z-a-meta-plugins", + "path": "z-a-meta-plugins.plugin.zsh", + "surfaces": ["annex-registration"], + "evidence": "https://github.com/z-shell/z-a-meta-plugins/blob/caf156e15148031df6a144f0a8f8c88627eda78a/z-a-meta-plugins.plugin.zsh" + }, + { + "repository": "z-shell/z-a-test", + "path": "z-a-test.plugin.zsh", + "surfaces": ["annex-registration"], + "evidence": "https://github.com/z-shell/z-a-test/blob/7a75709aab32988279676b1574bf24a824acb2ff/z-a-test.plugin.zsh" + }, + { + "repository": "z-shell/F-Sy-H", + "path": "chroma/-zi.ch", + "surfaces": ["ices"], + "evidence": "https://github.com/z-shell/F-Sy-H/blob/e9f6487a654ae870154fd9e3384853f37ae01087/chroma/-zi.ch" + }, + { + "repository": "z-shell/zsh-lint", + "path": "legacy/functions/zsh-lint", + "surfaces": ["ices"], + "evidence": "https://github.com/z-shell/zsh-lint/blob/5c6cf1337803921a062560eec477cc93f2e590b8/legacy/functions/zsh-lint" + }, + { + "repository": "z-shell/pm-perf-test", + "path": "zi-turbo/.zshrc", + "surfaces": ["compatibility-functions"], + "evidence": "https://github.com/z-shell/pm-perf-test/blob/c705f881837f8e3cc6c04a3efebcd1ba9c338742/zi-turbo/.zshrc" + }, + { + "repository": "z-shell/subversion", + "path": "docs/README.md", + "surfaces": ["compatibility-functions"], + "evidence": "https://github.com/z-shell/subversion/blob/6d4f18be2b0c61790bf94325d8bc857625328253/docs/README.md" + } + ] +} diff --git a/scripts/public-contract-impact.zsh b/scripts/public-contract-impact.zsh new file mode 100644 index 00000000..dad8cfaa --- /dev/null +++ b/scripts/public-contract-impact.zsh @@ -0,0 +1,588 @@ +#!/usr/bin/env zsh + +emulate -LR zsh +setopt errexit nounset pipefail extendedglob + +typeset repository="." +typeset manifest_path="contracts/public-contract-v1.json" +typeset base_ref="" head_ref="" +integer enforce_policy=1 + +usage() { + print "usage: ${0:t} --base REF --head REF [--repository DIR] [--manifest PATH] [--no-policy]" +} + +while (( $# )); do + case "$1" in + --base) + base_ref="${2-}" + shift 2 + ;; + --head) + head_ref="${2-}" + shift 2 + ;; + --repository) + repository="${2-}" + shift 2 + ;; + --manifest) + manifest_path="${2-}" + shift 2 + ;; + --no-policy) + enforce_policy=0 + shift + ;; + --help|-h) + usage + exit 0 + ;; + *) + print -u2 "public-contract-impact: unknown argument: $1" + usage >&2 + exit 2 + ;; + esac +done + +[[ -n $base_ref && -n $head_ref ]] || { + print -u2 "public-contract-impact: --base and --head are required" + exit 2 +} + +for command_name in git jq sort tr; do + (( $+commands[$command_name] )) || { + print -u2 "public-contract-impact: required command not found: $command_name" + exit 2 + } +done + +repository="$(command git -C "$repository" rev-parse --show-toplevel)" +typeset temp_dir +temp_dir="$(command mktemp -d "${TMPDIR:-/tmp}/zi-contract.XXXXXXXX")" +trap 'command rm -rf -- "$temp_dir"' EXIT INT TERM + +typeset empty_manifest='{"schema_version":1,"contract_version":0,"renames":[],"surfaces":[],"consumers":[]}' +typeset base_manifest="$temp_dir/base-manifest.json" +typeset head_manifest="$temp_dir/head-manifest.json" + +materialize_manifest() { + local ref="$1" destination="$2" + if ! command git -C "$repository" show "${ref}:${manifest_path}" > "$destination" 2>/dev/null; then + print -r -- "$empty_manifest" > "$destination" + fi +} + +validate_manifest() { + local file="$1" label="$2" + jq -e ' + . as $manifest + | .schema_version == 1 + and (.contract_version | type == "number") + and (.renames | type == "array") + and (.surfaces | type == "array") + and (.consumers | type == "array") + and all(.surfaces[]; + (.id | type == "string" and length > 0) + and (.kind | IN("token-set", "function", "function-region", "aliases", "path")) + and (.description | type == "string" and length > 0) + ) + and ([.surfaces[].id] | length == (unique | length)) + and all(.consumers[]; + . as $consumer + | ("https://github.com/" + $consumer.repository + "/blob/") as $prefix + | ($consumer.evidence | ltrimstr($prefix)) as $evidence_path + | ($consumer.repository | test("^z-shell/[A-Za-z0-9_.-]+$")) + and ($consumer.path | type == "string" and length > 0) + and (($consumer.path | startswith("/")) | not) + and ($consumer.surfaces | type == "array" and length > 0) + and ($consumer.evidence | startswith($prefix)) + and ($evidence_path | test("^[0-9a-f]{40}/")) + and ($evidence_path[41:] == $consumer.path) + ) + and ([.consumers[].surfaces[]] - [.surfaces[].id] | length == 0) + and ([ + .surfaces[].id + | . as $id + | any($manifest.consumers[]; .surfaces | index($id)) + ] | all) + ' "$file" >/dev/null || { + print -u2 "public-contract-impact: invalid ${label} manifest: ${manifest_path}" + exit 2 + } +} + +materialize_manifest "$base_ref" "$base_manifest" +materialize_manifest "$head_ref" "$head_manifest" +validate_manifest "$base_manifest" base +validate_manifest "$head_manifest" head + +materialize_file() { + local ref="$1" file_path="$2" destination="$3" + command git -C "$repository" show "${ref}:${file_path}" > "$destination" 2>/dev/null +} + +extract_token_set() { + local source_file="$1" assignment="$2" output="$3" + local line trimmed rest value="" token + integer collecting=0 + + while IFS= read -r line; do + if (( ! collecting )); then + trimmed="${line##[[:space:]]#}" + [[ ${trimmed%%=*} == "$assignment" ]] || continue + rest="${trimmed#*=}" + rest="${rest##[[:space:]]#}" + [[ ${rest[1]-} == '"' ]] || continue + rest="${rest[2,-1]}" + collecting=1 + else + rest="$line" + fi + + trimmed="${rest%%[[:space:]]#}" + if [[ ${trimmed[-1]-} == '"' ]]; then + value+="${trimmed[1,-2]}" + collecting=0 + break + fi + value+="${trimmed%\\}" + done < "$source_file" + + for token in "${(@s:|:)value}"; do + [[ -n $token ]] && print -r -- "${token}"$'\t'"${token}" + done | LC_ALL=C sort -u > "$output" +} + +function_body_arity() { + local source_file="$1" symbol="$2" + local line code compact body="" + local needle="${symbol}(){" + integer found=0 + + while IFS= read -r line; do + code="${line%%\#*}" + compact="${code//[ $'\t']/}" + if (( ! found )); then + [[ $compact == *"$needle"* ]] || continue + found=1 + fi + body+="${code}"$'\n' + [[ $compact == '}' ]] && break + done < "$source_file" + + (( found )) || return 1 + local arity + arity="$(print -r -- "$body" | + command grep -oE '\$\{?[0-9]+' 2>/dev/null | + command grep -oE '[0-9]+' 2>/dev/null | + LC_ALL=C sort -n | + command tail -1 || true)" + print -r -- "${arity:-0}" +} + +extract_function_region() { + local source_file="$1" start_marker="$2" end_marker="$3" output="$4" + local line code compact name + integer in_region=0 + + while IFS= read -r line; do + if (( ! in_region )); then + [[ $line == *"$start_marker"* ]] && in_region=1 + continue + fi + [[ $line == *"$end_marker"* ]] && break + code="${line%%\#*}" + compact="${code//[ $'\t']/}" + if [[ $compact == (#b)(*)'(){'* ]]; then + name="${match[1]}" + [[ -n $name ]] && print -r -- "${name}"$'\t'"variadic" + fi + done < "$source_file" | LC_ALL=C sort -u > "$output" +} + +extract_aliases() { + local source_file="$1" target="$2" output="$3" + local line code token name alias_target + + while IFS= read -r line; do + code="${line%%\#*}" + [[ $code == *"builtin alias"* ]] || continue + for token in ${(z)code}; do + [[ $token == *=* ]] || continue + name="${token%%=*}" + alias_target="${token#*=}" + [[ $alias_target == "$target" ]] || continue + print -r -- "${name}"$'\t'"${alias_target}" + done + done < "$source_file" | LC_ALL=C sort -u > "$output" +} + +extract_surface() { + local ref="$1" definition="$2" output="$3" + local kind file source_file symbol arity + : > "$output" + kind="$(jq -r '.kind' <<< "$definition")" + + case "$kind" in + path) + file="$(jq -r '.path' <<< "$definition")" + if command git -C "$repository" cat-file -e "${ref}:${file}" 2>/dev/null; then + print -r -- "${file}"$'\t'"exists" > "$output" + fi + ;; + token-set|function|function-region|aliases) + file="$(jq -r '.file' <<< "$definition")" + source_file="$temp_dir/source-${RANDOM}-${RANDOM}" + materialize_file "$ref" "$file" "$source_file" || return 0 + case "$kind" in + token-set) + extract_token_set "$source_file" "$(jq -r '.assignment' <<< "$definition")" "$output" + ;; + function) + symbol="$(jq -r '.symbol' <<< "$definition")" + if arity="$(function_body_arity "$source_file" "$symbol")"; then + if [[ $(jq -r '.presence_only // false' <<< "$definition") == true ]]; then + arity="present" + fi + print -r -- "${symbol}"$'\t'"${arity}" > "$output" + fi + ;; + function-region) + extract_function_region \ + "$source_file" \ + "$(jq -r '.start_marker' <<< "$definition")" \ + "$(jq -r '.end_marker' <<< "$definition")" \ + "$output" + ;; + aliases) + extract_aliases "$source_file" "$(jq -r '.target' <<< "$definition")" "$output" + ;; + esac + ;; + *) + print -u2 "public-contract-impact: unsupported surface kind: $kind" + exit 2 + ;; + esac +} + +typeset -a impacts=() + +impact_id() { + local raw="$1" + local id + id="$(print -rn -- "$raw" | + LC_ALL=C tr '[:upper:]' '[:lower:]' | + LC_ALL=C tr -cs '[:alnum:]._/-' '-')" + id="${id##-##}" + id="${id%%-##}" + print -r -- "$id" +} + +record_impact() { + local severity="$1" classification="$2" surface="$3" before="$4" after="$5" summary="$6" + local id + id="$(impact_id "${surface}/${classification}/${before:-none}-to-${after:-none}")" + impacts+=( "${severity}"$'\x1f'"${id}"$'\x1f'"${classification}"$'\x1f'"${surface}"$'\x1f'"${before}"$'\x1f'"${after}"$'\x1f'"${summary}" ) +} + +compare_surface() { + local surface_id="$1" kind="$2" base_snapshot="$3" head_snapshot="$4" + typeset -A base_items=() head_items=() + typeset -a additions=() removals=() + local item detail + + while IFS=$'\t' read -r item detail; do + [[ -n $item ]] && base_items[$item]="$detail" + done < "$base_snapshot" + while IFS=$'\t' read -r item detail; do + [[ -n $item ]] && head_items[$item]="$detail" + done < "$head_snapshot" + + for item in ${(ok)base_items}; do + (( ${+head_items[$item]} )) || removals+=( "$item" ) + done + for item in ${(ok)head_items}; do + (( ${+base_items[$item]} )) || additions+=( "$item" ) + done + + if (( ${#removals} == 1 && ${#additions} == 1 )) && + jq -e \ + --arg surface "$surface_id" \ + --arg from "$removals[1]" \ + --arg to "$additions[1]" \ + 'any(.renames[]?; .surface == $surface and .from == $from and .to == $to)' \ + "$head_manifest" >/dev/null; then + record_impact breaking rename "$surface_id" "$removals[1]" "$additions[1]" \ + "\`${removals[1]}\` was replaced by \`${additions[1]}\`." + else + for item in "${removals[@]}"; do + record_impact breaking removal "$surface_id" "$item" "" "\`${item}\` was removed." + done + for item in "${additions[@]}"; do + record_impact info addition "$surface_id" "" "$item" "\`${item}\` was added." + done + fi + + for item in ${(ok)base_items}; do + (( ${+head_items[$item]} )) || continue + [[ ${base_items[$item]} == ${head_items[$item]} ]] && continue + case "$kind" in + function) + if [[ ${base_items[$item]} == <-> && ${head_items[$item]} == <-> ]] && + (( head_items[$item] < base_items[$item] )); then + record_impact breaking signature-narrowing "$surface_id" \ + "${item}/${base_items[$item]}-args" "${item}/${head_items[$item]}-args" \ + "\`${item}\` narrowed from ${base_items[$item]} to ${head_items[$item]} positional arguments." + else + record_impact info compatible-signature-change "$surface_id" \ + "${item}/${base_items[$item]}" "${item}/${head_items[$item]}" \ + "\`${item}\` changed from ${base_items[$item]} to ${head_items[$item]}." + fi + ;; + aliases) + record_impact breaking retargeting "$surface_id" \ + "${item}=${base_items[$item]}" "${item}=${head_items[$item]}" \ + "\`${item}\` was retargeted from \`${base_items[$item]}\` to \`${head_items[$item]}\`." + ;; + esac + done +} + +typeset -a surface_ids +typeset surface_id base_definition head_definition base_semantics head_semantics kind +typeset base_snapshot head_snapshot +surface_ids=( "${(@f)$(jq -r -s '[.[].surfaces[].id] | unique[]' "$base_manifest" "$head_manifest")}" ) +for surface_id in "${surface_ids[@]}"; do + [[ -n $surface_id ]] || continue + base_definition="$(jq -c --arg id "$surface_id" '.surfaces[] | select(.id == $id)' "$base_manifest")" + head_definition="$(jq -c --arg id "$surface_id" '.surfaces[] | select(.id == $id)' "$head_manifest")" + + if [[ -z $base_definition ]]; then + record_impact info contract-definition-addition "$surface_id" "" "$surface_id" \ + "Monitoring for \`${surface_id}\` was added to contract version $(jq -r '.contract_version' "$head_manifest")." + continue + fi + if [[ -z $head_definition ]]; then + record_impact breaking contract-definition-removal "$surface_id" "$surface_id" "" \ + "Monitoring for \`${surface_id}\` was removed from the versioned definition." + continue + fi + + base_semantics="$(jq -Sc 'del(.description)' <<< "$base_definition")" + head_semantics="$(jq -Sc 'del(.description)' <<< "$head_definition")" + if [[ $base_semantics != "$head_semantics" ]]; then + record_impact breaking contract-definition-change "$surface_id" \ + "base-definition" "head-definition" \ + "The extraction definition changed; this requires explicit review because it can alter monitoring coverage." + fi + + kind="$(jq -r '.kind' <<< "$head_definition")" + base_snapshot="$temp_dir/base-${surface_id}.tsv" + head_snapshot="$temp_dir/head-${surface_id}.tsv" + extract_surface "$base_ref" "$base_definition" "$base_snapshot" + extract_surface "$head_ref" "$head_definition" "$head_snapshot" + compare_surface "$surface_id" "$kind" "$base_snapshot" "$head_snapshot" +done + +compare_consumers() { + local base_file="$1" head_file="$2" + typeset -A base_consumers=() head_consumers=() + local surface repository_name consumer_path evidence key + + while IFS=$'\t' read -r surface repository_name consumer_path evidence; do + [[ -n $surface ]] || continue + key="${surface}"$'\t'"${repository_name}"$'\t'"${consumer_path}" + base_consumers[$key]="$evidence" + done < <(jq -r ' + .consumers[] + | . as $consumer + | .surfaces[] + | [., $consumer.repository, $consumer.path, $consumer.evidence] + | @tsv + ' "$base_file") + + while IFS=$'\t' read -r surface repository_name consumer_path evidence; do + [[ -n $surface ]] || continue + key="${surface}"$'\t'"${repository_name}"$'\t'"${consumer_path}" + head_consumers[$key]="$evidence" + done < <(jq -r ' + .consumers[] + | . as $consumer + | .surfaces[] + | [., $consumer.repository, $consumer.path, $consumer.evidence] + | @tsv + ' "$head_file") + + for key in ${(ok)base_consumers}; do + IFS=$'\t' read -r surface repository_name consumer_path <<< "$key" + if (( ! ${+head_consumers[$key]} )); then + record_impact breaking consumer-evidence-removal "$surface" \ + "${repository_name}:${consumer_path}" "" \ + "Checked-in consumer evidence for \`${repository_name}:${consumer_path}\` was removed." + elif [[ ${base_consumers[$key]} != ${head_consumers[$key]} ]]; then + record_impact info consumer-evidence-refresh "$surface" \ + "${repository_name}:${consumer_path}" "${repository_name}:${consumer_path}" \ + "Evidence for \`${repository_name}:${consumer_path}\` was refreshed." + fi + done + for key in ${(ok)head_consumers}; do + (( ${+base_consumers[$key]} )) && continue + IFS=$'\t' read -r surface repository_name consumer_path <<< "$key" + record_impact info consumer-evidence-addition "$surface" "" \ + "${repository_name}:${consumer_path}" \ + "Checked-in consumer evidence for \`${repository_name}:${consumer_path}\` was added." + done +} + +if (( $(jq -r '.contract_version' "$base_manifest") > 0 )); then + compare_consumers "$base_manifest" "$head_manifest" +fi + +typeset consumers_manifest="$temp_dir/consumers.json" +jq -s '{ + consumers: ([.[].consumers[]] | unique_by([.repository, .path, .surfaces, .evidence])) +}' "$base_manifest" "$head_manifest" > "$consumers_manifest" + +typeset report="$temp_dir/report.md" +typeset consumer_lines +{ + print "## Public contract impact" + print + print -r -- "- Base: \`${base_ref}\`" + print -r -- "- Head: \`${head_ref}\`" + print -r -- "- Definition: \`${manifest_path}\` (schema v1, contract v$(jq -r '.contract_version' "$head_manifest"))" + print + print "### Monitored contract" + print + jq -r '.surfaces[] | "- `\(.id)` — \(.description)"' "$head_manifest" + print + + if (( ! ${#impacts} )); then + print "No public-contract changes detected." + else + print "### Impacts" + print + print "| Classification | Surface | Change | Policy |" + print "| --- | --- | --- | --- |" + for impact in "${impacts[@]}"; do + IFS=$'\x1f' read -r severity id classification surface before after summary <<< "$impact" + if [[ $severity == breaking ]]; then + policy="breaking gate" + else + policy="informational" + fi + print "| \`${classification}\` | \`${surface}\` | ${summary} | ${policy} |" + done + print + print "### Likely consumers" + print + typeset -A reported_surfaces=() + for impact in "${impacts[@]}"; do + IFS=$'\x1f' read -r severity id classification surface before after summary <<< "$impact" + (( ${+reported_surfaces[$surface]} )) && continue + reported_surfaces[$surface]=1 + print "#### \`${surface}\`" + consumer_lines="$(jq -r --arg surface "$surface" ' + .consumers[] + | select(.surfaces | index($surface)) + | "- [`\(.repository):\(.path)`](\(.evidence))" + ' "$consumers_manifest")" + if [[ -n $consumer_lines ]]; then + print -r -- "$consumer_lines" + else + print -r -- "- No checked-in consumer reference is currently recorded." + fi + print + done + fi +} > "$report" + +integer policy_failures=0 +typeset -a breaking_impacts=() +typeset annotation_message +for impact in "${impacts[@]}"; do + IFS=$'\x1f' read -r severity id classification surface before after summary <<< "$impact" + [[ $severity == breaking ]] && breaking_impacts+=( "$impact" ) + annotation_message="${surface}: ${summary}" + annotation_message="${annotation_message//'%'/'%25'}" + annotation_message="${annotation_message//$'\r'/'%0D'}" + annotation_message="${annotation_message//$'\n'/'%0A'}" + if [[ $severity == breaking ]]; then + print "::error title=Public contract ${classification}::${annotation_message}" + else + print "::notice title=Public contract ${classification}::${annotation_message}" + fi +done + +if (( enforce_policy && ${#breaking_impacts} )); then + typeset labels_json="${PR_LABELS_JSON-[]}" + typeset pr_body="${PR_BODY-}" + typeset marker disposition_line disposition body_line trimmed_line + if ! jq -e 'index("breaking-change") != null' <<< "$labels_json" >/dev/null 2>&1; then + print "::error title=Breaking-change label required::Add the breaking-change label." + (( policy_failures += 1 )) + fi + + typeset migration_section="" visible_migration="" migration="" + if [[ $pr_body == *"## Migration plan"* ]]; then + migration_section="${pr_body#*"## Migration plan"}" + migration_section="${migration_section%%$'\n## '*}" + visible_migration="${migration_section///}" + migration="${visible_migration//[[:space:]]/}" + fi + if (( ${#migration} < 20 )); then + print "::error title=Migration plan required::Add a linked or descriptive ## Migration plan section to the PR body." + (( policy_failures += 1 )) + fi + + for impact in "${breaking_impacts[@]}"; do + IFS=$'\x1f' read -r severity id classification surface before after summary <<< "$impact" + marker="[contract-impact:${id}]" + disposition_line="" + for body_line in "${(@f)visible_migration}"; do + trimmed_line="${body_line##[[:space:]]#}" + if [[ ${trimmed_line[1,${#marker}]-} == "$marker" ]]; then + disposition_line="${trimmed_line[${#marker}+1,-1]-}" + break + fi + done + disposition="${disposition_line##[[:space:]]#}" + if [[ $disposition == "updated here"* ]]; then + continue + elif [[ $disposition == "follow-up issue linked:"*https://github.com/*/issues/<->* ]]; then + continue + elif [[ ${#disposition} -ge 24 && $disposition == "not affected:"?* ]]; then + continue + elif [[ $disposition == "deprecated with removal target:"?* ]]; then + continue + fi + print "::error title=Impact disposition required::Add ${marker} followed by an allowed disposition." + (( policy_failures += 1 )) + done +fi + +if (( ${#breaking_impacts} )); then + { + print + print "### Breaking-change policy" + print + print "Destructive impacts require the \`breaking-change\` label, a descriptive \`## Migration plan\`, and one PR-body line per impact:" + print + for impact in "${breaking_impacts[@]}"; do + IFS=$'\x1f' read -r severity id classification surface before after summary <<< "$impact" + print -r -- "- \`[contract-impact:${id}] updated here\`" + done + print + print "Allowed dispositions are \`updated here\`, \`follow-up issue linked: \`, \`not affected: \`, and \`deprecated with removal target: \`." + } >> "$report" +fi + +command cat "$report" +if [[ -n ${GITHUB_STEP_SUMMARY-} ]]; then + command cat "$report" >> "$GITHUB_STEP_SUMMARY" +fi + +(( policy_failures == 0 )) diff --git a/tests/fixtures/public-contract/addition/zi.zsh b/tests/fixtures/public-contract/addition/zi.zsh new file mode 100644 index 00000000..0f2dd981 --- /dev/null +++ b/tests/fixtures/public-contract/addition/zi.zsh @@ -0,0 +1,26 @@ +ZI[ice-list]="wait|load|atclone|debug" +ZI[cmd-list]="help|load|update|doctor" + +@zi-register-annex() { + local name="$1" type="$2" handler="$3" helphandler="$4" icemods="$5" +} + +@zi-register-hook() { + local name="$1" type="$2" handler="$3" icemods="$4" +} + +(( ${+functions[pmodload]} )) || pmodload() { + print -r -- "$@" +} + +# Compatibility functions. [[[ +❮▼❯() { zi "$@"; } +zpcdreplay() { zicdreplay "$@"; } +zpcdclear() { zicdclear "$@"; } +zpcompinit() { zicompinit "$@"; } +zpcompdef() { zicompdef "$@"; } +zpextract() { ziextract "$@"; } +zplegacy() { zi "$@"; } +# ]]] + +(( ZI[INTERNAL_ALIASES] )) && builtin alias zini=zi zinit=zi zplugin=zi diff --git a/tests/fixtures/public-contract/base/lib/zsh/git-process-output.zsh b/tests/fixtures/public-contract/base/lib/zsh/git-process-output.zsh new file mode 100644 index 00000000..97f2e197 --- /dev/null +++ b/tests/fixtures/public-contract/base/lib/zsh/git-process-output.zsh @@ -0,0 +1,3 @@ +#!/usr/bin/env zsh + +command cat diff --git a/tests/fixtures/public-contract/base/zi.zsh b/tests/fixtures/public-contract/base/zi.zsh new file mode 100644 index 00000000..4b35738d --- /dev/null +++ b/tests/fixtures/public-contract/base/zi.zsh @@ -0,0 +1,25 @@ +ZI[ice-list]="wait|load|atclone" +ZI[cmd-list]="help|load|update" + +@zi-register-annex() { + local name="$1" type="$2" handler="$3" helphandler="$4" icemods="$5" +} + +@zi-register-hook() { + local name="$1" type="$2" handler="$3" icemods="$4" +} + +(( ${+functions[pmodload]} )) || pmodload() { + print -r -- "$@" +} + +# Compatibility functions. [[[ +❮▼❯() { zi "$@"; } +zpcdreplay() { zicdreplay "$@"; } +zpcdclear() { zicdclear "$@"; } +zpcompinit() { zicompinit "$@"; } +zpcompdef() { zicompdef "$@"; } +zpextract() { ziextract "$@"; } +# ]]] + +(( ZI[INTERNAL_ALIASES] )) && builtin alias zini=zi zinit=zi zplugin=zi diff --git a/tests/fixtures/public-contract/breaking/zi.zsh b/tests/fixtures/public-contract/breaking/zi.zsh new file mode 100644 index 00000000..bb115799 --- /dev/null +++ b/tests/fixtures/public-contract/breaking/zi.zsh @@ -0,0 +1,24 @@ +ZI[ice-list]="wait|load|atclone" +ZI[cmd-list]="help|fetch|update" + +@zi-register-annex() { + local name="$1" type="$2" handler="$3" helphandler="$4" +} + +@zi-register-hook() { + local name="$1" type="$2" handler="$3" icemods="$4" +} + +(( ${+functions[pmodload]} )) || pmodload() { + print -r -- "$@" +} + +# Compatibility functions. [[[ +❮▼❯() { zi "$@"; } +zpcdreplay() { zicdreplay "$@"; } +zpcompinit() { zicompinit "$@"; } +zpcompdef() { zicompdef "$@"; } +zpextract() { ziextract "$@"; } +# ]]] + +(( ZI[INTERNAL_ALIASES] )) && builtin alias zini=zi zinit=zi diff --git a/tests/fixtures/public-contract/refactor/zi.zsh b/tests/fixtures/public-contract/refactor/zi.zsh new file mode 100644 index 00000000..e7a43034 --- /dev/null +++ b/tests/fixtures/public-contract/refactor/zi.zsh @@ -0,0 +1,37 @@ +# A comment mentioning fake() and ZI[cmd-list]="removed" is not a contract. +ZI[ice-list]="atclone|\ +wait|load" +ZI[cmd-list]="update|help|\ +load" + +@zi-register-annex () { + # $99 in a comment must not narrow or widen the signature. + local name="$1" + local type="$2" handler="$3" + local helphandler="$4" icemods="$5" +} + +@zi-register-hook() { + local name="$1" + local type="$2" + local handler="$3" + local icemods="$4" +} + +(( ${+functions[pmodload]} )) || pmodload () { + print -r -- "$@" +} + +# Compatibility functions. [[[ +❮▼❯ () { zi "$@"; } +zpcdreplay () { zicdreplay "$@"; } +zpcdclear () { zicdclear "$@"; } +zpcompinit () { zicompinit "$@"; } +zpcompdef () { zicompdef "$@"; } +zpextract () { ziextract "$@"; } +# fake-compat() { :; } +# ]]] + +internal_helper() { :; } +(( ZI[INTERNAL_ALIASES] )) && + builtin alias zplugin=zi zini=zi zinit=zi diff --git a/tests/public-contract-impact.zsh b/tests/public-contract-impact.zsh new file mode 100644 index 00000000..a3ef7e44 --- /dev/null +++ b/tests/public-contract-impact.zsh @@ -0,0 +1,179 @@ +#!/usr/bin/env zsh + +emulate -LR zsh +setopt errexit nounset pipefail + +typeset project_root="${0:A:h:h}" +typeset fixture_root="${project_root}/tests/fixtures/public-contract" +typeset temp_root +temp_root="$(command mktemp -d "${TMPDIR:-/tmp}/zi-contract-test.XXXXXXXX")" +trap 'command rm -rf -- "$temp_root"' EXIT INT TERM + +fail() { + print -u2 "not ok - $1" + exit 1 +} + +assert_contains() { + local output="$1" expected="$2" + [[ $output == *"$expected"* ]] || fail "expected output to contain: $expected" +} + +assert_not_contains() { + local output="$1" unexpected="$2" + [[ $output != *"$unexpected"* ]] || fail "expected output not to contain: $unexpected" +} + +new_case_repository() { + local name="$1" + local repository="${temp_root}/${name}" + command mkdir -p "$repository/contracts" "$repository/lib/zsh" + command cp "$project_root/contracts/public-contract-v1.json" "$repository/contracts/" + command cp "$fixture_root/base/zi.zsh" "$repository/zi.zsh" + command cp "$fixture_root/base/lib/zsh/git-process-output.zsh" "$repository/lib/zsh/" + command git -C "$repository" init -q + command git -C "$repository" config user.name "Contract Test" + command git -C "$repository" config user.email "contract-test@example.invalid" + command git -C "$repository" add . + command git -C "$repository" commit -qm "test: base contract" + command git -C "$repository" tag base + print -r -- "$repository" +} + +run_detector() { + local repository="$1" + shift + zsh "$project_root/scripts/public-contract-impact.zsh" \ + --repository "$repository" \ + --base base \ + --head HEAD \ + "$@" +} + +typeset repository output + +repository="$(new_case_repository refactor)" +command cp "$fixture_root/refactor/zi.zsh" "$repository/zi.zsh" +command git -C "$repository" add . +command git -C "$repository" commit -qm "refactor: reformat internals" +output="$(run_detector "$repository" --no-policy)" +assert_contains "$output" "No public-contract changes detected." +print "ok - comments, formatting, ordering, and internal refactors are ignored" + +repository="$(new_case_repository addition)" +command cp "$fixture_root/addition/zi.zsh" "$repository/zi.zsh" +command git -C "$repository" add . +command git -C "$repository" commit -qm "feat: add contracts" +output="$(run_detector "$repository")" +assert_contains "$output" '`addition`' +assert_contains "$output" '`doctor` was added' +assert_contains "$output" '`debug` was added' +assert_contains "$output" '`zplegacy` was added' +print "ok - additions are informational" + +repository="$(new_case_repository replacement)" +command cp "$fixture_root/breaking/zi.zsh" "$repository/zi.zsh" +command git -C "$repository" add . +command git -C "$repository" commit -qm "feat: remove and add independently" +output="$(run_detector "$repository" --no-policy)" +assert_contains "$output" '`load` was removed' +assert_contains "$output" '`fetch` was added' +assert_not_contains "$output" '`rename`' +print "ok - independent removals and additions are not inferred as renames" + +repository="$(new_case_repository breaking)" +command cp "$fixture_root/breaking/zi.zsh" "$repository/zi.zsh" +command rm "$repository/lib/zsh/git-process-output.zsh" +changed_manifest="${repository}/contracts/public-contract-v1.json.new" +jq '.renames += [{"surface":"commands","from":"load","to":"fetch"}]' \ + "$repository/contracts/public-contract-v1.json" > "$changed_manifest" +command mv "$changed_manifest" "$repository/contracts/public-contract-v1.json" +command git -C "$repository" add -A +command git -C "$repository" commit -qm "feat!: alter contracts" +if output="$(run_detector "$repository" 2>&1)"; then + fail "breaking changes passed without policy metadata" +fi +assert_contains "$output" '`rename`' +assert_contains "$output" '`signature-narrowing`' +assert_contains "$output" '`removal`' +assert_contains "$output" "Breaking-change label required" +assert_contains "$output" "Migration plan required" + +typeset -a impact_markers +impact_markers=( "${(@f)$(print -r -- "$output" | command grep -o '\[contract-impact:[^]]*\]' | LC_ALL=C sort -u)}" ) +typeset pr_body="## Migration plan +Consumers migrate using https://github.com/z-shell/zi/issues/376." +typeset marker +for marker in "${impact_markers[@]}"; do + pr_body+=$'\n'"${marker} updated here" +done + +typeset commented_body="## Migration plan +Consumers migrate using https://github.com/z-shell/zi/issues/376." +for marker in "${impact_markers[@]}"; do + commented_body+=$'\n'"" +done +if output="$( + PR_LABELS_JSON='["breaking-change"]' \ + PR_BODY="$commented_body" \ + run_detector "$repository" 2>&1 +)"; then + fail "hidden disposition comments passed the breaking-change gate" +fi +assert_contains "$output" "Impact disposition required" + +output="$( + PR_LABELS_JSON='["breaking-change"]' \ + PR_BODY="$pr_body" \ + run_detector "$repository" +)" +assert_contains "$output" "Breaking-change policy" +print "ok - destructive changes require label, migration plan, and dispositions" + +repository="$(new_case_repository manifest-tampering)" +typeset changed_manifest="${repository}/contracts/public-contract-v1.json.new" +jq '(.surfaces[] | select(.id == "annex-registration")).presence_only = true' \ + "$repository/contracts/public-contract-v1.json" > "$changed_manifest" +command mv "$changed_manifest" "$repository/contracts/public-contract-v1.json" +command git -C "$repository" add . +command git -C "$repository" commit -qm "test: weaken contract definition" +output="$(run_detector "$repository" --no-policy)" +assert_contains "$output" '`contract-definition-change`' +print "ok - extraction-definition changes are destructive impacts" + +repository="$(new_case_repository invalid-evidence)" +changed_manifest="${repository}/contracts/public-contract-v1.json.new" +jq '.consumers[0].evidence = "https://github.com/z-shell/wiki/blob/main/example"' \ + "$repository/contracts/public-contract-v1.json" > "$changed_manifest" +command mv "$changed_manifest" "$repository/contracts/public-contract-v1.json" +command git -C "$repository" add . +command git -C "$repository" commit -qm "test: unpin evidence" +if output="$(run_detector "$repository" --no-policy 2>&1)"; then + fail "mutable consumer evidence passed manifest validation" +fi +assert_contains "$output" "invalid head manifest" +print "ok - consumer evidence must be complete and SHA-pinned" + +repository="$(new_case_repository mismatched-evidence)" +changed_manifest="${repository}/contracts/public-contract-v1.json.new" +jq '.consumers[0].repository = "z-shell/zd" | .consumers[0].path = "tests/helpers.zsh"' \ + "$repository/contracts/public-contract-v1.json" > "$changed_manifest" +command mv "$changed_manifest" "$repository/contracts/public-contract-v1.json" +command git -C "$repository" add . +command git -C "$repository" commit -qm "test: mismatch evidence identity" +if output="$(run_detector "$repository" --no-policy 2>&1)"; then + fail "consumer evidence for another repo and path passed validation" +fi +assert_contains "$output" "invalid head manifest" +print "ok - evidence URLs must match their declared repository and path" + +repository="$(new_case_repository consumer-removal)" +changed_manifest="${repository}/contracts/public-contract-v1.json.new" +jq 'del(.consumers[] | select(.repository == "z-shell/pm-perf-test"))' \ + "$repository/contracts/public-contract-v1.json" > "$changed_manifest" +command mv "$changed_manifest" "$repository/contracts/public-contract-v1.json" +command git -C "$repository" add . +command git -C "$repository" commit -qm "test: remove consumer evidence" +output="$(run_detector "$repository" --no-policy)" +assert_contains "$output" '`consumer-evidence-removal`' +print "ok - removing consumer evidence is a destructive impact" From a44f25a553e1525b55656accefb984aaef4eba13 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 15 Aug 2026 12:50:41 +0100 Subject: [PATCH 31/44] fix(contract): harden impact detection --- .github/workflows/public-contract-impact.yml | 42 +++++++-- contracts/public-contract-v1.json | 2 +- scripts/public-contract-impact.zsh | 85 ++++++++++++++++--- tests/fixtures/public-contract/base/zi.zsh | 6 +- .../public-contract/conditional/zi.zsh | 27 ++++++ .../fixtures/public-contract/refactor/zi.zsh | 4 +- tests/public-contract-impact.zsh | 59 +++++++++++++ 7 files changed, 201 insertions(+), 24 deletions(-) create mode 100644 tests/fixtures/public-contract/conditional/zi.zsh diff --git a/.github/workflows/public-contract-impact.yml b/.github/workflows/public-contract-impact.yml index e4c19e8b..b4438499 100644 --- a/.github/workflows/public-contract-impact.yml +++ b/.github/workflows/public-contract-impact.yml @@ -27,22 +27,29 @@ jobs: - name: Install dependencies run: sudo apt-get update && sudo apt-get install -yq jq zsh - - name: Test public contract detector - run: zsh tests/public-contract-impact.zsh - - name: Prepare trusted detector id: detector env: BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | + trusted_root="$RUNNER_TEMP/trusted-contract" + mkdir -p "$trusted_root" if git cat-file -e "${BASE_SHA}:scripts/public-contract-impact.zsh" 2>/dev/null; then - git show "${BASE_SHA}:scripts/public-contract-impact.zsh" > "$RUNNER_TEMP/public-contract-impact.zsh" + git archive "$BASE_SHA" \ + contracts/public-contract-v1.json \ + scripts/public-contract-impact.zsh \ + tests | + tar -x -C "$trusted_root" echo "policy_arg=" >> "$GITHUB_OUTPUT" + echo "has_trusted_tests=true" >> "$GITHUB_OUTPUT" else - cp scripts/public-contract-impact.zsh "$RUNNER_TEMP/public-contract-impact.zsh" + mkdir -p "$trusted_root/scripts" + cp scripts/public-contract-impact.zsh "$trusted_root/scripts/public-contract-impact.zsh" echo "policy_arg=--no-policy" >> "$GITHUB_OUTPUT" + echo "has_trusted_tests=false" >> "$GITHUB_OUTPUT" echo "::notice title=Public contract bootstrap::The base branch has no trusted detector; this introducing run is informational-only." fi + echo "root=$trusted_root" >> "$GITHUB_OUTPUT" - name: Compare public contract env: @@ -51,4 +58,27 @@ jobs: POLICY_ARG: ${{ steps.detector.outputs.policy_arg }} PR_BODY: ${{ github.event.pull_request.body }} PR_LABELS_JSON: ${{ toJSON(github.event.pull_request.labels.*.name) }} - run: zsh "$RUNNER_TEMP/public-contract-impact.zsh" --base "$BASE_SHA" --head "$HEAD_SHA" ${POLICY_ARG:+"$POLICY_ARG"} + TRUSTED_ROOT: ${{ steps.detector.outputs.root }} + run: zsh "$TRUSTED_ROOT/scripts/public-contract-impact.zsh" --base "$BASE_SHA" --head "$HEAD_SHA" ${POLICY_ARG:+"$POLICY_ARG"} + + - name: Test trusted public contract detector + if: steps.detector.outputs.has_trusted_tests == 'true' + env: + TRUSTED_ROOT: ${{ steps.detector.outputs.root }} + run: cd "$TRUSTED_ROOT" && zsh tests/public-contract-impact.zsh + + detector-head-tests: + name: Public contract detector tests + needs: contract-impact + runs-on: ubuntu-latest + steps: + - name: Check out pull request head + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -yq jq zsh + + - name: Test pull request detector + run: zsh tests/public-contract-impact.zsh diff --git a/contracts/public-contract-v1.json b/contracts/public-contract-v1.json index d0e09536..97c5e9a6 100644 --- a/contracts/public-contract-v1.json +++ b/contracts/public-contract-v1.json @@ -66,7 +66,7 @@ { "repository": "z-shell/wiki", "path": "ecosystem/annexes/0_overview.mdx", - "surfaces": ["annex-registration", "hook-registration"], + "surfaces": ["annex-registration"], "evidence": "https://github.com/z-shell/wiki/blob/7fca4272b6fbb40afeaa2c25288c1089d887c7d4/ecosystem/annexes/0_overview.mdx" }, { diff --git a/scripts/public-contract-impact.zsh b/scripts/public-contract-impact.zsh index dad8cfaa..3d5c7aea 100644 --- a/scripts/public-contract-impact.zsh +++ b/scripts/public-contract-impact.zsh @@ -84,9 +84,32 @@ validate_manifest() { and (.surfaces | type == "array") and (.consumers | type == "array") and all(.surfaces[]; - (.id | type == "string" and length > 0) + . as $surface + | (.id | type == "string" and test("^[a-z0-9][a-z0-9._-]*$")) and (.kind | IN("token-set", "function", "function-region", "aliases", "path")) and (.description | type == "string" and length > 0) + and ( + if $surface.kind == "token-set" then + ($surface.file | type == "string" and length > 0) + and ($surface.assignment | type == "string" and length > 0) + elif $surface.kind == "function" then + ($surface.file | type == "string" and length > 0) + and ($surface.symbol | type == "string" and length > 0) + and (($surface.presence_only // false) | type == "boolean") + elif $surface.kind == "function-region" then + ($surface.file | type == "string" and length > 0) + and ($surface.start_marker | type == "string" and length > 0) + and ($surface.end_marker | type == "string" and length > 0) + elif $surface.kind == "aliases" then + ($surface.file | type == "string" and length > 0) + and ($surface.target | type == "string" and length > 0) + elif $surface.kind == "path" then + ($surface.path | type == "string" and length > 0) + and (($surface.path | startswith("/")) | not) + else + false + end + ) ) and ([.surfaces[].id] | length == (unique | length)) and all(.consumers[]; @@ -138,7 +161,7 @@ extract_token_set() { rest="${rest[2,-1]}" collecting=1 else - rest="$line" + rest="${line##[[:space:]]#}" fi trimmed="${rest%%[[:space:]]#}" @@ -157,15 +180,21 @@ extract_token_set() { function_body_arity() { local source_file="$1" symbol="$2" - local line code compact body="" + local line code compact pending="" body="" local needle="${symbol}(){" integer found=0 while IFS= read -r line; do code="${line%%\#*}" + if [[ $code == *\\ ]]; then + pending+="${code%\\}" + continue + fi + code="${pending}${code}" + pending="" compact="${code//[ $'\t']/}" if (( ! found )); then - [[ $compact == *"$needle"* ]] || continue + [[ $compact == "$needle"* || $compact == *'||'"$needle"* ]] || continue found=1 fi body+="${code}"$'\n' @@ -184,7 +213,7 @@ function_body_arity() { extract_function_region() { local source_file="$1" start_marker="$2" end_marker="$3" output="$4" - local line code compact name + local line code compact declaration name pending="" integer in_region=0 while IFS= read -r line; do @@ -194,10 +223,24 @@ extract_function_region() { fi [[ $line == *"$end_marker"* ]] && break code="${line%%\#*}" + if [[ $code == *\\ ]]; then + pending+="${code%\\}" + continue + fi + code="${pending}${code}" + pending="" compact="${code//[ $'\t']/}" - if [[ $compact == (#b)(*)'(){'* ]]; then - name="${match[1]}" - [[ -n $name ]] && print -r -- "${name}"$'\t'"variadic" + if [[ $compact =~ '^([^()]*)\(\)\{' ]]; then + declaration="${match[1]}" + if [[ $declaration == *'||'* ]]; then + name="${declaration##*'||'}" + elif [[ $declaration == *('&&'|'|'|';')* ]]; then + continue + else + name="$declaration" + fi + [[ -n $name && $name != *('&&'|'|'|';')* ]] && + print -r -- "${name}"$'\t'"variadic" fi done < "$source_file" | LC_ALL=C sort -u > "$output" } @@ -442,9 +485,20 @@ if (( $(jq -r '.contract_version' "$base_manifest") > 0 )); then fi typeset consumers_manifest="$temp_dir/consumers.json" -jq -s '{ - consumers: ([.[].consumers[]] | unique_by([.repository, .path, .surfaces, .evidence])) -}' "$base_manifest" "$head_manifest" > "$consumers_manifest" +jq -s ' + def by_identity: + map({ + key: (.repository + "\u001f" + .path), + value: . + }) + | from_entries; + + (.[0].consumers | by_identity) as $base + | (.[1].consumers | by_identity) as $head + | { + consumers: (($base + $head) | to_entries | map(.value)) + } +' "$base_manifest" "$head_manifest" > "$consumers_manifest" typeset report="$temp_dir/report.md" typeset consumer_lines @@ -526,12 +580,17 @@ if (( enforce_policy && ${#breaking_impacts} )); then (( policy_failures += 1 )) fi - typeset migration_section="" visible_migration="" migration="" + typeset migration_section="" visible_migration="" migration_guidance="" migration="" if [[ $pr_body == *"## Migration plan"* ]]; then migration_section="${pr_body#*"## Migration plan"}" migration_section="${migration_section%%$'\n## '*}" visible_migration="${migration_section///}" - migration="${visible_migration//[[:space:]]/}" + for body_line in "${(@f)visible_migration}"; do + trimmed_line="${body_line##[[:space:]]#}" + [[ $trimmed_line == '[contract-impact:'* ]] && continue + migration_guidance+="${trimmed_line}" + done + migration="${migration_guidance//[[:space:]]/}" fi if (( ${#migration} < 20 )); then print "::error title=Migration plan required::Add a linked or descriptive ## Migration plan section to the PR body." diff --git a/tests/fixtures/public-contract/base/zi.zsh b/tests/fixtures/public-contract/base/zi.zsh index 4b35738d..505d265e 100644 --- a/tests/fixtures/public-contract/base/zi.zsh +++ b/tests/fixtures/public-contract/base/zi.zsh @@ -1,5 +1,7 @@ -ZI[ice-list]="wait|load|atclone" -ZI[cmd-list]="help|load|update" +ZI[ice-list]="wait|load|\ + atclone" +ZI[cmd-list]="help|load|\ + update" @zi-register-annex() { local name="$1" type="$2" handler="$3" helphandler="$4" icemods="$5" diff --git a/tests/fixtures/public-contract/conditional/zi.zsh b/tests/fixtures/public-contract/conditional/zi.zsh new file mode 100644 index 00000000..cf609ce4 --- /dev/null +++ b/tests/fixtures/public-contract/conditional/zi.zsh @@ -0,0 +1,27 @@ +ZI[ice-list]="wait|load|atclone" +ZI[cmd-list]="help|load|update" + +@zi-register-annex() { + local name="$1" type="$2" handler="$3" helphandler="$4" icemods="$5" +} + +@zi-register-hook() { + local name="$1" type="$2" handler="$3" icemods="$4" +} + +false && \ + pmodload() { + print -r -- "$@" +} + +# Compatibility functions. [[[ +❮▼❯() { zi "$@"; } +zpcdreplay() { zicdreplay "$@"; } +false && \ + zpcdclear() { zicdclear "$@"; } +zpcompinit() { zicompinit "$@"; } +zpcompdef() { zicompdef "$@"; } +zpextract() { ziextract "$@"; } +# ]]] + +(( ZI[INTERNAL_ALIASES] )) && builtin alias zini=zi zinit=zi zplugin=zi diff --git a/tests/fixtures/public-contract/refactor/zi.zsh b/tests/fixtures/public-contract/refactor/zi.zsh index e7a43034..26bf902a 100644 --- a/tests/fixtures/public-contract/refactor/zi.zsh +++ b/tests/fixtures/public-contract/refactor/zi.zsh @@ -1,8 +1,8 @@ # A comment mentioning fake() and ZI[cmd-list]="removed" is not a contract. ZI[ice-list]="atclone|\ -wait|load" + wait|load" ZI[cmd-list]="update|help|\ -load" + load" @zi-register-annex () { # $99 in a comment must not narrow or widen the signature. diff --git a/tests/public-contract-impact.zsh b/tests/public-contract-impact.zsh index a3ef7e44..fd0adb41 100644 --- a/tests/public-contract-impact.zsh +++ b/tests/public-contract-impact.zsh @@ -60,6 +60,16 @@ output="$(run_detector "$repository" --no-policy)" assert_contains "$output" "No public-contract changes detected." print "ok - comments, formatting, ordering, and internal refactors are ignored" +repository="$(new_case_repository conditional)" +command cp "$fixture_root/conditional/zi.zsh" "$repository/zi.zsh" +command git -C "$repository" add . +command git -C "$repository" commit -qm "test: guard compatibility function with false" +output="$(run_detector "$repository" --no-policy)" +assert_contains "$output" '`zpcdclear` was removed' +assert_contains "$output" '`pmodload` was removed' +assert_not_contains "$output" 'false&&zpcdclear' +print "ok - dead conditional function definitions are unavailable across logical lines" + repository="$(new_case_repository addition)" command cp "$fixture_root/addition/zi.zsh" "$repository/zi.zsh" command git -C "$repository" add . @@ -108,6 +118,19 @@ for marker in "${impact_markers[@]}"; do pr_body+=$'\n'"${marker} updated here" done +typeset marker_only_body="## Migration plan" +for marker in "${impact_markers[@]}"; do + marker_only_body+=$'\n'"${marker} updated here" +done +if output="$( + PR_LABELS_JSON='["breaking-change"]' \ + PR_BODY="$marker_only_body" \ + run_detector "$repository" 2>&1 +)"; then + fail "dispositions without migration guidance passed the breaking-change gate" +fi +assert_contains "$output" "Migration plan required" + typeset commented_body="## Migration plan Consumers migrate using https://github.com/z-shell/zi/issues/376." for marker in "${impact_markers[@]}"; do @@ -141,6 +164,42 @@ output="$(run_detector "$repository" --no-policy)" assert_contains "$output" '`contract-definition-change`' print "ok - extraction-definition changes are destructive impacts" +typeset malformed_id malformed_field +for malformed_id malformed_field in \ + commands assignment \ + annex-registration symbol \ + hook-registration file \ + compatibility-functions start_marker \ + compatibility-aliases target \ + installer-git-output path; do + repository="$(new_case_repository "malformed-${malformed_id}-${malformed_field}")" + changed_manifest="${repository}/contracts/public-contract-v1.json.new" + jq --arg id "$malformed_id" --arg field "$malformed_field" \ + 'del((.surfaces[] | select(.id == $id))[$field])' \ + "$repository/contracts/public-contract-v1.json" > "$changed_manifest" + command mv "$changed_manifest" "$repository/contracts/public-contract-v1.json" + command git -C "$repository" add . + command git -C "$repository" commit -qm "test: remove required surface field" + if output="$(run_detector "$repository" --no-policy 2>&1)"; then + fail "malformed ${malformed_id}.${malformed_field} passed manifest validation" + fi + assert_contains "$output" "invalid head manifest" +done +print "ok - each surface kind requires its extraction fields" + +repository="$(new_case_repository unsafe-surface-id)" +changed_manifest="${repository}/contracts/public-contract-v1.json.new" +jq '.surfaces[0].id = "unsafe/id"' \ + "$repository/contracts/public-contract-v1.json" > "$changed_manifest" +command mv "$changed_manifest" "$repository/contracts/public-contract-v1.json" +command git -C "$repository" add . +command git -C "$repository" commit -qm "test: add unsafe surface id" +if output="$(run_detector "$repository" --no-policy 2>&1)"; then + fail "unsafe surface ID passed manifest validation" +fi +assert_contains "$output" "invalid head manifest" +print "ok - surface IDs are safe for deterministic snapshot paths" + repository="$(new_case_repository invalid-evidence)" changed_manifest="${repository}/contracts/public-contract-v1.json.new" jq '.consumers[0].evidence = "https://github.com/z-shell/wiki/blob/main/example"' \ From e46e433865ab26f4e834a77d17882b54529304a7 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 15 Aug 2026 13:02:43 +0100 Subject: [PATCH 32/44] fix(contract): close review bypasses --- scripts/public-contract-impact.zsh | 60 ++++++++++++++++++- .../public-contract/conditional/zi.zsh | 13 ++-- tests/public-contract-impact.zsh | 18 +++++- 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/scripts/public-contract-impact.zsh b/scripts/public-contract-impact.zsh index 3d5c7aea..4700a91e 100644 --- a/scripts/public-contract-impact.zsh +++ b/scripts/public-contract-impact.zsh @@ -178,11 +178,21 @@ extract_token_set() { done | LC_ALL=C sort -u > "$output" } +is_conditional_start() { + local trimmed="${1##[[:space:]]#}" + [[ $trimmed == if[[:space:]]* ]] +} + +is_conditional_end() { + local trimmed="${1##[[:space:]]#}" + [[ $trimmed == fi || $trimmed == fi[[:space:]]* || $trimmed == 'fi;'* ]] +} + function_body_arity() { local source_file="$1" symbol="$2" local line code compact pending="" body="" local needle="${symbol}(){" - integer found=0 + integer found=0 conditional_depth=0 while IFS= read -r line; do code="${line%%\#*}" @@ -194,6 +204,15 @@ function_body_arity() { pending="" compact="${code//[ $'\t']/}" if (( ! found )); then + if is_conditional_end "$code"; then + (( conditional_depth > 0 )) && conditional_depth=$(( conditional_depth - 1 )) + continue + fi + if is_conditional_start "$code"; then + (( conditional_depth += 1 )) + continue + fi + (( conditional_depth == 0 )) || continue [[ $compact == "$needle"* || $compact == *'||'"$needle"* ]] || continue found=1 fi @@ -214,7 +233,7 @@ function_body_arity() { extract_function_region() { local source_file="$1" start_marker="$2" end_marker="$3" output="$4" local line code compact declaration name pending="" - integer in_region=0 + integer in_region=0 conditional_depth=0 while IFS= read -r line; do if (( ! in_region )); then @@ -230,6 +249,15 @@ extract_function_region() { code="${pending}${code}" pending="" compact="${code//[ $'\t']/}" + if is_conditional_end "$code"; then + (( conditional_depth > 0 )) && conditional_depth=$(( conditional_depth - 1 )) + continue + fi + if is_conditional_start "$code"; then + (( conditional_depth += 1 )) + continue + fi + (( conditional_depth == 0 )) || continue if [[ $compact =~ '^([^()]*)\(\)\{' ]]; then declaration="${match[1]}" if [[ $declaration == *'||'* ]]; then @@ -324,6 +352,32 @@ impact_id() { print -r -- "$id" } +strip_html_comments() { + local remaining="$1" visible="" before + integer in_comment=0 + + while [[ -n $remaining ]]; do + if (( in_comment )); then + if [[ $remaining == *'-->'* ]]; then + remaining="${remaining#*'-->'}" + in_comment=0 + else + remaining="" + fi + elif [[ $remaining == *'/}" + visible_migration="$(strip_html_comments "$migration_section")" for body_line in "${(@f)visible_migration}"; do trimmed_line="${body_line##[[:space:]]#}" [[ $trimmed_line == '[contract-impact:'* ]] && continue diff --git a/tests/fixtures/public-contract/conditional/zi.zsh b/tests/fixtures/public-contract/conditional/zi.zsh index cf609ce4..4fec7282 100644 --- a/tests/fixtures/public-contract/conditional/zi.zsh +++ b/tests/fixtures/public-contract/conditional/zi.zsh @@ -9,14 +9,19 @@ ZI[cmd-list]="help|load|update" local name="$1" type="$2" handler="$3" icemods="$4" } -false && \ +if false; then pmodload() { - print -r -- "$@" -} + print -r -- "$@" + } +fi # Compatibility functions. [[[ ❮▼❯() { zi "$@"; } -zpcdreplay() { zicdreplay "$@"; } +if false; then + if true; then + zpcdreplay() { zicdreplay "$@"; } + fi +fi false && \ zpcdclear() { zicdclear "$@"; } zpcompinit() { zicompinit "$@"; } diff --git a/tests/public-contract-impact.zsh b/tests/public-contract-impact.zsh index fd0adb41..11073591 100644 --- a/tests/public-contract-impact.zsh +++ b/tests/public-contract-impact.zsh @@ -67,8 +67,9 @@ command git -C "$repository" commit -qm "test: guard compatibility function with output="$(run_detector "$repository" --no-policy)" assert_contains "$output" '`zpcdclear` was removed' assert_contains "$output" '`pmodload` was removed' +assert_contains "$output" '`zpcdreplay` was removed' assert_not_contains "$output" 'false&&zpcdclear' -print "ok - dead conditional function definitions are unavailable across logical lines" +print "ok - dead and nested conditional function definitions are unavailable" repository="$(new_case_repository addition)" command cp "$fixture_root/addition/zi.zsh" "$repository/zi.zsh" @@ -131,6 +132,21 @@ if output="$( fi assert_contains "$output" "Migration plan required" +typeset unclosed_comment_body="## Migration plan +'* ]]; then - remaining="${remaining#*'-->'}" - in_comment=0 + for line in "${(@f)input}"; do + remaining="$line" + visible_line="" + while [[ -n $remaining ]]; do + if (( in_comment )); then + if [[ $remaining == *'-->'* ]]; then + remaining="${remaining#*'-->'}" + visible_line+=" " + in_comment=0 + else + remaining="" + fi + elif [[ $remaining == *'' +if output="$( + PR_LABELS_JSON='["breaking-change"]' \ + PR_BODY="$spanning_comment_body" \ + run_detector "$repository" 2>&1 +)"; then + fail "comment spanning the migration heading passed the breaking-change gate" +fi +assert_contains "$output" "Migration plan required" +assert_contains "$output" "Impact disposition required" + +typeset prose_heading_body="Prose mentioning ## Migration plan is not a heading. +Consumers migrate using https://github.com/z-shell/zi/issues/376." +for marker in "${impact_markers[@]}"; do + prose_heading_body+=$'\n'"${marker} updated here" +done +if output="$( + PR_LABELS_JSON='["breaking-change"]' \ + PR_BODY="$prose_heading_body" \ + run_detector "$repository" 2>&1 +)"; then + fail "migration heading mentioned in prose passed the gate" +fi +assert_contains "$output" "Migration plan required" + +typeset spliced_heading_body="## Migration plan +Consumers migrate using https://github.com/z-shell/zi/issues/376." +for marker in "${impact_markers[@]}"; do + spliced_heading_body+=$'\n'"${marker} updated here" +done +if output="$( + PR_LABELS_JSON='["breaking-change"]' \ + PR_BODY="$spliced_heading_body" \ + run_detector "$repository" 2>&1 +)"; then + fail "comment-spliced migration heading passed the gate" +fi +assert_contains "$output" "Migration plan required" + +typeset spliced_disposition_body="## Migration plan +Consumers migrate using https://github.com/z-shell/zi/issues/376." +for marker in "${impact_markers[@]}"; do + spliced_disposition_body+=$'\n'"${marker} updated here" +done +if output="$( + PR_LABELS_JSON='["breaking-change"]' \ + PR_BODY="$spliced_disposition_body" \ + run_detector "$repository" 2>&1 +)"; then + fail "comment-spliced dispositions passed the gate" +fi +assert_contains "$output" "Impact disposition required" + +typeset fenced_heading_body='```markdown +## Migration plan +Hidden guidance' +for marker in "${impact_markers[@]}"; do + fenced_heading_body+=$'\n'"${marker} updated here" +done +fenced_heading_body+=$'\n```' +if output="$( + PR_LABELS_JSON='["breaking-change"]' \ + PR_BODY="$fenced_heading_body" \ + run_detector "$repository" 2>&1 +)"; then + fail "migration heading in a code fence passed the gate" +fi +assert_contains "$output" "Migration plan required" + typeset commented_body="## Migration plan Consumers migrate using https://github.com/z-shell/zi/issues/376." for marker in "${impact_markers[@]}"; do From d5e8e7faa81e10b066d5c059d691c10d5884312c Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 15 Aug 2026 13:32:27 +0100 Subject: [PATCH 34/44] fix(contract): fail closed on declarations --- contracts/public-contract-v1.json | 12 + scripts/public-contract-impact.zsh | 270 +++++++++--------- .../fixtures/public-contract/addition/zi.zsh | 3 + tests/fixtures/public-contract/base/zi.zsh | 3 + .../fixtures/public-contract/breaking/zi.zsh | 3 + .../public-contract/conditional/zi.zsh | 3 + tests/fixtures/public-contract/foreach/zi.zsh | 3 + tests/fixtures/public-contract/guards/zi.zsh | 32 +++ tests/fixtures/public-contract/nested/zi.zsh | 3 + .../fixtures/public-contract/refactor/zi.zsh | 3 + tests/public-contract-impact.zsh | 37 ++- 11 files changed, 232 insertions(+), 140 deletions(-) create mode 100644 tests/fixtures/public-contract/guards/zi.zsh diff --git a/contracts/public-contract-v1.json b/contracts/public-contract-v1.json index 97c5e9a6..bdd8869d 100644 --- a/contracts/public-contract-v1.json +++ b/contracts/public-contract-v1.json @@ -23,6 +23,7 @@ "kind": "function", "file": "zi.zsh", "symbol": "@zi-register-annex", + "declaration_marker": "# FUNCTION: @zi-register-annex. [[[", "description": "Annex registration API positional signature" }, { @@ -30,6 +31,7 @@ "kind": "function", "file": "zi.zsh", "symbol": "@zi-register-hook", + "declaration_marker": "# FUNCTION: @zi-register-hook. [[[", "description": "Hook registration API positional signature" }, { @@ -38,6 +40,14 @@ "file": "zi.zsh", "start_marker": "# Compatibility functions. [[[", "end_marker": "# ]]]", + "symbols": [ + "❮▼❯", + "zpcdreplay", + "zpcdclear", + "zpcompinit", + "zpcompdef", + "zpextract" + ], "description": "Named compatibility wrapper functions" }, { @@ -45,6 +55,8 @@ "kind": "function", "file": "zi.zsh", "symbol": "pmodload", + "declaration_marker": "# FUNCTION: pmodload. [[[", + "allowed_declaration_prefix": "(( ${+functions[pmodload]} )) || ", "presence_only": true, "description": "Prezto compatibility function" }, diff --git a/scripts/public-contract-impact.zsh b/scripts/public-contract-impact.zsh index 185cbaf6..c4e6d364 100644 --- a/scripts/public-contract-impact.zsh +++ b/scripts/public-contract-impact.zsh @@ -62,6 +62,8 @@ repository="$(command git -C "$repository" rev-parse --show-toplevel)" typeset temp_dir temp_dir="$(command mktemp -d "${TMPDIR:-/tmp}/zi-contract.XXXXXXXX")" trap 'command rm -rf -- "$temp_dir"' EXIT INT TERM +typeset violations_file="$temp_dir/extraction-violations.tsv" +: > "$violations_file" typeset empty_manifest='{"schema_version":1,"contract_version":0,"renames":[],"surfaces":[],"consumers":[]}' typeset base_manifest="$temp_dir/base-manifest.json" @@ -95,11 +97,16 @@ validate_manifest() { elif $surface.kind == "function" then ($surface.file | type == "string" and length > 0) and ($surface.symbol | type == "string" and length > 0) + and ($surface.declaration_marker | type == "string" and length > 0) + and (($surface.allowed_declaration_prefix // "") | type == "string") and (($surface.presence_only // false) | type == "boolean") elif $surface.kind == "function-region" then ($surface.file | type == "string" and length > 0) and ($surface.start_marker | type == "string" and length > 0) and ($surface.end_marker | type == "string" and length > 0) + and ($surface.symbols | type == "array" and length > 0) + and all($surface.symbols[]; type == "string" and length > 0) + and ($surface.symbols | length == (unique | length)) elif $surface.kind == "aliases" then ($surface.file | type == "string" and length > 0) and ($surface.target | type == "string" and length > 0) @@ -178,65 +185,19 @@ extract_token_set() { done | LC_ALL=C sort -u > "$output" } -compound_context_start() { - local trimmed="${1##[[:space:]]#}" - case "$trimmed" in - if[[:space:]]*|case[[:space:]]*|for[[:space:]]*|foreach[[:space:]]*|\ - select[[:space:]]*|\ - while[[:space:]]*|until[[:space:]]*|repeat[[:space:]]*|'('|\ - '('[[:space:]]*|'{'|'{'[[:space:]]*) - return 0 - ;; - esac - return 1 -} - -compound_context_end() { - local trimmed="${1##[[:space:]]#}" - case "$trimmed" in - fi|fi[[:space:]]*|'fi;'*|done|done[[:space:]]*|'done;'*|\ - end|end[[:space:]]*|'end;'*|esac|esac[[:space:]]*|'esac;'*|\ - ')'|')'[[:space:]]*|\ - ');'*|'}'|'}'[[:space:]]*|'};'*) - return 0 - ;; - esac - return 1 -} - -compound_context_inline_closed() { - local code="$1" compact="$2" - local trimmed="${code##[[:space:]]#}" - [[ $compact == *';fi' || $compact == *';done' || $compact == *';end' || - $compact == *';esac' ]] && - return 0 - [[ $compact == *'}' ]] && return 0 - (( ${#compact} > 1 )) && [[ $trimmed == '('* && $compact == *')' ]] -} - -declaration_name() { - local compact="$1" declaration name - REPLY="" - [[ $compact =~ '^(.+)\(\)\{' ]] || return 1 - declaration="${match[1]}" - if [[ $declaration == *'||'* ]]; then - name="${declaration##*'||'}" - elif [[ $declaration == *('&&'|'|'|';')* ]]; then - return 1 - else - name="$declaration" - fi - [[ -n $name && $name != *('&&'|'|'|';')* ]] || return 1 - REPLY="$name" -} - -function_body_arity() { - local source_file="$1" symbol="$2" - local line code compact trimmed pending="" body="" name - integer found=0 compound_depth=0 inside_other_function=0 - integer function_indent=0 target_indent=0 indent=0 +inspect_canonical_function() { + local source_file="$1" symbol="$2" allowed_prefix="$3" marker="$4" + local surface_id="$5" ref="$6" + local line code compact pending="" body="" expected + integer canonical=0 invalid=0 collecting=0 marker_seen=0 awaiting_declaration=0 + expected="${allowed_prefix//[ $'\t']/}${symbol}(){" while IFS= read -r line; do + if [[ $line == "$marker" ]]; then + marker_seen=1 + awaiting_declaration=1 + continue + fi code="${line%%\#*}" if [[ $code == *\\ ]]; then pending+="${code%\\}" @@ -244,50 +205,43 @@ function_body_arity() { fi code="${pending}${code}" pending="" - trimmed="${code##[[:space:]]#}" - indent=$(( ${#code} - ${#trimmed} )) compact="${code//[ $'\t']/}" + [[ -n $compact ]] || continue - if (( found )); then + if (( collecting )); then body+="${code}"$'\n' - [[ $compact == '}' && $indent == $target_indent ]] && break - continue - fi - - if (( inside_other_function )); then - [[ $compact == '}' && $indent == $function_indent ]] && - inside_other_function=0 - continue + [[ $code == '}'* ]] && collecting=0 fi - if compound_context_end "$code"; then - (( compound_depth > 0 )) && compound_depth=$(( compound_depth - 1 )) - continue - fi - - if declaration_name "$compact"; then - name="$REPLY" - if (( compound_depth == 0 )) && [[ $name == "$symbol" ]]; then - found=1 - target_indent=$indent + if [[ $compact == *"${symbol}(){"* || + $compact == *"function${symbol}{"* || + $compact == *"function${symbol}(){"* ]]; then + if (( awaiting_declaration )) && + [[ $code == "${code##[[:space:]]#}" && $compact == "$expected"* ]]; then + canonical=1 + awaiting_declaration=0 body+="${code}"$'\n' - [[ $compact == *'}' ]] && break - elif [[ $compact != *'}' ]]; then - inside_other_function=1 - function_indent=$indent + [[ $compact == *'}' ]] || collecting=1 + else + invalid=1 + if [[ $ref == "$head_ref" ]]; then + print -r -- "${surface_id}"$'\t'"${symbol}"$'\t'"noncanonical declaration" \ + >> "$violations_file" + fi fi - continue - fi - - if compound_context_start "$code"; then - if ! compound_context_inline_closed "$code" "$compact"; then - (( compound_depth += 1 )) - fi - continue + elif (( awaiting_declaration )); then + invalid=1 + awaiting_declaration=0 fi done < "$source_file" - (( found )) || return 1 + (( marker_seen == 1 && invalid == 0 && canonical == 1 )) || { + if [[ $ref == "$head_ref" && ( $marker_seen == 0 || $canonical == 0 ) ]]; then + print -r -- "${surface_id}"$'\t'"${symbol}"$'\t'"missing canonical declaration" \ + >> "$violations_file" + fi + return 1 + } local arity arity="$(print -r -- "$body" | command grep -oE '\$\{?[0-9]+' 2>/dev/null | @@ -298,17 +252,24 @@ function_body_arity() { } extract_function_region() { - local source_file="$1" start_marker="$2" end_marker="$3" output="$4" - local line code compact trimmed name pending="" - integer in_region=0 compound_depth=0 inside_function=0 - integer function_indent=0 indent=0 + local source_file="$1" definition="$2" output="$3" ref="$4" + local surface_id start_marker end_marker line code compact pending="" + local raw="$temp_dir/region-${RANDOM}-${RANDOM}.tsv" + typeset -a symbols + integer in_region=0 inside_function=0 failed=0 index=1 match_index=0 cursor + surface_id="$(jq -r '.id' <<< "$definition")" + start_marker="$(jq -r '.start_marker' <<< "$definition")" + end_marker="$(jq -r '.end_marker' <<< "$definition")" + symbols=( "${(@f)$(jq -r '.symbols[]' <<< "$definition")}" ) + : > "$raw" while IFS= read -r line; do if (( ! in_region )); then - [[ $line == *"$start_marker"* ]] && in_region=1 + [[ $line == "$start_marker" ]] && in_region=1 continue fi - [[ $line == *"$end_marker"* ]] && break + [[ $line == "$end_marker" ]] && break + code="${line%%\#*}" if [[ $code == *\\ ]]; then pending+="${code%\\}" @@ -316,39 +277,42 @@ extract_function_region() { fi code="${pending}${code}" pending="" - trimmed="${code##[[:space:]]#}" - indent=$(( ${#code} - ${#trimmed} )) compact="${code//[ $'\t']/}" + [[ -n $compact ]] || continue if (( inside_function )); then - [[ $compact == '}' && $indent == $function_indent ]] && - inside_function=0 - continue - fi - - if compound_context_end "$code"; then - (( compound_depth > 0 )) && compound_depth=$(( compound_depth - 1 )) + [[ $code == '}'* ]] && inside_function=0 continue fi - if declaration_name "$compact"; then - name="$REPLY" - (( compound_depth == 0 )) && - print -r -- "${name}"$'\t'"variadic" - if [[ $compact != *'}' ]]; then - inside_function=1 - function_indent=$indent + match_index=0 + for (( cursor = index; cursor <= ${#symbols}; cursor += 1 )); do + if [[ $code == "${code##[[:space:]]#}" && + $compact == "${symbols[cursor]}(){"* ]]; then + match_index=$cursor + break fi - continue - fi + done - if compound_context_start "$code"; then - if ! compound_context_inline_closed "$code" "$compact"; then - (( compound_depth += 1 )) + if (( match_index == 0 )); then + failed=1 + if [[ $ref == "$head_ref" ]]; then + print -r -- "${surface_id}"$'\t'"compatibility region"$'\t'"unsupported declaration or content" \ + >> "$violations_file" fi - continue + break fi - done < "$source_file" | LC_ALL=C sort -u > "$output" + + print -r -- "${symbols[match_index]}"$'\t'"variadic" >> "$raw" + index=$(( match_index + 1 )) + [[ $compact == *'}' ]] || inside_function=1 + done < "$source_file" + + if (( failed )); then + : > "$output" + else + LC_ALL=C sort -u "$raw" > "$output" + fi } extract_aliases() { @@ -370,7 +334,7 @@ extract_aliases() { extract_surface() { local ref="$1" definition="$2" output="$3" - local kind file source_file symbol arity + local kind file source_file symbol arity surface_id allowed_prefix marker : > "$output" kind="$(jq -r '.kind' <<< "$definition")" @@ -390,8 +354,12 @@ extract_surface() { extract_token_set "$source_file" "$(jq -r '.assignment' <<< "$definition")" "$output" ;; function) + surface_id="$(jq -r '.id' <<< "$definition")" symbol="$(jq -r '.symbol' <<< "$definition")" - if arity="$(function_body_arity "$source_file" "$symbol")"; then + allowed_prefix="$(jq -r '.allowed_declaration_prefix // ""' <<< "$definition")" + marker="$(jq -r '.declaration_marker' <<< "$definition")" + if arity="$(inspect_canonical_function \ + "$source_file" "$symbol" "$allowed_prefix" "$marker" "$surface_id" "$ref")"; then if [[ $(jq -r '.presence_only // false' <<< "$definition") == true ]]; then arity="present" fi @@ -399,11 +367,7 @@ extract_surface() { fi ;; function-region) - extract_function_region \ - "$source_file" \ - "$(jq -r '.start_marker' <<< "$definition")" \ - "$(jq -r '.end_marker' <<< "$definition")" \ - "$output" + extract_function_region "$source_file" "$definition" "$output" "$ref" ;; aliases) extract_aliases "$source_file" "$(jq -r '.target' <<< "$definition")" "$output" @@ -462,22 +426,43 @@ strip_html_comments() { print -r -- "$visible" } +fence_run_length() { + local text="$1" character="$2" + integer count=0 + while [[ ${text[count+1]-} == "$character" ]]; do + (( count += 1 )) + done + REPLY=$count +} + extract_migration_section() { - local body="$1" line trimmed fence="" - integer found=0 + local body="$1" line trimmed first remainder fence_character="" + integer found=0 fence_length=0 run_length=0 REPLY="" for line in "${(@f)body}"; do trimmed="${line##[[:space:]]#}" - if [[ -n $fence ]]; then - [[ $trimmed == "$fence"* ]] && fence="" - continue - elif [[ $trimmed == '```'* ]]; then - fence='```' - continue - elif [[ $trimmed == '~~~'* ]]; then - fence='~~~' + first="${trimmed[1]-}" + if [[ -n $fence_character ]]; then + if [[ $first == "$fence_character" ]]; then + fence_run_length "$trimmed" "$fence_character" + run_length=$REPLY + remainder="${trimmed[run_length+1,-1]-}" + if (( run_length >= fence_length )) && + [[ -z ${remainder//[[:space:]]/} ]]; then + fence_character="" + fence_length=0 + fi + fi continue + elif [[ $first == '`' || $first == '~' ]]; then + fence_run_length "$trimmed" "$first" + run_length=$REPLY + if (( run_length >= 3 )); then + fence_character="$first" + fence_length=$run_length + continue + fi fi if (( ! found )); then @@ -582,8 +567,8 @@ for surface_id in "${surface_ids[@]}"; do continue fi - base_semantics="$(jq -Sc 'del(.description)' <<< "$base_definition")" - head_semantics="$(jq -Sc 'del(.description)' <<< "$head_definition")" + base_semantics="$(jq -Sc 'del(.description, .symbols)' <<< "$base_definition")" + head_semantics="$(jq -Sc 'del(.description, .symbols)' <<< "$head_definition")" if [[ $base_semantics != "$head_semantics" ]]; then record_impact breaking contract-definition-change "$surface_id" \ "base-definition" "head-definition" \ @@ -598,6 +583,13 @@ for surface_id in "${surface_ids[@]}"; do compare_surface "$surface_id" "$kind" "$base_snapshot" "$head_snapshot" done +if [[ -s $violations_file ]]; then + while IFS=$'\t' read -r surface_id symbol violation; do + record_impact breaking extraction-policy "$surface_id" "$symbol" "" \ + "\`${symbol}\` uses a noncanonical or unsupported declaration form." + done < <(LC_ALL=C sort -u "$violations_file") +fi + compare_consumers() { local base_file="$1" head_file="$2" typeset -A base_consumers=() head_consumers=() diff --git a/tests/fixtures/public-contract/addition/zi.zsh b/tests/fixtures/public-contract/addition/zi.zsh index 0f2dd981..6e21b145 100644 --- a/tests/fixtures/public-contract/addition/zi.zsh +++ b/tests/fixtures/public-contract/addition/zi.zsh @@ -1,14 +1,17 @@ ZI[ice-list]="wait|load|atclone|debug" ZI[cmd-list]="help|load|update|doctor" +# FUNCTION: @zi-register-annex. [[[ @zi-register-annex() { local name="$1" type="$2" handler="$3" helphandler="$4" icemods="$5" } +# FUNCTION: @zi-register-hook. [[[ @zi-register-hook() { local name="$1" type="$2" handler="$3" icemods="$4" } +# FUNCTION: pmodload. [[[ (( ${+functions[pmodload]} )) || pmodload() { print -r -- "$@" } diff --git a/tests/fixtures/public-contract/base/zi.zsh b/tests/fixtures/public-contract/base/zi.zsh index 505d265e..4ed44cb5 100644 --- a/tests/fixtures/public-contract/base/zi.zsh +++ b/tests/fixtures/public-contract/base/zi.zsh @@ -3,14 +3,17 @@ ZI[ice-list]="wait|load|\ ZI[cmd-list]="help|load|\ update" +# FUNCTION: @zi-register-annex. [[[ @zi-register-annex() { local name="$1" type="$2" handler="$3" helphandler="$4" icemods="$5" } +# FUNCTION: @zi-register-hook. [[[ @zi-register-hook() { local name="$1" type="$2" handler="$3" icemods="$4" } +# FUNCTION: pmodload. [[[ (( ${+functions[pmodload]} )) || pmodload() { print -r -- "$@" } diff --git a/tests/fixtures/public-contract/breaking/zi.zsh b/tests/fixtures/public-contract/breaking/zi.zsh index bb115799..84db6167 100644 --- a/tests/fixtures/public-contract/breaking/zi.zsh +++ b/tests/fixtures/public-contract/breaking/zi.zsh @@ -1,14 +1,17 @@ ZI[ice-list]="wait|load|atclone" ZI[cmd-list]="help|fetch|update" +# FUNCTION: @zi-register-annex. [[[ @zi-register-annex() { local name="$1" type="$2" handler="$3" helphandler="$4" } +# FUNCTION: @zi-register-hook. [[[ @zi-register-hook() { local name="$1" type="$2" handler="$3" icemods="$4" } +# FUNCTION: pmodload. [[[ (( ${+functions[pmodload]} )) || pmodload() { print -r -- "$@" } diff --git a/tests/fixtures/public-contract/conditional/zi.zsh b/tests/fixtures/public-contract/conditional/zi.zsh index a82386a5..cd6e2b95 100644 --- a/tests/fixtures/public-contract/conditional/zi.zsh +++ b/tests/fixtures/public-contract/conditional/zi.zsh @@ -1,14 +1,17 @@ ZI[ice-list]="wait|load|atclone" ZI[cmd-list]="help|load|update" +# FUNCTION: @zi-register-annex. [[[ @zi-register-annex() { local name="$1" type="$2" handler="$3" helphandler="$4" icemods="$5" } +# FUNCTION: @zi-register-hook. [[[ @zi-register-hook() { local name="$1" type="$2" handler="$3" icemods="$4" } +# FUNCTION: pmodload. [[[ ( pmodload() { print -r -- "$@" diff --git a/tests/fixtures/public-contract/foreach/zi.zsh b/tests/fixtures/public-contract/foreach/zi.zsh index a4f0ab39..28aa4273 100644 --- a/tests/fixtures/public-contract/foreach/zi.zsh +++ b/tests/fixtures/public-contract/foreach/zi.zsh @@ -1,14 +1,17 @@ ZI[ice-list]="wait|load|atclone" ZI[cmd-list]="help|load|update" +# FUNCTION: @zi-register-annex. [[[ @zi-register-annex() { local name="$1" type="$2" handler="$3" helphandler="$4" icemods="$5" } +# FUNCTION: @zi-register-hook. [[[ @zi-register-hook() { local name="$1" type="$2" handler="$3" icemods="$4" } +# FUNCTION: pmodload. [[[ foreach item (one) pmodload() { print -r -- "$@" diff --git a/tests/fixtures/public-contract/guards/zi.zsh b/tests/fixtures/public-contract/guards/zi.zsh new file mode 100644 index 00000000..6ad47cc6 --- /dev/null +++ b/tests/fixtures/public-contract/guards/zi.zsh @@ -0,0 +1,32 @@ +ZI[ice-list]="wait|load|atclone" +ZI[cmd-list]="help|load|update" + +# FUNCTION: @zi-register-annex. [[[ +false && { + @zi-register-annex() { + local name="$1" type="$2" handler="$3" helphandler="$4" icemods="$5" + } +} + +# FUNCTION: @zi-register-hook. [[[ +true || @zi-register-hook() { + local name="$1" type="$2" handler="$3" icemods="$4" +} + +# FUNCTION: pmodload. [[[ +true || pmodload() { + print -r -- "$@" +} + +# Compatibility functions. [[[ +❮▼❯() { zi "$@"; } +true || zpcdreplay() { zicdreplay "$@"; } +false && { + zpcdclear() { zicdclear "$@"; } +} +custom_guard || zpcompinit() { zicompinit "$@"; } +zpcompdef() { zicompdef "$@"; } +zpextract() { ziextract "$@"; } +# ]]] + +(( ZI[INTERNAL_ALIASES] )) && builtin alias zini=zi zinit=zi zplugin=zi diff --git a/tests/fixtures/public-contract/nested/zi.zsh b/tests/fixtures/public-contract/nested/zi.zsh index 82a90542..4273fda5 100644 --- a/tests/fixtures/public-contract/nested/zi.zsh +++ b/tests/fixtures/public-contract/nested/zi.zsh @@ -1,14 +1,17 @@ ZI[ice-list]="wait|load|atclone" ZI[cmd-list]="help|load|update" +# FUNCTION: @zi-register-annex. [[[ @zi-register-annex() { local name="$1" type="$2" handler="$3" helphandler="$4" icemods="$5" } +# FUNCTION: @zi-register-hook. [[[ @zi-register-hook() { local name="$1" type="$2" handler="$3" icemods="$4" } +# FUNCTION: pmodload. [[[ outer_runtime() { helper_runtime() { : diff --git a/tests/fixtures/public-contract/refactor/zi.zsh b/tests/fixtures/public-contract/refactor/zi.zsh index 26bf902a..282f3229 100644 --- a/tests/fixtures/public-contract/refactor/zi.zsh +++ b/tests/fixtures/public-contract/refactor/zi.zsh @@ -4,6 +4,7 @@ ZI[ice-list]="atclone|\ ZI[cmd-list]="update|help|\ load" +# FUNCTION: @zi-register-annex. [[[ @zi-register-annex () { # $99 in a comment must not narrow or widen the signature. local name="$1" @@ -11,6 +12,7 @@ ZI[cmd-list]="update|help|\ local helphandler="$4" icemods="$5" } +# FUNCTION: @zi-register-hook. [[[ @zi-register-hook() { local name="$1" local type="$2" @@ -18,6 +20,7 @@ ZI[cmd-list]="update|help|\ local icemods="$4" } +# FUNCTION: pmodload. [[[ (( ${+functions[pmodload]} )) || pmodload () { print -r -- "$@" } diff --git a/tests/public-contract-impact.zsh b/tests/public-contract-impact.zsh index 740db568..db705abb 100644 --- a/tests/public-contract-impact.zsh +++ b/tests/public-contract-impact.zsh @@ -72,7 +72,6 @@ assert_contains "$output" '`zpcompinit` was removed' assert_contains "$output" '`zpcompdef` was removed' assert_contains "$output" '`zpextract` was removed' assert_not_contains "$output" 'false&&zpcdclear' -assert_not_contains "$output" '`❮▼❯` was removed' print "ok - only executable top-level function declarations are available" repository="$(new_case_repository foreach)" @@ -92,8 +91,26 @@ assert_contains "$output" '`pmodload` was removed' assert_contains "$output" '`zpcdclear` was removed' print "ok - nested function declarations are not top-level" +repository="$(new_case_repository guards)" +command cp "$fixture_root/guards/zi.zsh" "$repository/zi.zsh" +command git -C "$repository" add . +command git -C "$repository" commit -qm "test: use unsupported declaration guards" +output="$(run_detector "$repository" --no-policy)" +assert_contains "$output" '`zpcdreplay` was removed' +assert_contains "$output" '`zpcdclear` was removed' +assert_contains "$output" '`zpcompinit` was removed' +assert_contains "$output" '`pmodload` was removed' +assert_contains "$output" '`@zi-register-annex` was removed' +assert_contains "$output" '`@zi-register-hook` was removed' +assert_contains "$output" '`extraction-policy`' +print "ok - only manifested declaration guards are accepted" + repository="$(new_case_repository addition)" command cp "$fixture_root/addition/zi.zsh" "$repository/zi.zsh" +changed_manifest="${repository}/contracts/public-contract-v1.json.new" +jq '(.surfaces[] | select(.id == "compatibility-functions").symbols) += ["zplegacy"]' \ + "$repository/contracts/public-contract-v1.json" > "$changed_manifest" +command mv "$changed_manifest" "$repository/contracts/public-contract-v1.json" command git -C "$repository" add . command git -C "$repository" commit -qm "feat: add contracts" output="$(run_detector "$repository")" @@ -244,6 +261,24 @@ if output="$( fi assert_contains "$output" "Migration plan required" +typeset long_fence_body='````markdown +## Migration plan +Hidden guidance +``` +Still hidden after a shorter fence' +for marker in "${impact_markers[@]}"; do + long_fence_body+=$'\n'"${marker} updated here" +done +long_fence_body+=$'\n````' +if output="$( + PR_LABELS_JSON='["breaking-change"]' \ + PR_BODY="$long_fence_body" \ + run_detector "$repository" 2>&1 +)"; then + fail "shorter embedded fence closed a longer migration fence" +fi +assert_contains "$output" "Migration plan required" + typeset commented_body="## Migration plan Consumers migrate using https://github.com/z-shell/zi/issues/376." for marker in "${impact_markers[@]}"; do From c58d21d355bd0f04bf9cdd244c01e54bd1e7f442 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 22 Aug 2026 13:14:41 +0100 Subject: [PATCH 35/44] fix(contract): close remaining extraction gaps --- scripts/public-contract-impact.zsh | 44 ++++++++++++------- .../public-contract/dead-alias/zi.zsh | 30 +++++++++++++ .../public-contract/multiple-renames/zi.zsh | 30 +++++++++++++ tests/public-contract-impact.zsh | 31 +++++++++++++ 4 files changed, 118 insertions(+), 17 deletions(-) create mode 100644 tests/fixtures/public-contract/dead-alias/zi.zsh create mode 100644 tests/fixtures/public-contract/multiple-renames/zi.zsh diff --git a/scripts/public-contract-impact.zsh b/scripts/public-contract-impact.zsh index c4e6d364..b2e07370 100644 --- a/scripts/public-contract-impact.zsh +++ b/scripts/public-contract-impact.zsh @@ -1,4 +1,6 @@ #!/usr/bin/env zsh +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et emulate -LR zsh setopt errexit nounset pipefail extendedglob @@ -317,11 +319,14 @@ extract_function_region() { extract_aliases() { local source_file="$1" target="$2" output="$3" - local line code token name alias_target + local line code prefix compact_prefix token name alias_target while IFS= read -r line; do code="${line%%\#*}" [[ $code == *"builtin alias"* ]] || continue + prefix="${code%%builtin alias*}" + compact_prefix="${prefix//[ $'\t']/}" + [[ $compact_prefix == false'&&'* || $compact_prefix == true'||'* ]] && continue for token in ${(z)code}; do [[ $token == *=* ]] || continue name="${token%%=*}" @@ -486,9 +491,9 @@ record_impact() { compare_surface() { local surface_id="$1" kind="$2" base_snapshot="$3" head_snapshot="$4" - typeset -A base_items=() head_items=() + typeset -A base_items=() head_items=() matched_removals=() matched_additions=() typeset -a additions=() removals=() - local item detail + local item detail rename_to while IFS=$'\t' read -r item detail; do [[ -n $item ]] && base_items[$item]="$detail" @@ -504,23 +509,28 @@ compare_surface() { (( ${+base_items[$item]} )) || additions+=( "$item" ) done - if (( ${#removals} == 1 && ${#additions} == 1 )) && - jq -e \ + for item in "${removals[@]}"; do + rename_to="$(jq -r \ --arg surface "$surface_id" \ - --arg from "$removals[1]" \ - --arg to "$additions[1]" \ - 'any(.renames[]?; .surface == $surface and .from == $from and .to == $to)' \ - "$head_manifest" >/dev/null; then - record_impact breaking rename "$surface_id" "$removals[1]" "$additions[1]" \ - "\`${removals[1]}\` was replaced by \`${additions[1]}\`." - else - for item in "${removals[@]}"; do + --arg from "$item" \ + '[.renames[]? | select(.surface == $surface and .from == $from) | .to][0] // empty' \ + "$head_manifest")" + [[ -n $rename_to && ${+head_items[$rename_to]} == 1 && + ${+matched_additions[$rename_to]} == 0 ]] || continue + matched_removals[$item]=1 + matched_additions[$rename_to]=1 + record_impact breaking rename "$surface_id" "$item" "$rename_to" \ + "\`${item}\` was replaced by \`${rename_to}\`." + done + + for item in "${removals[@]}"; do + (( ${+matched_removals[$item]} )) || record_impact breaking removal "$surface_id" "$item" "" "\`${item}\` was removed." - done - for item in "${additions[@]}"; do + done + for item in "${additions[@]}"; do + (( ${+matched_additions[$item]} )) || record_impact info addition "$surface_id" "" "$item" "\`${item}\` was added." - done - fi + done for item in ${(ok)base_items}; do (( ${+head_items[$item]} )) || continue diff --git a/tests/fixtures/public-contract/dead-alias/zi.zsh b/tests/fixtures/public-contract/dead-alias/zi.zsh new file mode 100644 index 00000000..75958034 --- /dev/null +++ b/tests/fixtures/public-contract/dead-alias/zi.zsh @@ -0,0 +1,30 @@ +ZI[ice-list]="wait|load|\ + atclone" +ZI[cmd-list]="help|load|\ + update" + +# FUNCTION: @zi-register-annex. [[[ +@zi-register-annex() { + local name="$1" type="$2" handler="$3" helphandler="$4" icemods="$5" +} + +# FUNCTION: @zi-register-hook. [[[ +@zi-register-hook() { + local name="$1" type="$2" handler="$3" icemods="$4" +} + +# FUNCTION: pmodload. [[[ +(( ${+functions[pmodload]} )) || pmodload() { + print -r -- "$@" +} + +# Compatibility functions. [[[ +❮▼❯() { zi "$@"; } +zpcdreplay() { zicdreplay "$@"; } +zpcdclear() { zicdclear "$@"; } +zpcompinit() { zicompinit "$@"; } +zpcompdef() { zicompdef "$@"; } +zpextract() { ziextract "$@"; } +# ]]] + +false && builtin alias zini=zi zinit=zi zplugin=zi diff --git a/tests/fixtures/public-contract/multiple-renames/zi.zsh b/tests/fixtures/public-contract/multiple-renames/zi.zsh new file mode 100644 index 00000000..124b353f --- /dev/null +++ b/tests/fixtures/public-contract/multiple-renames/zi.zsh @@ -0,0 +1,30 @@ +ZI[ice-list]="wait|load|\ + atclone" +ZI[cmd-list]="help|fetch|\ + upgrade" + +# FUNCTION: @zi-register-annex. [[[ +@zi-register-annex() { + local name="$1" type="$2" handler="$3" helphandler="$4" icemods="$5" +} + +# FUNCTION: @zi-register-hook. [[[ +@zi-register-hook() { + local name="$1" type="$2" handler="$3" icemods="$4" +} + +# FUNCTION: pmodload. [[[ +(( ${+functions[pmodload]} )) || pmodload() { + print -r -- "$@" +} + +# Compatibility functions. [[[ +❮▼❯() { zi "$@"; } +zpcdreplay() { zicdreplay "$@"; } +zpcdclear() { zicdclear "$@"; } +zpcompinit() { zicompinit "$@"; } +zpcompdef() { zicompdef "$@"; } +zpextract() { ziextract "$@"; } +# ]]] + +(( ZI[INTERNAL_ALIASES] )) && builtin alias zini=zi zinit=zi zplugin=zi diff --git a/tests/public-contract-impact.zsh b/tests/public-contract-impact.zsh index db705abb..9c5f2510 100644 --- a/tests/public-contract-impact.zsh +++ b/tests/public-contract-impact.zsh @@ -1,4 +1,6 @@ #!/usr/bin/env zsh +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et emulate -LR zsh setopt errexit nounset pipefail @@ -74,6 +76,16 @@ assert_contains "$output" '`zpextract` was removed' assert_not_contains "$output" 'false&&zpcdclear' print "ok - only executable top-level function declarations are available" +repository="$(new_case_repository dead-alias)" +command cp "$fixture_root/dead-alias/zi.zsh" "$repository/zi.zsh" +command git -C "$repository" add . +command git -C "$repository" commit -qm "test: guard compatibility aliases with false" +output="$(run_detector "$repository" --no-policy)" +assert_contains "$output" '`zini` was removed' +assert_contains "$output" '`zinit` was removed' +assert_contains "$output" '`zplugin` was removed' +print "ok - statically dead alias declarations are unavailable" + repository="$(new_case_repository foreach)" command cp "$fixture_root/foreach/zi.zsh" "$repository/zi.zsh" command git -C "$repository" add . @@ -130,6 +142,25 @@ assert_contains "$output" '`fetch` was added' assert_not_contains "$output" '`rename`' print "ok - independent removals and additions are not inferred as renames" +repository="$(new_case_repository multiple-renames)" +command cp "$fixture_root/multiple-renames/zi.zsh" "$repository/zi.zsh" +changed_manifest="${repository}/contracts/public-contract-v1.json.new" +jq '.renames += [ + {"surface":"commands","from":"load","to":"fetch"}, + {"surface":"commands","from":"update","to":"upgrade"} +]' "$repository/contracts/public-contract-v1.json" > "$changed_manifest" +command mv "$changed_manifest" "$repository/contracts/public-contract-v1.json" +command git -C "$repository" add . +command git -C "$repository" commit -qm "feat: rename multiple commands" +output="$(run_detector "$repository" --no-policy)" +assert_contains "$output" '`load` was replaced by `fetch`' +assert_contains "$output" '`update` was replaced by `upgrade`' +assert_not_contains "$output" '`load` was removed' +assert_not_contains "$output" '`update` was removed' +assert_not_contains "$output" '`fetch` was added' +assert_not_contains "$output" '`upgrade` was added' +print "ok - each declared rename is matched independently" + repository="$(new_case_repository breaking)" command cp "$fixture_root/breaking/zi.zsh" "$repository/zi.zsh" command rm "$repository/lib/zsh/git-process-output.zsh" From 76aaf176c7d1dda9312b23fd2561fcd2e10e0cb4 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 22 Aug 2026 13:16:34 +0100 Subject: [PATCH 36/44] test(contract): cover one-line registrations --- .../public-contract/one-line-functions/zi.zsh | 23 +++++++++++++++++++ tests/public-contract-impact.zsh | 8 +++++++ 2 files changed, 31 insertions(+) create mode 100644 tests/fixtures/public-contract/one-line-functions/zi.zsh diff --git a/tests/fixtures/public-contract/one-line-functions/zi.zsh b/tests/fixtures/public-contract/one-line-functions/zi.zsh new file mode 100644 index 00000000..ba0aaa68 --- /dev/null +++ b/tests/fixtures/public-contract/one-line-functions/zi.zsh @@ -0,0 +1,23 @@ +ZI[ice-list]="wait|load|atclone" +ZI[cmd-list]="help|load|update" + +# FUNCTION: @zi-register-annex. [[[ +@zi-register-annex() { local name="$1" type="$2" handler="$3" helphandler="$4" icemods="$5"; } + +# FUNCTION: @zi-register-hook. [[[ +@zi-register-hook() { local name="$1" type="$2" handler="$3" icemods="$4"; } + +# FUNCTION: pmodload. [[[ +(( ${+functions[pmodload]} )) || pmodload() { print -r -- "$@"; } + +# Compatibility functions. [[[ +❮▼❯() { zi "$@"; } +zpcdreplay() { zicdreplay "$@"; } +zpcdclear() { zicdclear "$@"; } +zpcompinit() { zicompinit "$@"; } +zpcompdef() { zicompdef "$@"; } +zpextract() { ziextract "$@"; } +# ]]] + +internal_helper() { print -r -- "$9"; } +(( ZI[INTERNAL_ALIASES] )) && builtin alias zini=zi zinit=zi zplugin=zi diff --git a/tests/public-contract-impact.zsh b/tests/public-contract-impact.zsh index 9c5f2510..f6dd8e02 100644 --- a/tests/public-contract-impact.zsh +++ b/tests/public-contract-impact.zsh @@ -62,6 +62,14 @@ output="$(run_detector "$repository" --no-policy)" assert_contains "$output" "No public-contract changes detected." print "ok - comments, formatting, ordering, and internal refactors are ignored" +repository="$(new_case_repository one-line-functions)" +command cp "$fixture_root/one-line-functions/zi.zsh" "$repository/zi.zsh" +command git -C "$repository" add . +command git -C "$repository" commit -qm "refactor: use one-line registration functions" +output="$(run_detector "$repository" --no-policy)" +assert_contains "$output" "No public-contract changes detected." +print "ok - one-line registration functions do not absorb later argument references" + repository="$(new_case_repository conditional)" command cp "$fixture_root/conditional/zi.zsh" "$repository/zi.zsh" command git -C "$repository" add . From a00bd1ac183a6720a0b4b7597584e5d50a0cd5c8 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 22 Aug 2026 13:27:55 +0100 Subject: [PATCH 37/44] fix(ci): align commit lint with org policy --- .github/workflows/commit-lint.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml index 1ff1d2d5..5e858617 100644 --- a/.github/workflows/commit-lint.yml +++ b/.github/workflows/commit-lint.yml @@ -30,7 +30,8 @@ jobs: env: BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} - DISALLOWED_TRAILER_PATTERN: "^[[:space:]]*Co-authored-by:" + POLICY_BASELINE_SHA: d092066be26d4ec5aedc7a19ed3d85e03a343b35 + DISALLOWED_TRAILER_PATTERN: '^[[:space:]]*Co-authored-by:.*(\[bot\]|noreply@anthropic\.com|noreply@openai\.com|noreply@x\.ai|codex@openai\.com|copilot@github\.com|claude@anthropic\.com)' run: | set -euo pipefail @@ -48,6 +49,13 @@ jobs: parent_count=$(git cat-file -p "$sha" | grep -c '^parent' || true) [ "$parent_count" -gt 1 ] && continue + # The repository adopted enforced Conventional Commits at this + # baseline. Preserve older history instead of requiring a rewrite + # when the persistent next branch is promoted. + if git merge-base --is-ancestor "$sha" "$POLICY_BASELINE_SHA"; then + continue + fi + subject=$(git show -s --format='%s' "$sha") checked=$((checked + 1)) From 65bb825248fa815ff38ed8a6dac4f3613ced26aa Mon Sep 17 00:00:00 2001 From: Sal <59910950+ss-o@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:31:27 +0100 Subject: [PATCH 38/44] docs(readme): rebuild project landing page Replace the broken fallback README with a modern repository landing page, an accurate Zi overview, and a practical quick start. Related to #276. --- README.md | 120 +++++++++++++++++++++++++++++++ docs/README.md | 186 ------------------------------------------------- 2 files changed, 120 insertions(+), 186 deletions(-) create mode 100644 README.md delete mode 100644 docs/README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..f084ad23 --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +

+ Zi logo +

+ +

Zi

+ +

+ A high-performance plugin manager and toolkit for Zsh. +

+ +

+ Install plugins, snippets, completions, themes, and command-line tools with precise control over how they are + downloaded, configured, and loaded. +

+ +

+ Latest release + MIT License + Zsh +

+ +

+ Quick start +  ·  + Documentation +  ·  + Releases +  ·  + Discussions +

+ +Zi is the Z-Shell ecosystem's plugin manager. It can source a single remote +script, manage a complete plugin, install a binary release, run build steps, or +defer work until after the first prompt. Zi was formerly known as zplugin and +zinit. + +## Built for real Zsh setups + +| Capability | What it gives you | +| :--------------------- | :------------------------------------------------------------------------------------------------------------ | +| **Responsive startup** | Turbo mode defers plugin work until after the prompt without giving up declarative configuration. | +| **Flexible sources** | Load Git repositories, standalone scripts, Oh My Zsh and Prezto snippets, completions, and release artifacts. | +| **Precise control** | Ice modifiers describe download, build, selection, loading, and update behavior for each dependency. | +| **Tool management** | Install command-line tools and packages without root access, then expose them through your shell environment. | +| **Extensible design** | Annexes add focused capabilities without expanding the core into a monolith. | + +## Quick start + +### 1. Install Zi + +Run the minimal installer. It downloads Zi and adds the minimal setup to your +`.zshrc`: + +```sh +sh -c "$(curl -fsSL get.zshell.dev)" -- +``` + +> Review the +> [installer source](https://raw.githubusercontent.com/z-shell/src/main/public/sh/install.sh) +> and its published [SHA-256 checksum](https://raw.githubusercontent.com/z-shell/src/main/public/checksum.txt) +> before running a remote installation script. + +### 2. Add your first plugins + +Add Zi commands to `.zshrc`. This example loads an Oh My Zsh snippet, command +suggestions, and fast syntax highlighting: + +```zsh +# ~/.zshrc +zi snippet OMZ::plugins/git/git.plugin.zsh +zi light zsh-users/zsh-autosuggestions +zi light z-shell/F-Sy-H +``` + +`snippet` loads a standalone script. `light` loads a plugin without tracking +and reporting overhead. + +### 3. Reload Zsh + +```zsh +exec zsh -il +``` + +Run `zi -h` to explore the available commands. For alternate installers, +manual setup, completions, and post-install steps, see the full +[installation guide](https://wiki.zshell.dev/docs/getting_started/installation/). + +## Learn by task + +| I want to... | Read... | +| :----------------------------------------------- | :------------------------------------------------------------------------- | +| Understand plugin and snippet loading | [General overview](https://wiki.zshell.dev/docs/getting_started/overview/) | +| Control how a dependency is installed and loaded | [Ice modifiers](https://wiki.zshell.dev/docs/guides/syntax/ice-modifiers/) | +| Customize Zi paths and behavior | [Configuration](https://wiki.zshell.dev/docs/guides/customization/) | +| Explore commands and maintenance workflows | [Commands](https://wiki.zshell.dev/docs/guides/commands/) | +| Discover plugins, annexes, and packages | [Ecosystem](https://wiki.zshell.dev/ecosystem/) | + +## Community and support + +- Ask usage questions in [Z-Shell Discussions](https://github.com/orgs/z-shell/discussions). +- Report reproducible defects through [Zi issues](https://github.com/z-shell/zi/issues/new/choose). +- Browse releases and changelogs on the [releases page](https://github.com/z-shell/zi/releases). +- Explore the wider [Z-Shell organization](https://github.com/z-shell). + +## Contributing + +Contributions are welcome. Read the [contribution guide](docs/CONTRIBUTING.md) +before opening an issue or pull request. Development work targets the `next` +branch, while `main` contains production releases. + +## Security + +Please follow the +[Z-Shell security policy](https://github.com/z-shell/.github/security/policy) +to report vulnerabilities privately. Do not disclose a vulnerability in a +public issue before a fix is available. + +## License + +Zi is available under the [MIT License](LICENSE). diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 15af91fe..00000000 --- a/docs/README.md +++ /dev/null @@ -1,186 +0,0 @@ -

- ❮ Zi ❯ Logo + Zi Logo ❮ Zi ❯

🧙‍♂️ 🪄 A Swiss Army Knife for Zsh - - Unix Shell ✨ +

Unix Shell ✨

-
- +
-
- - + +

- Zi Logo + Zi Logo ❮ Zi ❯

@@ -37,10 +37,10 @@ Version Project License - VIM + VIM - Visual Studio Code -

+ Visual Studio Code +

- + Zi demonstration recording
- - + Zi repository activity metrics + Z-Shell organization metrics
- - - - - - - - - - - - - - - -
-

- - Zi Logo - ❮ Zi ❯ -

-

- 🧙‍♂️ 🪄 A Swiss Army Knife for Zsh - -

Unix Shell ✨

-

- -
- -
-

- Zi Matrix - Zi Gitter Matrix - Crowdin localization status - Version - Project License - VIM - - Visual Studio Code -

-
-
-
- Zi demonstration recording -
-
-
- Zi repository activity metrics - Z-Shell organization metrics -
-
-
-

🎯 Roadmap

-
- Generally, new features will be introduced as a component to improve performance and compatibility and not to - force, but to allow the user decide and - choose the features. Let us know what should be improved and - maintained with a ⭐ on features you like and use:

-
- - -
-

💞 Contributing

-
- Contributions are what make the open-source community such an amazing place to learn, inspire, and create. - Any contributions you make will benefit everybody else and greatly appreciated and our - target support each other. -
-
- Please read our contribution - guidelines - before you start. Thank you for being involved! -
-
-

🆙 Project assistance

-
If you want to say thank you or/and support the active development, add a GitHub Star to the project, and follow the - Z-Shell organization. -

-
Write interesting articles about the project on: - Dev.to, Medium.com, - Hacker News, - Reddit, Twitter, or your blog. - Participate in the community by showing interest - in the project, as it may bring us together, then great things may happen. If you want but are not confident - about participating - do not hesitate to contact us so we can discuss the situation 👍. -
-
-

🛡️ Security

-
- - Z-Shell Zi follows good practices of security, but 100% security cannot be assured.
-
- - Z-Shell Zi is provided "as is" without any warranty. Use at your - own risk. -
-
- - For more information and to report security issues, please refer to our - security - documentation. -
-
-

ℹ️ Acknowledgements

-
- The Z-Shell organization was created to recover the - Zdharma organization which was deleted by the owner for an unknown - reason. It took a lot of time and effort for all those who liked the project and were depending on it don't - want to depend on an unreliable source. -
-
- - Zi - , formerly known as zplugin, zinit, is an open-source - community project released under the - MIT License. -
-
-

🥇 Authors & contributors

-
- Check the list of authors and contributors in this - repository and all other repositories under the - Z-Shell organization. We will include - all showing interest or dedicating their time to taking part. -
-
-

⭐ Credits

- From 570a6e6400c76b3a62cc1d2862b45517ba569485 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 22 Aug 2026 17:54:06 +0100 Subject: [PATCH 39/44] fix(ci): repair ZD integration workflow --- .github/workflows/zd-integration.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/zd-integration.yml b/.github/workflows/zd-integration.yml index 1685af9a..1f0d5f6c 100644 --- a/.github/workflows/zd-integration.yml +++ b/.github/workflows/zd-integration.yml @@ -5,10 +5,12 @@ on: push: branches: [main, next] paths: + - ".github/workflows/zd-integration.yml" - "zi.zsh" - "lib/**" pull_request: paths: + - ".github/workflows/zd-integration.yml" - "zi.zsh" - "lib/**" workflow_dispatch: {} @@ -22,7 +24,8 @@ permissions: jobs: zd-test: - uses: z-shell/zd/.github/workflows/test-native.yml@191642eaf9587e64f910c79aaf750d2cfe237241 # z-shell/zd#95, #96 + uses: z-shell/zd/.github/workflows/test-native.yml@fd0995d6f7958fea6c4abbc4fff81a6da2277b66 # z-shell/zd#95, #96, #107 with: zi_repo: ${{ github.event.pull_request.head.repo.full_name || github.repository }} zi_ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + include_compat: false From c751c663cca75f1a3fad808db74b900221ce5395 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 22 Aug 2026 17:56:51 +0100 Subject: [PATCH 40/44] fix(ci): separate ZD concurrency group --- .github/workflows/zd-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/zd-integration.yml b/.github/workflows/zd-integration.yml index 1f0d5f6c..3515b92b 100644 --- a/.github/workflows/zd-integration.yml +++ b/.github/workflows/zd-integration.yml @@ -16,7 +16,7 @@ on: workflow_dispatch: {} concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref }}-caller cancel-in-progress: true permissions: From f3a5e39b94c6a97fa02f26e30f6881c56f9378c7 Mon Sep 17 00:00:00 2001 From: Sal <59910950+ss-o@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:58:47 +0100 Subject: [PATCH 41/44] fix(ci): repair ZD integration workflow Prevent reusable-workflow concurrency deadlocks, update the ZD workflow pin and input contract, and make workflow-only changes self-validating. Part of #377. --- .github/workflows/zd-integration.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/zd-integration.yml b/.github/workflows/zd-integration.yml index 1685af9a..3515b92b 100644 --- a/.github/workflows/zd-integration.yml +++ b/.github/workflows/zd-integration.yml @@ -5,16 +5,18 @@ on: push: branches: [main, next] paths: + - ".github/workflows/zd-integration.yml" - "zi.zsh" - "lib/**" pull_request: paths: + - ".github/workflows/zd-integration.yml" - "zi.zsh" - "lib/**" workflow_dispatch: {} concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref }}-caller cancel-in-progress: true permissions: @@ -22,7 +24,8 @@ permissions: jobs: zd-test: - uses: z-shell/zd/.github/workflows/test-native.yml@191642eaf9587e64f910c79aaf750d2cfe237241 # z-shell/zd#95, #96 + uses: z-shell/zd/.github/workflows/test-native.yml@fd0995d6f7958fea6c4abbc4fff81a6da2277b66 # z-shell/zd#95, #96, #107 with: zi_repo: ${{ github.event.pull_request.head.repo.full_name || github.repository }} zi_ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + include_compat: false From b0e8083e995427c93d84474e061e0a6fd2121e0b Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 22 Aug 2026 21:02:25 +0100 Subject: [PATCH 42/44] test(version): cover version reporting contract Exercise version reporting for tagged, untagged, and archive checkouts in ordinary and promotion CI. Require a manual historical inventory when contract monitoring is introduced without a base definition. --- .github/PULL_REQUEST_TEMPLATE/promotion.md | 5 ++ .github/workflows/promotion-readiness.yml | 3 + .github/workflows/zsh-n.yml | 13 ++++ tests/version-reporting.zsh | 76 ++++++++++++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 tests/version-reporting.zsh diff --git a/.github/PULL_REQUEST_TEMPLATE/promotion.md b/.github/PULL_REQUEST_TEMPLATE/promotion.md index e6b92ac9..49516c84 100644 --- a/.github/PULL_REQUEST_TEMPLATE/promotion.md +++ b/.github/PULL_REQUEST_TEMPLATE/promotion.md @@ -52,6 +52,11 @@ Link documentation and consumer follow-ups identified by the [#376 public-contract impact review](https://github.com/z-shell/zi/issues/376). Write `None` only with a rationale. +- [ ] If the candidate introduces monitoring for a contract surface that is + absent from the base, manually inventory that surface across the complete + compare. The detector can report the new monitoring definition, but it + cannot classify changes that predate that baseline. + ## Rollback readiness - Rollback owner: `@________________` diff --git a/.github/workflows/promotion-readiness.yml b/.github/workflows/promotion-readiness.yml index 39c18336..f161b1cd 100644 --- a/.github/workflows/promotion-readiness.yml +++ b/.github/workflows/promotion-readiness.yml @@ -45,6 +45,9 @@ jobs: exit 1 fi + - name: Test version reporting + run: zsh tests/version-reporting.zsh + zd: name: ZD integration uses: z-shell/zd/.github/workflows/test-native.yml@fd0995d6f7958fea6c4abbc4fff81a6da2277b66 # z-shell/zd#95, #96, #107 diff --git a/.github/workflows/zsh-n.yml b/.github/workflows/zsh-n.yml index 92c87373..9fdf16d8 100644 --- a/.github/workflows/zsh-n.yml +++ b/.github/workflows/zsh-n.yml @@ -9,10 +9,12 @@ on: paths: - "zi.zsh" - "lib/**" + - "tests/version-reporting.zsh" pull_request: paths: - "zi.zsh" - "lib/**" + - "tests/version-reporting.zsh" workflow_dispatch: {} concurrency: @@ -59,3 +61,14 @@ jobs: run: | zsh -fc "zcompile ${ZSH_FILE}"; rc=$? ls -al "${ZSH_FILE}.zwc"; exit "$rc" + + version-reporting: + name: Version reporting + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Zsh + run: sudo apt update && sudo apt-get install -yq zsh + - name: Test version reporting + run: zsh tests/version-reporting.zsh diff --git a/tests/version-reporting.zsh b/tests/version-reporting.zsh new file mode 100644 index 00000000..9f8cc9bd --- /dev/null +++ b/tests/version-reporting.zsh @@ -0,0 +1,76 @@ +#!/usr/bin/env zsh +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et + +builtin emulate -LR zsh +setopt errexit nounset pipefail + +typeset project_root="${0:A:h:h}" +typeset temp_root +temp_root="$(command mktemp -d "${TMPDIR:-/tmp}/zi-version-test.XXXXXXXX")" +trap 'command rm -rf -- "$temp_root"' EXIT INT TERM + +fail() { + print -u2 "not ok - $1" + exit 1 +} + +run_case() { + local label="$1" checkout="$2" expected="$3" + local case_root="${temp_root}/${label}" + command mkdir -p \ + "${case_root}/home" \ + "${case_root}/cache" \ + "${case_root}/config" \ + "${case_root}/data" \ + "${case_root}/zdotdir" + + env \ + HOME="${case_root}/home" \ + XDG_CACHE_HOME="${case_root}/cache" \ + XDG_CONFIG_HOME="${case_root}/config" \ + XDG_DATA_HOME="${case_root}/data" \ + ZDOTDIR="${case_root}/zdotdir" \ + ZI_TEST_CHECKOUT="$checkout" \ + ZI_TEST_EXPECTED="$expected" \ + zsh -f <<'ZSH' || fail "$label" +builtin emulate -LR zsh +setopt errexit pipefail + +typeset -gAH ZI +ZI[BIN_DIR]="$ZI_TEST_CHECKOUT" +builtin source "${ZI[BIN_DIR]}/zi.zsh" + +[[ ${ZI[VERSION]} == "$ZI_TEST_EXPECTED" ]] + +typeset argument output +for argument in version --version -V; do + output="$(zi "$argument")" + [[ $output == *"Zi version: ${ZI_TEST_EXPECTED}"* ]] +done +ZSH + + print "ok - ${label}" +} + +typeset untagged_checkout="${temp_root}/untagged-checkout" +command git clone -q --no-hardlinks "$project_root" "$untagged_checkout" +command git -C "$untagged_checkout" config user.name "Version Test" +command git -C "$untagged_checkout" config user.email "version-test@example.invalid" +command git -C "$untagged_checkout" commit -qm "test: create untagged fixture" --allow-empty +typeset untagged_version +untagged_version="$(command git -C "$untagged_checkout" rev-parse --short HEAD)" +run_case untagged "$untagged_checkout" "$untagged_version" + +typeset tagged_checkout="${temp_root}/tagged-checkout" +command git clone -q --no-hardlinks "$project_root" "$tagged_checkout" +command git -C "$tagged_checkout" config user.name "Version Test" +command git -C "$tagged_checkout" config user.email "version-test@example.invalid" +command git -C "$tagged_checkout" commit -qm "test: create tagged fixture" --allow-empty +command git -C "$tagged_checkout" tag zi-version-test +run_case tagged "$tagged_checkout" zi-version-test + +typeset archive_checkout="${temp_root}/archive-checkout" +command cp -R "$project_root" "$archive_checkout" +command rm -f -- "$archive_checkout/.git" +run_case archive "$archive_checkout" unknown From fa9641b06ebc6b43fedac3cf8b4d74a26a088cb4 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 22 Aug 2026 21:14:12 +0100 Subject: [PATCH 43/44] test(version): make fixtures checkout independent --- tests/version-reporting.zsh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/version-reporting.zsh b/tests/version-reporting.zsh index 9f8cc9bd..06995540 100644 --- a/tests/version-reporting.zsh +++ b/tests/version-reporting.zsh @@ -41,12 +41,18 @@ typeset -gAH ZI ZI[BIN_DIR]="$ZI_TEST_CHECKOUT" builtin source "${ZI[BIN_DIR]}/zi.zsh" -[[ ${ZI[VERSION]} == "$ZI_TEST_EXPECTED" ]] +if [[ ${ZI[VERSION]} != "$ZI_TEST_EXPECTED" ]]; then + print -u2 -r -- "expected ZI[VERSION] '$ZI_TEST_EXPECTED', got '${ZI[VERSION]}'" + return 1 +fi typeset argument output for argument in version --version -V; do output="$(zi "$argument")" - [[ $output == *"Zi version: ${ZI_TEST_EXPECTED}"* ]] + if [[ $output != *"Zi version: ${ZI_TEST_EXPECTED}"* ]]; then + print -u2 -r -- "expected '$argument' to report '$ZI_TEST_EXPECTED', got '${(q)output}'" + return 1 + fi done ZSH @@ -71,6 +77,6 @@ command git -C "$tagged_checkout" tag zi-version-test run_case tagged "$tagged_checkout" zi-version-test typeset archive_checkout="${temp_root}/archive-checkout" -command cp -R "$project_root" "$archive_checkout" -command rm -f -- "$archive_checkout/.git" +command mkdir -p "$archive_checkout" +command git -C "$project_root" archive HEAD | command tar -x -C "$archive_checkout" run_case archive "$archive_checkout" unknown From f63cf8be73490e5db774c1c5ba8a5dfd935f6698 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 22 Aug 2026 21:26:53 +0100 Subject: [PATCH 44/44] test(version): use clean shell option profile --- tests/version-reporting.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/version-reporting.zsh b/tests/version-reporting.zsh index 06995540..f92b2b18 100644 --- a/tests/version-reporting.zsh +++ b/tests/version-reporting.zsh @@ -35,7 +35,7 @@ run_case() { ZI_TEST_EXPECTED="$expected" \ zsh -f <<'ZSH' || fail "$label" builtin emulate -LR zsh -setopt errexit pipefail +setopt pipefail typeset -gAH ZI ZI[BIN_DIR]="$ZI_TEST_CHECKOUT"