Compare commits

..

No commits in common. "bc9b0ce88f11d6db0acd3dcf937a38197b148d62" and "e69fd33931aed767932da17850feb4fe08b6eba8" have entirely different histories.

16 changed files with 108 additions and 460 deletions

View File

@ -1,19 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.go]
indent_style = tab
indent_size = 4
[*.md]
trim_trailing_whitespace = false
[*.ps1]
end_of_line = crlf

18
.gitattributes vendored
View File

@ -1,18 +0,0 @@
* text=auto
# Ensure LF for shell scripts and common source/docs files
*.sh text eol=lf
*.bash text eol=lf
*.zsh text eol=lf
*.go text eol=lf
*.mod text eol=lf
*.sum text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
Makefile text eol=lf
# Keep native Windows script formats
*.ps1 text eol=crlf
*.bat text eol=crlf
*.cmd text eol=crlf

View File

@ -1,16 +0,0 @@
# Git Hooks
This repository standard uses a project-local hooks directory:
- `.githooks/pre-commit`
Activate it once per repository:
```sh
git config core.hooksPath .githooks
```
The pre-commit hook validates for staged `.sh` files:
- executable bit in Git index (`100755`)
- LF line endings (no CRLF)

View File

@ -1,25 +0,0 @@
#!/usr/bin/env sh
set -eu
failed=0
cr=$(printf '\r')
staged_shell_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.sh$' || true)
for file in $staged_shell_files; do
mode=$(git ls-files --stage -- "$file" | awk '{print $1}')
if [ "$mode" != "100755" ]; then
echo "ERROR: $file is not executable in Git index. Run: git add --chmod=+x $file" >&2
failed=1
fi
if git show ":$file" | grep -q "$cr"; then
echo "ERROR: $file contains CRLF in staged content. Use LF line endings." >&2
failed=1
fi
done
if [ "$failed" -ne 0 ]; then
echo "Pre-commit check failed." >&2
exit 1
fi

13
.gitignore vendored
View File

@ -1,13 +0,0 @@
.build/
bin/
dist/
tmp/
coverage/
coverage.out
*.coverprofile
*.test
*.out
.DS_Store
Thumbs.db
.vscode/
.idea/

View File

