From 1da053fb67bb4e8934e943745de8bfd267fca7e3 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 14:15:29 +0200 Subject: [PATCH] - apply latest AGENTS and DoD templates - replace PowerShell reconcile script with shell-based reconcile workflow - add .gitattributes, .editorconfig, and project-local git hooks - configure .githooks as core.hooksPath - refresh .gitignore entries from the shared standard --- .editorconfig | 3 - .gitattributes | 1 - .gitignore | 1 + AGENTS.md | 7 + scripts/Reconcile-ProjectStandards.ps1 | 217 ------------------------- scripts/reconcile-project-standards.sh | 25 ++- 6 files changed, 30 insertions(+), 224 deletions(-) delete mode 100644 scripts/Reconcile-ProjectStandards.ps1 diff --git a/.editorconfig b/.editorconfig index b9dc5ac..fb18251 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,6 +14,3 @@ indent_size = 4 [*.md] trim_trailing_whitespace = false - -[*.ps1] -end_of_line = crlf diff --git a/.gitattributes b/.gitattributes index 0b57dc4..826bbe8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,6 +13,5 @@ Makefile text eol=lf # Keep native Windows script formats -*.ps1 text eol=crlf *.bat text eol=crlf *.cmd text eol=crlf diff --git a/.gitignore b/.gitignore index 4b00f8d..d22f48a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ coverage.out Thumbs.db .vscode/ .idea/ + diff --git a/AGENTS.md b/AGENTS.md index 92be300..32da315 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,13 @@ All project documentation files must be stored under `docs/`, except `README.md` 2. Shell scripts committed to the repository must be executable in Git index (mode `100755`). 3. When adding a new shell script, set execute permissions before commit: `git add --chmod=+x path/to/script.sh`. +## Git Bash Execution Defaults +1. Repository maintenance scripts are executed with Git Bash shell on Windows. +2. Default repository root is `~/git`. +3. The repository root can be overridden via `GO_GIT_ROOT`. +4. The standards repository defaults to `$ROOT_PATH/project-standards`. +5. The standards repository path can be overridden via `GO_PROJECT_STANDARDS`. + ## Definition of Done (DoD) ### Purpose diff --git a/scripts/Reconcile-ProjectStandards.ps1 b/scripts/Reconcile-ProjectStandards.ps1 deleted file mode 100644 index ad39809..0000000 --- a/scripts/Reconcile-ProjectStandards.ps1 +++ /dev/null @@ -1,217 +0,0 @@ -[CmdletBinding()] -param( - [string]$RootPath = "C:\Users\stefan\git", - [string]$StandardsRepoPath = "C:\Users\stefan\git\project-standards", - [string[]]$Exclude = @('project-standards'), - [switch]$CheckOnly, - [switch]$Watch, - [int]$IntervalSeconds = 60 -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$agentsTemplate = Join-Path $StandardsRepoPath 'templates/AGENTS.base.md' -$dodTemplate = Join-Path $StandardsRepoPath 'templates/DEFINITION_OF_DONE.base.md' -$gitIgnoreTemplate = Join-Path $StandardsRepoPath 'templates/.gitignore.base' -$gitAttributesTemplate = Join-Path $StandardsRepoPath 'templates/.gitattributes.base' -$editorConfigTemplate = Join-Path $StandardsRepoPath 'templates/.editorconfig.base' -$preCommitHookTemplate = Join-Path $StandardsRepoPath 'templates/pre-commit.base.sh' -$hooksReadmeTemplate = Join-Path $StandardsRepoPath 'templates/.githooks.README.base.md' - -if (-not (Test-Path -Path $agentsTemplate -PathType Leaf)) { - throw "AGENTS template not found: $agentsTemplate" -} - -if (-not (Test-Path -Path $dodTemplate -PathType Leaf)) { - throw "DoD template not found: $dodTemplate" -} - -if (-not (Test-Path -Path $gitIgnoreTemplate -PathType Leaf)) { - throw "gitignore template not found: $gitIgnoreTemplate" -} - -if (-not (Test-Path -Path $gitAttributesTemplate -PathType Leaf)) { - throw "gitattributes template not found: $gitAttributesTemplate" -} - -if (-not (Test-Path -Path $editorConfigTemplate -PathType Leaf)) { - throw "editorconfig template not found: $editorConfigTemplate" -} - -if (-not (Test-Path -Path $preCommitHookTemplate -PathType Leaf)) { - throw "pre-commit hook template not found: $preCommitHookTemplate" -} - -if (-not (Test-Path -Path $hooksReadmeTemplate -PathType Leaf)) { - throw "hooks readme template not found: $hooksReadmeTemplate" -} - -if ($IntervalSeconds -lt 5) { - throw 'IntervalSeconds must be >= 5.' -} - -function Get-ContentHashOrMissing { - param([string]$Path) - - if (-not (Test-Path -Path $Path -PathType Leaf)) { - return '__MISSING__' - } - - return (Get-FileHash -Path $Path -Algorithm SHA256).Hash -} - -function Ensure-FileFromTemplate { - param( - [string]$Template, - [string]$Target, - [switch]$OnlyCheck - ) - - $templateHash = Get-ContentHashOrMissing -Path $Template - $targetHash = Get-ContentHashOrMissing -Path $Target - - if ($templateHash -eq $targetHash) { - return 'ok' - } - - if ($OnlyCheck) { - return 'drift' - } - - $parent = Split-Path -Parent $Target - if (-not (Test-Path -Path $parent -PathType Container)) { - New-Item -ItemType Directory -Path $parent | Out-Null - } - - Copy-Item -Path $Template -Destination $Target -Force - return 'updated' -} - -function Ensure-GitIgnoreEntriesFromTemplate { - param( - [string]$TemplatePath, - [string]$GitIgnorePath, - [switch]$OnlyCheck - ) - - $requiredEntries = Get-Content -Path $TemplatePath | Where-Object { - $_.Trim() -and -not $_.Trim().StartsWith('#') - } - - if (-not (Test-Path -Path $GitIgnorePath -PathType Leaf)) { - if ($OnlyCheck) { - return 'drift' - } - - New-Item -ItemType File -Path $GitIgnorePath | Out-Null - } - - $lines = Get-Content -Path $GitIgnorePath - - $missingEntry = $false - foreach ($entry in $requiredEntries) { - if ($lines -contains $entry) { - continue - } - - $missingEntry = $true - if (-not $OnlyCheck) { - Add-Content -Path $GitIgnorePath -Value $entry - $lines += $entry - } - } - - if (-not $missingEntry) { - return 'ok' - } - - if ($OnlyCheck) { - return 'drift' - } - - return 'updated' -} - -function Invoke-ReconcileOnce { - param([switch]$OnlyCheck) - - $resolvedRoot = (Resolve-Path -Path $RootPath -ErrorAction Stop).Path - $repos = Get-ChildItem -Path $resolvedRoot -Directory | Where-Object { - $Exclude -notcontains $_.Name - } - - $summary = [ordered]@{ - scanned = 0 - updated = 0 - drift = 0 - } - - foreach ($repo in $repos) { - $repoPath = $repo.FullName - $agentsTarget = Join-Path $repoPath 'AGENTS.md' - $dodTarget = Join-Path (Join-Path $repoPath 'docs') 'DEFINITION_OF_DONE.md' - $gitIgnoreTarget = Join-Path $repoPath '.gitignore' - $gitAttributesTarget = Join-Path $repoPath '.gitattributes' - $editorConfigTarget = Join-Path $repoPath '.editorconfig' - $preCommitHookTarget = Join-Path (Join-Path $repoPath '.githooks') 'pre-commit' - $hooksReadmeTarget = Join-Path (Join-Path $repoPath '.githooks') 'README.md' - - $summary.scanned++ - - $agentsState = Ensure-FileFromTemplate -Template $agentsTemplate -Target $agentsTarget -OnlyCheck:$OnlyCheck - $dodState = Ensure-FileFromTemplate -Template $dodTemplate -Target $dodTarget -OnlyCheck:$OnlyCheck - $gitAttributesState = Ensure-FileFromTemplate -Template $gitAttributesTemplate -Target $gitAttributesTarget -OnlyCheck:$OnlyCheck - $editorConfigState = Ensure-FileFromTemplate -Template $editorConfigTemplate -Target $editorConfigTarget -OnlyCheck:$OnlyCheck - $preCommitHookState = Ensure-FileFromTemplate -Template $preCommitHookTemplate -Target $preCommitHookTarget -OnlyCheck:$OnlyCheck - $hooksReadmeState = Ensure-FileFromTemplate -Template $hooksReadmeTemplate -Target $hooksReadmeTarget -OnlyCheck:$OnlyCheck - $gitIgnoreState = Ensure-GitIgnoreEntriesFromTemplate -TemplatePath $gitIgnoreTemplate -GitIgnorePath $gitIgnoreTarget -OnlyCheck:$OnlyCheck - - if ( - $agentsState -eq 'updated' -or - $dodState -eq 'updated' -or - $gitAttributesState -eq 'updated' -or - $editorConfigState -eq 'updated' -or - $preCommitHookState -eq 'updated' -or - $hooksReadmeState -eq 'updated' -or - $gitIgnoreState -eq 'updated' - ) { - $summary.updated++ - Write-Host "UPDATED: $repoPath" - continue - } - - if ( - $agentsState -eq 'drift' -or - $dodState -eq 'drift' -or - $gitAttributesState -eq 'drift' -or - $editorConfigState -eq 'drift' -or - $preCommitHookState -eq 'drift' -or - $hooksReadmeState -eq 'drift' -or - $gitIgnoreState -eq 'drift' - ) { - $summary.drift++ - Write-Host "DRIFT: $repoPath" - continue - } - - Write-Host "OK: $repoPath" - } - - Write-Host "Summary -> scanned=$($summary.scanned), updated=$($summary.updated), drift=$($summary.drift)" - return $summary -} - -if ($Watch) { - while ($true) { - $now = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' - Write-Host "[$now] Reconciling project standards..." - [void](Invoke-ReconcileOnce -OnlyCheck:$CheckOnly) - Start-Sleep -Seconds $IntervalSeconds - } -} - -$result = Invoke-ReconcileOnce -OnlyCheck:$CheckOnly -if ($CheckOnly -and $result.drift -gt 0) { - exit 1 -} diff --git a/scripts/reconcile-project-standards.sh b/scripts/reconcile-project-standards.sh index 68fa9a9..aa8cf9c 100755 --- a/scripts/reconcile-project-standards.sh +++ b/scripts/reconcile-project-standards.sh @@ -1,8 +1,9 @@ #!/usr/bin/env sh set -eu -ROOT_PATH="/c/Users/stefan/git" -STANDARDS_REPO="/c/Users/stefan/git/project-standards" +ROOT_PATH="${GO_GIT_ROOT:-$HOME/git}" +STANDARDS_REPO="" +STANDARDS_REPO_SET=0 EXCLUDE_NAME="project-standards" CHECK_ONLY=0 WATCH=0 @@ -21,6 +22,14 @@ Options: --watch Continuously scan and reconcile --interval Watch interval in seconds (default: 60) -h, --help Show this help + +Environment: + GO_GIT_ROOT Overrides default root path (default: ~/git) + GO_PROJECT_STANDARDS Overrides standards repository path + +Default resolution: + ROOT_PATH defaults to GO_GIT_ROOT or ~/git. + STANDARDS_REPO defaults to GO_PROJECT_STANDARDS or $ROOT_PATH/project-standards. EOF } @@ -32,6 +41,7 @@ while [ "$#" -gt 0 ]; do ;; --standards-repo) STANDARDS_REPO=$2 + STANDARDS_REPO_SET=1 shift 2 ;; --exclude) @@ -62,6 +72,14 @@ while [ "$#" -gt 0 ]; do esac done +if [ "$STANDARDS_REPO_SET" -ne 1 ]; then + if [ -n "${GO_PROJECT_STANDARDS:-}" ]; then + STANDARDS_REPO="$GO_PROJECT_STANDARDS" + else + STANDARDS_REPO="$ROOT_PATH/project-standards" + fi +fi + if [ "$INTERVAL" -lt 5 ]; then echo "interval must be >= 5" >&2 exit 1 @@ -162,6 +180,7 @@ ensure_gitignore_entries_from_template() { fi while IFS= read -r entry; do + entry=$(printf '%s' "$entry" | tr -d '\r') [ -n "$entry" ] || continue case "$entry" in \#*) @@ -169,7 +188,7 @@ ensure_gitignore_entries_from_template() { ;; esac - if grep -Fqx "$entry" "$gitignore_path"; then + if tr -d '\r' < "$gitignore_path" | grep -Fqx "$entry"; then continue fi