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
69 changes: 67 additions & 2 deletions Pull-SDLC.ai.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2439,6 +2439,71 @@ Describe 'Invoke-SetupGitHubSsh' -Skip:(-not $IsWindows) {
($addCalls | Where-Object { $_ -match '\bgit@github\.com:$' }) | Should -Not -BeNullOrEmpty
# The already-present https value must NOT be re-added.
($addCalls | Where-Object { $_ -match '\bhttps://github\.com/$' }) | Should -BeNullOrEmpty
}
}
}
}

Describe 'Test-LocalDriftOnManagedPaths (issue #178: EOL false positive)' {
BeforeEach {
$script:driftRepo = Join-Path ([System.IO.Path]::GetTempPath()) ("drift-eol-" + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $script:driftRepo | Out-Null
Push-Location $script:driftRepo
git init -q -b main
git config user.email 't@t.t'
git config user.name 't'
# Disable autocrlf so the test controls blob bytes deterministically.
git config core.autocrlf false
}

AfterEach {
Pop-Location
Remove-Item -Recurse -Force -LiteralPath $script:driftRepo -ErrorAction SilentlyContinue
}

It 'does NOT report drift when HEAD differs from the anchor only by CRLF/LF line endings' {
# Anchor commit: managed file stored with LF (the upstream form).
[System.IO.File]::WriteAllText((Join-Path $script:driftRepo 'CLAUDE.md'), "line one`nline two`n")
git add -A | Out-Null
git commit -q -m 'anchor (LF)'
$anchor = (git rev-parse HEAD).Trim()
# HEAD commit: identical content re-committed with CRLF (a Windows consumer).
[System.IO.File]::WriteAllText((Join-Path $script:driftRepo 'CLAUDE.md'), "line one`r`nline two`r`n")
git add -A | Out-Null
git commit -q -m 'consumer sync (CRLF)'

# Sanity: the raw blob SHAs really do differ (otherwise the test is vacuous).
(git rev-parse "HEAD:CLAUDE.md").Trim() | Should -Not -Be (git rev-parse "${anchor}:CLAUDE.md").Trim()

$drift = @(Test-LocalDriftOnManagedPaths -Anchor $anchor -ManagedPaths 'CLAUDE.md' -RepoRoot $script:driftRepo)
$drift.Count | Should -Be 0
}

It 'reports drift when HEAD has a genuine content change beyond line endings' {
[System.IO.File]::WriteAllText((Join-Path $script:driftRepo 'CLAUDE.md'), "line one`nline two`n")
git add -A | Out-Null
git commit -q -m 'anchor'
$anchor = (git rev-parse HEAD).Trim()
[System.IO.File]::WriteAllText((Join-Path $script:driftRepo 'CLAUDE.md'), "line one`nEDITED line two`n")
git add -A | Out-Null
git commit -q -m 'real local edit'

$drift = @(Test-LocalDriftOnManagedPaths -Anchor $anchor -ManagedPaths 'CLAUDE.md' -RepoRoot $script:driftRepo)
$drift.Count | Should -Be 1
$drift[0].Path | Should -Be 'CLAUDE.md'
}

It 'reports drift for an upstream-deleted file the consumer still has committed' {
# Anchor does NOT contain CLAUDE.md (simulates a file deleted upstream).
[System.IO.File]::WriteAllText((Join-Path $script:driftRepo 'README.txt'), "seed`n")
git add -A | Out-Null
git commit -q -m 'anchor without managed file'
$anchor = (git rev-parse HEAD).Trim()
[System.IO.File]::WriteAllText((Join-Path $script:driftRepo 'CLAUDE.md'), "orphaned content`n")
git add -A | Out-Null
git commit -q -m 'consumer still carries deleted file'

$drift = @(Test-LocalDriftOnManagedPaths -Anchor $anchor -ManagedPaths 'CLAUDE.md' -RepoRoot $script:driftRepo)
$drift.Count | Should -Be 1
$drift[0].Path | Should -Be 'CLAUDE.md'
}
}

13 changes: 13 additions & 0 deletions Pull-SDLC.ai.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,15 @@ function Test-LocalDriftOnManagedPaths {
For each upstream-managed path that currently exists at HEAD, compares
its HEAD blob to the same path's blob at $Anchor. Returns an array of
@{ Path = ...; Commit = '<sha> <subject>' } for drift entries.
.DESCRIPTION
A fast blob-SHA comparison is used as a pre-filter. Because blob SHAs are
end-of-line sensitive, a Windows consumer whose git committed the synced
files with CRLF will have HEAD blobs that differ from the upstream LF
blobs even though the content is identical. To avoid this false positive,
any path whose blobs differ is confirmed with an EOL-insensitive content
diff (git diff --ignore-cr-at-eol); only paths that still differ after
ignoring CR-at-EOL are reported as drift. Genuine divergence -- including
upstream-deleted files the consumer still has committed -- is preserved.
#>
[CmdletBinding()]
param(
Expand All @@ -1101,6 +1110,10 @@ function Test-LocalDriftOnManagedPaths {
$headSha = (& git rev-parse "HEAD:$p" 2>$null)
$anchorSha = (& git rev-parse "${Anchor}:$p" 2>$null)
if ($headSha -and $headSha -ne $anchorSha) {
# Blobs differ. Confirm this is a real content change and not just
# CRLF/LF normalization before flagging it as a policy violation.
& git diff --quiet --ignore-cr-at-eol $Anchor HEAD -- $p 2>$null
if ($LASTEXITCODE -eq 0) { continue }
$log = (& git log -1 --pretty='%h %s' -- $p 2>$null) -join ''
$drift.Add(@{ Path = $p; Commit = $log }) | Out-Null
}
Expand Down
Loading