diff --git a/AGENTS.md b/AGENTS.md index 4a932eb..075aa92 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,8 @@ ff script # 运行脚本 make -f Makefile.scripts # 同上(make 入口) ff region # 切换 CN_REGION(国内/国外镜像) ff reload # 重载 fish 配置 +ff update # 立即 git pull origin main 并 reload +fish fish/tests/ff-auto-update.fish # ff 自动更新回归 ``` ## 目录职责 @@ -59,6 +61,7 @@ ff reload # 重载 fish 配置 - 跑脚本:`ff script ` 或 `ff s `(走 `execute-script.fish`) - 列脚本:`ff script -h` - 新增 `ff` 子命令时:改 `fish/functions/ff.fish` + `fish/completions/ff.fish` +- **自动更新**:日常 `ff` 命令超过间隔(默认开、24h)会顺带 `git pull --ff-only origin main` 更新 `~/.dotfile`,成功且有新提交后再 reload。配置在本机 `~/.config/ff/config`(`enabled`、`interval`),时间戳 `~/.local/share/ff/state`,不要进 git。跳过:`FF_NO_AUTO_UPDATE=1` 或 `ff --no-auto-update …`。显式 `ff update` 立即拉取并刷新时间戳,这次不再走自动更新。脏工作区、超时、网络失败时 stderr 一行并继续当前命令。回归:`fish fish/tests/ff-auto-update.fish` ## Git 配置 @@ -78,6 +81,7 @@ ff reload # 重载 fish 配置 - 不要把密钥、webhook、本机专用路径写进 git(`script/clash.sh` 等已在 `.gitignore`) - 不要改 `~/.config/fish/config.fish` 进仓库;本机差异放 `custom.fish` - 不要把本机 `mise use -g` 的工具写进 `mise/config.toml`;写 `~/.config/mise/config.toml` +- 不要把 `~/.config/ff/config` 或 `~/.local/share/ff/state` 写进 git(本机自动更新配置) - 不要为 init 复制整份配置到 `$HOME`;**优先软链接**到 dotfile - 脚本改动保持最小 diff,风格与相邻脚本一致(bash、`set -euo pipefail` 视情况) - 未经明确要求不要 `git commit` / `git push` diff --git a/fish/completions/ff.fish b/fish/completions/ff.fish index deb5601..1a7b307 100644 --- a/fish/completions/ff.fish +++ b/fish/completions/ff.fish @@ -11,7 +11,9 @@ complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "pst" -d # complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "make" -d "Run dotfile Makefile commands" complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "script" -d "Run script from script directory" complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "s" -d "Run script (alias for script)" +complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "update" -d "Update dotfile now (also auto every 24h)" complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "help" -d "Show this help message" +complete -c ff -l no-auto-update -d "Skip auto-update this run" complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "region" -d "Toggle or set CN_REGION (mirror/proxy region)" complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "ssh" -d "SSH init / copy key + add host / copy config to remote" diff --git a/fish/functions/ff.fish b/fish/functions/ff.fish index 138d475..850bb7c 100644 --- a/fish/functions/ff.fish +++ b/fish/functions/ff.fish @@ -1,4 +1,214 @@ # Fast commands management + +function _ff_config_file + echo ~/.config/ff/config +end + +function _ff_state_file + echo ~/.local/share/ff/state +end + +function _ff_config_value --argument-names key default + set -l file (_ff_config_file) + if not test -f $file + echo $default + return + end + set -l found + for line in (cat $file) + set line (string trim -- $line) + if test -z "$line"; or string match -q '#*' -- $line + continue + end + if string match -q -- "$key=*" $line + set found (string split -m 1 -- '=' $line)[2] + end + end + if test -n "$found" + echo (string trim -- $found) + else + echo $default + end +end + +function _ff_parse_interval --argument-names raw + set -l text (string trim -- (string lower -- $raw)) + if test -z "$text" + echo 86400 + return + end + if string match -qr '^[0-9]+$' -- $text + echo $text + return + end + set -l unit (string sub -s -1 $text) + set -l amount (string sub -e -1 $text) + if not string match -qr '^[0-9]+$' -- $amount + echo 86400 + return + end + switch $unit + case s + echo $amount + case m + math $amount '*' 60 + case h + math $amount '*' 3600 + case d + math $amount '*' 86400 + case '*' + echo 86400 + end +end + +function _ff_env_no_auto_update + if not set -q FF_NO_AUTO_UPDATE + return 1 + end + switch (string lower -- (string trim -- $FF_NO_AUTO_UPDATE)) + case 1 true yes on + return 0 + case '*' + return 1 + end +end + +function _ff_auto_update_enabled + switch (string lower -- (_ff_config_value enabled true)) + case 0 false no off + return 1 + case '*' + return 0 + end +end + +function _ff_record_auto_update + set -l file (_ff_state_file) + mkdir -p (dirname $file) + or return + echo "last_auto_update="(date +%s) >$file +end + +function _ff_last_auto_update + set -l file (_ff_state_file) + if not test -f $file + echo 0 + return + end + set -l val 0 + for line in (cat $file) + if string match -q 'last_auto_update=*' -- $line + set val (string split -m 1 -- '=' $line)[2] + end + end + if string match -qr '^[0-9]+$' -- (string trim -- $val) + echo (string trim -- $val) + else + echo 0 + end +end + +function _ff_interval_elapsed --argument-names interval + if test "$interval" -le 0 + return 0 + end + set -l last (_ff_last_auto_update) + if test "$last" -eq 0 + return 0 + end + set -l now (date +%s) + test (math $now - $last) -ge $interval +end + +function _ff_reload + for __f in ~/.dotfile/fish/functions/*.fish + if test -f $__f + functions -e (basename $__f .fish) 2>/dev/null + end + end + source ~/.config/fish/config.fish + # fish 对已加载过的 autoload 函数不会在 functions -e 后重新加载, + # 必须显式 source 一遍,否则函数(含 ff 自身)会丢失 + for __f in ~/.dotfile/fish/functions/*.fish + if test -f $__f + source $__f + end + end + set -e __f +end + +# 自动 git pull ~/.dotfile。失败/跳过不得打断当前 ff 子命令。 +# 返回: 0 有更新, 1 失败或脏树, 2 静默跳过, 3 已是最新 +function _ff_auto_update_try_pull + set -l dir ~/.dotfile + if not test -d $dir + return 2 + end + if not git -C $dir rev-parse --is-inside-work-tree >/dev/null 2>&1 + return 2 + end + set -l porcelain (git -C $dir status --porcelain 2>/dev/null) + if test -n "$porcelain" + echo "[ff] 自动更新已跳过: 工作区不干净" >&2 + return 1 + end + + set -l before (git -C $dir rev-parse HEAD 2>/dev/null) + set -lx GIT_TERMINAL_PROMPT 0 + set -l st 0 + if command -q timeout + command timeout 20 git -C $dir pull --ff-only origin main >/dev/null 2>&1 + set st $status + else + git -C $dir pull --ff-only origin main >/dev/null 2>&1 + set st $status + end + if test $st -eq 124 + echo "[ff] 自动更新失败,已跳过: git 超时" >&2 + return 1 + end + if test $st -ne 0 + echo "[ff] 自动更新失败,已跳过" >&2 + return 1 + end + set -l after (git -C $dir rev-parse HEAD 2>/dev/null) + if test -n "$before" -a -n "$after" -a "$before" != "$after" + echo "[ff] 已自动更新 dotfile" >&2 + return 0 + end + return 3 +end + +function _ff_maybe_auto_update --argument-names command no_auto_flag + if test "$no_auto_flag" = 1 + return + end + if _ff_env_no_auto_update + return + end + # 显式 ff update 自己拉,这里不要再拉一遍 + if test "$command" = update + return + end + if not _ff_auto_update_enabled + return + end + set -l interval (_ff_parse_interval (_ff_config_value interval 24h)) + if not _ff_interval_elapsed $interval + return + end + + _ff_auto_update_try_pull + set -l st $status + if test $st -eq 2 + return + end + _ff_record_auto_update + if test $st -eq 0 + _ff_reload + end +end + function ff # 定义命令和描述(格式:命令|描述) set -l commands \ @@ -12,13 +222,28 @@ function ff "make|Run dotfile Makefile commands" \ "script|Run script from script directory" \ "s|Alias for script" \ - "update|Update dotfile" \ + "update|Update dotfile now (git pull + reload); also auto every 24h" \ "region|Toggle or set CN_REGION (mirror/proxy region)" \ "ssh|SSH init, copy key + add host config, or copy config to remote" \ "dirsize|Show directory sizes (du wrapper)" \ "pouch|Clone and install pouch to ~/.pouch" \ "help|Show this help message" + set -l no_auto 0 + set -l args + set -l past_command 0 + for a in $argv + if test $past_command -eq 0; and test "$a" = "--no-auto-update" + set no_auto 1 + continue + end + set past_command 1 + set -a args $a + end + set argv $args + + _ff_maybe_auto_update "$argv[1]" $no_auto + # 确保 execute-script 可用(在 fish 自动加载前手动引入一次) if not functions -q execute-script set -l execute_script_path ~/.dotfile/fish/functions/execute-script.fish @@ -30,16 +255,7 @@ function ff switch $argv[1] case reload # autoload 会缓存函数定义,先清掉 dotfile 里的函数再重载 - for __f in ~/.dotfile/fish/functions/*.fish - functions -e (basename $__f .fish) 2>/dev/null - end - source ~/.config/fish/config.fish - # fish 对已加载过的 autoload 函数不会在 functions -e 后重新加载, - # 必须显式 source 一遍,否则函数(含 ff 自身)会丢失 - for __f in ~/.dotfile/fish/functions/*.fish - source $__f - end - set -e __f + _ff_reload echo "Fish configuration reloaded!" case edit vim ~/.config/fish/custom.fish @@ -88,6 +304,10 @@ function ff set -l parts (string split '|' $cmd) printf " %-10s %s\n" $parts[1] $parts[2] end + echo "" + echo "Auto-update: ordinary ff commands git-pull ~/.dotfile every 24h (default on)." + echo " Config: ~/.config/ff/config (enabled=true, interval=24h)" + echo " Skip: FF_NO_AUTO_UPDATE=1 or ff --no-auto-update " case update set -l dotfile_dir ~/.dotfile if not test -d $dotfile_dir @@ -97,6 +317,7 @@ function ff set -l prev_dir (pwd) cd $dotfile_dir if git pull origin main + _ff_record_auto_update cd $prev_dir echo "Reloading fish configuration..." ff reload diff --git a/fish/tests/ff-auto-update.fish b/fish/tests/ff-auto-update.fish new file mode 100755 index 0000000..1ccbed0 --- /dev/null +++ b/fish/tests/ff-auto-update.fish @@ -0,0 +1,181 @@ +#!/usr/bin/env fish +# 回归:ff 日常命令按间隔自动更新 ~/.dotfile + +set -g REPO_ROOT (cd (dirname (status filename))/../.. && pwd) +source $REPO_ROOT/fish/functions/ff.fish + +set -e GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR +set -g _t_ok 0 +set -g _t_fail 0 + +function t_ok --argument-names msg + set -g _t_ok (math $_t_ok + 1) + echo "ok - $msg" +end + +function t_fail --argument-names msg + set -g _t_fail (math $_t_fail + 1) + echo "not ok - $msg" +end + +function t_assert --argument-names cond msg + if test $cond -eq 0 + t_ok $msg + else + t_fail $msg + end +end + +function setup_env + set -g TEST_HOME (mktemp -d) + set -gx HOME $TEST_HOME + set -e FF_NO_AUTO_UPDATE + mkdir -p $HOME/.config/fish $HOME/.config/ff $HOME/.local/share/ff + echo '# test fish config' >$HOME/.config/fish/config.fish + + set -g UPSTREAM $TEST_HOME/upstream + mkdir -p $UPSTREAM/fish/functions + echo init >$UPSTREAM/README + echo '# stub' >$UPSTREAM/fish/functions/stub.fish + git init -q -b main $UPSTREAM + git -C $UPSTREAM config user.name Test + git -C $UPSTREAM config user.email test@example.com + git -C $UPSTREAM config commit.gpgsign false + git -C $UPSTREAM add . + git -C $UPSTREAM commit -q -m init + git clone -q $UPSTREAM $HOME/.dotfile + git -C $HOME/.dotfile config pull.ff only + git -C $HOME/.dotfile config user.name Test + git -C $HOME/.dotfile config user.email test@example.com + git -C $HOME/.dotfile config commit.gpgsign false +end + +function cleanup_env + if set -q TEST_HOME + rm -rf $TEST_HOME + end +end + +function add_upstream_marker --argument-names name + echo $name >$UPSTREAM/$name + git -C $UPSTREAM add $name + git -C $UPSTREAM commit -q -m $name +end + +function write_state --argument-names ts + echo "last_auto_update=$ts" >$HOME/.local/share/ff/state +end + +function capture_ff + set -g FF_OUT $TEST_HOME/out.txt + set -g FF_ERR $TEST_HOME/err.txt + ff $argv >$FF_OUT 2>$FF_ERR + set -g FF_STATUS $status +end + +# --- tests --- + +setup_env +add_upstream_marker MARKER_FIRST +capture_ff help +string match -q '*Usage: ff *' <$FF_OUT +t_assert $status "interval elapsed: ff help still prints usage" +test -f $HOME/.dotfile/MARKER_FIRST +t_assert $status "interval elapsed: first command pulls new commit" +test -f $HOME/.local/share/ff/state +t_assert $status "interval elapsed: records last_auto_update" +string match -q '*Auto-update:*' <$FF_OUT +t_assert $status "ff help documents auto-update" +string match -q '*已自动更新*' <$FF_ERR +t_assert $status "interval elapsed: stderr says updated" +cleanup_env + +setup_env +add_upstream_marker MARKER_RECENT +write_state (date +%s) +capture_ff help +test ! -f $HOME/.dotfile/MARKER_RECENT +t_assert $status "interval not elapsed: does not pull" +cleanup_env + +setup_env +echo "enabled=false" >$HOME/.config/ff/config +add_upstream_marker MARKER_DISABLED +capture_ff help +test ! -f $HOME/.dotfile/MARKER_DISABLED +t_assert $status "enabled=false: does not pull" +cleanup_env + +setup_env +add_upstream_marker MARKER_DIRTY +echo local >$HOME/.dotfile/dirty.txt +capture_ff help +string match -q '*Usage: ff *' <$FF_OUT +t_assert $status "dirty tree: current command still runs" +test ! -f $HOME/.dotfile/MARKER_DIRTY +t_assert $status "dirty tree: skips pull" +string match -q '*工作区不干净*' <$FF_ERR +t_assert $status "dirty tree: stderr skip message" +cleanup_env + +setup_env +write_state (date +%s) +add_upstream_marker MARKER_EXPLICIT +capture_ff update +test -f $HOME/.dotfile/MARKER_EXPLICIT +t_assert $status "explicit ff update still pulls when interval not elapsed" +set -l after_ts (string split -m 1 -- '=' (cat $HOME/.local/share/ff/state))[2] +test -n "$after_ts" -a "$after_ts" -gt 0 +t_assert $status "explicit ff update refreshes timestamp" +cleanup_env + +setup_env +add_upstream_marker MARKER_ENV +set -gx FF_NO_AUTO_UPDATE 1 +capture_ff help +test ! -f $HOME/.dotfile/MARKER_ENV +t_assert $status "FF_NO_AUTO_UPDATE=1: does not pull" +set -e FF_NO_AUTO_UPDATE +cleanup_env + +setup_env +add_upstream_marker MARKER_FLAG +capture_ff --no-auto-update help +test ! -f $HOME/.dotfile/MARKER_FLAG +t_assert $status "--no-auto-update: does not pull" +cleanup_env + +setup_env +add_upstream_marker MARKER_TRAILING +capture_ff help --no-auto-update +test -f $HOME/.dotfile/MARKER_TRAILING +t_assert $status "trailing --no-auto-update after command is not a global skip" +cleanup_env + +setup_env +add_upstream_marker MARKER_FAIL +git -C $HOME/.dotfile remote set-url origin /nonexistent/ff-auto-update.git +capture_ff help +string match -q '*Usage: ff *' <$FF_OUT +t_assert $status "pull failure: current command still runs" +test ! -f $HOME/.dotfile/MARKER_FAIL +t_assert $status "pull failure: does not apply update" +string match -q '*自动更新失败*' <$FF_ERR +t_assert $status "pull failure: stderr skip message" +cleanup_env + +setup_env +echo "interval=24h" >$HOME/.config/ff/config +write_state (math (date +%s) - 86401) +add_upstream_marker MARKER_INTERVAL +capture_ff help +test -f $HOME/.dotfile/MARKER_INTERVAL +t_assert $status "interval 24h elapsed: pulls" +cleanup_env + +echo "" +echo "$_t_ok passed, $_t_fail failed" +if test $_t_fail -gt 0 + exit 1 +end +exit 0