Compare commits

..

3 Commits

Author SHA1 Message Date
YANG JIANKUAN
d5631930c4 chore: 清理 settings.local.json 中 vfox 迁移遗留的权限项
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:20:55 -07:00
YANG JIANKUAN
5b217ab032 feat(sync-air-cargo): 以 Synced-From trailer 记录同步点,同步后自动在 air-cargo 本地提交
- 默认区间模式:基线取 air-cargo 最近的 Synced-From,目标为 main,内容取自提交对象
- 无记录时按内容反推基线(仅预览,apply 须 --base 确认),每次预览做基线校验
- 新增三方校验,列出将覆盖的 air-cargo 侧独立修改;分支/工作区/祖先关系保护
- --worktree 同步未提交变更且不推进同步点;提交信息仅「<Label> <日期>」+ trailer,不 push
- 新增 config(main → feature/hefei,air-cargo 位于项目根目录)
- 排除清单补充 .mcp.json、.claude.*、CLAUDE.local.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:20:55 -07:00
YANG JIANKUAN
f1ff42f2bf chore: 忽略根目录 air-cargo/,CLAUDE.md 声明其仅作另一仓库的提交通道
- .gitignore 新增 air-cargo/(同一份源码的独立 git 仓库,远程不同)
- CLAUDE.md 新增「air-cargo 目录:一律忽略」:扫描、开发、查历史均跳过,
  仅 sync-air-cargo skill 可操作

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:20:55 -07:00
6 changed files with 310 additions and 95 deletions

View File

@@ -39,9 +39,6 @@
"Bash(git -C /Users/kid/Development/Fusion/Projects/aerologic-app stash)",
"WebFetch(domain:api.apifox.cn)",
"Bash(sips:*)",
"Bash(vfox list:*)",
"Bash(vf list:*)",
"Bash(vfox use:*)",
"Bash(wait)",
"Bash(while read file)",
"Bash(do sed -i '' 's/MutableLiveData\\(DateUtils\\\\.getCurrentTime\\(\\)\\\\.formatDate\\(\\)\\)/MutableLiveData<String>\\(DateUtils.getCurrentTime\\(\\).formatDate\\(\\)\\)/g' \"$file\" echo \"Modified: $file\" done)",

View File

@@ -1,49 +1,70 @@
---
name: sync-air-cargo
description: 将 aerologic-app 项目中未提交的变更(已修改、新增、已删除、包括未跟踪的文件)以文件级 cp/rm 方式同步到同级目录下的 air-cargo 项目——两者是同一份源码的两个独立 git 仓库副本,分别推送到不同的远程地址。只做文件复制/删除,不对内容做任何二次编辑或转换。每当用户提到"同步到 air-cargo"、"同步到另一个仓库/项目"、"推送/复制到 cargo 项目"、"两个项目要保持一致",或者在 aerologic-app 里改完代码后问要不要同步/推送到另一个仓库时,应主动使用此 skill,而不是手动逐个 cp 文件。
description: 将 aerologic-app 的变更以文件级 cp/rm 方式同步到项目根目录下的 air-cargo/ 仓库并在其本地提交(不 push)——两者是同一份源码的两个独立 git 仓库副本,分别推送到不同的远程地址。同步点记录在 air-cargo 提交信息的 Synced-From trailer 中,自动从上次同步点同步到 main 最新提交(覆盖已提交未 push、已 push 的变更),也支持只同步未提交的工作区变更。只做文件复制/删除,不对内容做任何二次编辑或转换。每当用户提到"同步到 air-cargo"、"同步到另一个仓库/项目"、"推送/复制到 cargo 项目"、"两个项目要保持一致"、"把某几个 commit 同步过去",或者在 aerologic-app 里改完代码后问要不要同步/推送到另一个仓库时,应主动使用此 skill,而不是手动逐个 cp 文件。
---
# 同步变更到 air-cargo
## 背景
`aerologic-app` 和同级目录下的 `air-cargo` 是同一份项目源码的两个独立 git 仓库副本,目录结构完全一致,分别推送到不同的远程仓库地址。日常开发只在 `aerologic-app` 中进行,完成后需要把未提交的变更原样同步一份到 `air-cargo`,再由用户在 `air-cargo` 里自行提交、推送。
`aerologic-app` 和 `air-cargo` 是同一份项目源码的两个独立 git 仓库副本,分别推送到不同的远程仓库地址。日常开发只在 `aerologic-app` 中进行,完成后把变更同步到 `air-cargo` 并在本地提交,**push 由用户自己做**。
同步的判定完全基于 `git status`:在 `aerologic-app` 里,工作区中存在的文件(无论是修改、新增还是未跟踪)就复制覆盖到 `air-cargo` 对应路径;工作区中已不存在的文件(即被删除的)就在 `air-cargo` 里删除对应路径。这样天然覆盖了修改/新增/删除/未跟踪/重命名(重命名会被当作"删除旧路径 + 新增新路径"处理)等所有情况,不需要逐个状态码去分支判断。
- **位置**:`air-cargo` 在 aerologic-app 根目录下(`/Users/tim/Development/Projects/aerologic-app/air-cargo/`),已在 aerologic-app 的 `.gitignore` 中忽略
- **分支映射**(`config` 文件):aerologic-app `main` → air-cargo `feature/hefei`。air-cargo 的 `develop` 是 2023 年的旧快照,不能往上同步;当前分支不是 `feature/hefei` 时脚本直接报错
- **不参与同步**:`CLAUDE.md` / `CLAUDE.local.md`、`.claude/` 目录、根目录 `.claude.*`(如 `.claude.local.md`)与 `.mcp.json`(Claude Code 的配置/记忆),不复制也不删除。air-cargo 中不应存在任何 Claude / Agent 相关文件,其本地 `.git/info/exclude` 已屏蔽这些文件名,防止误提交
- **air-cargo 只是提交通道**:不参与本项目开发,代码扫描、功能实现、查历史时都忽略它(见 CLAUDE.md「air-cargo 目录」一节)
**只同步工程项目本身的文件**:`CLAUDE.md` 以及 `.claude/` 目录(包括其下的 `skills/`、`commands/`、`settings.local.json`、`mcp.json` 等)属于 Claude Code 的工具配置/记忆,两个仓库各自独立维护,不属于"项目源码",因此始终排除在同步范围之外,不会被复制也不会被删除。脚本会在输出里单独列出"已忽略,不参与同步"的清单,方便核对。
## 同步点:air-cargo 提交信息里的 Synced-From trailer
每次同步提交的信息只有一句带日期的通用语句加一个 trailer,**不写修改明细**:
```
Update 20261012
Synced-From: aerologic-app@388d6058696c05cfd5892018f52120880a440524
```
脚本以 air-cargo 当前分支上最近一个 `Synced-From` 作为基线,把 `基线..main` 的变更同步过去。记录跟着 air-cargo 的提交历史走,只有真正提交了才算数。
- **区间模式(默认)**:基线 = 最近的 `Synced-From`,目标 = aerologic-app `main` 最新提交。**文件内容从目标提交对象中读取**,与 aerologic-app 工作区无关,因此已 commit 未 push、已 push 的变更都用它。提交时写入新的 `Synced-From`
- **工作区模式(`--worktree`)**:同步 aerologic-app 未提交的变更。提交信息**不写** `Synced-From`,因为这份内容不对应任何提交,不能推进同步点。之后这些改动在 aerologic-app 提交后,下次区间同步会再覆盖一次(内容相同,无副作用)
- **兜底:按内容反推**:找不到 `Synced-From` 时(首次同步,或有人手工提交时丢了 trailer),在 main 的第一父链上找与 air-cargo HEAD 内容差异最少的提交作为候选基线。只用于预览,`apply` 必须显式传 `--base`
- **基线校验**:每次预览都会比较 air-cargo HEAD 和基线的内容。正常情况下只剩 air-cargo 自己的 6 个元文件:`.gitignore`(air-cargo 额外忽略 `*.jks`)、`key密码`、`CONTRIBUTING.md`、`备忘录.md`、`国际出港进港权限对照表.xlsx`、`.docs/大写字母数字输入过滤器使用指南.md`。超过 50 个会提示基线或分支可能不对
## 使用方法
脚本位于 `scripts/sync.sh`,用法:
```bash
bash scripts/sync.sh [SOURCE_DIR] [TARGET_DIR] [preview|apply]
cd /Users/tim/Development/Projects/aerologic-app
bash .claude/skills/sync-air-cargo/scripts/sync.sh [preview|apply] [--base <ref>] [--to <ref>] [--worktree] [--label <word>]
```
- `SOURCE_DIR` 默认为当前目录(一般就是 aerologic-app 项目根目录)
- `TARGET_DIR` 默认为 `SOURCE_DIR` 同级的 `air-cargo` 目录,一般不需要手动指定
- 第三个参数默认 `preview`(只打印清单,不做任何改动),传 `apply` 才会真正执行复制/删除
| 参数 | 说明 |
|------|------|
| `preview` / `apply` | 默认 `preview`,只打印计划;`apply` 复制/删除并在 air-cargo 本地提交 |
| `--base <ref>` | 指定基线:反推结果经确认后传入,或用来纠正错误的同步点 |
| `--to <ref>` | 指定目标提交,默认 `main`(只同步到某个较早的提交时使用) |
| `--worktree` | 工作区模式,不能和 `--base`/`--to` 同时用 |
| `--label <word>` | 提交信息的通用词,默认 `Update`。用户要求时可换成 `Fixed`、`Optimized` 等 |
### 步骤
1. **先预览**,让用户看到将要发生什么:
```bash
bash /Users/kid/Development/Fusion/Projects/aerologic-app/.claude/skills/sync-air-cargo/scripts/sync.sh
```
输出会分别列出"待复制/覆盖"和"待删除"的文件清单。
1. **预览**:先确定模式——用户要同步已提交的代码用默认区间模式;明确说的是"没提交的改动"才加 `--worktree`。然后执行 `sync.sh`(区间模式不加参数)。
- 输出「无需同步」:直接告诉用户,结束
- 基线来源是「按内容反推」:把候选基线连同基线校验结果一起拿给用户确认(反推可能取到比预期更早的提交,例如 air-cargo 从未同步过的环境配置提交),确认后加 `--base <sha>` 再预览一次
2. **用 `AskUserQuestion` 确认**,不要只在文字里问一句。问题描述里要包含:基线 → 目标、区间提交数、待复制/待删除数量(及主要模块分布)、**「⚠ 将覆盖 air-cargo 侧独立修改」清单(逐个列出)**、提交信息。选项至少包含「确认同步并提交」和「取消」。覆盖清单不为空时要特别说明这些文件是 air-cargo 自己改过的(如 `.gitignore` 里的 `*.jks` 忽略),由用户决定。
3. **执行**:把预览时的参数原样带上,`preview` 换成 `apply`。脚本会复制/删除文件、精确暂存这些路径并提交,最后输出提交号和变更统计。
4. **不要 push**。向用户报告提交号,并说明 air-cargo 比 `origin/feature/hefei` 超前几个提交,push 由用户自己做。
2. **用 `AskUserQuestion` 工具向用户确认**,不要只是在文字里问一句让用户打字回复。这是一个会覆盖 `air-cargo` 中已有文件、且删除操作不可逆的操作,必须先把预览清单(待复制、待删除的文件数量和列表)放进问题描述里,让用户能一眼看到影响范围,选项至少包含"确认同步"和"取消"。得到确认后才能继续执行 `apply`(这遵循整体规则:覆盖性/破坏性操作需要用户确认,不要因为 skill 本身能直接跑就跳过确认)。
### 脚本的保护措施
3. **用户确认后执行同步**:
```bash
bash /Users/kid/Development/Fusion/Projects/aerologic-app/.claude/skills/sync-air-cargo/scripts/sync.sh . /Users/kid/Development/Fusion/Projects/air-cargo apply
```
执行完会输出"复制/更新 N 个文件,删除 M 个文件"的汇总。
4. **同步之后不要自动 commit/push**。脚本只做文件复制,`air-cargo` 那边的 `git add` / `commit` / `push` 由用户自行决定和操作——两个仓库指向不同的远程地址,提交推送这类会影响远程共享状态的操作应始终由用户主导确认。
- air-cargo 当前分支不是 `config` 里的 `TARGET_BRANCH` → 报错
- air-cargo 工作区有本次同步范围**以外**的未提交改动 → 报错;范围内的改动(例如上次同步还没提交)允许,重跑会覆盖成相同内容
- 基线不是目标的祖先(aerologic-app 被 rebase / force-push 过)→ 报错,用 `--base` 重新指定
- 区间内只有不参与同步的文件变化 → 仍然提交一个空提交,推进同步点
## 注意事项
- 脚本只识别 git 能感知到的变更(`git status` 看得到的),`.gitignore` 忽略的文件(如构建产物 `build/`、`.gradle/`)不会被同步,这也是符合预期的——不应该把本地构建缓存同步过去。
- 如果 `air-cargo` 里对应路径本身也有未提交的本地修改,会被直接覆盖覆盖掉,预览阶段要留意这一点并提醒用户。
- 只同步 git 跟踪的内容,`.gitignore` 忽略的构建产物(`build/`、`.gradle/`)不会同步,`air-cargo/` 本身也不在变更清单里
- 不要在 air-cargo 里手动提交同步内容。如果确实手动提交了,trailer 格式必须完全一致(`Synced-From: aerologic-app@<完整 sha>`),否则下次只能走反推
- 首个同步点:2026-10-12,air-cargo `3dd792f9`,对应 aerologic-app `388d605`(基线选 `871fa4c`)。跳过的 `dda0ff8`(vfox→mise)随后在 air-cargo 单独提交 `3c76385b` 补上:删除 `.tool-versions` / `.vfox.toml` / `.vfox/`,新增与 aerologic-app 相同的 `mise.toml`。这个提交不带 trailer,同步点不变
- 两个仓库都用 mise 管理 JDK(`mise.toml`:`java = "17"`)。air-cargo 位于 aerologic-app 之下,mise 会逐级读取配置,所以 air-cargo 里不能再出现 `.tool-versions`,否则会覆盖成未安装的旧版本

View File

@@ -0,0 +1,8 @@
# sync-air-cargo 配置,由 scripts/sync.sh 以 shell 变量形式读入
# aerologic-app 的哪个分支同步到 air-cargo 的哪个分支(一对一)
SOURCE_BRANCH=main
TARGET_BRANCH=feature/hefei
# air-cargo 仓库相对 aerologic-app 根目录的位置(已在 aerologic-app 的 .gitignore 中忽略)
TARGET_SUBDIR=air-cargo

View File

@@ -1,98 +1,276 @@
#!/usr/bin/env bash
# 将 SOURCE_DIR 中未提交的变更(修改/新增/删除)以 cp/rm 的方式同步到 TARGET_DIR。
# 将 aerologic-app 的变更以文件级 cp/rm 同步到 air-cargo,并在 air-cargo 本地提交(不 push)。
# 两个目录是同一份项目源码的独立 git 仓库副本,只做文件级复制/删除,不做任何内容编辑。
#
# 同步点记录在 air-cargo 提交信息的 trailer「Synced-From: aerologic-app@<sha>」中:
# - 区间模式(默认):基线 = air-cargo 当前分支最近一个 Synced-From,目标 = 源仓库 SOURCE_BRANCH 最新提交。
# 文件内容取自目标提交对象,与源仓库工作区无关;提交时写入新的 Synced-From,推进同步点
# - 工作区模式(--worktree):同步源仓库未提交的变更;提交时不写 Synced-From,不推进同步点
# 没有同步记录时按内容反推基线(目标第一父链上与 air-cargo HEAD 差异最少的提交),仅供确认,apply 须显式 --base
set -euo pipefail
export GIT_LITERAL_PATHSPECS=1
SOURCE_DIR="${1:-$(pwd)}"
SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)"
TARGET_DIR="${2:-$(dirname "$SOURCE_DIR")/air-cargo}"
MODE="${3:-preview}" # preview | apply
usage() {
cat <<'EOF'
用法: sync.sh [preview|apply] [选项]
preview 只打印同步计划,不做任何改动(默认)
apply 复制/删除文件并在 air-cargo 本地提交(不 push)
选项:
--base <ref> 指定基线:没有 Synced-From 记录(首次同步)或需要纠正时使用
--to <ref> 指定目标提交,默认 config 中的 SOURCE_BRANCH
--worktree 工作区模式:同步源仓库未提交的变更,不推进同步点
--label <word> 提交信息的通用词,默认 Update(可用 Fixed、Optimized 等)
EOF
}
if [ ! -d "$SOURCE_DIR/.git" ]; then
echo "错误:$SOURCE_DIR 不是一个 git 仓库根目录" >&2
exit 1
fi
if [ ! -d "$TARGET_DIR" ]; then
echo "错误:目标目录不存在:$TARGET_DIR" >&2
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../config
. "$SCRIPT_DIR/../config"
SOURCE_DIR="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
TARGET_DIR="$SOURCE_DIR/$TARGET_SUBDIR"
SOURCE_NAME="$(basename "$SOURCE_DIR")"
# 只同步工程项目本身的文件,Claude Code 相关的配置/记忆文件(CLAUDE.md、.claude/ 目录,
MODE=preview BASE_ARG="" TO_ARG="" WORKTREE=0 LABEL=Update
while [ $# -gt 0 ]; do
case "$1" in
preview|apply) MODE="$1" ;;
--base) BASE_ARG="${2:?--base 缺少参数}"; shift ;;
--to) TO_ARG="${2:?--to 缺少参数}"; shift ;;
--worktree) WORKTREE=1 ;;
--label) LABEL="${2:?--label 缺少参数}"; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "未知参数:$1" >&2; usage >&2; exit 1 ;;
esac
shift
done
die() { echo "错误:$*" >&2; exit 1; }
S() { git -C "$SOURCE_DIR" "$@"; }
T() { git -C "$TARGET_DIR" "$@"; }
short() { S rev-parse --short "$1"; }
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
# 只同步工程项目本身的文件,Claude Code 相关的配置/记忆文件(CLAUDE.md、CLAUDE.local.md、.claude/ 目录、根目录 .claude.* 与 .mcp.json,
# 包括其中的 skills/commands 等)两个仓库各自独立维护,不参与同步。
is_excluded() {
local path="$1"
case "$path" in
.claude/*|.claude) return 0 ;;
CLAUDE.md|*/CLAUDE.md) return 0 ;;
case "$1" in
.claude|.claude/*|.claude.*|.mcp.json) return 0 ;;
CLAUDE.md|*/CLAUDE.md|CLAUDE.local.md|*/CLAUDE.local.md) return 0 ;;
esac
return 1
}
# --no-renames:把 rename 拆成"删除旧路径 + 新增新路径"两条记录,避免解析组合格式
# --untracked-files=all:新增的整个目录也会逐个文件列出,而不是折叠成一行目录
# -z:以 NUL 分隔,规避文件名转义/含空格的问题
copy_list=()
delete_list=()
excluded_list=()
# 列出提交中参与同步的文件「路径<TAB>blob」(已排序),用于按内容比对两个仓库
filtered_tree() {
git -C "$1" -c core.quotepath=false ls-tree -r "$2" | awk -F'\t' '
$2 ~ /^\.claude($|\/|\.)/ || $2 == ".mcp.json" || $2 ~ /(^|\/)CLAUDE(\.local)?\.md$/ { next }
{ split($1, m, " "); print $2 "\t" m[3] }' | LC_ALL=C sort
}
while IFS= read -r -d '' entry; do
status="${entry:0:2}"
path="${entry:3}"
[ -z "$path" ] && continue
if is_excluded "$path"; then
excluded_list+=("$status|$path")
continue
fi
if [ -e "$SOURCE_DIR/$path" ]; then
copy_list+=("$status|$path")
# 两份 filtered_tree 输出中内容不同的路径(去重)
diff_paths() {
LC_ALL=C comm -3 "$1" "$2" | sed $'s/^\t//' | cut -f1 | LC_ALL=C sort -u
}
blob_at() { S rev-parse -q --verify "$1:$2" 2>/dev/null || true; }
# ---------- 前置检查 ----------
[ -d "$SOURCE_DIR/.git" ] || die "$SOURCE_DIR 不是 git 仓库根目录"
[ -d "$TARGET_DIR/.git" ] || die "目标仓库不存在:$TARGET_DIR"
cur_branch="$(T branch --show-current)"
[ "$cur_branch" = "$TARGET_BRANCH" ] ||
die "air-cargo 当前分支为「${cur_branch:-游离 HEAD}」,应为「${TARGET_BRANCH}」(见 config),请先与用户确认"
# ---------- 确定基线与目标 ----------
BASE="" BASE_FROM="" TO="" INFERRED=0
if [ "$WORKTREE" -eq 1 ]; then
[ -z "$BASE_ARG$TO_ARG" ] || die "--worktree 不能与 --base / --to 同时使用"
BASE="$(S rev-parse HEAD)"
BASE_FROM="源仓库 HEAD(工作区模式)"
else
TO="$(S rev-parse -q --verify "${TO_ARG:-$SOURCE_BRANCH}^{commit}")" || die "源仓库找不到 ${TO_ARG:-$SOURCE_BRANCH}"
if [ -n "$BASE_ARG" ]; then
BASE="$(S rev-parse -q --verify "$BASE_ARG^{commit}")" || die "源仓库找不到基线 $BASE_ARG"
BASE_FROM="--base 参数"
else
delete_list+=("$status|$path")
record="$(T log -1 --grep='^Synced-From: ' --format='%(trailers:key=Synced-From,valueonly)' | sed -n '1p')"
if [ -n "$record" ]; then
BASE="$(S rev-parse -q --verify "${record##*@}^{commit}")" ||
die "air-cargo 记录的同步点 ${record##*@} 在源仓库中不存在(源仓库被 rebase / force-push 过?),请用 --base 指定"
BASE_FROM="air-cargo 提交记录中的 Synced-From"
fi
fi
done < <(git -C "$SOURCE_DIR" status --porcelain=v1 -z --no-renames --untracked-files=all)
echo "源目录: $SOURCE_DIR"
echo "目标目录: $TARGET_DIR"
echo ""
echo "=== 待复制/覆盖(${#copy_list[@]})==="
for item in "${copy_list[@]+"${copy_list[@]}"}"; do
echo " [${item%%|*}] ${item#*|}"
filtered_tree "$TARGET_DIR" HEAD > "$TMP/target.tree"
if [ -z "$BASE" ]; then
# 兜底:按内容反推,在目标的第一父链上找与 air-cargo HEAD 差异最少的提交
best_n=-1
for c in $(S rev-list --first-parent --max-count=300 "$TO"); do
filtered_tree "$SOURCE_DIR" "$c" > "$TMP/cand.tree"
n="$(diff_paths "$TMP/cand.tree" "$TMP/target.tree" | wc -l | tr -d ' ')"
if [ "$best_n" -lt 0 ] || [ "$n" -lt "$best_n" ]; then BASE="$c"; best_n="$n"; fi
[ "$n" -eq 0 ] && break
done
[ -n "$BASE" ] || die "无法反推基线,请用 --base 指定"
INFERRED=1
BASE_FROM="按内容反推(air-cargo 无 Synced-From 记录),需确认后以 --base 传入"
fi
S merge-base --is-ancestor "$BASE" "$TO" || die "基线 $(short "$BASE") 不是目标 $(short "$TO") 的祖先"
fi
# ---------- 计算变更清单 ----------
copy_list=() delete_list=() excluded_list=()
classify() { # $1=状态 $2=路径 $3=同步后文件是否应存在(1/0)
[ -z "$2" ] && return
if is_excluded "$2"; then excluded_list+=("$1|$2")
elif [ "$3" -eq 1 ]; then copy_list+=("$1|$2")
else delete_list+=("$1|$2")
fi
}
if [ "$WORKTREE" -eq 1 ]; then
# --no-renames:rename 拆成「删除旧路径 + 新增新路径」;--untracked-files=all:新目录逐个文件列出
while IFS= read -r -d '' entry; do
p="${entry:3}"
if [ -e "$SOURCE_DIR/$p" ] || [ -L "$SOURCE_DIR/$p" ]; then classify "${entry:0:2}" "$p" 1; else classify "${entry:0:2}" "$p" 0; fi
done < <(S status --porcelain=v1 -z --no-renames --untracked-files=all)
else
# git diff -z 的 name-status 输出为「状态\0路径\0」交替
while IFS= read -r -d '' st && IFS= read -r -d '' p; do
if [ "$st" = D ]; then classify "$st" "$p" 0; else classify "$st" "$p" 1; fi
done < <(S diff --name-status --no-renames -z "$BASE" "$TO")
fi
new_blob() { # 同步后该路径应有的内容
if [ "$WORKTREE" -eq 1 ]; then S hash-object -- "$1"; else blob_at "$TO" "$1"; fi
}
# 三方校验:air-cargo 现有内容既不等于基线、也不等于同步后的内容,说明 air-cargo 侧有独立修改,会被覆盖
conflict_list=()
for item in ${copy_list[@]+"${copy_list[@]}"} ${delete_list[@]+"${delete_list[@]}"}; do
st="${item%%|*}" p="${item#*|}"
[ -f "$TARGET_DIR/$p" ] || continue
tb="$(T hash-object -- "$p")"
[ "$tb" = "$(blob_at "$BASE" "$p")" ] && continue
[ "$st" != D ] && [ "$tb" = "$(new_blob "$p")" ] && continue
conflict_list+=("$p")
done
echo ""
echo "=== 待删除(${#delete_list[@]})==="
for item in "${delete_list[@]+"${delete_list[@]}"}"; do
echo " [${item%%|*}] ${item#*|}"
done
# air-cargo 工作区只允许存在本次同步范围内的改动(如上次同步未提交,重跑会覆盖为相同内容)
for item in ${copy_list[@]+"${copy_list[@]}"} ${delete_list[@]+"${delete_list[@]}"}; do
printf '%s\n' "${item#*|}"
done | LC_ALL=C sort -u > "$TMP/synced"
T status --porcelain=v1 -z --no-renames --untracked-files=all | tr '\0' '\n' | cut -c4- | LC_ALL=C sort -u > "$TMP/dirty"
unrelated="$(LC_ALL=C comm -23 "$TMP/dirty" "$TMP/synced")"
echo ""
echo "=== 已忽略,不参与同步(${#excluded_list[@]})==="
for item in "${excluded_list[@]+"${excluded_list[@]}"}"; do
echo " [${item%%|*}] ${item#*|}"
done
# ---------- 提交信息 ----------
printf '%s %s\n' "$LABEL" "$(date +%Y%m%d)" > "$TMP/msg"
[ "$WORKTREE" -eq 1 ] || printf '\nSynced-From: %s@%s\n' "$SOURCE_NAME" "$TO" >> "$TMP/msg"
if [ "$MODE" != "apply" ]; then
# ---------- 打印计划 ----------
print_list() { # $1=标题 其余=「状态|路径」
local title="$1"; shift
echo ""
echo "(预览模式,未执行任何操作。确认无误后加 apply 参数执行同步)"
echo "=== ${title}($#)==="
for item in "$@"; do echo " [${item%%|*}] ${item#*|}"; done
}
echo "源仓库: $SOURCE_DIR"
echo "目标仓库: ${TARGET_DIR}(分支 ${TARGET_BRANCH})"
if [ "$WORKTREE" -eq 1 ]; then
echo "模式: 工作区模式(同步未提交变更,不推进同步点)"
else
echo "模式: 区间模式"
echo "基线: $(short "$BASE") $(S log -1 --format=%s "$BASE")"
echo " 来源:$BASE_FROM"
echo "目标: $(short "$TO") $(S log -1 --format=%s "$TO")(${TO_ARG:-$SOURCE_BRANCH})"
echo "区间提交数: $(S rev-list --count "$BASE..$TO")"
[ "$(S rev-parse HEAD)" = "$TO" ] || echo "提示: 源仓库 HEAD 不在目标提交上,同步内容以目标提交为准"
# 内容校验:同步点记录正确时,air-cargo HEAD 与基线应只有少量元文件差异
filtered_tree "$SOURCE_DIR" "$BASE" > "$TMP/base.tree"
diff_paths "$TMP/base.tree" "$TMP/target.tree" > "$TMP/verify"
vn="$(wc -l < "$TMP/verify" | tr -d ' ')"
echo ""
echo "=== 基线校验:air-cargo HEAD 与基线有 $vn 个文件内容不同 ==="
sed -n '1,20s/^/ /p' "$TMP/verify"
[ "$vn" -le 20 ] || echo " ……(其余 $((vn - 20)) 个省略)"
[ "$vn" -le 50 ] || echo " ⚠ 差异较大:基线可能不对,或 air-cargo 分支落后/不对,请先核实"
fi
if [ "$WORKTREE" -eq 0 ] && [ "$BASE" = "$TO" ]; then
echo ""
echo "air-cargo 已同步到 $(short "$TO"),无需同步。"
exit 0
fi
copied=0
deleted=0
print_list "待复制/覆盖" ${copy_list[@]+"${copy_list[@]}"}
print_list "待删除" ${delete_list[@]+"${delete_list[@]}"}
print_list "已忽略,不参与同步" ${excluded_list[@]+"${excluded_list[@]}"}
echo ""
echo "=== ⚠ 将覆盖 air-cargo 侧独立修改(${#conflict_list[@]})==="
for p in ${conflict_list[@]+"${conflict_list[@]}"}; do echo " $p"; done
if [ -n "$unrelated" ]; then
echo ""
echo "=== ✖ air-cargo 工作区存在与本次同步无关的未提交改动 ==="
printf '%s\n' "$unrelated" | sed 's/^/ /'
fi
echo ""
echo "=== 提交信息 ==="
sed 's/^/ /' "$TMP/msg"
for item in "${copy_list[@]+"${copy_list[@]}"}"; do
path="${item#*|}"
mkdir -p "$(dirname "$TARGET_DIR/$path")"
cp -f "$SOURCE_DIR/$path" "$TARGET_DIR/$path"
copied=$((copied+1))
done
if [ "$MODE" != apply ]; then
echo ""
echo "(预览模式,未执行任何操作)"
exit 0
fi
for item in "${delete_list[@]+"${delete_list[@]}"}"; do
path="${item#*|}"
if [ -e "$TARGET_DIR/$path" ]; then
rm -f "$TARGET_DIR/$path"
deleted=$((deleted+1))
# ---------- 执行 ----------
[ "$INFERRED" -eq 0 ] || die "基线为反推结果,确认无误后请用 --base $(short "$BASE") 重新执行"
[ -z "$unrelated" ] || die "air-cargo 工作区存在与本次同步无关的未提交改动,请先处理"
copied=0 deleted=0
stage_list=()
for item in ${copy_list[@]+"${copy_list[@]}"}; do
p="${item#*|}" dst="$TARGET_DIR/${item#*|}"
mkdir -p "$(dirname "$dst")"
rm -f "$dst"
if [ "$WORKTREE" -eq 1 ]; then
cp -f "$SOURCE_DIR/$p" "$dst"
else
mode="$(S ls-tree "$TO" -- "$p" | cut -d' ' -f1)"
case "$mode" in
120000) ln -s "$(S cat-file blob "$TO:$p")" "$dst" ;;
160000) echo "跳过子模块:$p" >&2; continue ;;
*) S cat-file blob "$TO:$p" > "$dst"; [ "$mode" != 100755 ] || chmod +x "$dst" ;;
esac
fi
stage_list+=("$p")
copied=$((copied + 1))
done
for item in ${delete_list[@]+"${delete_list[@]}"}; do
p="${item#*|}"
if [ -e "$TARGET_DIR/$p" ] || [ -L "$TARGET_DIR/$p" ]; then
rm -f "$TARGET_DIR/$p"
deleted=$((deleted + 1))
fi
# 只暂存 air-cargo 中已跟踪的路径,否则 git add 会因 pathspec 不匹配而失败
! T ls-files --error-unmatch -- "$p" > /dev/null 2>&1 || stage_list+=("$p")
done
[ ${#stage_list[@]} -eq 0 ] || printf '%s\0' "${stage_list[@]}" | T add -A --pathspec-from-file=- --pathspec-file-nul
echo ""
echo "同步完成:复制/更新 $copied 个文件,删除 $deleted 个文件。"
if T diff --cached --quiet; then
if [ "$WORKTREE" -eq 1 ]; then
echo "air-cargo 内容已一致,无需提交。"
exit 0
fi
# 区间内只有不参与同步的文件变化时,仍提交一个空提交推进同步点
T commit -q --allow-empty -F "$TMP/msg"
else
T commit -q -F "$TMP/msg"
fi
echo "已在 air-cargo 本地提交(未 push):$(T log -1 --format='%h %s')"
T show --stat --format= HEAD | tail -1

2
.gitignore vendored
View File

@@ -1,6 +1,8 @@
# Created by https://www.toptal.com/developers/gitignore/api/androidstudio,gradle,java,kotlin
# Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio,gradle,java,kotlin
air-cargo/
### Java ###
# Compiled class file
*.class

View File

@@ -51,6 +51,15 @@ aerologic-app/
└── 其他业务模块...
```
### ⛔ `air-cargo/` 目录:一律忽略
根目录下的 `air-cargo/` 是同一份源码的另一个独立 git 仓库(远程地址不同,工作分支 `feature/hefei`),**只用于把本项目代码提交到另一个仓库**,不参与本项目任何开发;它已在 `.gitignore` 中忽略。
- 代码扫描、搜索、实现功能、排查问题、查 git 历史时**一律跳过该目录**,不读取、不修改、不当作参考实现(其中文件是同步过去的副本,可能落后于主干)
- `find` / `grep -r` 这类不认 `.gitignore` 的命令要显式排除(如 `find . -path ./air-cargo -prune -o …`、`grep -r --exclude-dir=air-cargo`);`git grep`、`rg` 默认已跳过
- 唯一允许操作它的场景是 `sync-air-cargo` skill(同步并在其本地提交,不 push)
- 其中不应存在任何 Claude / Agent 相关文件(`CLAUDE.md`、`.claude/`、`.mcp.json` 等),sync 脚本和 air-cargo 本地的 `.git/info/exclude` 都已屏蔽
---
## 6 种典型页面类型