feat: Initialize-GitDefaults.ps1 -- compose .gitattributes/.gitignore from upstream templates#161
feat: Initialize-GitDefaults.ps1 -- compose .gitattributes/.gitignore from upstream templates#161MarkMichaelis wants to merge 10 commits into
Conversation
Compose .gitattributes and .gitignore for a consumer project from per-language community templates (alexkaratarakis/gitattributes, github/gitignore) plus a curated PowerShell block. Languages: CSharp, PowerShell, TypeScript, ASP.NET (depends on CSharp). Pinned SHAs bundled under .github/templates/git-defaults. Discovery note: alexkaratarakis/gitattributes has no VisualStudio.gitattributes at the pinned SHA; ASP.NET inherits CSharp only. Tests: 28/28 Pester green. Anti-collusion sabotage verified: 11 behavioural failures when Resolve-GitDefaultsLanguages returns a fixed value. Refs #160
…aults.ps1 - Remove .gitattributes.template from upstream (deleted file). - Remove the .gitattributes.template -> .gitattributes scaffold map entry; drop .gitattributes.template from $script:UpstreamManagedPaths. - Add .github/templates/git-defaults/ to $script:UpstreamManagedPaths so the bundled snapshots flow into consumer projects on sync. - Extend Write-NextStepsBanner: new Write-GitDefaultsHint prints a one-line pointer to ./Initialize-GitDefaults.ps1 when a fresh consumer worktree is missing .gitattributes or .gitignore. We do NOT auto-invoke -- language selection is project-specific. - Replace the obsolete '.gitattributes.template bootstrap (#119)' Describe block with a generic Invoke-TemplateScaffold block plus new migration regression tests covering both data structures and the hint banner. - README.md / CLAUDE.md / copilot-instructions.md: document Initialize-GitDefaults.ps1 alongside Pull-SDLC.ai.ps1; remove .gitattributes.template references. - SOURCES.md: pinned-SHA + authority distinction + refresh recipe. - PSScriptAnalyzer: 0 new findings on Initialize-GitDefaults.ps1 (file-level suppressions for the Write-Host / ShouldProcess / SingularNouns false positives the project already tolerates) and Pull-SDLC.ai.ps1 stays at the 94-finding main baseline. Tests: - Initialize-GitDefaults.Tests.ps1: 28/28 GREEN - Targeted Pull-SDLC.ai.Tests.ps1 (87 tests in the changed-code groups, incl. the issue #148 carve-out e2e fixture): GREEN - Anti-collusion sabotage confirmed behavioural sensitivity in both test files. Closes #160
Phase 5b Evidence -- Initialize-GitDefaults.ps1 end-to-endIssue: #160 Intent (from issue #160)Compose
Canonical command./Initialize-GitDefaults.ps1 -Language CSharp,PowerShell,TypeScript,ASP.NET -Forcerun inside a fresh temp git repo. Captured stdoutResulting .gitattributes (head)Line count: 354 Section dividers found: Resulting .gitignore (head)Line count: 592 Section dividers found: Test summary
VerdictA = yes: the captured artifact shows both files written end-to-end with B = no: no regressions in adjacent test groups; analyzer baseline holds. |
There was a problem hiding this comment.
Pull request overview
This PR replaces the static .gitattributes.template bootstrap path with a new Initialize-GitDefaults.ps1 workflow that composes .gitattributes and .gitignore from bundled upstream language templates plus curated PowerShell defaults.
Changes:
- Adds
Initialize-GitDefaults.ps1, tests, and pinned git-default template snapshots. - Updates
Pull-SDLC.ai.ps1to stop scaffolding.gitattributes.templateand print a git-defaults hint. - Updates README and agent instruction docs to describe the new initialization flow.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents removal of static .gitattributes scaffolding and new git-defaults script. |
| CLAUDE.md | Adds guidance for running Initialize-GitDefaults.ps1. |
| .github/copilot-instructions.md | Mirrors the new git-defaults initialization guidance. |
| Pull-SDLC.ai.ps1 | Removes .gitattributes.template management and adds git-defaults hinting. |
| Pull-SDLC.ai.Tests.ps1 | Updates scaffold tests and adds migration/hint coverage. |
| Initialize-GitDefaults.ps1 | New script to compose .gitattributes and .gitignore. |
| Initialize-GitDefaults.Tests.ps1 | New Pester coverage for language resolution, content generation, and integration behavior. |
| .gitattributes.template | Removes the retired static template. |
| .github/templates/git-defaults/* | Adds pinned bundled gitignore/gitattributes source snapshots and source documentation. |
- UpstreamManagedPaths now lists Initialize-GitDefaults.ps1 and its Tests file so Pull-SDLC.ai will replace local edits on sync. - -Refresh hard-errors with a clear "not yet implemented" message instead of silently using bundled snapshots under a misleading header. Overriding -GitattributesRef/-GitignoreRef without -Refresh is also blocked to prevent header drift. - When -Language is omitted in an interactive host, a simple comma-separated picker now runs with detected languages offered as the default. New Get-GitDefaultsDetectedLanguages function scans the working tree for .csproj/.psm1/tsconfig.json/appsettings.json indicator files; ASP.NET requires both .csproj AND appsettings. - Backup-GitDefaultsFile now uses tick-resolution (100ns) suffixes on .bak collisions, with a uniqueness loop, so two backups in the same second can never overwrite each other. - Help block rewritten to honestly describe the implemented behaviour (no more reserved-future-feature wording in the synopsis). Tests: 36 Pester tests passing (was 28); new coverage for the four review threads. PSScriptAnalyzer clean. Co-authored-by: GitHub Copilot <noreply@github.com>
Seven new findings, all addressed: - Preflight existence check: Initialize-GitDefaults now tests for ALL requested target files before writing ANY of them, so when -Force is omitted and (say) only .gitignore exists, the script aborts without leaving a freshly-written .gitattributes behind. Previously the first write succeeded and the second threw, producing a partial result. - Curated PowerShell block: the in-script comment and the generated file header both said "no upstream template" but SOURCES.md documents that PowerShell.gitattributes DOES exist upstream and is intentionally overridden. Comments + header now describe the override honestly and point at SOURCES.md. - Generated-file footer no longer tells consumers to "re-run to refresh" -- that wording implied -Refresh works, which it does not. Footer now reads "regenerate or add languages". - Get-GitDefaultsTemplateContent error no longer suggests -Refresh for a missing bundled snapshot; it suggests re-running Pull-SDLC.ai instead (which is what actually restores .github/templates/). - Global/Backup.gitignore was bundled but never emitted; .gitignore now always includes the cross-platform Backup section as its final block. - Write-GitDefaultsHint (Pull-SDLC.ai.ps1) now branches three ways -- both missing / only .gitattributes missing / only .gitignore missing -- and gives the appropriate single-file command (-IncludeGitignore:$false or -IncludeGitattributes:$false) so the hint does not steer users into the preflight abort. Tests: 39 Pester tests on Initialize-GitDefaults (added 5); Write-GitDefaultsHint suite expanded from 3 to 5 cases. All targeted suites green, PSScriptAnalyzer baseline unchanged. Co-authored-by: GitHub Copilot <noreply@github.com>
Three findings, all addressed: - Kind-aware curated PowerShell header: the previous "intentional override of upstream" line was emitted for both generated files, but it is only honest for .gitattributes (where upstream DOES ship a PowerShell.gitattributes that we replace). For .gitignore the header now says "no upstream PowerShell.gitignore exists in github/gitignore", matching the in-script comment on the curated block and SOURCES.md. - Removed dead GiboName metadata from $script:GitDefaultsLanguages. No code reads it; carrying it invited drift between the registry and actual composition behaviour. Re-introduce it if/when the network-fetch path is wired in. - New behaviour-first tests for the no-Language branch using Pester Mock to simulate both the non-interactive picker (returns null, Initialize-GitDefaults throws with the supported-language list) and the interactive picker (returns CSharp+PowerShell, full generation proceeds end-to-end). Plus an additional kitchen-sink test that exercises every heuristic in Get-GitDefaultsDetectedLanguages. Tests: 45 Pester (added 6); all green. PSScriptAnalyzer clean. Co-authored-by: GitHub Copilot <noreply@github.com>
Four findings, all addressed:
- Strict mode: Initialize-GitDefaults.ps1 now sets
Set-StrictMode -Version Latest and $ErrorActionPreference = 'Stop'
immediately after the param block, matching the convention used by
Pull-SDLC.ai.ps1, Cleanup-Worktree.ps1, and Consolidate-Tasks.ps1.
Catches undefined variables and non-terminating cmdlet errors that
would previously continue silently while composing consumer files.
- -Refresh is now implemented (previously hard-errored). New helpers:
* Get-GitDefaultsCacheRoot -- platform-aware cache dir
($LOCALAPPDATA on Windows, $XDG_CACHE_HOME / $HOME/.cache on
Unix, falling back to temp).
* Resolve-GitDefaultsSourceRepo -- maps a template file name to
(Repo, Ref) using the appropriate ref param.
* Get-GitDefaultsRefreshedContent -- fetches from
raw.githubusercontent.com/<repo>/<ref>/<file>, writes the
response into the cache directory, and returns the content.
If the fetch fails AND a previously-cached copy exists, falls
back to the cache with a warning. If both fail, throws with a
clear message naming the URL and cache path.
Get-GitDefaultsTemplateContent now accepts -Refresh and routes
through Get-GitDefaultsRefreshedContent in that case. The compose
functions thread -Refresh / -GitattributesRef / -GitignoreRef
through to each fetch call. Generated-file headers now include a
"Source mode: bundled snapshot" or "Source mode: fetched from
upstream" line so consumers can tell at a glance which path was
used. The curated PowerShell block is always in-script and is
unaffected by -Refresh.
- gibo non-dependency: SOURCES.md now has a "Why not gibo?" section
explaining why Initialize-GitDefaults talks directly to
raw.githubusercontent.com instead of shelling out to the gibo
helper (same upstream, but with SHA pinning + cache-fallback that
gibo does not offer). Closes the spec-vs-implementation gap raised
by the reviewer.
- Tests for one-file invocations: -IncludeGitignore:$false and
-IncludeGitattributes:$false each get their own behaviour-first
test proving the existing untouched-file is preserved (no .bak
written, content byte-identical), the requested file is created,
and the omitted file is not silently created in the empty-tree
case. Plus 6 new tests exercising the -Refresh code path end-to-
end via mocked Invoke-WebRequest: verifies the fetched URLs
(CSharp set), verifies sentinel content lands in the generated
files, verifies the "fetched" / "bundled" header label, verifies
override of -GitattributesRef changes the fetch URL, verifies
cache fallback on network failure, and verifies the no-cache hard
error message.
Tests: 53 Pester (was 45). All targeted Pull-SDLC tests still green.
PSScriptAnalyzer baseline unchanged (0 on Initialize-GitDefaults.ps1,
94 pre-existing on Pull-SDLC.ai.ps1).
Co-authored-by: GitHub Copilot <noreply@github.com>
Five threads from the round-5 Copilot review on PR #161: 1. d9R atomic compose-then-write: Initialize-GitDefaults composes both .gitattributes and .gitignore in memory BEFORE writing either. A failure during the second compose (network/cache miss under -Refresh) no longer leaves the consumer half-updated. 2. d9T -WhatIf cache safety: Get-GitDefaultsRefreshedContent now uses SupportsShouldProcess and gates the cache write through ShouldProcess so -WhatIf does not mutate the on-disk cache. Fetched content is still returned so the dry-run preview composes correctly. 3. d9X SOURCES.md stale procedure: rewrote the Refresh procedure section to distinguish (A) the now-implemented runtime cache refresh consumers get via -Refresh from (B) the maintainer-only bundled-snapshot bump procedure in this repo. 4. d9Y + d9d tombstone: retained .gitattributes.template in $script:UpstreamManagedPaths as a tombstone (with explanatory comment) so already-onboarded consumers receive the deletion op from Get-UpstreamOps. Inverted the corresponding Pester assertion. Tests: 55 Initialize-GitDefaults tests + 21 targeted Pull-SDLC tests all GREEN. PSSA unchanged from prior commit (3 baseline on Initialize-GitDefaults.ps1, 146 baseline on Pull-SDLC.ai.ps1; the 3 on the new script are pre-existing PSUseOutputTypeCorrectly findings unrelated to this change). Refs #160 Co-authored-by: GitHub Copilot <noreply@github.com>
Five new threads from the round-6 Copilot review on PR #161: 1. hJM cache pollution from tests: the round-4 -Refresh tests used the real default pinned SHAs while mocking Invoke-WebRequest, so sentinel content was being written into the user's real cache for production refs. The tests now generate a synthetic per-test ref, pass it explicitly to every Initialize-GitDefaults call, and clean up the cache entries in AfterEach. 2. hJQ dynamic-scope hazard in New-GitDefaultsHeader: -GitattributesRef / -GitignoreRef are now explicit parameters of the helper with defaults from $script:DefaultGit*Ref. Both internal call sites (New-GitAttributesContent, New-GitIgnoreContent) pass them through. The helper no longer relies on the caller's scope. 3. hJY swallowed cache-write failures: split the single try/catch in Get-GitDefaultsRefreshedContent so that (a) fetch failures fall back to a cached copy if available, and (b) a successful fetch whose cache write later fails warns once and still returns the freshly-fetched content. Stale cached copies can no longer silently replace a good download. 4. hJd .slnx detection gap: Get-GitDefaultsDetectedLanguages now includes *.slnx alongside *.csproj and *.sln when detecting CSharp, matching the generated .gitattributes which already handles .slnx. 5. hJg tombstone staging gap: the Pull-SDLC.ai staging loop now includes a tracked-but-deleted managed path (via git ls-files --error-unmatch) so the D op produced for the .gitattributes.template tombstone reaches the sync commit instead of being left unstaged. Tests: 57 Initialize-GitDefaults tests + 22 targeted Pull-SDLC tests (including a new "Tombstone deletion staging" describe that verifies a tracked-but-deleted path gets staged as a deletion) all GREEN. PSSA baseline unchanged: 3 on Initialize-GitDefaults.ps1, 146 on Pull-SDLC.ai.ps1. Refs #160 Co-authored-by: GitHub Copilot <noreply@github.com>
Three new threads from the round-7 Copilot review on PR #161: 1. jkb auto-detection of PowerShell from IntelliSDLC.ai's own .ps1 files: Get-GitDefaultsDetectedLanguages now excludes a known list of tool files (Pull-SDLC.ai.ps1, Initialize-GitDefaults.ps1, Cleanup-Worktree.ps1, Consolidate-Tasks.ps1, run.ps1, their *.Tests.ps1 siblings) and anything under .github/. Consumers who only have the synced tooling no longer get PowerShell pre-selected; adding a single consumer .ps1 alongside the tooling still detects. 2. jkg Write-GitDefaultsHint suppressed when Pull-SDLC.ai had merge-created a vanilla .gitignore: the hint now uses a new Test-GitDefaultsGeneratedMarker helper that scans the first 4 KB of each file for the "# Generated by Initialize-GitDefaults.ps1" header. Mere file presence is no longer treated as "the consumer has the language-aware file they need." Hint copy updated to "No language-aware ..." for honesty. 3. jkk in-process tombstone test was simulating production logic instead of exercising it: replaced with a real end-to-end test that builds a New-DiffReplayFixture where the anchor commit has .gitattributes.template and the next upstream commit deletes it, then invokes Invoke-PullSDLC and asserts (a) the file is gone from the working tree, (b) git status is clean (no unstaged deletion), and (c) the tip commit actually contains the D entry. Tests: 59 Initialize-GitDefaults tests + 23 targeted Pull-SDLC tests (added 2 detection-exclusion tests, 1 merge-created hint test, replaced 1 tombstone test with a real e2e). PSSA baseline unchanged: 3 / 146. Refs #160 Co-authored-by: GitHub Copilot <noreply@github.com>
Two new threads from the round-8 Copilot review on PR #161: 1. k2H Windows path-separator gap: GetRelativePath returns backslash-separated paths on Windows, so the round-7 `.github/*` filter never matched tooling files under `.github\` and PowerShell could still be wrongly pre-selected. The filter now normalizes the relative path with `-replace '\\','/'` before the like-match. 2. k2P round-7's "Generated by" header check would mistreat every hand-maintained .gitattributes/.gitignore as missing and badger consumers every sync. Replaced with file-presence PLUS an explicit `-NewlyMergedFiles` signal from Pull-SDLC.ai: Invoke-PullSDLC now snapshots which paths in $script:MergePaths existed BEFORE the union-merge step and passes the set of freshly-created ones (e.g. a vanilla .gitignore that the sync just merge-created from upstream) to Write-NextStepsBanner -> Write-GitDefaultsHint. The hint fires when a file is missing OR was just merge-created on this sync; pre-existing hand-maintained files are left alone. Test-GitDefaultsGeneratedMarker helper removed. Tests: 60 Initialize-GitDefaults tests (added 1 .github\ backslash test) + targeted Pull-SDLC tests including the full 10-test Invoke-PullSDLC end-to-end suite and 7 Write-GitDefaultsHint cases (added 2 round-8 cases: "merge-created file still prompts" and "hand-maintained files leave the consumer alone") all GREEN. PSSA baseline unchanged: 3 / 146. Refs #160 Co-authored-by: GitHub Copilot <noreply@github.com>
| function New-GitIgnoreContent { | ||
| <# | ||
| .SYNOPSIS | ||
| Compose a complete .gitignore file body for the supplied languages. | ||
| #> | ||
| [CmdletBinding()] |
| # Default both switches to on when neither is supplied. | ||
| if (-not $PSBoundParameters.ContainsKey('IncludeGitignore') -and | ||
| -not $PSBoundParameters.ContainsKey('IncludeGitattributes')) { | ||
| $IncludeGitignore = $true | ||
| $IncludeGitattributes = $true | ||
| } | ||
| elseif (-not $PSBoundParameters.ContainsKey('IncludeGitignore')) { | ||
| $IncludeGitignore = $true | ||
| } | ||
| elseif (-not $PSBoundParameters.ContainsKey('IncludeGitattributes')) { | ||
| $IncludeGitattributes = $true | ||
| } |
| $resolved = [System.Collections.Generic.HashSet[string]]::new() | ||
| foreach ($lang in $Language) { | ||
| if (-not $lang) { continue } | ||
| $key = $lang.ToLowerInvariant() | ||
| if (-not $canonicalMap.ContainsKey($key)) { | ||
| $supported = ($script:GitDefaultsLanguages.Keys | Sort-Object) -join ', ' | ||
| throw "Unknown language '$lang'. Supported languages: $supported." | ||
| } | ||
| $canonical = $canonicalMap[$key] | ||
| [void]$resolved.Add($canonical) | ||
| foreach ($dep in $script:GitDefaultsLanguages[$canonical].Deps) { | ||
| [void]$resolved.Add($dep) | ||
| } | ||
| } | ||
| return @($resolved | Sort-Object) |
| @@ -0,0 +1,739 @@ | |||
| <# | |||
| if (-not (Test-GitDefaultsRepo)) { | ||
| throw "Current directory is not a git repository. Run 'git init' first or cd into a repo." | ||
| } |
Summary
Ship
Initialize-GitDefaults.ps1(repo root) and retire the static.gitattributes.templatefirst-sync scaffold. Composes.gitattributesand.gitignorefor a consumer project from per-language community templates plus a curated PowerShell block.Closes #160
Pinned upstream sources
*.gitattributesalexkaratarakis/gitattributesfddc586cf0f10ec4485028d0d2dd6f73197a4258*.gitignoregithub/gitignoredcc0fc7bc2b5ba480cf117ad1be31bafceeaff46(verified:
gh api repos/github/gitattributes/commits/mainreturns 404 at pin time.)Discoveries (documented in
SOURCES.md)VisualStudio.gitattributesdoes NOT exist inalexkaratarakis/gitattributesat the pinned SHA. Decision: dropped from bundle; ASP.NET inherits onlyCSharp.gitattributes.PowerShell.gitattributesDOES exist upstream but per the spec we ship a curated in-script block (smaller surface, signing-aware,linguist-language=PowerShellhints).Language coverage
Common+CSharpVisualStudioCommon+ curated blockCommon+WebNodeCommon+CSharp(dep)VisualStudio(shared)Dependency graph:
ASP.NET -> CSharp. Backed byResolve-GitDefaultsLanguages.Pull-SDLC.ai.ps1 integration
git rm .gitattributes.template.gitattributes.templatefrom$script:TemplateScaffoldMapand$script:UpstreamManagedPaths..github/templates/git-defaults/to$script:UpstreamManagedPathsso bundled snapshots flow to consumers on sync.Write-GitDefaultsHint(called fromWrite-NextStepsBanner) prints a one-liner pointing at./Initialize-GitDefaults.ps1when.gitattributesor.gitignoreis missing. Never auto-invokes.Tests
Initialize-GitDefaults.Tests.ps1-- 28/28 GREEN (unit + integration; -WhatIf; backup/force; full target stack).Pull-SDLC.ai.Tests.ps1targeted groups (Invoke-TemplateScaffold, README.md.template, Write-NextStepsBanner, Issue Pull-SDLC.ai bootstrap: 4 findings on fresh-repo carve-out path #148 carve-out, Initialize-GitDefaults migration, Write-GitDefaultsHint, etc.) -- 87/87 GREEN.Resolve-GitDefaultsLanguagesis short-circuited; targeted failures in the migration suite when managed-paths surgery is reverted.PSScriptAnalyzer
Initialize-GitDefaults.ps1-- 0 findings (file-levelSuppressMessageAttributefor project-wide accepted patterns:Write-Host,New-*pure-string-builderShouldProcess, plural noun for set operations).Pull-SDLC.ai.ps1-- 94 findings, identical tomainbaseline (0 new).Acceptance criteria status
Invoke-PesteronInitialize-GitDefaults.Tests.ps1-- 28/28 PASSInvoke-Pesteron targetedPull-SDLC.ai.Tests.ps1groups -- 87/87 PASS (full file scheduled for CI)./Initialize-GitDefaults.ps1 -Language CSharp,PowerShell,TypeScript -Forcein fresh temp dir composes both files end-to-end, exit 0 (see evidence)./Initialize-GitDefaults.ps1 -Language ASP.NET -Forceincludes CSharp content (dependency resolved).gitattributes.templategone from repo.gitattributesprints the hint (observed in carve-out integration test stdout)How to verify locally
End-to-end smoke: