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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ Git checkouts are updated with Git.
`z.install` replaces the core while preserving installed mods.
Use `z.install.mod <mod_name>` to update a single mod.

`z.install.mod.list` lists available and installed mods: green for installed,
orange for different contents, and red for not installed. It compares against
`Z_INSTALL_BRANCH` (default: `main`); local edits also appear orange.

### Uninstall

`z.uninstall`
Expand Down
32 changes: 32 additions & 0 deletions lib/install/mod/list/analysis.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# compare an installed mod with the selected distribution
#
# $source: distribution mod directory
# $installed: installed mod directory
# REPLY: installed|update-available|not-installed
# return: 0 if compared, otherwise 1
#
# example:
# z.install.mod.list._status source=/tmp/source/git installed=~/.z/mod/git
z.install.mod.list._status() {
z.arg.named source "$@" && local source_dir=$REPLY
z.arg.named installed "$@" && local installed_dir=$REPLY

if z.dir.not.exists "$installed_dir"; then
z.return not-installed
return
fi

if z.dir.not.exists "$source_dir"; then
z.return installed
return
fi

diff -qr -- "$source_dir" "$installed_dir" >/dev/null
local result_code=$?

case $result_code in
0) z.return installed ;;
1) z.return update-available ;;
*) return 1 ;;
esac
}
27 changes: 27 additions & 0 deletions lib/install/mod/list/process.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# fetch the selected distribution's mod directory into a work directory
#
# $dir: existing work directory
# $branch?: distribution branch (default: main)
# REPLY: extracted mod directory
# return: 0|1
#
# example:
# z.install.mod.list._fetch dir=/tmp/mod-list branch=main
z.install.mod.list._fetch() {
z.arg.named dir "$@" && local dir=$REPLY
z.arg.named branch "$@" default=main && local branch=$REPLY

local archive="$dir/source.tar.gz"
local url="https://github.com/milkeclair/z/archive/refs/heads/$branch.tar.gz"

curl -fLsS "$url" -o "$archive" || return 1
tar -xzf "$archive" -C "$dir" || return 1

local roots=("$dir"/*(N/))
if ! z.int.is.eq ${#roots} 1 || z.dir.not.exists "$roots[1]/mod"; then
z.io.error "mod directory not found in the distribution"
return 1
fi

z.return "$roots[1]/mod"
}
40 changes: 40 additions & 0 deletions lib/install/mod/process.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# list available and installed mods from the selected distribution branch
# green: installed, orange: different contents, red: not installed
# local edits are also shown as update-available
#
# REPLY: null
# return: 0|1
#
# example:
# z.install.mod.list
z.install.mod.list() {
local work_dir=$(mktemp -d)
z.dir.exists "$work_dir" || return 1

{
z.install.mod.list._fetch dir="$work_dir" branch="${Z_INSTALL_BRANCH:-main}" || return 1
local source_dir=$REPLY

local names=("$source_dir"/*(N/:t) "$z_root"/mod/*(N/:t))
z.arr.unique "${names[@]}"
z.arr.sort "${(@)REPLY}"
local mod_names=(${(@)REPLY})

for mod_name in "${mod_names[@]}"; do
z.install.mod.list._status source="$source_dir/$mod_name" installed="$z_root/mod/$mod_name" || return 1
local mod_state=$REPLY
local color=green

case $mod_state in
update-available) color=orange ;;
not-installed) color=red ;;
esac

z.io color="$color" "$mod_name [$mod_state]"
done
} always {
z.dir.remove path="$work_dir" || return 1
}

z.return
}
1 change: 1 addition & 0 deletions lib/str/process.zsh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
typeset -gA z_color_palette=(
[red]=$'\033[31m'
[green]=$'\033[32m'
[orange]=$'\033[38;5;208m'
[yellow]=$'\033[33m'
[blue]=$'\033[34m'
[magenta]=$'\033[35m'
Expand Down
2 changes: 2 additions & 0 deletions main.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ local -A z_modules=(
[file]="operator:process"
[fn]="analysis:operator:process"
[hash]="analysis:operator:process"
[install]="process"
[int]="operator"
[io]="process"
[mode]="process"
Expand Down Expand Up @@ -42,6 +43,7 @@ local z_module_depends_order=(
str
t
update
install
)

local -A loaded_files=()
Expand Down
93 changes: 93 additions & 0 deletions test/lib/install/mod/list/analysis_test.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
source "$z_main"

z.t.describe "z.install.mod.list._status"; {
z.t.context "未インストールの場合"; {
z.t.it "not-installedを返す"; {
local fixture=$(mktemp -d /tmp/z_mod_status.XXXXXX)
z.dir.make path="$fixture/source"

z.install.mod.list._status source="$fixture/source" installed="$fixture/installed"

z.t.expect.reply not-installed

z.dir.remove path="$fixture"
}
}

z.t.context "配布元と内容が一致する場合"; {
z.t.it "隠しfileとsubdirectoryも比較してinstalledを返す"; {
local fixture=$(mktemp -d /tmp/z_mod_status.XXXXXX)
z.dir.make path="$fixture/source/nested"
z.file.write path="$fixture/source/nested/value" content=same
z.file.write path="$fixture/source/.settings" content=same
cp -R "$fixture/source" "$fixture/installed"

z.install.mod.list._status source="$fixture/source" installed="$fixture/installed"

z.t.expect.reply installed

z.dir.remove path="$fixture"
}
}

z.t.context "配布元とfileの内容が異なる場合"; {
z.t.it "update-availableを返す"; {
local fixture=$(mktemp -d /tmp/z_mod_status.XXXXXX)
z.dir.make path="$fixture/source"
z.dir.make path="$fixture/installed"
z.file.write path="$fixture/source/value" content=new
z.file.write path="$fixture/installed/value" content=old

z.install.mod.list._status source="$fixture/source" installed="$fixture/installed"

z.t.expect.reply update-available

z.dir.remove path="$fixture"
}
}

z.t.context "fileの追加・削除がある場合"; {
for side in source installed; do
z.t.it "$side側だけにfileがあるとupdate-availableを返す"; {
local fixture=$(mktemp -d /tmp/z_mod_status.XXXXXX)
z.dir.make path="$fixture/source"
z.dir.make path="$fixture/installed"
z.file.write path="$fixture/$side/only" content=changed

z.install.mod.list._status source="$fixture/source" installed="$fixture/installed"

z.t.expect.reply update-available

z.dir.remove path="$fixture"
}
done
}

z.t.context "導入済みのmodが配布元にない場合"; {
z.t.it "installedを返す"; {
local fixture=$(mktemp -d /tmp/z_mod_status.XXXXXX)
z.dir.make path="$fixture/installed"

z.install.mod.list._status source="$fixture/missing" installed="$fixture/installed"

z.t.expect.reply installed

z.dir.remove path="$fixture"
}
}

z.t.context "比較中に読み取りerrorが起きた場合"; {
z.t.it "更新ありと誤認せず失敗する"; {
local fixture=$(mktemp -d /tmp/z_mod_status.XXXXXX)
z.dir.make path="$fixture/source"
z.dir.make path="$fixture/installed"
z.t.mock name=diff behavior="return 2"

z.install.mod.list._status source="$fixture/source" installed="$fixture/installed" 2>/dev/null

z.t.expect.status.is.false

z.dir.remove path="$fixture"
}
}
}
94 changes: 94 additions & 0 deletions test/lib/install/mod/list/process_test.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
source "$z_main"

z.t.describe "z.install.mod.list._fetch"; {
z.t.context "配布archiveを取得できる場合"; {
z.t.it "指定branchを一度取得してmod配布元を返す"; {
local fixture=$(mktemp -d /tmp/z_mod_fetch.XXXXXX)
z.dir.make path="$fixture/input/z-topic/mod/example"
z.dir.make path="$fixture/work"
z.file.write path="$fixture/input/z-topic/mod/example/value" content=remote
tar -czf "$fixture/archive.tar.gz" -C "$fixture/input" z-topic

z.t.mock name=curl behavior="cp \"$fixture/archive.tar.gz\" \"$fixture/work/source.tar.gz\""

z.install.mod.list._fetch dir="$fixture/work" branch=topic/change
local result_code=$?
local source_dir=$REPLY

z.t.mock.result name=curl
local call=$REPLY

z.t.expect "$result_code" 0
z.t.expect "$source_dir" "$fixture/work/z-topic/mod"
z.t.expect "$call" "-fLsS https://github.com/milkeclair/z/archive/refs/heads/topic/change.tar.gz -o $fixture/work/source.tar.gz"

z.file.read path="$source_dir/example/value"
z.t.expect.reply remote

z.dir.remove path="$fixture"
}
}

z.t.context "取得に失敗した場合"; {
z.t.it "展開を行わず失敗する"; {
local fixture=$(mktemp -d /tmp/z_mod_fetch.XXXXXX)
local -a extracted=()
z.t.mock name=curl behavior="return 22"
z.t.mock name=tar behavior='extracted+=(called); return 0'

z.install.mod.list._fetch dir="$fixture" 2>/dev/null
local result_code=$?

z.t.expect "$result_code" 1 skip_unmock=true
z.t.expect "$extracted" ""

z.dir.remove path="$fixture"
}
}

z.t.context "archiveが壊れている場合"; {
z.t.it "失敗する"; {
local fixture=$(mktemp -d /tmp/z_mod_fetch.XXXXXX)
z.t.mock name=curl behavior="z.file.write path=\"$fixture/source.tar.gz\" content=invalid"

z.install.mod.list._fetch dir="$fixture" 2>/dev/null

z.t.expect.status.is.false

z.dir.remove path="$fixture"
}
}

z.t.context "mod directoryが含まれない場合"; {
z.t.it "一覧を空として扱わず失敗する"; {
local fixture=$(mktemp -d /tmp/z_mod_fetch.XXXXXX)
z.dir.make path="$fixture/input/z-main/lib"
z.dir.make path="$fixture/work"
tar -czf "$fixture/archive.tar.gz" -C "$fixture/input" z-main
z.t.mock name=curl behavior="cp \"$fixture/archive.tar.gz\" \"$fixture/work/source.tar.gz\""

z.install.mod.list._fetch dir="$fixture/work" 2>/dev/null

z.t.expect.status.is.false

z.dir.remove path="$fixture"
}
}

z.t.context "配布元directoryが複数ある場合"; {
z.t.it "任意の配布元を選ばず失敗する"; {
local fixture=$(mktemp -d /tmp/z_mod_fetch.XXXXXX)
z.dir.make path="$fixture/input/one/mod"
z.dir.make path="$fixture/input/two/mod"
z.dir.make path="$fixture/work"
tar -czf "$fixture/archive.tar.gz" -C "$fixture/input" one two
z.t.mock name=curl behavior="cp \"$fixture/archive.tar.gz\" \"$fixture/work/source.tar.gz\""

z.install.mod.list._fetch dir="$fixture/work" 2>/dev/null

z.t.expect.status.is.false

z.dir.remove path="$fixture"
}
}
}
Loading
Loading