@ -20,16 +20,6 @@ All project documentation files must be stored under `docs/`, except `README.md`
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` as the standard test library.
## Build Artifacts and Reports
1. Builder logs and generated reports must be created under `.build/`.
2. The `.build/` directory must be excluded from version control via `.gitignore`.
## Git and Script Standards
1. Shell scripts (`*.sh`) must use LF line endings.
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`.
## Definition of Done (DoD) ## Definition of Done (DoD)
@ -41,8 +31,6 @@ The Definition of Done defines the minimum quality bar for every completed chang
- 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.
- For Go projects, tests use `github.com/smartystreets/goconvey`.
- Automated test coverage is at least 80%.
1. Functional documentation 1. Functional documentation
- Implemented functionality is documented. - Implemented functionality is documented.
@ -69,8 +57,6 @@ The Definition of Done defines the minimum quality bar for every completed chang
### 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`.
- [ ] Automated test coverage is at least 80%.
- [ ] 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`).

View File

@ -4,57 +4,9 @@
[![Build Status](https://drone.yoorie.de/api/badges/go-lib/util/status.svg)](https://drone.yoorie.de/go-lib/util) [![Build Status](https://drone.yoorie.de/api/badges/go-lib/util/status.svg)](https://drone.yoorie.de/go-lib/util)
## Project Description ## Documentation
This repository provides a small, cross-platform utility package for Go projects. Is missed so far and will be created soon.
It focuses on common helpers that are often reimplemented in multiple services,
such as file checks, safe path joining for URL-like strings, and OS-specific
configuration directory handling.
The package is intentionally lightweight and easy to reuse in CLI tools,
daemons, and backend services.
## Included Utilities
- `FileExists(fileName string) bool`
- Returns whether a file exists on disk.
- `JoiningSlash(elem ...string) string`
- Joins path segments with exactly one slash between elements.
- `GetGlobalConfigurationDirectory(appname string) string`
- Returns an operating-system-specific global configuration directory.
- Linux and macOS: `/etc/<appname>`
- Windows: `%APPDATA%\\<appname>`
- `GetGlobalConfigurationFile(appname string, file string) string`
- Builds a full path to a config file inside the global config directory.
- `IsSuperUser() bool`
- Detects whether the current process runs with elevated privileges.
## Installation
```bash
go get scm.yoorie.de/go-lib/util
```
## Example
```go
package main
import (
"fmt"
"scm.yoorie.de/go-lib/util"
)
func main() {
if util.FileExists("config.yaml") {
fmt.Println("config file found")
}
fmt.Println(util.JoiningSlash("/api", "v1", "users"))
fmt.Println(util.GetGlobalConfigurationFile("myapp", "config.yaml"))
}
```
--- ---
Copyright &copy; 2023 yoorie.de Copyright &copy; 2023 yoorie.de

View File

@ -1 +0,0 @@
mode: set

View File

@ -10,8 +10,6 @@ This Definition of Done defines the minimum quality bar for every completed chan
- 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.
- For Go projects, tests use `github.com/smartystreets/goconvey`.
- Automated test coverage is at least 80%.
1. Functional documentation 1. Functional documentation
- Implemented functionality is documented. - Implemented functionality is documented.
@ -40,8 +38,6 @@ This Definition of Done defines the minimum quality bar for every completed chan
- [ ] 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`.
- [ ] Automated test coverage is at least 80%.
- [ ] 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`).

2
go.mod
View File

@ -3,6 +3,6 @@ module scm.yoorie.de/go-lib/util
go 1.16 go 1.16
require ( require (
github.com/smartystreets/goconvey v1.6.4 github.com/stretchr/testify v1.8.2
golang.org/x/sys v0.6.0 golang.org/x/sys v0.6.0
) )

30
go.sum
View File

@ -1,15 +1,19 @@
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -11,13 +11,6 @@ import (
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
var (
allocateAndInitializeSid = windows.AllocateAndInitializeSid
freeSid = windows.FreeSid
tokenIsMember = func(token windows.Token, sid *windows.SID) (bool, error) { return token.IsMember(sid) }
fatalf = log.Fatalf
)
// IsSuperUser returns true, if the current user is a super user // IsSuperUser returns true, if the current user is a super user
// A.K.A root, Administrator etc // A.K.A root, Administrator etc
func IsSuperUser() bool { func IsSuperUser() bool {
@ -27,7 +20,7 @@ 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 := allocateAndInitializeSid( err := windows.AllocateAndInitializeSid(
&windows.SECURITY_NT_AUTHORITY, &windows.SECURITY_NT_AUTHORITY,
2, 2,
windows.SECURITY_BUILTIN_DOMAIN_RID, windows.SECURITY_BUILTIN_DOMAIN_RID,
@ -35,19 +28,19 @@ func IsSuperUser() bool {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
&sid) &sid)
if err != nil { if err != nil {
fatalf("SID Error: %s", err) log.Fatalf("SID Error: %s", err)
return false return false
} }
defer freeSid(sid) defer windows.FreeSid(sid)
// This appears to cast a null pointer so I'm not sure why this // This appears to cast a null pointer so I'm not sure why this
// works, but this guy says it does and it Works for Me™: // works, but this guy says it does and it Works for Me™:
// https://github.com/golang/go/issues/28804#issuecomment-438838144 // https://github.com/golang/go/issues/28804#issuecomment-438838144
token := windows.Token(0) token := windows.Token(0)
member, err := tokenIsMember(token, sid) member, err := token.IsMember(sid)
if err != nil { if err != nil {
fatalf("Token Membership Error: %s", err) log.Fatalf("Token Membership Error: %s", err)
return false return false
} }

View File

@ -1,47 +0,0 @@
//go:build windows
// +build windows
package util
import (
"errors"
"testing"
. "github.com/smartystreets/goconvey/convey"
"golang.org/x/sys/windows"
)
func TestIsSuperUser_SidAllocationError(t *testing.T) {
Convey("IsSuperUser should return false when SID allocation fails", t, func() {
origAllocate := allocateAndInitializeSid
origFatalf := fatalf
defer func() {
allocateAndInitializeSid = origAllocate
fatalf = origFatalf
}()
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")
}
fatalCalled := false
fatalf = func(format string, v ...interface{}) {
fatalCalled = true
}
So(IsSuperUser(), ShouldBeFalse)
So(fatalCalled, ShouldBeTrue)
})
}

View File

@ -13,11 +13,6 @@ $ErrorActionPreference = 'Stop'
$agentsTemplate = Join-Path $StandardsRepoPath 'templates/AGENTS.base.md' $agentsTemplate = Join-Path $StandardsRepoPath 'templates/AGENTS.base.md'
$dodTemplate = Join-Path $StandardsRepoPath 'templates/DEFINITION_OF_DONE.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)) { if (-not (Test-Path -Path $agentsTemplate -PathType Leaf)) {
throw "AGENTS template not found: $agentsTemplate" throw "AGENTS template not found: $agentsTemplate"
@ -27,26 +22,6 @@ if (-not (Test-Path -Path $dodTemplate -PathType Leaf)) {
throw "DoD template not found: $dodTemplate" 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) { if ($IntervalSeconds -lt 5) {
throw 'IntervalSeconds must be >= 5.' throw 'IntervalSeconds must be >= 5.'
} }
@ -88,51 +63,6 @@ function Ensure-FileFromTemplate {
return 'updated' 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 { function Invoke-ReconcileOnce {
param([switch]$OnlyCheck) param([switch]$OnlyCheck)
@ -151,45 +81,19 @@ function Invoke-ReconcileOnce {
$repoPath = $repo.FullName $repoPath = $repo.FullName
$agentsTarget = Join-Path $repoPath 'AGENTS.md' $agentsTarget = Join-Path $repoPath 'AGENTS.md'
$dodTarget = Join-Path (Join-Path $repoPath 'docs') 'DEFINITION_OF_DONE.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++ $summary.scanned++
$agentsState = Ensure-FileFromTemplate -Template $agentsTemplate -Target $agentsTarget -OnlyCheck:$OnlyCheck $agentsState = Ensure-FileFromTemplate -Template $agentsTemplate -Target $agentsTarget -OnlyCheck:$OnlyCheck
$dodState = Ensure-FileFromTemplate -Template $dodTemplate -Target $dodTarget -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 ( if ($agentsState -eq 'updated' -or $dodState -eq 'updated') {
$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++ $summary.updated++
Write-Host "UPDATED: $repoPath" Write-Host "UPDATED: $repoPath"
continue continue
} }
if ( if ($agentsState -eq 'drift' -or $dodState -eq 'drift') {
$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++ $summary.drift++
Write-Host "DRIFT: $repoPath" Write-Host "DRIFT: $repoPath"
continue continue

89
scripts/reconcile-project-standards.sh Executable file → Normal file
View File

@ -69,11 +69,6 @@ fi
AGENTS_TEMPLATE="$STANDARDS_REPO/templates/AGENTS.base.md" AGENTS_TEMPLATE="$STANDARDS_REPO/templates/AGENTS.base.md"
DOD_TEMPLATE="$STANDARDS_REPO/templates/DEFINITION_OF_DONE.base.md" DOD_TEMPLATE="$STANDARDS_REPO/templates/DEFINITION_OF_DONE.base.md"
GITIGNORE_TEMPLATE="$STANDARDS_REPO/templates/.gitignore.base"
GITATTRIBUTES_TEMPLATE="$STANDARDS_REPO/templates/.gitattributes.base"
EDITORCONFIG_TEMPLATE="$STANDARDS_REPO/templates/.editorconfig.base"
PRECOMMIT_TEMPLATE="$STANDARDS_REPO/templates/pre-commit.base.sh"
HOOKS_README_TEMPLATE="$STANDARDS_REPO/templates/.githooks.README.base.md"
if [ ! -f "$AGENTS_TEMPLATE" ]; then if [ ! -f "$AGENTS_TEMPLATE" ]; then
echo "AGENTS template not found: $AGENTS_TEMPLATE" >&2 echo "AGENTS template not found: $AGENTS_TEMPLATE" >&2
@ -85,31 +80,6 @@ if [ ! -f "$DOD_TEMPLATE" ]; then
exit 1 exit 1
fi fi
if [ ! -f "$GITIGNORE_TEMPLATE" ]; then
echo "gitignore template not found: $GITIGNORE_TEMPLATE" >&2
exit 1
fi
if [ ! -f "$GITATTRIBUTES_TEMPLATE" ]; then
echo "gitattributes template not found: $GITATTRIBUTES_TEMPLATE" >&2
exit 1
fi
if [ ! -f "$EDITORCONFIG_TEMPLATE" ]; then
echo "editorconfig template not found: $EDITORCONFIG_TEMPLATE" >&2
exit 1
fi
if [ ! -f "$PRECOMMIT_TEMPLATE" ]; then
echo "pre-commit hook template not found: $PRECOMMIT_TEMPLATE" >&2
exit 1
fi
if [ ! -f "$HOOKS_README_TEMPLATE" ]; then
echo "hooks readme template not found: $HOOKS_README_TEMPLATE" >&2
exit 1
fi
hash_or_missing() { hash_or_missing() {
path=$1 path=$1
if [ ! -f "$path" ]; then if [ ! -f "$path" ]; then
@ -146,51 +116,6 @@ ensure_file() {
printf "%s" "updated" printf "%s" "updated"
} }
ensure_gitignore_entries_from_template() {
template_path=$1
gitignore_path=$2
missing=0
if [ ! -f "$gitignore_path" ]; then
if [ "$CHECK_ONLY" -eq 1 ]; then
printf "%s" "drift"
return 0
fi
: > "$gitignore_path"
fi
while IFS= read -r entry; do
[ -n "$entry" ] || continue
case "$entry" in
\#*)
continue
;;
esac
if grep -Fqx "$entry" "$gitignore_path"; then
continue
fi
missing=1
if [ "$CHECK_ONLY" -ne 1 ]; then
printf '%s\n' "$entry" >> "$gitignore_path"
fi
done < "$template_path"
if [ "$missing" -eq 0 ]; then
printf "%s" "ok"
return 0
fi
if [ "$CHECK_ONLY" -eq 1 ]; then
printf "%s" "drift"
else
printf "%s" "updated"
fi
}
run_once() { run_once() {
scanned=0 scanned=0
updated=0 updated=0
@ -206,27 +131,17 @@ run_once() {
agents_target="$repo/AGENTS.md" agents_target="$repo/AGENTS.md"
dod_target="$repo/docs/DEFINITION_OF_DONE.md" dod_target="$repo/docs/DEFINITION_OF_DONE.md"
gitattributes_target="$repo/.gitattributes"
editorconfig_target="$repo/.editorconfig"
precommit_target="$repo/.githooks/pre-commit"
hooks_readme_target="$repo/.githooks/README.md"
gitignore_target="$repo/.gitignore"
agents_state=$(ensure_file "$AGENTS_TEMPLATE" "$agents_target") agents_state=$(ensure_file "$AGENTS_TEMPLATE" "$agents_target")
dod_state=$(ensure_file "$DOD_TEMPLATE" "$dod_target") dod_state=$(ensure_file "$DOD_TEMPLATE" "$dod_target")
gitattributes_state=$(ensure_file "$GITATTRIBUTES_TEMPLATE" "$gitattributes_target")
editorconfig_state=$(ensure_file "$EDITORCONFIG_TEMPLATE" "$editorconfig_target")
precommit_state=$(ensure_file "$PRECOMMIT_TEMPLATE" "$precommit_target")
hooks_readme_state=$(ensure_file "$HOOKS_README_TEMPLATE" "$hooks_readme_target")
gitignore_state=$(ensure_gitignore_entries_from_template "$GITIGNORE_TEMPLATE" "$gitignore_target")
if [ "$agents_state" = "updated" ] || [ "$dod_state" = "updated" ] || [ "$gitattributes_state" = "updated" ] || [ "$editorconfig_state" = "updated" ] || [ "$precommit_state" = "updated" ] || [ "$hooks_readme_state" = "updated" ] || [ "$gitignore_state" = "updated" ]; then if [ "$agents_state" = "updated" ] || [ "$dod_state" = "updated" ]; then
updated=$((updated + 1)) updated=$((updated + 1))
echo "UPDATED: $repo" echo "UPDATED: $repo"
continue continue
fi fi
if [ "$agents_state" = "drift" ] || [ "$dod_state" = "drift" ] || [ "$gitattributes_state" = "drift" ] || [ "$editorconfig_state" = "drift" ] || [ "$precommit_state" = "drift" ] || [ "$hooks_readme_state" = "drift" ] || [ "$gitignore_state" = "drift" ]; then if [ "$agents_state" = "drift" ] || [ "$dod_state" = "drift" ]; then
drift=$((drift + 1)) drift=$((drift + 1))
echo "DRIFT: $repo" echo "DRIFT: $repo"
continue continue

View File

@ -2,60 +2,97 @@ package util
import ( import (
"os" "os"
"os/user"
"path/filepath" "path/filepath"
"runtime"
"strings"
"testing" "testing"
. "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/assert"
) )
func TestFileExists(t *testing.T) { func TestFileExist(t *testing.T) {
Convey("FileExists should report existing and missing files", t, func() { assert.True(t, FileExists("utils.go"))
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "exists.txt")
err := os.WriteFile(tmpFile, []byte("ok"), 0o600)
So(err, ShouldBeNil)
So(FileExists(tmpFile), ShouldBeTrue)
So(FileExists(filepath.Join(tmpDir, "missing.txt")), ShouldBeFalse)
})
} }
func TestJoiningSlash(t *testing.T) { func TestFileExistNot(t *testing.T) {
Convey("JoiningSlash should combine URL-like segments safely", t, func() { assert.True(t, !FileExists("Utils2.go"))
So(JoiningSlash("http://my.tld/docs/", "bla/", "blub/"), ShouldEqual, "http://my.tld/docs/bla/blub/")
So(JoiningSlash("http://my.tld", "bla", "blub"), ShouldEqual, "http://my.tld/bla/blub")
So(JoiningSlash("http://my.tld/", "bla", "blub"), ShouldEqual, "http://my.tld/bla/blub")
So(JoiningSlash("http://my.tld", "bla/", "blub"), ShouldEqual, "http://my.tld/bla/blub")
So(JoiningSlash("http://my.tld/docs", "bla/", "blub"), ShouldEqual, "http://my.tld/docs/bla/blub")
So(JoiningSlash("http://my.tld/docs/", "bla/", "blub"), ShouldEqual, "http://my.tld/docs/bla/blub")
So(JoiningSlash("", "api", "v1"), ShouldEqual, "api/v1")
So(JoiningSlash("", "", ""), ShouldEqual, "")
})
} }
func TestSingleJoiningSlash(t *testing.T) { func TestJoiningSlash1(t *testing.T) {
Convey("singleJoiningSlash should handle slash edge cases", t, func() { actual := JoiningSlash("http://my.tld/docs/", "bla/", "blub/")
So(singleJoiningSlash("a/", "/b"), ShouldEqual, "a/b") expected := "http://my.tld/docs/bla/blub/"
So(singleJoiningSlash("a", "b"), ShouldEqual, "a/b") assert.Equal(t, expected, actual)
So(singleJoiningSlash("a/", "b"), ShouldEqual, "a/b")
So(singleJoiningSlash("a", "/b"), ShouldEqual, "a/b")
})
} }
func TestGetGlobalConfiguration(t *testing.T) { func TestJoiningSlash2(t *testing.T) {
Convey("GetGlobalConfigurationFile should create the expected path", t, func() { actual := JoiningSlash("http://my.tld", "bla", "blub")
appName := "myapp" assert.Equal(t, "http://my.tld/bla/blub", actual)
fileName := "config.yaml"
dir := GetGlobalConfigurationDirectory(appName)
So(GetGlobalConfigurationFile(appName, fileName), ShouldEqual, filepath.Join(dir, fileName))
})
} }
func TestJoiningSlash3(t *testing.T) {
actual := JoiningSlash("http://my.tld/", "bla", "blub")
assert.Equal(t, "http://my.tld/bla/blub", actual)
}
func TestJoiningSlash4(t *testing.T) {
actual := JoiningSlash("http://my.tld", "bla/", "blub")
assert.Equal(t, "http://my.tld/bla/blub", actual)
}
func TestJoiningSlash5(t *testing.T) {
actual := JoiningSlash("http://my.tld/docs", "bla/", "blub")
expected := "http://my.tld/docs/bla/blub"
assert.Equal(t, expected, actual)
}
func TestJoiningSlash6(t *testing.T) {
actual := JoiningSlash("http://my.tld/docs/", "bla/", "blub")
expected := "http://my.tld/docs/bla/blub"
assert.Equal(t, expected, actual)
}
/*
Can run only as admin within windows or linux
e.g. sudo TESTASSUDO=yes /usr/local/go/bin/go test -timeout 30s -run ^TestIsSuperUser$
*/
func TestIsSuperUser(t *testing.T) { func TestIsSuperUser(t *testing.T) {
Convey("IsSuperUser should return a boolean without requiring elevated rights", t, func() { if !strings.EqualFold(os.Getenv("TESTASSUDO"), "yes") {
result := IsSuperUser() t.Skip("Skipping in normal tests")
So(result, ShouldBeIn, []bool{true, false}) }
}) cuser, err := user.Current()
assert.Nil(t, err)
assert.NotNil(t, cuser)
assert.True(t, IsSuperUser())
}
func TestGlobalConfigurationDirectoryWindows(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skipf("Skipping on OS %s", runtime.GOOS)
}
appFolder := GetGlobalConfigurationDirectory("myapp")
assert.Equal(t, filepath.Join(os.Getenv("APPDATA"), "myapp"), appFolder)
}
func TestGlobalConfigurationDirectoryLinux(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skipf("Skipping on OS %s", runtime.GOOS)
}
appFolder := GetGlobalConfigurationDirectory("myapp")
assert.Equal(t, "/etc/myapp", appFolder)
}
func TestGlobalConfigurationDirectoryMacOS(t *testing.T) {
if runtime.GOOS != "darwin" {
t.Skipf("Skipping on OS %s", runtime.GOOS)
}
appFolder := GetGlobalConfigurationDirectory("myapp")
assert.Equal(t, "/etc/myapp", appFolder)
}
func TestMkDir(t *testing.T) {
path := "c:/tmp/bla/blub"
if !FileExists(path) {
err := os.MkdirAll(path, os.ModeDir)
assert.Nil(t, err)
assert.True(t, FileExists(path))
}
} }