From e97fb006492684a79415eba097f65ff3f11da5e3 Mon Sep 17 00:00:00 2001 From: csh Date: Thu, 8 Jan 2026 12:17:00 +0800 Subject: [PATCH] :sparkles: feat(sync_standards): default gitattributes to append --- README.md | 11 ++-- scripts/sync_standards.bat | 82 +++++++++++++++++++++++++- scripts/sync_standards.ps1 | 67 +++++++++++++++++++-- scripts/sync_standards.sh | 68 +++++++++++++++++++-- tests/README.md | 2 +- tests/scripts/test_sync_standards.bats | 28 ++++++++- 6 files changed, 237 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 6524786..a112396 100644 --- a/README.md +++ b/README.md @@ -153,13 +153,12 @@ git commit -m ":package: deps(playbook): add tsl standards" - 规则入口可读:`.agents/tsl/index.md` - (可选)C++ 规则入口可读:`.agents/cpp/index.md` - 标准文档可读:`docs/standards/playbook/docs/index.md` - - `.gitattributes` 包含区块:`# BEGIN playbook .gitattributes` / - `# END playbook .gitattributes` + - `.gitattributes` 包含追加块头:`# Added from playbook .gitattributes` 4. 将同步产物纳入版本控制(目标项目建议提交): - `docs/standards/playbook/`(标准快照) - `.agents/tsl/`(落地规则集) - - `.gitattributes`(managed block 更新) + - `.gitattributes`(追加缺失规则) - `AGENTS.md`(若本次自动生成) #### 新项目 / 旧项目(命令示例) @@ -240,8 +239,8 @@ sh docs/standards/playbook/scripts/sync_standards.sh tsl cpp 同步脚本行为(目标项目内的最终落地内容): - 覆盖/更新:`.agents//`(默认 `.agents/tsl/`) -- 更新 `.gitattributes`:默认只维护 `# BEGIN playbook .gitattributes` 区块(可用 - `SYNC_GITATTR_MODE=block|overwrite|skip` 控制) +- 更新 `.gitattributes`:默认追加缺失规则(可用 + `SYNC_GITATTR_MODE=append|block|overwrite|skip` 控制) - 缺省创建:`.agents/index.md` - 覆盖前备份:写入同目录的 `*.bak.*`(或 Windows 下随机后缀) - 不修改:`.gitignore`(项目自行维护) @@ -256,7 +255,7 @@ sh docs/standards/playbook/scripts/sync_standards.sh tsl cpp | 变量名 | 默认值 | 说明 | | ------------------- | ------- | ------------------------------------------------------------------------------------------- | | `AGENTS_NS` | `tsl` | 同步的规则集名/落地目录名:`.agents//`(例如 `tsl`、`cpp`) | -| `SYNC_GITATTR_MODE` | `block` | `.gitattributes` 同步模式:`block` 仅维护 managed 区块;`overwrite` 全量覆盖;`skip` 不更新 | +| `SYNC_GITATTR_MODE` | `append` | `.gitattributes` 同步模式:`append` 仅追加缺失规则(忽略注释/空行,比对后按块追加);`block` 仅维护 managed 区块;`overwrite` 全量覆盖;`skip` 不更新 | diff --git a/scripts/sync_standards.bat b/scripts/sync_standards.bat index aca801e..1c2a837 100644 --- a/scripts/sync_standards.bat +++ b/scripts/sync_standards.bat @@ -3,7 +3,7 @@ setlocal enabledelayedexpansion rem Sync standards snapshot to project root. rem - Copies \.agents\ -> \.agents\ -rem - Updates \.gitattributes (managed block by default) +rem - Updates \.gitattributes (append missing rules by default) rem Existing targets are backed up before overwrite. rem rem Multi rulesets: @@ -34,7 +34,7 @@ set "AGENTS_ROOT=%ROOT%\.agents" set "AGENTS_DST=%AGENTS_ROOT%\%AGENTS_NS%" set "GITATTR_DST=%ROOT%\.gitattributes" set "SYNC_GITATTR_MODE=%SYNC_GITATTR_MODE%" -if "%SYNC_GITATTR_MODE%"=="" set "SYNC_GITATTR_MODE=block" +if "%SYNC_GITATTR_MODE%"=="" set "SYNC_GITATTR_MODE=append" rem Multi rulesets: only on outer invocation. if "%SYNC_STANDARDS_INNER%"=="" ( @@ -155,8 +155,84 @@ if exist "%GITATTR_SRC%" ( goto AfterGitAttr ) + if /I "%SYNC_GITATTR_MODE%"=="append" ( + for %%I in ("%GITATTR_SRC%") do set "GITATTR_SRC_F=%%~fI" + for %%I in ("%GITATTR_DST%") do set "GITATTR_DST_F=%%~fI" + if /I "!GITATTR_SRC_F!"=="!GITATTR_DST_F!" ( + echo Skip: .gitattributes source equals destination. + goto AfterGitAttr + ) + + set "TMP_DST=%TEMP%\\gitattributes.dst.%RANDOM%.tmp" + set "TMP_MISS=%TEMP%\\gitattributes.missing.%RANDOM%.tmp" + if exist "!TMP_DST!" del /q "!TMP_DST!" >nul 2>nul + if exist "!TMP_MISS!" del /q "!TMP_MISS!" >nul 2>nul + type nul > "!TMP_DST!" + type nul > "!TMP_MISS!" + + if exist "%GITATTR_DST%" ( + for /f "usebackq delims=" %%L in ("%GITATTR_DST%") do ( + set "LINE=%%L" + for /f "tokens=* delims= " %%A in ("!LINE!") do set "LINE=%%A" + if not "!LINE!"=="" ( + if /I not "!LINE:~0,1!"=="#" ( + echo(!LINE!>>"!TMP_DST!" + ) + ) + ) + ) + + for /f "usebackq delims=" %%L in ("%GITATTR_SRC%") do ( + set "LINE=%%L" + for /f "tokens=* delims= " %%A in ("!LINE!") do set "LINE=%%A" + if not "!LINE!"=="" ( + if /I not "!LINE:~0,1!"=="#" ( + findstr /x /l /c:"!LINE!" "!TMP_DST!" >nul || ( + findstr /x /l /c:"!LINE!" "!TMP_MISS!" >nul || echo(!LINE!>>"!TMP_MISS!" + ) + ) + ) + ) + + set "MISS_SIZE=0" + if exist "!TMP_MISS!" for %%S in ("!TMP_MISS!") do set "MISS_SIZE=%%~zS" + if "!MISS_SIZE!"=="0" ( + del /q "!TMP_DST!" "!TMP_MISS!" >nul 2>nul + echo No missing .gitattributes rules to append. + goto AfterGitAttr + ) + + if exist "%GITATTR_DST%" ( + set "RAND=%RANDOM%" + set "BAK_NAME=.gitattributes.bak.!RAND!" + ren "%GITATTR_DST%" "!BAK_NAME!" + echo Backed up existing .gitattributes -> !BAK_NAME! + set "DST_IN=%ROOT%\\!BAK_NAME!" + ) else ( + set "DST_IN=" + ) + + set "TMP_OUT=%TEMP%\\gitattributes.out.%RANDOM%.tmp" + if exist "!TMP_OUT!" del /q "!TMP_OUT!" >nul 2>nul + + if not "!DST_IN!"=="" ( + type "!DST_IN!" > "!TMP_OUT!" + for %%S in ("!DST_IN!") do set "DST_SIZE=%%~zS" + if not "!DST_SIZE!"=="0" echo.>>"!TMP_OUT!" + ) + + set "SOURCE_NOTE=%GITATTR_SRC%" + >>"!TMP_OUT!" echo # Added from playbook .gitattributes ^(source: !SOURCE_NOTE!^) + type "!TMP_MISS!" >> "!TMP_OUT!" + + copy /y "!TMP_OUT!" "%GITATTR_DST%" >nul + del /q "!TMP_DST!" "!TMP_MISS!" "!TMP_OUT!" >nul 2>nul + echo Appended missing .gitattributes rules from standards. + goto AfterGitAttr + ) + if /I not "%SYNC_GITATTR_MODE%"=="block" ( - echo ERROR: invalid SYNC_GITATTR_MODE=%SYNC_GITATTR_MODE% ^(use block^|overwrite^|skip^) + echo ERROR: invalid SYNC_GITATTR_MODE=%SYNC_GITATTR_MODE% ^(use block^|overwrite^|append^|skip^) exit /b 1 ) diff --git a/scripts/sync_standards.ps1 b/scripts/sync_standards.ps1 index e62eb71..0a2aba7 100644 --- a/scripts/sync_standards.ps1 +++ b/scripts/sync_standards.ps1 @@ -1,6 +1,6 @@ # Sync standards snapshot to project root. # - Copies /.agents/ -> /.agents/ -# - Updates /.gitattributes (managed block by default) +# - Updates /.gitattributes (append missing rules by default) # Existing targets are backed up before overwrite. [CmdletBinding()] param( @@ -38,7 +38,7 @@ if (-not $env:SYNC_STANDARDS_INNER -and $Langs -and $Langs.Count -gt 0) { $oldMode = $env:SYNC_GITATTR_MODE $syncModeFirst = $env:SYNC_GITATTR_MODE - if (-not $syncModeFirst) { $syncModeFirst = "block" } + if (-not $syncModeFirst) { $syncModeFirst = "append" } $first = $true foreach ($ns in $Langs) { @@ -136,7 +136,7 @@ if (-not (Test-Path $AgentsMd)) { $GitAttrDst = Join-Path $Root ".gitattributes" if (Test-Path $GitAttrSrc) { $mode = $env:SYNC_GITATTR_MODE - if (-not $mode) { $mode = "block" } + if (-not $mode) { $mode = "append" } switch ($mode.ToLowerInvariant()) { "skip" { Write-Host "Skip: .gitattributes sync (SYNC_GITATTR_MODE=skip)." @@ -156,6 +156,65 @@ if (Test-Path $GitAttrSrc) { Write-Host "Synced .gitattributes from standards (overwrite)." break } + "append" { + if ($GitAttrSrc -ieq $GitAttrDst) { + Write-Host "Skip: .gitattributes source equals destination." + break + } + + $dstLines = @{} + if (Test-Path $GitAttrDst) { + Get-Content $GitAttrDst | ForEach-Object { + $line = $_.Trim() + if ($line -eq "" -or $line.StartsWith("#")) { return } + $dstLines[$line] = $true + } + } + + $missing = New-Object System.Collections.Generic.List[string] + Get-Content $GitAttrSrc | ForEach-Object { + $line = $_.Trim() + if ($line -eq "" -or $line.StartsWith("#")) { return } + if (-not $dstLines.ContainsKey($line)) { + $dstLines[$line] = $true + $missing.Add($line) + } + } + + if ($missing.Count -eq 0) { + Write-Host "No missing .gitattributes rules to append." + break + } + + $bak = $null + if (Test-Path $GitAttrDst) { + $bak = "$GitAttrDst.bak.$timestamp" + Move-Item $GitAttrDst $bak -Force + Write-Host "Backed up existing .gitattributes -> $bak" + } + + $sourceNote = $GitAttrSrc + $rootPrefix = "$Root\" + if ($sourceNote.StartsWith($rootPrefix)) { + $sourceNote = $sourceNote.Substring($rootPrefix.Length) + } + $header = "# Added from playbook .gitattributes (source: $sourceNote)" + + $content = @() + if ($bak -and (Test-Path $bak)) { + $existing = Get-Content $bak + if ($existing.Count -gt 0) { + $content += $existing + $content += "" + } + } + $content += $header + $content += $missing + $content | Set-Content -Path $GitAttrDst -Encoding UTF8 + + Write-Host "Appended missing .gitattributes rules from standards." + break + } "block" { $begin = "# BEGIN playbook .gitattributes" $end = "# END playbook .gitattributes" @@ -186,7 +245,7 @@ if (Test-Path $GitAttrSrc) { break } default { - throw "Invalid SYNC_GITATTR_MODE=$mode (use block|overwrite|skip)" + throw "Invalid SYNC_GITATTR_MODE=$mode (use block|overwrite|append|skip)" } } } diff --git a/scripts/sync_standards.sh b/scripts/sync_standards.sh index 674e875..6c3dd8f 100644 --- a/scripts/sync_standards.sh +++ b/scripts/sync_standards.sh @@ -3,7 +3,7 @@ set -eu # Sync standards snapshot to project root. # - Copies /.agents/ -> /.agents/ -# - Updates /.gitattributes (managed block by default) +# - Updates /.gitattributes (append missing rules by default) # Existing targets are backed up before overwrite. # # Multi rulesets: @@ -48,7 +48,7 @@ if [ "${SYNC_STANDARDS_INNER:-}" != "1" ]; then langs="$*" fi if [ -n "${langs:-}" ]; then - sync_mode_first="${SYNC_GITATTR_MODE:-block}" + sync_mode_first="${SYNC_GITATTR_MODE:-append}" first=1 old_ifs="${IFS}" @@ -141,7 +141,7 @@ echo "Synced agents ruleset to $AGENTS_DST." GITATTR_DST="$ROOT/.gitattributes" if [ -f "$GITATTR_SRC" ]; then - : "${SYNC_GITATTR_MODE:=block}" + : "${SYNC_GITATTR_MODE:=append}" case "$SYNC_GITATTR_MODE" in skip) echo "Skip: .gitattributes sync (SYNC_GITATTR_MODE=skip)." @@ -158,6 +158,66 @@ if [ -f "$GITATTR_SRC" ]; then echo "Synced .gitattributes from standards (overwrite)." fi ;; + append) + if [ "$(CDPATH= cd -- "$(dirname -- "$GITATTR_SRC")" && pwd -P)/$(basename -- "$GITATTR_SRC")" = "$GITATTR_DST" ]; then + echo "Skip: .gitattributes source equals destination." + else + if [ -f "$GITATTR_DST" ]; then + dst_file="$GITATTR_DST" + else + dst_file="/dev/null" + fi + missing_tmp="$(mktemp 2>/dev/null || echo "$ROOT/.gitattributes.missing.$timestamp")" + awk ' + function norm(line) { + gsub(/^[ \t]+|[ \t]+$/, "", line) + return line + } + FNR==NR { + line=norm($0) + if (line == "" || line ~ /^#/) next + seen[line]=1 + next + } + { + line=norm($0) + if (line == "" || line ~ /^#/) next + if (!seen[line] && !out[line]++) print line + } + ' "$dst_file" "$GITATTR_SRC" >"$missing_tmp" + + if [ ! -s "$missing_tmp" ]; then + rm -f "$missing_tmp" + echo "No missing .gitattributes rules to append." + echo "Done." + exit 0 + fi + + if [ -e "$GITATTR_DST" ]; then + mv "$GITATTR_DST" "$ROOT/.gitattributes.bak.$timestamp" + echo "Backed up existing .gitattributes -> .gitattributes.bak.$timestamp" + fi + + source_note="$GITATTR_SRC" + case "$GITATTR_SRC" in + "$ROOT"/*) source_note="${GITATTR_SRC#$ROOT/}" ;; + esac + header="# Added from playbook .gitattributes (source: $source_note)" + + { + if [ -f "$ROOT/.gitattributes.bak.$timestamp" ]; then + cat "$ROOT/.gitattributes.bak.$timestamp" + if [ -s "$ROOT/.gitattributes.bak.$timestamp" ]; then + printf "\n" + fi + fi + printf "%s\n" "$header" + cat "$missing_tmp" + } >"$GITATTR_DST" + rm -f "$missing_tmp" + echo "Appended missing .gitattributes rules from standards." + fi + ;; block) begin="# BEGIN playbook .gitattributes" end="# END playbook .gitattributes" @@ -207,7 +267,7 @@ if [ -f "$GITATTR_SRC" ]; then echo "Updated .gitattributes from standards (managed block)." ;; *) - echo "ERROR: invalid SYNC_GITATTR_MODE=$SYNC_GITATTR_MODE (use block|overwrite|skip)" >&2 + echo "ERROR: invalid SYNC_GITATTR_MODE=$SYNC_GITATTR_MODE (use block|overwrite|append|skip)" >&2 exit 1 ;; esac diff --git a/tests/README.md b/tests/README.md index e309191..64a6b80 100644 --- a/tests/README.md +++ b/tests/README.md @@ -72,7 +72,7 @@ sh check_doc_links.sh - 单语言同步(tsl/cpp) - 多语言同步(tsl cpp) - **.gitattributes 同步**: - - 默认模式创建 managed block + - 默认模式追加缺失规则 - 保留现有内容 - 更新已存在的 managed block - **AGENTS.md 处理**: diff --git a/tests/scripts/test_sync_standards.bats b/tests/scripts/test_sync_standards.bats index 0b864c0..988f657 100644 --- a/tests/scripts/test_sync_standards.bats +++ b/tests/scripts/test_sync_standards.bats @@ -81,15 +81,15 @@ teardown() { # .gitattributes 同步测试 # ============================================== -@test ".gitattributes - 默认模式创建 managed block" { +@test ".gitattributes - 默认模式追加缺失规则" { cd "$TEST_DIR" [ ! -f ".gitattributes" ] sh "$SCRIPT_PATH" tsl [ -f ".gitattributes" ] - grep -q "# BEGIN playbook .gitattributes" .gitattributes - grep -q "# END playbook .gitattributes" .gitattributes + grep -q "Added from playbook .gitattributes" .gitattributes + grep -q "\\*.tsl" .gitattributes } @test ".gitattributes - 保留现有内容" { @@ -111,6 +111,7 @@ teardown() { # END playbook .gitattributes EOF + export SYNC_GITATTR_MODE=block sh "$SCRIPT_PATH" tsl # 验证 block 已更新(不再包含 "Old content") @@ -232,6 +233,27 @@ EOF ! grep -q "# Custom content" .gitattributes } +@test "环境变量 - SYNC_GITATTR_MODE=append 追加缺失规则" { + cd "$TEST_DIR" + echo "# Custom rules only" > .gitattributes + export SYNC_GITATTR_MODE=append + + sh "$SCRIPT_PATH" tsl + + grep -q "Added from playbook .gitattributes" .gitattributes + grep -q "\\*.tsl" .gitattributes +} + +@test "环境变量 - SYNC_GITATTR_MODE=append 无缺失规则不追加" { + cd "$TEST_DIR" + cp "$PLAYBOOK_ROOT/.gitattributes" .gitattributes + export SYNC_GITATTR_MODE=append + + sh "$SCRIPT_PATH" tsl + + ! grep -q "Added from playbook .gitattributes" .gitattributes +} + # ============================================== # 幂等性测试 # ==============================================