Compare commits
10 Commits
improvemen
...
strta-conf
| Author | SHA1 | Date | |
|---|---|---|---|
| 6230bb1c3a | |||
| 975116f13d | |||
| 1bf1c892f4 | |||
| 629613de1b | |||
| 5c2338d590 | |||
| 9f86bf1dc1 | |||
| 2e44e5bb78 | |||
| b9d65115c2 | |||
| 144d2de243 | |||
| d848c5e51b |
9
.gitea/workflows/check.yml
Normal file
9
.gitea/workflows/check.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
name: check
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# actions/checkout v4.2.2, 2026-02-28
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
- run: script/cibuild
|
||||||
32
.golangci.yml
Normal file
32
.golangci.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
version: "2"
|
||||||
|
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
modules-download-mode: readonly
|
||||||
|
|
||||||
|
linters:
|
||||||
|
default: all
|
||||||
|
disable:
|
||||||
|
# Genuinely incompatible with project patterns
|
||||||
|
- exhaustruct # Requires all struct fields
|
||||||
|
- depguard # Dependency allow/block lists
|
||||||
|
- godot # Requires comments to end with periods
|
||||||
|
- wsl # Deprecated, replaced by wsl_v5
|
||||||
|
- wrapcheck # Too verbose for internal packages
|
||||||
|
- varnamelen # Short names like db, id are idiomatic Go
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
lll:
|
||||||
|
line-length: 88
|
||||||
|
funlen:
|
||||||
|
lines: 80
|
||||||
|
statements: 50
|
||||||
|
cyclop:
|
||||||
|
max-complexity: 15
|
||||||
|
dupl:
|
||||||
|
threshold: 100
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-use-default: false
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
34
Dockerfile
Normal file
34
Dockerfile
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Build stage
|
||||||
|
# golang 1.25-alpine, 2026-02-28
|
||||||
|
FROM golang@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced AS builder
|
||||||
|
|
||||||
|
RUN apk add --no-cache git make gcc musl-dev binutils-gold
|
||||||
|
|
||||||
|
# golangci-lint v2.10.1
|
||||||
|
RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@5d1e709b7be35cb2025444e19de266b056b7b7ee
|
||||||
|
# goimports v0.42.0
|
||||||
|
RUN go install golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Run all checks - build fails if any check fails
|
||||||
|
RUN make check
|
||||||
|
|
||||||
|
# Build the binary
|
||||||
|
RUN make build
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
# alpine 3.21, 2026-02-28
|
||||||
|
FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /src/attrsum /app/attrsum
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/attrsum"]
|
||||||
34
Makefile
34
Makefile
@@ -1,9 +1,38 @@
|
|||||||
|
.PHONY: default bootstrap setup test lint fmt fmt-check check docker hooks build clean try
|
||||||
|
|
||||||
TESTDIR := $(HOME)/Documents/_SYSADMIN/cyberdyne
|
TESTDIR := $(HOME)/Documents/_SYSADMIN/cyberdyne
|
||||||
|
|
||||||
default: test
|
# Standard targets are thin shims; the implementations live in script/
|
||||||
|
# per the scripts-to-rule-them-all pattern.
|
||||||
|
|
||||||
|
default: check build
|
||||||
|
|
||||||
|
bootstrap:
|
||||||
|
@script/bootstrap
|
||||||
|
|
||||||
|
setup:
|
||||||
|
@script/setup
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@go test ./... -v
|
@script/test
|
||||||
|
|
||||||
|
lint:
|
||||||
|
@script/lint
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
@script/fmt
|
||||||
|
|
||||||
|
fmt-check:
|
||||||
|
@script/fmt-check
|
||||||
|
|
||||||
|
check:
|
||||||
|
@script/check
|
||||||
|
|
||||||
|
docker:
|
||||||
|
@script/docker
|
||||||
|
|
||||||
|
hooks:
|
||||||
|
@script/install-precommit
|
||||||
|
|
||||||
build: clean
|
build: clean
|
||||||
@go build .
|
@go build .
|
||||||
@@ -21,4 +50,3 @@ try: build
|
|||||||
touch $(TESTDIR)/*
|
touch $(TESTDIR)/*
|
||||||
./attrsum sum update -v $(TESTDIR)
|
./attrsum sum update -v $(TESTDIR)
|
||||||
./attrsum check -v $(TESTDIR)
|
./attrsum check -v $(TESTDIR)
|
||||||
|
|
||||||
|
|||||||
55
TODO.md
Normal file
55
TODO.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Workflow
|
||||||
|
|
||||||
|
* branch (from `main`)
|
||||||
|
* do the work in Next Step
|
||||||
|
* move Next Step to the top of Completed Steps
|
||||||
|
* move the top item of Future Steps into Next Step
|
||||||
|
* commit (`TODO.md` changes in the same commit as the work)
|
||||||
|
* merge to `main` if the branch is not protected, otherwise open a PR
|
||||||
|
* push
|
||||||
|
|
||||||
|
# Status
|
||||||
|
|
||||||
|
1.0+
|
||||||
|
|
||||||
|
Tagged 1.0.0 (2025-05-08). Substantial correctness fixes and features
|
||||||
|
have landed since the tag.
|
||||||
|
|
||||||
|
# Next Step
|
||||||
|
|
||||||
|
Policy scaffold commit: add LICENSE, REPO_POLICIES.md, .editorconfig,
|
||||||
|
.golangci.yml, and a comprehensive .gitignore (currently only the
|
||||||
|
attrsum binary), and extend the Makefile (only test/build/clean/try
|
||||||
|
today) with lint, fmt, fmt-check, check, and hooks targets. Fix the try
|
||||||
|
target to use a temp fixture instead of the hardcoded
|
||||||
|
$(HOME)/Documents/_SYSADMIN/cyberdyne path.
|
||||||
|
|
||||||
|
# Completed Steps
|
||||||
|
|
||||||
|
* 2026-02-02: correctness pass: track actual bytes read instead of
|
||||||
|
stale file size, atomic failure tracking in ProcessCheck, detect
|
||||||
|
file modification during checksum (TOCTOU), propagate countFiles
|
||||||
|
errors, single progress bar across paths, error on empty stdin,
|
||||||
|
dead code removal
|
||||||
|
* 2026-02-01: added quiet mode, progress bar, summary report, and stdin
|
||||||
|
path input; multiple file/directory arguments for all commands
|
||||||
|
* 2025-07-12: README update
|
||||||
|
* 2025-05-08: initial working tool with passing tests, skips
|
||||||
|
non-regular files, Makefile, README; tagged 1.0.0
|
||||||
|
|
||||||
|
# Future Steps
|
||||||
|
|
||||||
|
* Add Dockerfile and .dockerignore that run make check, images pinned
|
||||||
|
by sha256, plus a Makefile docker target
|
||||||
|
* Add .gitea/workflows/check.yml
|
||||||
|
* Restructure README.md into the standard sections: Description,
|
||||||
|
Getting Started, Rationale, Design, TODO, License, Author (Getting
|
||||||
|
Started, Why?, TODO, License exist; Description, Design, Author are
|
||||||
|
missing)
|
||||||
|
* Tag a patch release to ship the 2026-02-02 correctness fixes
|
||||||
|
* Dry-run mode (--dry-run, -n): show what would be done without making
|
||||||
|
changes (from README TODO)
|
||||||
|
* JSON output (--json) for scripting and integration (from README TODO)
|
||||||
|
* Parallel processing (-j N) with multiple goroutines for faster
|
||||||
|
checksumming on large trees (from README TODO)
|
||||||
|
* Formalize and document exit codes for scripting (from README TODO)
|
||||||
192
attrsum.go
192
attrsum.go
@@ -99,8 +99,10 @@ func main() {
|
|||||||
// expandPaths expands the given paths, reading from stdin if "-" is present
|
// expandPaths expands the given paths, reading from stdin if "-" is present
|
||||||
func expandPaths(args []string) ([]string, error) {
|
func expandPaths(args []string) ([]string, error) {
|
||||||
var paths []string
|
var paths []string
|
||||||
|
readFromStdin := false
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
if arg == "-" {
|
if arg == "-" {
|
||||||
|
readFromStdin = true
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := strings.TrimSpace(scanner.Text())
|
line := strings.TrimSpace(scanner.Text())
|
||||||
@@ -115,6 +117,12 @@ func expandPaths(args []string) ([]string, error) {
|
|||||||
paths = append(paths, arg)
|
paths = append(paths, arg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(paths) == 0 {
|
||||||
|
if readFromStdin {
|
||||||
|
return nil, errors.New("no paths provided on stdin")
|
||||||
|
}
|
||||||
|
return nil, errors.New("no paths provided")
|
||||||
|
}
|
||||||
return paths, nil
|
return paths, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,10 +146,24 @@ func newSumCmd() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
stats := &Stats{StartTime: time.Now()}
|
stats := &Stats{StartTime: time.Now()}
|
||||||
for _, p := range paths {
|
var bar *progressbar.ProgressBar
|
||||||
if err := ProcessSumAdd(p, stats); err != nil {
|
if !quiet {
|
||||||
|
total, err := countFilesMultiple(paths)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
bar = newProgressBar(total, "Adding checksums")
|
||||||
|
}
|
||||||
|
for _, p := range paths {
|
||||||
|
if err := ProcessSumAdd(p, stats, bar); err != nil {
|
||||||
|
if bar != nil {
|
||||||
|
bar.Finish()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bar != nil {
|
||||||
|
bar.Finish()
|
||||||
}
|
}
|
||||||
stats.Print("sum add")
|
stats.Print("sum add")
|
||||||
return nil
|
return nil
|
||||||
@@ -158,10 +180,24 @@ func newSumCmd() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
stats := &Stats{StartTime: time.Now()}
|
stats := &Stats{StartTime: time.Now()}
|
||||||
for _, p := range paths {
|
var bar *progressbar.ProgressBar
|
||||||
if err := ProcessSumUpdate(p, stats); err != nil {
|
if !quiet {
|
||||||
|
total, err := countFilesMultiple(paths)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
bar = newProgressBar(total, "Updating checksums")
|
||||||
|
}
|
||||||
|
for _, p := range paths {
|
||||||
|
if err := ProcessSumUpdate(p, stats, bar); err != nil {
|
||||||
|
if bar != nil {
|
||||||
|
bar.Finish()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bar != nil {
|
||||||
|
bar.Finish()
|
||||||
}
|
}
|
||||||
stats.Print("sum update")
|
stats.Print("sum update")
|
||||||
return nil
|
return nil
|
||||||
@@ -172,8 +208,8 @@ func newSumCmd() *cobra.Command {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProcessSumAdd(dir string, stats *Stats) error {
|
func ProcessSumAdd(dir string, stats *Stats, bar *progressbar.ProgressBar) error {
|
||||||
return walkAndProcess(dir, stats, "Adding checksums", func(p string, info os.FileInfo, s *Stats) error {
|
return walkAndProcess(dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error {
|
||||||
if hasXattr(p, checksumKey) {
|
if hasXattr(p, checksumKey) {
|
||||||
atomic.AddInt64(&s.FilesSkipped, 1)
|
atomic.AddInt64(&s.FilesSkipped, 1)
|
||||||
return nil
|
return nil
|
||||||
@@ -186,8 +222,8 @@ func ProcessSumAdd(dir string, stats *Stats) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProcessSumUpdate(dir string, stats *Stats) error {
|
func ProcessSumUpdate(dir string, stats *Stats, bar *progressbar.ProgressBar) error {
|
||||||
return walkAndProcess(dir, stats, "Updating checksums", func(p string, info os.FileInfo, s *Stats) error {
|
return walkAndProcess(dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error {
|
||||||
t, err := readSumTime(p)
|
t, err := readSumTime(p)
|
||||||
if err != nil || info.ModTime().After(t) {
|
if err != nil || info.ModTime().After(t) {
|
||||||
if err := writeChecksumAndTime(p, info, s); err != nil {
|
if err := writeChecksumAndTime(p, info, s); err != nil {
|
||||||
@@ -202,10 +238,23 @@ func ProcessSumUpdate(dir string, stats *Stats) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeChecksumAndTime(path string, info os.FileInfo, stats *Stats) error {
|
func writeChecksumAndTime(path string, info os.FileInfo, stats *Stats) error {
|
||||||
hash, err := fileMultihash(path)
|
// Record mtime before hashing to detect modifications during hash
|
||||||
|
mtimeBefore := info.ModTime()
|
||||||
|
|
||||||
|
hash, bytesRead, err := fileMultihash(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if file was modified during hashing
|
||||||
|
infoAfter, err := os.Lstat(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stat after hash: %w", err)
|
||||||
|
}
|
||||||
|
if !infoAfter.ModTime().Equal(mtimeBefore) {
|
||||||
|
return fmt.Errorf("%s: file modified during checksum calculation", path)
|
||||||
|
}
|
||||||
|
|
||||||
if err := xattr.Set(path, checksumKey, hash); err != nil {
|
if err := xattr.Set(path, checksumKey, hash); err != nil {
|
||||||
return fmt.Errorf("set checksum attr: %w", err)
|
return fmt.Errorf("set checksum attr: %w", err)
|
||||||
}
|
}
|
||||||
@@ -213,7 +262,9 @@ func writeChecksumAndTime(path string, info os.FileInfo, stats *Stats) error {
|
|||||||
fmt.Printf("%s %s written\n", path, hash)
|
fmt.Printf("%s %s written\n", path, hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := time.Now().UTC().Format(time.RFC3339Nano)
|
// Store the file's mtime as sumtime (not wall-clock time)
|
||||||
|
// This makes update comparisons semantically correct
|
||||||
|
ts := mtimeBefore.UTC().Format(time.RFC3339Nano)
|
||||||
if err := xattr.Set(path, sumTimeKey, []byte(ts)); err != nil {
|
if err := xattr.Set(path, sumTimeKey, []byte(ts)); err != nil {
|
||||||
return fmt.Errorf("set sumtime attr: %w", err)
|
return fmt.Errorf("set sumtime attr: %w", err)
|
||||||
}
|
}
|
||||||
@@ -222,7 +273,7 @@ func writeChecksumAndTime(path string, info os.FileInfo, stats *Stats) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
atomic.AddInt64(&stats.FilesProcessed, 1)
|
atomic.AddInt64(&stats.FilesProcessed, 1)
|
||||||
atomic.AddInt64(&stats.BytesProcessed, info.Size())
|
atomic.AddInt64(&stats.BytesProcessed, bytesRead)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,10 +300,24 @@ func newClearCmd() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
stats := &Stats{StartTime: time.Now()}
|
stats := &Stats{StartTime: time.Now()}
|
||||||
for _, p := range paths {
|
var bar *progressbar.ProgressBar
|
||||||
if err := ProcessClear(p, stats); err != nil {
|
if !quiet {
|
||||||
|
total, err := countFilesMultiple(paths)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
bar = newProgressBar(total, "Clearing checksums")
|
||||||
|
}
|
||||||
|
for _, p := range paths {
|
||||||
|
if err := ProcessClear(p, stats, bar); err != nil {
|
||||||
|
if bar != nil {
|
||||||
|
bar.Finish()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bar != nil {
|
||||||
|
bar.Finish()
|
||||||
}
|
}
|
||||||
stats.Print("clear")
|
stats.Print("clear")
|
||||||
return nil
|
return nil
|
||||||
@@ -260,8 +325,8 @@ func newClearCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProcessClear(dir string, stats *Stats) error {
|
func ProcessClear(dir string, stats *Stats, bar *progressbar.ProgressBar) error {
|
||||||
return walkAndProcess(dir, stats, "Clearing checksums", func(p string, info os.FileInfo, s *Stats) error {
|
return walkAndProcess(dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error {
|
||||||
cleared := false
|
cleared := false
|
||||||
for _, k := range []string{checksumKey, sumTimeKey} {
|
for _, k := range []string{checksumKey, sumTimeKey} {
|
||||||
v, err := xattr.Get(p, k)
|
v, err := xattr.Get(p, k)
|
||||||
@@ -307,17 +372,31 @@ func newCheckCmd() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
stats := &Stats{StartTime: time.Now()}
|
stats := &Stats{StartTime: time.Now()}
|
||||||
|
var bar *progressbar.ProgressBar
|
||||||
|
if !quiet {
|
||||||
|
total, err := countFilesMultiple(paths)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bar = newProgressBar(total, "Verifying checksums")
|
||||||
|
}
|
||||||
var finalErr error
|
var finalErr error
|
||||||
for _, p := range paths {
|
for _, p := range paths {
|
||||||
if err := ProcessCheck(p, cont, stats); err != nil {
|
if err := ProcessCheck(p, cont, stats, bar); err != nil {
|
||||||
if cont {
|
if cont {
|
||||||
finalErr = err
|
finalErr = err
|
||||||
} else {
|
} else {
|
||||||
|
if bar != nil {
|
||||||
|
bar.Finish()
|
||||||
|
}
|
||||||
stats.Print("check")
|
stats.Print("check")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if bar != nil {
|
||||||
|
bar.Finish()
|
||||||
|
}
|
||||||
stats.Print("check")
|
stats.Print("check")
|
||||||
return finalErr
|
return finalErr
|
||||||
},
|
},
|
||||||
@@ -326,15 +405,15 @@ func newCheckCmd() *cobra.Command {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProcessCheck(dir string, cont bool, stats *Stats) error {
|
func ProcessCheck(dir string, cont bool, stats *Stats, bar *progressbar.ProgressBar) error {
|
||||||
fail := errors.New("verification failed")
|
fail := errors.New("verification failed")
|
||||||
bad := false
|
// Track initial failed count to detect failures during this walk
|
||||||
|
initialFailed := atomic.LoadInt64(&stats.FilesFailed)
|
||||||
|
|
||||||
err := walkAndProcess(dir, stats, "Verifying checksums", func(p string, info os.FileInfo, s *Stats) error {
|
err := walkAndProcess(dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error {
|
||||||
exp, err := xattr.Get(p, checksumKey)
|
exp, err := xattr.Get(p, checksumKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, xattr.ENOATTR) {
|
if errors.Is(err, xattr.ENOATTR) {
|
||||||
bad = true
|
|
||||||
atomic.AddInt64(&s.FilesFailed, 1)
|
atomic.AddInt64(&s.FilesFailed, 1)
|
||||||
if verbose && !quiet {
|
if verbose && !quiet {
|
||||||
fmt.Printf("%s <none> ERROR\n", p)
|
fmt.Printf("%s <none> ERROR\n", p)
|
||||||
@@ -347,18 +426,17 @@ func ProcessCheck(dir string, cont bool, stats *Stats) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
act, err := fileMultihash(p)
|
act, bytesRead, err := fileMultihash(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
atomic.AddInt64(&s.FilesFailed, 1)
|
atomic.AddInt64(&s.FilesFailed, 1)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ok := bytes.Equal(exp, act)
|
ok := bytes.Equal(exp, act)
|
||||||
if !ok {
|
if !ok {
|
||||||
bad = true
|
|
||||||
atomic.AddInt64(&s.FilesFailed, 1)
|
atomic.AddInt64(&s.FilesFailed, 1)
|
||||||
} else {
|
} else {
|
||||||
atomic.AddInt64(&s.FilesProcessed, 1)
|
atomic.AddInt64(&s.FilesProcessed, 1)
|
||||||
atomic.AddInt64(&s.BytesProcessed, info.Size())
|
atomic.AddInt64(&s.BytesProcessed, bytesRead)
|
||||||
}
|
}
|
||||||
if verbose && !quiet {
|
if verbose && !quiet {
|
||||||
status := "OK"
|
status := "OK"
|
||||||
@@ -379,7 +457,8 @@ func ProcessCheck(dir string, cont bool, stats *Stats) error {
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if bad {
|
// Check if any failures occurred during this walk
|
||||||
|
if atomic.LoadInt64(&stats.FilesFailed) > initialFailed {
|
||||||
return fail
|
return fail
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -390,17 +469,16 @@ func ProcessCheck(dir string, cont bool, stats *Stats) error {
|
|||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// countFiles counts the total number of regular files that will be processed
|
// countFiles counts the total number of regular files that will be processed
|
||||||
func countFiles(root string) int64 {
|
func countFiles(root string) (int64, error) {
|
||||||
var count int64
|
var count int64
|
||||||
root = filepath.Clean(root)
|
root = filepath.Clean(root)
|
||||||
filepath.Walk(root, func(p string, info os.FileInfo, err error) error {
|
err := filepath.Walk(root, func(p string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
// Skip symlinks - note: filepath.Walk uses Lstat, so symlinks are
|
||||||
|
// reported as ModeSymlink, never as directories. Walk doesn't follow them.
|
||||||
if info.Mode()&os.ModeSymlink != 0 {
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
if info.IsDir() {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
rel, _ := filepath.Rel(root, p)
|
rel, _ := filepath.Rel(root, p)
|
||||||
@@ -419,19 +497,25 @@ func countFiles(root string) int64 {
|
|||||||
count++
|
count++
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
return count
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func walkAndProcess(root string, stats *Stats, description string, fn func(string, os.FileInfo, *Stats) error) error {
|
// countFilesMultiple counts files across multiple roots
|
||||||
root = filepath.Clean(root)
|
func countFilesMultiple(roots []string) (int64, error) {
|
||||||
|
var total int64
|
||||||
|
for _, root := range roots {
|
||||||
|
count, err := countFiles(root)
|
||||||
|
if err != nil {
|
||||||
|
return total, err
|
||||||
|
}
|
||||||
|
total += count
|
||||||
|
}
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Count files first for progress bar
|
// newProgressBar creates a new progress bar with standard options
|
||||||
total := countFiles(root)
|
func newProgressBar(total int64, description string) *progressbar.ProgressBar {
|
||||||
|
return progressbar.NewOptions64(total,
|
||||||
// Create progress bar
|
|
||||||
var bar *progressbar.ProgressBar
|
|
||||||
if !quiet {
|
|
||||||
bar = progressbar.NewOptions64(total,
|
|
||||||
progressbar.OptionSetDescription(description),
|
progressbar.OptionSetDescription(description),
|
||||||
progressbar.OptionSetWriter(os.Stderr),
|
progressbar.OptionSetWriter(os.Stderr),
|
||||||
progressbar.OptionShowCount(),
|
progressbar.OptionShowCount(),
|
||||||
@@ -449,21 +533,22 @@ func walkAndProcess(root string, stats *Stats, description string, fn func(strin
|
|||||||
BarEnd: "]",
|
BarEnd: "]",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func walkAndProcess(root string, stats *Stats, bar *progressbar.ProgressBar, fn func(string, os.FileInfo, *Stats) error) error {
|
||||||
|
root = filepath.Clean(root)
|
||||||
|
|
||||||
err := filepath.Walk(root, func(p string, info os.FileInfo, err error) error {
|
err := filepath.Walk(root, func(p string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// skip symlinks entirely
|
// Skip symlinks - filepath.Walk uses Lstat, so symlinks are reported
|
||||||
|
// as ModeSymlink, never as directories. Walk doesn't follow them.
|
||||||
if info.Mode()&os.ModeSymlink != 0 {
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
if verbose && !quiet {
|
if verbose && !quiet {
|
||||||
log.Printf("skip symlink %s", p)
|
log.Printf("skip symlink %s", p)
|
||||||
}
|
}
|
||||||
if info.IsDir() {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,10 +577,6 @@ func walkAndProcess(root string, stats *Stats, description string, fn func(strin
|
|||||||
return fnErr
|
return fnErr
|
||||||
})
|
})
|
||||||
|
|
||||||
if bar != nil {
|
|
||||||
bar.Finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,20 +604,21 @@ func hasXattr(path, key string) bool {
|
|||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileMultihash(path string) ([]byte, error) {
|
func fileMultihash(path string) (hash []byte, bytesRead int64, err error) {
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
h := sha256.New()
|
h := sha256.New()
|
||||||
if _, err := io.Copy(h, f); err != nil {
|
bytesRead, err = io.Copy(h, f)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, bytesRead, err
|
||||||
}
|
}
|
||||||
mh, err := multihash.Encode(h.Sum(nil), multihash.SHA2_256)
|
mh, err := multihash.Encode(h.Sum(nil), multihash.SHA2_256)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, bytesRead, err
|
||||||
}
|
}
|
||||||
return []byte(base58.Encode(mh)), nil
|
return []byte(base58.Encode(mh)), bytesRead, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func TestSumAddAndUpdate(t *testing.T) {
|
|||||||
|
|
||||||
f := writeFile(t, dir, "a.txt", "hello")
|
f := writeFile(t, dir, "a.txt", "hello")
|
||||||
|
|
||||||
if err := ProcessSumAdd(dir, newTestStats()); err != nil {
|
if err := ProcessSumAdd(dir, newTestStats(), nil); err != nil {
|
||||||
t.Fatalf("add: %v", err)
|
t.Fatalf("add: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := xattr.Get(f, checksumKey); err != nil {
|
if _, err := xattr.Get(f, checksumKey); err != nil {
|
||||||
@@ -53,7 +53,7 @@ func TestSumAddAndUpdate(t *testing.T) {
|
|||||||
now := time.Now().Add(2 * time.Second)
|
now := time.Now().Add(2 * time.Second)
|
||||||
os.Chtimes(f, now, now)
|
os.Chtimes(f, now, now)
|
||||||
|
|
||||||
if err := ProcessSumUpdate(dir, newTestStats()); err != nil {
|
if err := ProcessSumUpdate(dir, newTestStats(), nil); err != nil {
|
||||||
t.Fatalf("update: %v", err)
|
t.Fatalf("update: %v", err)
|
||||||
}
|
}
|
||||||
tsb2, _ := xattr.Get(f, sumTimeKey)
|
tsb2, _ := xattr.Get(f, sumTimeKey)
|
||||||
@@ -68,17 +68,17 @@ func TestProcessCheckIntegration(t *testing.T) {
|
|||||||
skipIfNoXattr(t, dir)
|
skipIfNoXattr(t, dir)
|
||||||
|
|
||||||
writeFile(t, dir, "b.txt", "world")
|
writeFile(t, dir, "b.txt", "world")
|
||||||
if err := ProcessSumAdd(dir, newTestStats()); err != nil {
|
if err := ProcessSumAdd(dir, newTestStats(), nil); err != nil {
|
||||||
t.Fatalf("add: %v", err)
|
t.Fatalf("add: %v", err)
|
||||||
}
|
}
|
||||||
if err := ProcessCheck(dir, false, newTestStats()); err != nil {
|
if err := ProcessCheck(dir, false, newTestStats(), nil); err != nil {
|
||||||
t.Fatalf("check ok: %v", err)
|
t.Fatalf("check ok: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
f := filepath.Join(dir, "b.txt")
|
f := filepath.Join(dir, "b.txt")
|
||||||
os.WriteFile(f, []byte("corrupt"), 0o644)
|
os.WriteFile(f, []byte("corrupt"), 0o644)
|
||||||
|
|
||||||
if err := ProcessCheck(dir, false, newTestStats()); err == nil {
|
if err := ProcessCheck(dir, false, newTestStats(), nil); err == nil {
|
||||||
t.Fatalf("expected mismatch error, got nil")
|
t.Fatalf("expected mismatch error, got nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,11 +88,11 @@ func TestClearRemovesAttrs(t *testing.T) {
|
|||||||
skipIfNoXattr(t, dir)
|
skipIfNoXattr(t, dir)
|
||||||
|
|
||||||
f := writeFile(t, dir, "c.txt", "data")
|
f := writeFile(t, dir, "c.txt", "data")
|
||||||
if err := ProcessSumAdd(dir, newTestStats()); err != nil {
|
if err := ProcessSumAdd(dir, newTestStats(), nil); err != nil {
|
||||||
t.Fatalf("add: %v", err)
|
t.Fatalf("add: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ProcessClear(dir, newTestStats()); err != nil {
|
if err := ProcessClear(dir, newTestStats(), nil); err != nil {
|
||||||
t.Fatalf("clear: %v", err)
|
t.Fatalf("clear: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := xattr.Get(f, checksumKey); err == nil {
|
if _, err := xattr.Get(f, checksumKey); err == nil {
|
||||||
@@ -117,7 +117,7 @@ func TestExcludeDotfilesAndPatterns(t *testing.T) {
|
|||||||
excludePatterns = []string{"*.me"}
|
excludePatterns = []string{"*.me"}
|
||||||
defer func() { excludeDotfiles, excludePatterns = oldDot, oldPat }()
|
defer func() { excludeDotfiles, excludePatterns = oldDot, oldPat }()
|
||||||
|
|
||||||
if err := ProcessSumAdd(dir, newTestStats()); err != nil {
|
if err := ProcessSumAdd(dir, newTestStats(), nil); err != nil {
|
||||||
t.Fatalf("add with excludes: %v", err)
|
t.Fatalf("add with excludes: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ func TestSkipBrokenSymlink(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Should not error and should not create xattrs on link
|
// Should not error and should not create xattrs on link
|
||||||
if err := ProcessSumAdd(dir, newTestStats()); err != nil {
|
if err := ProcessSumAdd(dir, newTestStats(), nil); err != nil {
|
||||||
t.Fatalf("ProcessSumAdd with symlink: %v", err)
|
t.Fatalf("ProcessSumAdd with symlink: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := xattr.Get(link, checksumKey); err == nil {
|
if _, err := xattr.Get(link, checksumKey); err == nil {
|
||||||
@@ -159,13 +159,13 @@ func TestPermissionErrors(t *testing.T) {
|
|||||||
os.Chmod(secret, 0o000)
|
os.Chmod(secret, 0o000)
|
||||||
defer os.Chmod(secret, 0o644)
|
defer os.Chmod(secret, 0o644)
|
||||||
|
|
||||||
if err := ProcessSumAdd(dir, newTestStats()); err == nil {
|
if err := ProcessSumAdd(dir, newTestStats(), nil); err == nil {
|
||||||
t.Fatalf("expected permission error, got nil")
|
t.Fatalf("expected permission error, got nil")
|
||||||
}
|
}
|
||||||
if err := ProcessSumUpdate(dir, newTestStats()); err == nil {
|
if err := ProcessSumUpdate(dir, newTestStats(), nil); err == nil {
|
||||||
t.Fatalf("expected permission error on update, got nil")
|
t.Fatalf("expected permission error on update, got nil")
|
||||||
}
|
}
|
||||||
if err := ProcessCheck(dir, false, newTestStats()); err == nil {
|
if err := ProcessCheck(dir, false, newTestStats(), nil); err == nil {
|
||||||
t.Fatalf("expected permission error on check, got nil")
|
t.Fatalf("expected permission error on check, got nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
82
script/bootstrap
Executable file
82
script/bootstrap
Executable file
@@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/bootstrap: install all dependencies needed to build and develop
|
||||||
|
# this repo. Idempotent: every install is guarded by a check so already
|
||||||
|
# installed tools are skipped. Base tooling comes from nix, apt, brew,
|
||||||
|
# or apk (detected in that order); assumes nothing is present.
|
||||||
|
# golangci-lint and goimports are installed via `go install` at the same
|
||||||
|
# pinned commits the Dockerfile uses (never "latest").
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
# Pinned versions, 2026-07-07 (same pins as the Dockerfile)
|
||||||
|
# golangci-lint v2.10.1
|
||||||
|
GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@5d1e709b7be35cb2025444e19de266b056b7b7ee"
|
||||||
|
# goimports v0.42.0
|
||||||
|
GOIMPORTS_REF="golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0"
|
||||||
|
|
||||||
|
PKGMGR=""
|
||||||
|
SUDO=""
|
||||||
|
APT_UPDATED=""
|
||||||
|
|
||||||
|
detect_pkgmgr() {
|
||||||
|
[ -n "$PKGMGR" ] && return 0
|
||||||
|
if command -v nix-env >/dev/null 2>&1; then
|
||||||
|
PKGMGR="nix"
|
||||||
|
elif command -v apt-get >/dev/null 2>&1; then
|
||||||
|
PKGMGR="apt"
|
||||||
|
elif command -v brew >/dev/null 2>&1; then
|
||||||
|
PKGMGR="brew"
|
||||||
|
elif command -v apk >/dev/null 2>&1; then
|
||||||
|
PKGMGR="apk"
|
||||||
|
else
|
||||||
|
echo "bootstrap: no supported package manager (nix, apt, brew, apk)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$PKGMGR" = "apt" ]; then
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
SUDO="sudo"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# pkg_install <nix-attr> <apt-pkg> <brew-formula> <apk-pkg>
|
||||||
|
pkg_install() {
|
||||||
|
detect_pkgmgr
|
||||||
|
case "$PKGMGR" in
|
||||||
|
nix) nix-env -iA "nixpkgs.$1" ;;
|
||||||
|
apt)
|
||||||
|
if [ -z "$APT_UPDATED" ]; then
|
||||||
|
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get update
|
||||||
|
APT_UPDATED=1
|
||||||
|
fi
|
||||||
|
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2"
|
||||||
|
;;
|
||||||
|
brew) brew install "$3" ;;
|
||||||
|
apk) apk add --no-cache "$4" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
missing() {
|
||||||
|
! command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
if missing git; then pkg_install git git git git; fi
|
||||||
|
if missing make; then pkg_install gnumake make make make; fi
|
||||||
|
if missing go; then pkg_install go golang go go; fi
|
||||||
|
|
||||||
|
# Lint/format tools, pinned via go install (installs into
|
||||||
|
# "$(go env GOPATH)/bin"; ensure that is on your PATH).
|
||||||
|
if missing golangci-lint; then go install "$GOLANGCI_LINT_REF"; fi
|
||||||
|
if missing goimports; then go install "$GOIMPORTS_REF"; fi
|
||||||
|
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
echo "bootstrap complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
14
script/check
Executable file
14
script/check
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/check: run all checks (test, lint, fmt-check). Our own
|
||||||
|
# extension to scripts-to-rule-them-all. Must not modify any files.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
"$SCRIPT_DIR/test"
|
||||||
|
"$SCRIPT_DIR/lint"
|
||||||
|
"$SCRIPT_DIR/fmt-check"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
13
script/cibuild
Executable file
13
script/cibuild
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/cibuild: run the CI build. The Dockerfile runs make check, so
|
||||||
|
# a successful build implies all checks pass.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
docker build .
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
14
script/docker
Executable file
14
script/docker
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/docker: build the Docker image tagged with the project name.
|
||||||
|
# Identical in all repos; the tag comes from script/projectname.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
docker build -t "$("$SCRIPT_DIR/projectname")" .
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
13
script/fmt
Executable file
13
script/fmt
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/fmt: format all files (writes).
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
gofmt -s -w .
|
||||||
|
goimports -w .
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
18
script/fmt-check
Executable file
18
script/fmt-check
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/fmt-check: check formatting (read-only). Same scope as
|
||||||
|
# script/fmt, but fails instead of writing.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
files="$(gofmt -l .)"
|
||||||
|
if [ -n "$files" ]; then
|
||||||
|
echo "gofmt: files not formatted:" >&2
|
||||||
|
echo "$files" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
16
script/install-precommit
Executable file
16
script/install-precommit
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/install-precommit: install the git pre-commit hook that runs
|
||||||
|
# script/precommit. Our own extension to scripts-to-rule-them-all.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
hook=".git/hooks/pre-commit"
|
||||||
|
printf '#!/bin/sh\nset -e\nscript/precommit\n' > "$hook"
|
||||||
|
chmod +x "$hook"
|
||||||
|
echo "pre-commit hook installed: runs script/precommit"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
12
script/lint
Executable file
12
script/lint
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/lint: run the linter.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
golangci-lint run --config .golangci.yml ./...
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
21
script/precommit
Executable file
21
script/precommit
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/precommit: run by the git pre-commit hook; fails the commit if
|
||||||
|
# checks fail. Our own extension to scripts-to-rule-them-all. Go extra:
|
||||||
|
# go mod tidy must be a no-op before the checks run.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
go mod tidy
|
||||||
|
if ! git diff --exit-code -- go.mod go.sum; then
|
||||||
|
echo "precommit: go mod tidy changed go.mod/go.sum;" \
|
||||||
|
"stage the changes and retry" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"$SCRIPT_DIR/check"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
12
script/projectname
Executable file
12
script/projectname
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/projectname: output the name of this project. Our own
|
||||||
|
# extension to scripts-to-rule-them-all. Other scripts that need the
|
||||||
|
# name (e.g. script/docker) call this, so they can stay identical
|
||||||
|
# across all repos.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
main() {
|
||||||
|
echo "attrsum"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
13
script/setup
Executable file
13
script/setup
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/setup: set up the repo for development after a fresh clone:
|
||||||
|
# installs dependencies and the git pre-commit hook.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
"$SCRIPT_DIR/bootstrap"
|
||||||
|
"$SCRIPT_DIR/install-precommit"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
12
script/test
Executable file
12
script/test
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/test: run the test suite.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
go test -v -race -timeout 30s -cover ./...
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user