Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
72 changes: 67 additions & 5 deletions scripts/lib/gitignore-baseline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,57 @@ _gib_has_markers() {
&& printf '%s\n' "$content" | grep -qxF "$GIB_END_MARKER"
}

# _gib_neutralize_l2 <block>
# Read candidate L2 content on stdin; print it with every line that would fight
# the L1 block removed. A line is dropped when — after CRLF and trailing-whitespace
# trimming — it exactly matches a *pattern* line of <block> (comment and blank
# lines of the block are ignored, so they never strip a repo's own comments). This
# does double duty:
#
# • Negations win: a bare re-hiding pattern the target kept in L2 (e.g. `*.pem`
# with no `!public.pem`) is identical to the block's own broad pattern, so it
# is stripped. The file it re-hid stays ignored by the block, and the block's
# `!public.pem` (which sits after `*.pem` inside the block) is no longer
# overridden by a later L2 line — the negated path is re-allowed as intended.
# • Migrate, don't duplicate: an old *unmarkered* baseline pasted into L2 has its
# pattern lines folded away (they all live in the block now), so upsert stops
# shipping a second full copy. The genuine ecosystem/OS entries the repo added
# stay put.
#
# Only pattern lines are matched — never comments — because block comments include
# bare `#` separators that would otherwise clobber a repo's own comment lines.
# Runs of blank lines left behind by the removed patterns are squeezed (leading and
# consecutive blanks dropped) so migration output stays tidy. Idempotent: once the
# duplicate patterns are gone a second pass finds nothing to strip.
_gib_neutralize_l2() {
local block="$1"
awk -v block="$block" '
BEGIN {
n = split(block, barr, "\n")
for (i = 1; i <= n; i++) {
bl = barr[i]
sub(/\r$/, "", bl)
sub(/[ \t]+$/, "", bl)
if (bl ~ /^[ \t]*$/) continue # skip blank block lines
if (bl ~ /^[ \t]*#/) continue # skip comment block lines (incl. bare #)
drop[bl] = 1
}
prev_blank = 1 # squeeze any blank lines that would lead the output
}
{
line = $0
sub(/\r$/, "", line)
key = line
sub(/[ \t]+$/, "", key)
if (key in drop) next
Comment thread
don-petry marked this conversation as resolved.
Resolved
is_blank = (key ~ /^[ \t]*$/)
if (is_blank && prev_blank) next
print line
prev_blank = is_blank
}
'
}
Comment thread
don-petry marked this conversation as resolved.
Resolved

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
# upsert_gitignore_baseline <block_file> [existing_file]
# Print the upserted .gitignore content to stdout:
# • <block_file> — a file containing the marker-wrapped L1 block (e.g. the
Expand All @@ -64,10 +115,17 @@ _gib_has_markers() {
#
# Behaviour (idempotent):
# • existing has the markers → REPLACE the block in place; content above BEGIN
# and the entire L2 below END are preserved verbatim.
# • existing lacks the markers → INSERT the block on top; the whole existing file
# becomes L2 below the END marker.
# is preserved verbatim and the L2 below END is
# neutralized (see below).
# • existing lacks the markers → INSERT the block on top; the existing file
# becomes L2 below the END marker, neutralized.
# • existing empty/absent → output is exactly the block.
#
# L2 neutralization (#809): every L2 line that duplicates a block *pattern* line is
# dropped, so a re-hiding pattern the target kept in L2 (e.g. a bare `*.pem` with no
# `!public.pem`) can't override the block's negations, and an old unmarkered
# baseline pasted into L2 folds into the single block instead of duplicating.
# Genuine per-repo L2 (ecosystem/OS entries) is preserved. See _gib_neutralize_l2.
# Re-running on its own output is a no-op.
upsert_gitignore_baseline() {
local block_file="${1:-}" existing_file="${2:-}"
Expand Down Expand Up @@ -101,10 +159,13 @@ upsert_gitignore_baseline() {
return 2
fi

# Marker-less target: block on top, existing content becomes L2.
# Marker-less target: block on top, existing content becomes L2 — but neutralize
# any L2 line that duplicates/fights the block (see _gib_neutralize_l2).
if [ "$has_begin" -eq 0 ]; then
local l2
l2="$(_gib_neutralize_l2 "$block" <<< "$existing")"
printf '%s\n' "$block"
printf '%s\n' "$existing"
[ -n "$l2" ] && printf '%s\n' "$l2"
return 0
fi

Expand All @@ -114,6 +175,7 @@ upsert_gitignore_baseline() {
local pre post
pre="$(tr -d '\r' <<< "$existing" | awk -v b="$GIB_BEGIN_MARKER" '$0 == b { exit } { print }')"
post="$(tr -d '\r' <<< "$existing" | awk -v e="$GIB_END_MARKER" 'seen { print } $0 == e { seen = 1 }')"
post="$(_gib_neutralize_l2 "$block" <<< "$post")"

[ -n "$pre" ] && printf '%s\n' "$pre"
printf '%s\n' "$block"
Expand Down
102 changes: 102 additions & 0 deletions test/scripts/lib/gitignore-baseline.bats
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,105 @@ EOF
[ "$status" -eq 2 ]
[[ "$output" == *"half-open"* ]]
}

# ── #809: baseline negations must win over a re-hiding L2 pattern ──────────────
# Reproduces the fleet dry-run failure on `markets` / `google-app-scripts`: an
# ad-hoc L2 with a broad `*.pem` and no `!public.pem` re-hid the file the L1 block
# re-allows. Assert against real git semantics with `git check-ignore`.

# _gib_check_ignored <gitignore_content> <path>
# Return 0 if <path> is ignored, 1 if not — mirrors the issue's repro probe.
_gib_check_ignored() {
local content="$1" path="$2" d
d="$(mktemp -d)"
( cd "$d" && git init -q )
printf '%s\n' "$content" > "$d/.gitignore"
( cd "$d" && git check-ignore -q "$path" )
local rc=$?
Comment thread
don-petry marked this conversation as resolved.
Resolved
rm -rf "$d"
return "$rc"
}
Comment thread
don-petry marked this conversation as resolved.
Resolved

@test "#809 markets-like: broad *.pem in L2 does not re-hide baseline-negated public.pem" {
# Use the REAL canonical block (it carries !public.pem after *.pem).
local realblock="${TT_TMP}/realblock"
gib_extract_baseline_block "${TT_REPO_ROOT}/.gitignore" > "$realblock"

local existing="${TT_TMP}/markets"
printf '# markets ad-hoc\n*.pem\nnode_modules/\n' > "$existing"

# Capture the upsert output in its own var — bats clobbers $output on every `run`.
local gi
gi="$(upsert_gitignore_baseline "$realblock" "$existing")"

run _gib_check_ignored "$gi" public.pem
[ "$status" -eq 1 ] # NOT ignored
# A genuine secret pem is still ignored by the baseline.
run _gib_check_ignored "$gi" secret.pem
[ "$status" -eq 0 ]
# Genuine L2 survives.
grep -qxF 'node_modules/' <<< "$gi"
}

@test "#809 google-app-scripts-like: broad *.pem in L2 does not re-hide public.pem" {
local realblock="${TT_TMP}/realblock"
gib_extract_baseline_block "${TT_REPO_ROOT}/.gitignore" > "$realblock"

local existing="${TT_TMP}/gas"
printf '.clasp.json\n*.pem\n' > "$existing"

local gi
gi="$(upsert_gitignore_baseline "$realblock" "$existing")"

run _gib_check_ignored "$gi" public.pem
[ "$status" -eq 1 ] # NOT ignored
grep -qxF '.clasp.json' <<< "$gi"
}

# ── #809: migrate an existing unmarkered baseline, don't duplicate it ──────────
@test "#809 TalkTerm-like: unmarkered baseline in L2 is folded into one block, not duplicated" {
# L2 IS a copy of the old unmarkered baseline (same secret lines, no markers),
# plus genuine ecosystem/OS entries the repo legitimately added.
local existing="${TT_TMP}/talkterm"
cat > "$existing" <<EOF
.env
!.env.example
*.pem
!*.pub
*.key
node_modules/
dist/
*.log
.DS_Store
EOF

run upsert_gitignore_baseline "$BLOCK" "$existing"
[ "$status" -eq 0 ]

# Exactly one marker-wrapped block.
[ "$(printf '%s\n' "$output" | grep -cxF "$GIB_BEGIN_MARKER")" -eq 1 ]
[ "$(printf '%s\n' "$output" | grep -cxF "$GIB_END_MARKER")" -eq 1 ]
# Secret patterns appear exactly once (they live in the block; the L2 copy is folded away).
[ "$(printf '%s\n' "$output" | grep -cxF '.env')" -eq 1 ]
[ "$(printf '%s\n' "$output" | grep -cxF '*.pem')" -eq 1 ]
[ "$(printf '%s\n' "$output" | grep -cxF '!.env.example')" -eq 1 ]
# Genuine ecosystem/OS L2 entries are preserved.
grep -qxF 'node_modules/' <<< "$output"
grep -qxF 'dist/' <<< "$output"
grep -qxF '*.log' <<< "$output"
grep -qxF '.DS_Store' <<< "$output"
}

@test "#809 negation-win + migration is idempotent" {
local realblock="${TT_TMP}/realblock"
gib_extract_baseline_block "${TT_REPO_ROOT}/.gitignore" > "$realblock"

local existing="${TT_TMP}/markets2"
printf '# markets ad-hoc\n*.pem\nnode_modules/\n' > "$existing"

local first second
first="$(upsert_gitignore_baseline "$realblock" "$existing")"
printf '%s\n' "$first" > "${TT_TMP}/after1"
second="$(upsert_gitignore_baseline "$realblock" "${TT_TMP}/after1")"
[ "$first" = "$second" ]
}
Loading