Compare commits
No commits in common. "6ca2c4d25ffb66b3fda37024a3c7097d0e0b8ba2" and "bc9b0ce88f11d6db0acd3dcf937a38197b148d62" have entirely different histories.
6ca2c4d25f
...
bc9b0ce88f
|
|
@ -4,12 +4,9 @@ name: go-lib/util
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: test
|
- name: test
|
||||||
image: golang:1.25.8
|
image: golang:1.18
|
||||||
commands:
|
commands:
|
||||||
- go get ./...
|
- go get ./...
|
||||||
- go vet ./...
|
- go test ./...
|
||||||
- mkdir -p .build
|
|
||||||
- go test -v -coverprofile=.build/coverage.out ./...
|
|
||||||
- go tool cover -func=.build/coverage.out | tee .build/coverage.txt | awk '/^total:/ { gsub("%", "", $3); if ($3 + 0 < 80) { printf("Coverage %.1f%% is below 80%%\n", $3); exit 1 } }'
|
|
||||||
- go install golang.org/x/vuln/cmd/govulncheck@latest
|
- go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
- govulncheck -json ./... > vulncheck.json
|
- govulncheck -v -json ./... > vulncheck.json
|
||||||
|
|
@ -14,3 +14,6 @@ indent_size = 4
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.ps1]
|
||||||
|
end_of_line = crlf
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,6 @@
|
||||||
Makefile text eol=lf
|
Makefile text eol=lf
|
||||||
|
|
||||||
# Keep native Windows script formats
|
# Keep native Windows script formats
|
||||||
|
*.ps1 text eol=crlf
|
||||||
*.bat text eol=crlf
|
*.bat text eol=crlf
|
||||||
*.cmd text eol=crlf
|
*.cmd text eol=crlf
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,3 @@ The pre-commit hook validates for staged `.sh` files:
|
||||||
|
|
||||||
- executable bit in Git index (`100755`)
|
- executable bit in Git index (`100755`)
|
||||||
- LF line endings (no CRLF)
|
- LF line endings (no CRLF)
|
||||||
|
|
||||||
The pre-commit hook also validates staged `.md` files with `markdownlint`:
|
|
||||||
|
|
||||||
- no `markdownlint` errors or problems
|
|
||||||
- requires `markdownlint` CLI in PATH (for example via `npm install --global markdownlint-cli`)
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ failed=0
|
||||||
cr=$(printf '\r')
|
cr=$(printf '\r')
|
||||||
|
|
||||||
staged_shell_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.sh$' || true)
|
staged_shell_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.sh$' || true)
|
||||||
staged_markdown_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.md$' || true)
|
|
||||||
|
|
||||||
for file in $staged_shell_files; do
|
for file in $staged_shell_files; do
|
||||||
mode=$(git ls-files --stage -- "$file" | awk '{print $1}')
|
mode=$(git ls-files --stage -- "$file" | awk '{print $1}')
|
||||||
|
|
@ -20,20 +19,6 @@ for file in $staged_shell_files; do
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ -n "$staged_markdown_files" ]; then
|
|
||||||
if ! command -v markdownlint >/dev/null 2>&1; then
|
|
||||||
echo "ERROR: markdownlint is required to validate staged Markdown files (.md)." >&2
|
|
||||||
echo "Install with npm: npm install --global markdownlint-cli" >&2
|
|
||||||
failed=1
|
|
||||||
else
|
|
||||||
# Validate the staged markdown files currently present in the working tree.
|
|
||||||
# This keeps the hook simple and fast for standard project usage.
|
|
||||||
if ! markdownlint $staged_markdown_files; then
|
|
||||||
failed=1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$failed" -ne 0 ]; then
|
if [ "$failed" -ne 0 ]; then
|
||||||
echo "Pre-commit check failed." >&2
|
echo "Pre-commit check failed." >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,3 @@ coverage.out
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
|
|
||||||
57
AGENTS.md
57
AGENTS.md
|
|
@ -1,20 +1,14 @@
|
||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
This file defines the default working agreement for AI coding agents and contributors in this repository.
|
||||||
This file defines the default working agreement for AI coding agents
|
|
||||||
and contributors in this repository.
|
|
||||||
|
|
||||||
## Documentation Language
|
## Documentation Language
|
||||||
|
Project language is English. All documentation, issues, pull requests, commit messages, and code comments should be written in English.
|
||||||
Project language is English. All documentation, issues, pull requests,
|
|
||||||
commit messages, and code comments should be written in English.
|
|
||||||
All newly created documentation in this project must be written in English.
|
All newly created documentation in this project must be written in English.
|
||||||
All project documentation files must be stored under `docs/`,
|
All project documentation files must be stored under `docs/`, except `README.md` and `AGENTS.md`.
|
||||||
except `README.md` and `AGENTS.md`.
|
|
||||||
|
|
||||||
## Working Principles
|
## Working Principles
|
||||||
|
|
||||||
1. Keep changes minimal and focused on the requested task.
|
1. Keep changes minimal and focused on the requested task.
|
||||||
2. Preserve existing public APIs unless a breaking change is explicitly requested.
|
2. Preserve existing public APIs unless a breaking change is explicitly requested.
|
||||||
3. Prefer clear, maintainable code over clever shortcuts.
|
3. Prefer clear, maintainable code over clever shortcuts.
|
||||||
|
|
@ -22,46 +16,28 @@ except `README.md` and `AGENTS.md`.
|
||||||
5. Never add secrets, credentials, or tokens to the repository.
|
5. Never add secrets, credentials, or tokens to the repository.
|
||||||
|
|
||||||
## Testing Expectations
|
## Testing Expectations
|
||||||
|
|
||||||
1. Add or update tests for behavior changes.
|
1. Add or update tests for behavior changes.
|
||||||
2. Keep tests deterministic and fast.
|
2. Keep tests deterministic and fast.
|
||||||
3. Prefer table-driven tests where they improve readability.
|
3. Prefer table-driven tests where they improve readability.
|
||||||
4. Run relevant tests locally before finishing changes.
|
4. Run relevant tests locally before finishing changes.
|
||||||
5. For Go projects, use `github.com/smartystreets/goconvey`
|
5. For Go projects, use `github.com/smartystreets/goconvey` as the standard test library.
|
||||||
as the standard test library.
|
|
||||||
|
|
||||||
## Build Artifacts and Reports
|
## Build Artifacts and Reports
|
||||||
|
|
||||||
1. Builder logs and generated reports must be created under `.build/`.
|
1. Builder logs and generated reports must be created under `.build/`.
|
||||||
2. The `.build/` directory must be excluded from version control via `.gitignore`.
|
2. The `.build/` directory must be excluded from version control via `.gitignore`.
|
||||||
|
|
||||||
## Git and Script Standards
|
## Git and Script Standards
|
||||||
|
|
||||||
1. Shell scripts (`*.sh`) must use LF line endings.
|
1. Shell scripts (`*.sh`) must use LF line endings.
|
||||||
2. Shell scripts committed to the repository must be executable
|
2. Shell scripts committed to the repository must be executable in Git index (mode `100755`).
|
||||||
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`.
|
||||||
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)
|
## Definition of Done (DoD)
|
||||||
|
|
||||||
### DoD Purpose
|
### Purpose
|
||||||
|
The Definition of Done defines the minimum quality bar for every completed change in this repository.
|
||||||
The Definition of Done defines the minimum quality bar
|
|
||||||
for every completed change in this repository.
|
|
||||||
|
|
||||||
### Mandatory Criteria
|
### Mandatory Criteria
|
||||||
|
|
||||||
1. Tests
|
1. Tests
|
||||||
|
|
||||||
- Every code change is covered by tests where applicable.
|
- Every code change is covered by tests where applicable.
|
||||||
- New functionality includes new tests.
|
- New functionality includes new tests.
|
||||||
- Bug fixes include at least one regression test.
|
- Bug fixes include at least one regression test.
|
||||||
|
|
@ -69,41 +45,28 @@ for every completed change in this repository.
|
||||||
- Automated test coverage is at least 80%.
|
- Automated test coverage is at least 80%.
|
||||||
|
|
||||||
1. Functional documentation
|
1. Functional documentation
|
||||||
|
|
||||||
- Implemented functionality is documented.
|
- Implemented functionality is documented.
|
||||||
- Public API-relevant changes are reflected in README and/or docs.
|
- Public API-relevant changes are reflected in README and/or docs.
|
||||||
|
|
||||||
1. Documentation standards
|
1. Documentation standards
|
||||||
|
|
||||||
- Documentation is written in English.
|
- Documentation is written in English.
|
||||||
- Documentation files are placed under `docs/`.
|
- Documentation files are placed under `docs/`.
|
||||||
- Exceptions: `README.md` and `AGENTS.md` remain at repository root.
|
- Exceptions: `README.md` and `AGENTS.md` remain at repository root.
|
||||||
- Markdown files have no `markdownlint` errors or problems.
|
|
||||||
|
|
||||||
### Technical Completion Criteria
|
### Technical Completion Criteria
|
||||||
|
|
||||||
1. Build and test status
|
1. Build and test status
|
||||||
|
|
||||||
- The project builds successfully.
|
- The project builds successfully.
|
||||||
- Relevant test commands run successfully.
|
- Relevant test commands run successfully.
|
||||||
|
|
||||||
1. No unresolved critical issues
|
1. No unresolved critical issues
|
||||||
|
|
||||||
- No new blocking errors are introduced.
|
- No new blocking errors are introduced.
|
||||||
- Known non-blocking warnings are acceptable
|
- Known non-blocking warnings are acceptable only if unrelated to the change or documented.
|
||||||
only if unrelated to the change or documented.
|
|
||||||
|
|
||||||
1. SonarQube status
|
|
||||||
|
|
||||||
- No SonarQube errors are present.
|
|
||||||
|
|
||||||
1. Documentation structure
|
1. Documentation structure
|
||||||
|
|
||||||
- Links to moved or newly added docs are valid.
|
- Links to moved or newly added docs are valid.
|
||||||
- Documentation structure remains consistent with project rules.
|
- Documentation structure remains consistent with project rules.
|
||||||
|
|
||||||
### Review Checklist (Quick)
|
### Review Checklist (Quick)
|
||||||
|
|
||||||
- [ ] Change is implemented and meets acceptance criteria.
|
- [ ] Change is implemented and meets acceptance criteria.
|
||||||
- [ ] Tests were added/updated and pass.
|
- [ ] Tests were added/updated and pass.
|
||||||
- [ ] Go tests use `github.com/smartystreets/goconvey`.
|
- [ ] Go tests use `github.com/smartystreets/goconvey`.
|
||||||
|
|
@ -111,6 +74,4 @@ for every completed change in this repository.
|
||||||
- [ ] Functionality is documented.
|
- [ ] Functionality is documented.
|
||||||
- [ ] Documentation is in English.
|
- [ ] Documentation is in English.
|
||||||
- [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`).
|
- [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`).
|
||||||
- [ ] Markdown files have no `markdownlint` errors or problems.
|
|
||||||
- [ ] No SonarQube errors are present.
|
|
||||||
- [ ] No critical regressions found.
|
- [ ] No critical regressions found.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Go utility library
|
<div style="text-align:left"><img src="https://www.yoorie.de/img/favicon_32.png"/></div>
|
||||||
|
|
||||||

|
# Go utility library
|
||||||
|
|
||||||
[](https://drone.yoorie.de/go-lib/util)
|
[](https://drone.yoorie.de/go-lib/util)
|
||||||
|
|
||||||
|
|
@ -58,3 +58,5 @@ func main() {
|
||||||
|
|
||||||
---
|
---
|
||||||
Copyright © 2023 yoorie.de
|
Copyright © 2023 yoorie.de
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,11 @@
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
This Definition of Done defines the minimum quality bar
|
This Definition of Done defines the minimum quality bar for every completed change in this repository.
|
||||||
for every completed change in this repository.
|
|
||||||
|
|
||||||
## Mandatory Criteria
|
## Mandatory Criteria
|
||||||
|
|
||||||
1. Tests
|
1. Tests
|
||||||
|
|
||||||
- Every code change is covered by tests where applicable.
|
- Every code change is covered by tests where applicable.
|
||||||
- New functionality includes new tests.
|
- New functionality includes new tests.
|
||||||
- Bug fixes include at least one regression test.
|
- Bug fixes include at least one regression test.
|
||||||
|
|
@ -16,36 +14,25 @@ for every completed change in this repository.
|
||||||
- Automated test coverage is at least 80%.
|
- Automated test coverage is at least 80%.
|
||||||
|
|
||||||
1. Functional documentation
|
1. Functional documentation
|
||||||
|
|
||||||
- Implemented functionality is documented.
|
- Implemented functionality is documented.
|
||||||
- Public API-relevant changes are reflected in README and/or docs.
|
- Public API-relevant changes are reflected in README and/or docs.
|
||||||
|
|
||||||
1. Documentation standards
|
1. Documentation standards
|
||||||
|
|
||||||
- Documentation is written in English.
|
- Documentation is written in English.
|
||||||
- Documentation files are placed under `docs/`.
|
- Documentation files are placed under `docs/`.
|
||||||
- Exceptions: `README.md` and `AGENTS.md` remain at repository root.
|
- Exceptions: `README.md` and `AGENTS.md` remain at repository root.
|
||||||
- Markdown files have no `markdownlint` errors or problems.
|
|
||||||
|
|
||||||
## Technical Completion Criteria
|
## Technical Completion Criteria
|
||||||
|
|
||||||
1. Build and test status
|
1. Build and test status
|
||||||
|
|
||||||
- The project builds successfully.
|
- The project builds successfully.
|
||||||
- Relevant test commands run successfully.
|
- Relevant test commands run successfully.
|
||||||
|
|
||||||
1. No unresolved critical issues
|
1. No unresolved critical issues
|
||||||
|
|
||||||
- No new blocking errors are introduced.
|
- No new blocking errors are introduced.
|
||||||
- Known non-blocking warnings are acceptable
|
- Known non-blocking warnings are acceptable only if unrelated to the change or documented.
|
||||||
only if unrelated to the change or documented.
|
|
||||||
|
|
||||||
1. SonarQube status
|
|
||||||
|
|
||||||
- No SonarQube errors are present.
|
|
||||||
|
|
||||||
1. Documentation links and structure
|
1. Documentation links and structure
|
||||||
|
|
||||||
- Links to moved or newly added docs are valid.
|
- Links to moved or newly added docs are valid.
|
||||||
- Documentation structure remains consistent with project rules.
|
- Documentation structure remains consistent with project rules.
|
||||||
|
|
||||||
|
|
@ -58,6 +45,4 @@ for every completed change in this repository.
|
||||||
- [ ] Functionality is documented.
|
- [ ] Functionality is documented.
|
||||||
- [ ] Documentation is in English.
|
- [ ] Documentation is in English.
|
||||||
- [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`).
|
- [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`).
|
||||||
- [ ] Markdown files have no `markdownlint` errors or problems.
|
|
||||||
- [ ] No SonarQube errors are present.
|
|
||||||
- [ ] No critical regressions found.
|
- [ ] No critical regressions found.
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,6 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
allocateAndInitializeSid = windows.AllocateAndInitializeSid
|
allocateAndInitializeSid = windows.AllocateAndInitializeSid
|
||||||
allocateAdminGroupSid = func(sid **windows.SID) error {
|
|
||||||
return allocateAndInitializeSid(
|
|
||||||
&windows.SECURITY_NT_AUTHORITY,
|
|
||||||
2,
|
|
||||||
windows.SECURITY_BUILTIN_DOMAIN_RID,
|
|
||||||
windows.DOMAIN_ALIAS_RID_ADMINS,
|
|
||||||
0, 0, 0, 0, 0, 0,
|
|
||||||
sid,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
freeSid = windows.FreeSid
|
freeSid = windows.FreeSid
|
||||||
tokenIsMember = func(token windows.Token, sid *windows.SID) (bool, error) { return token.IsMember(sid) }
|
tokenIsMember = func(token windows.Token, sid *windows.SID) (bool, error) { return token.IsMember(sid) }
|
||||||
fatalf = log.Fatalf
|
fatalf = log.Fatalf
|
||||||
|
|
@ -37,7 +27,13 @@ func IsSuperUser() bool {
|
||||||
// official windows documentation. The Go API for this is a
|
// official windows documentation. The Go API for this is a
|
||||||
// direct wrap around the official C++ API.
|
// direct wrap around the official C++ API.
|
||||||
// See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
|
// See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
|
||||||
err := allocateAdminGroupSid(&sid)
|
err := allocateAndInitializeSid(
|
||||||
|
&windows.SECURITY_NT_AUTHORITY,
|
||||||
|
2,
|
||||||
|
windows.SECURITY_BUILTIN_DOMAIN_RID,
|
||||||
|
windows.DOMAIN_ALIAS_RID_ADMINS,
|
||||||
|
0, 0, 0, 0, 0, 0,
|
||||||
|
&sid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalf("SID Error: %s", err)
|
fatalf("SID Error: %s", err)
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -11,21 +11,33 @@ import (
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsSuperUserSidAllocationError(t *testing.T) {
|
func TestIsSuperUser_SidAllocationError(t *testing.T) {
|
||||||
Convey("IsSuperUser should return false when SID allocation fails", t, func() {
|
Convey("IsSuperUser should return false when SID allocation fails", t, func() {
|
||||||
origAllocate := allocateAdminGroupSid
|
origAllocate := allocateAndInitializeSid
|
||||||
origFatalf := fatalf
|
origFatalf := fatalf
|
||||||
defer func() {
|
defer func() {
|
||||||
allocateAdminGroupSid = origAllocate
|
allocateAndInitializeSid = origAllocate
|
||||||
fatalf = origFatalf
|
fatalf = origFatalf
|
||||||
}()
|
}()
|
||||||
|
|
||||||
allocateAdminGroupSid = func(_ **windows.SID) error {
|
allocateAndInitializeSid = func(
|
||||||
|
authority *windows.SidIdentifierAuthority,
|
||||||
|
subAuthorityCount byte,
|
||||||
|
subAuthority0 uint32,
|
||||||
|
subAuthority1 uint32,
|
||||||
|
subAuthority2 uint32,
|
||||||
|
subAuthority3 uint32,
|
||||||
|
subAuthority4 uint32,
|
||||||
|
subAuthority5 uint32,
|
||||||
|
subAuthority6 uint32,
|
||||||
|
subAuthority7 uint32,
|
||||||
|
sid **windows.SID,
|
||||||
|
) error {
|
||||||
return errors.New("forced sid allocation error")
|
return errors.New("forced sid allocation error")
|
||||||
}
|
}
|
||||||
|
|
||||||
fatalCalled := false
|
fatalCalled := false
|
||||||
fatalf = func(_ string, _ ...interface{}) {
|
fatalf = func(format string, v ...interface{}) {
|
||||||
fatalCalled = true
|
fatalCalled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
[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
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT_PATH="${GO_GIT_ROOT:-$HOME/git}"
|
ROOT_PATH="/c/Users/stefan/git"
|
||||||
STANDARDS_REPO=""
|
STANDARDS_REPO="/c/Users/stefan/git/project-standards"
|
||||||
STANDARDS_REPO_SET=0
|
|
||||||
EXCLUDE_NAME="project-standards"
|
EXCLUDE_NAME="project-standards"
|
||||||
CHECK_ONLY=0
|
CHECK_ONLY=0
|
||||||
WATCH=0
|
WATCH=0
|
||||||
|
|
@ -22,14 +21,6 @@ Options:
|
||||||
--watch Continuously scan and reconcile
|
--watch Continuously scan and reconcile
|
||||||
--interval <seconds> Watch interval in seconds (default: 60)
|
--interval <seconds> Watch interval in seconds (default: 60)
|
||||||
-h, --help Show this help
|
-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
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +32,6 @@ while [ "$#" -gt 0 ]; do
|
||||||
;;
|
;;
|
||||||
--standards-repo)
|
--standards-repo)
|
||||||
STANDARDS_REPO=$2
|
STANDARDS_REPO=$2
|
||||||
STANDARDS_REPO_SET=1
|
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
--exclude)
|
--exclude)
|
||||||
|
|
@ -72,14 +62,6 @@ while [ "$#" -gt 0 ]; do
|
||||||
esac
|
esac
|
||||||
done
|
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
|
if [ "$INTERVAL" -lt 5 ]; then
|
||||||
echo "interval must be >= 5" >&2
|
echo "interval must be >= 5" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -180,7 +162,6 @@ ensure_gitignore_entries_from_template() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
while IFS= read -r entry; do
|
while IFS= read -r entry; do
|
||||||
entry=$(printf '%s' "$entry" | tr -d '\r')
|
|
||||||
[ -n "$entry" ] || continue
|
[ -n "$entry" ] || continue
|
||||||
case "$entry" in
|
case "$entry" in
|
||||||
\#*)
|
\#*)
|
||||||
|
|
@ -188,7 +169,7 @@ ensure_gitignore_entries_from_template() {
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if tr -d '\r' < "$gitignore_path" | grep -Fqx "$entry"; then
|
if grep -Fqx "$entry" "$gitignore_path"; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
1
utils.go
1
utils.go
|
|
@ -34,6 +34,7 @@ func joiningSlash(elem []string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func singleJoiningSlash(a, b string) string {
|
func singleJoiningSlash(a, b string) string {
|
||||||
|
filepath.Join(a, b)
|
||||||
aslash := strings.HasSuffix(a, "/")
|
aslash := strings.HasSuffix(a, "/")
|
||||||
bslash := strings.HasPrefix(b, "/")
|
bslash := strings.HasPrefix(b, "/")
|
||||||
switch {
|
switch {
|
||||||
|
|
|
||||||
|
|
@ -23,20 +23,12 @@ func TestFileExists(t *testing.T) {
|
||||||
|
|
||||||
func TestJoiningSlash(t *testing.T) {
|
func TestJoiningSlash(t *testing.T) {
|
||||||
Convey("JoiningSlash should combine URL-like segments safely", t, func() {
|
Convey("JoiningSlash should combine URL-like segments safely", t, func() {
|
||||||
const (
|
So(JoiningSlash("http://my.tld/docs/", "bla/", "blub/"), ShouldEqual, "http://my.tld/docs/bla/blub/")
|
||||||
baseURL = "http://my.tld"
|
So(JoiningSlash("http://my.tld", "bla", "blub"), ShouldEqual, "http://my.tld/bla/blub")
|
||||||
docsURL = "http://my.tld/docs"
|
So(JoiningSlash("http://my.tld/", "bla", "blub"), ShouldEqual, "http://my.tld/bla/blub")
|
||||||
expectedRoot = "http://my.tld/bla/blub"
|
So(JoiningSlash("http://my.tld", "bla/", "blub"), ShouldEqual, "http://my.tld/bla/blub")
|
||||||
expectedDocs = "http://my.tld/docs/bla/blub"
|
So(JoiningSlash("http://my.tld/docs", "bla/", "blub"), ShouldEqual, "http://my.tld/docs/bla/blub")
|
||||||
expectedDocsS = "http://my.tld/docs/bla/blub/"
|
So(JoiningSlash("http://my.tld/docs/", "bla/", "blub"), ShouldEqual, "http://my.tld/docs/bla/blub")
|
||||||
)
|
|
||||||
|
|
||||||
So(JoiningSlash(docsURL+"/", "bla/", "blub/"), ShouldEqual, expectedDocsS)
|
|
||||||
So(JoiningSlash(baseURL, "bla", "blub"), ShouldEqual, expectedRoot)
|
|
||||||
So(JoiningSlash(baseURL+"/", "bla", "blub"), ShouldEqual, expectedRoot)
|
|
||||||
So(JoiningSlash(baseURL, "bla/", "blub"), ShouldEqual, expectedRoot)
|
|
||||||
So(JoiningSlash(docsURL, "bla/", "blub"), ShouldEqual, expectedDocs)
|
|
||||||
So(JoiningSlash(docsURL+"/", "bla/", "blub"), ShouldEqual, expectedDocs)
|
|
||||||
So(JoiningSlash("", "api", "v1"), ShouldEqual, "api/v1")
|
So(JoiningSlash("", "api", "v1"), ShouldEqual, "api/v1")
|
||||||
So(JoiningSlash("", "", ""), ShouldEqual, "")
|
So(JoiningSlash("", "", ""), ShouldEqual, "")
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue