diff --git a/cmd/meta/main.go b/cmd/meta/main.go index 5a78b3a..b46bed4 100644 --- a/cmd/meta/main.go +++ b/cmd/meta/main.go @@ -2,6 +2,7 @@ package meta import ( "fmt" + "log/slog" "github.com/calypr/forge/metadata" "github.com/spf13/cobra" @@ -19,6 +20,7 @@ var MetaCmd = &cobra.Command{ Example: "forge meta init --remote dev", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + slog.Info("Forge metadata generation started", "profile", args[0], "git_ref", "HEAD", "output", outPath, "remote", gitRemote) err := metadata.CreateMeta(outPath, args[0], gitRemote) if err != nil { return fmt.Errorf("could not create metadata: %w", err) diff --git a/cmd/publish/main.go b/cmd/publish/main.go index 825ad1d..6f5a757 100644 --- a/cmd/publish/main.go +++ b/cmd/publish/main.go @@ -130,7 +130,7 @@ var OutputCmd = &cobra.Command{ } func init() { - PublishCmd.Flags().StringVarP(&publishGitRemote, "remote", "r", "", "git remote name for repository URL lookup (default: dev/origin per git config)") + PublishCmd.Flags().StringVar(&publishGitRemote, "git-remote", "", "Git remote name for the repository URL (default: origin, or the only configured remote)") ListCmd.Flags().StringVarP(&listGitRemote, "remote", "r", "", "git remote name when you want to document repo context") StatusCmd.Flags().StringVarP(&statusGitRemote, "remote", "r", "", "git remote name when you want to document repo context") OutputCmd.Flags().StringVarP(&outputGitRemote, "remote", "r", "", "git remote name when you want to document repo context") diff --git a/docs/commands.md b/docs/commands.md index 7b1bd9c..34dc345 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -14,7 +14,7 @@ Create a metadata upload job for your project. **Usage:** ```bash -forge publish [--remote REMOTE_NAME] +forge publish [--git-remote GIT_REMOTE_NAME] ``` **What it does:** @@ -28,18 +28,19 @@ This is the main command you'll use. It dispatches a Sower job on the CALYPR pla **Example:** ```bash -$ forge publish ghp_abc123def456 +$ forge publish dev ghp_abc123def456 -Using remote: production Uid: job-xyz789-abc123 Name: fhir_import_export Status: Pending ``` **Flags:** -- `--remote`, `-r` - Specify which CALYPR remote to use (default: default_remote) +- `--git-remote` - Git remote containing the repository to publish. Forge uses `origin` when present, otherwise the only configured Git remote. Use this only when the repository has multiple Git remotes and the desired one is not `origin`. + +The project scope comes from the repository's default git-drs remote. Bucket selection is resolved by the server from the Syfon mapping for that scope; Forge does not send a local bucket override. **Common errors:** - `Error: invalid token` - Your GitHub token is expired or missing the `repo` scope -- `Error: repository has no origin` - Push your repository to GitHub first +- `Error: no git remotes configured` - Push your repository to GitHub first - `Error: could not locate remote` - Run this in a git-drs initialized repository **When to use:** After pushing new or updated files via git-drs, run this to make them discoverable on CALYPR. @@ -438,12 +439,12 @@ your-repo/ All commands support these flags: -- `--remote`, `-r` - Specify which git-drs remote to use (dev, staging, production, etc.) - `--help`, `-h` - Show help for a command **Examples:** ```bash -forge publish ghp_token --remote staging +forge publish staging ghp_token +forge publish staging ghp_token --git-remote mirror forge ping --remote dev forge list --remote production ``` diff --git a/docs/getting-started.md b/docs/getting-started.md index b93d21b..08c79f0 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -128,7 +128,7 @@ If you need to push to a different environment: ```bash # Specify which remote to use forge ping --remote dev -forge publish ghp_token --remote staging +forge publish staging ghp_token forge list --remote production ``` diff --git a/go.mod b/go.mod index 2745dd3..f96bafa 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,20 @@ module github.com/calypr/forge -go 1.26.4 +go 1.26.5 require ( github.com/bmeg/golib v0.0.0-20200725232156-e799a31439fc github.com/bmeg/grip v0.0.0-20260315213055-7b57be8d0a5d github.com/bmeg/jsonschema/v6 v6.0.4 github.com/bmeg/jsonschemagraph v0.0.4-0.20251017205345-236d2de9887c - github.com/bytedance/sonic v1.15.1 - github.com/calypr/calypr-cli v0.0.3 + github.com/bytedance/sonic v1.15.2 + github.com/calypr/calypr-cli v0.0.4-0.20260628024636-bb44afe0c576 github.com/calypr/gecko v0.0.0-20260608202450-85aef064d4ae - github.com/calypr/syfon/apigen v0.2.7 - github.com/calypr/syfon/client v0.3.0 + github.com/calypr/git-drs v0.7.10-0.20260727230551-41211e8c328b + github.com/calypr/syfon/apigen v0.2.9-0.20260616212227-547f1e22e1ac + github.com/calypr/syfon/client v0.3.1-0.20260616212227-547f1e22e1ac github.com/cockroachdb/errors v1.11.3 - github.com/go-git/go-git/v5 v5.19.0 + github.com/go-git/go-git/v5 v5.19.1 github.com/google/fhir/go v0.7.5-0.20250925033537-1f5b5b9427ff github.com/google/uuid v1.6.0 github.com/hashicorp/go-multierror v1.1.1 @@ -26,24 +27,24 @@ require ( dario.cat/mergo v1.0.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.4.1 // indirect - github.com/andybalholm/brotli v1.2.1 // indirect + github.com/andybalholm/brotli v1.2.2 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/bytedance/gopkg v0.1.4 // indirect github.com/bytedance/sonic/loader v0.5.1 // indirect - github.com/calypr/syfon v0.3.1-0.20260513001653-406639e16d27 // indirect - github.com/cloudflare/circl v1.6.3 // indirect + github.com/calypr/syfon v0.3.2-0.20260616212227-547f1e22e1ac // indirect + github.com/cloudflare/circl v1.6.4 // indirect github.com/cloudwego/base64x v0.1.7 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect github.com/cockroachdb/redact v1.1.5 // indirect - github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/cyphar/filepath-securejoin v0.7.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/getsentry/sentry-go v0.28.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.9.0 // indirect - github.com/gofiber/fiber/v3 v3.2.0 // indirect - github.com/gofiber/schema v1.7.1 // indirect - github.com/gofiber/utils/v2 v2.0.5 // indirect + github.com/gofiber/fiber/v3 v3.4.0 // indirect + github.com/gofiber/schema v1.8.2 // indirect + github.com/gofiber/utils/v2 v2.2.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect @@ -59,16 +60,16 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.6.0 // indirect - github.com/klauspost/compress v1.18.6 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/klauspost/compress v1.19.0 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect - github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.22 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/oapi-codegen/runtime v1.4.0 // indirect + github.com/oapi-codegen/runtime v1.5.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -81,20 +82,20 @@ require ( github.com/tinylib/msgp v1.6.4 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.71.0 // indirect + github.com/valyala/fasthttp v1.72.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/arch v0.27.0 // indirect - golang.org/x/crypto v0.51.0 // indirect + golang.org/x/arch v0.29.0 // indirect + golang.org/x/crypto v0.54.0 // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 // indirect - google.golang.org/grpc v1.81.1 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.40.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260713224248-f5fc221cf8c4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260713224248-f5fc221cf8c4 // indirect + google.golang.org/grpc v1.82.0 // indirect google.golang.org/protobuf v1.36.11 // indirect - gopkg.in/ini.v1 v1.67.1 // indirect + gopkg.in/ini.v1 v1.67.3 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 876dbf9..dfc1a8c 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= -github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM= +github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= @@ -32,27 +32,29 @@ github.com/bmeg/jsonschemagraph v0.0.4-0.20251017205345-236d2de9887c h1:J1EhcEmL github.com/bmeg/jsonschemagraph v0.0.4-0.20251017205345-236d2de9887c/go.mod h1:Ve7jAQhYAMkHUiko99+2CwqXI4Ur0ty/ai8Tfa2ONz4= github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= -github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw= -github.com/bytedance/sonic v1.15.1/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA= +github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo= +github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA= github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI= github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= -github.com/calypr/calypr-cli v0.0.3 h1:Nn9QFziLshI0hYKxzbFb0QO9NrwYSmhbLeLa3Roo58o= -github.com/calypr/calypr-cli v0.0.3/go.mod h1:9r9ffAA6QUCjJlrdJIlu0jPmS7rwv977Uau2xipVkaM= +github.com/calypr/calypr-cli v0.0.4-0.20260628024636-bb44afe0c576 h1:ToXXURxRa8Cev0WjAvwDoOFYof5TDbrOjV+gNBz7v60= +github.com/calypr/calypr-cli v0.0.4-0.20260628024636-bb44afe0c576/go.mod h1:9r9ffAA6QUCjJlrdJIlu0jPmS7rwv977Uau2xipVkaM= github.com/calypr/gecko v0.0.0-20260608202450-85aef064d4ae h1:ua2aNZwA3xL170QmJ7XgOLoHH0UIRqllKhlKGGcwQaM= github.com/calypr/gecko v0.0.0-20260608202450-85aef064d4ae/go.mod h1:zR+2G3lrgcUSijbCGCK3U/Lah6LAtt04mNBzToe0MxQ= -github.com/calypr/syfon v0.3.1-0.20260513001653-406639e16d27 h1:Rid6vVH+kfic0g/50A5/cM7lidaLe+hOF71isno43AQ= -github.com/calypr/syfon v0.3.1-0.20260513001653-406639e16d27/go.mod h1:N1JGIHUuNBuWWstzBNj6DiI6Ol6WUm2uh/hGtbzzdy8= -github.com/calypr/syfon/apigen v0.2.7 h1:h5ZcxoLFPuLXt+8EUWICbKQgE/MMu2jym4oUshV3m8k= -github.com/calypr/syfon/apigen v0.2.7/go.mod h1:VrRZ2A17YV91Zsm7CF/u1/Z+DfcZAk8Q4Pk1xklb5xU= -github.com/calypr/syfon/client v0.3.0 h1:iSXFf3+35T7ClK7JnCUv8kdaqBo1YLerw4bvqhZggog= -github.com/calypr/syfon/client v0.3.0/go.mod h1:XMky3qJ6zyOjlBzK2uqN1oSTJOTfD6KT1KPTFc+NPzQ= +github.com/calypr/git-drs v0.7.10-0.20260727230551-41211e8c328b h1:kHJ/K3yfi47Bq2WMrBeinF4vhSa1snahkxgdzRedBZ4= +github.com/calypr/git-drs v0.7.10-0.20260727230551-41211e8c328b/go.mod h1:0Nl6tf17uvZ875w0i20L+zHDiP3rdGGW3B6BsI2DmMI= +github.com/calypr/syfon v0.3.2-0.20260616212227-547f1e22e1ac h1:pNwp1J6K3BP6PieEzQrpLyt5xSFm2H4jAJDD2PRoV6A= +github.com/calypr/syfon v0.3.2-0.20260616212227-547f1e22e1ac/go.mod h1:MNfdZjCOeg7dMQU3P19gh8h0tezh4dCe5uYvCC2qu9U= +github.com/calypr/syfon/apigen v0.2.9-0.20260616212227-547f1e22e1ac h1:Oidz0ogmUHFD/9TkW5KsrSdCcuMrISawzIumOwtONss= +github.com/calypr/syfon/apigen v0.2.9-0.20260616212227-547f1e22e1ac/go.mod h1:/NCUo74xusygxyf9zpMFcE9TfGFxh6abFsplVyMArkw= +github.com/calypr/syfon/client v0.3.1-0.20260616212227-547f1e22e1ac h1:6pywyLmzjc17tyw2GYpO0SpaFbUu11N+qyUYEBWg534= +github.com/calypr/syfon/client v0.3.1-0.20260616212227-547f1e22e1ac/go.mod h1:96He/A5DYwkeQM5XuzYgduJnP8gbat8lMYELIZcC0W0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U= +github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY= github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI= github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -65,8 +67,8 @@ github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creachadair/staticfile v0.1.3/go.mod h1:a3qySzCIXEprDGxk6tSxSI+dBBdLzqeBOMhZ+o2d3pM= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= -github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE= +github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -83,8 +85,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k= @@ -99,8 +101,8 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc= -github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= +github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -108,12 +110,12 @@ github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gofiber/fiber/v3 v3.2.0 h1:g9+09D320foINPpCnR3ibQ5oBEFHjAWRRfDG1te54u8= -github.com/gofiber/fiber/v3 v3.2.0/go.mod h1:FHOsc2Db7HhHpsE62QAaJlXVV1pNkbZEptZ4jtti7m4= -github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= -github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= -github.com/gofiber/utils/v2 v2.0.5 h1:IMXoI2A5Dao/aMMBURTNxnhbtQO4kUwUFOgcwFSIjLU= -github.com/gofiber/utils/v2 v2.0.5/go.mod h1:FwwopfzwAQsoXLCHhOT24eH2jQfBgrrra9S5p0+luxg= +github.com/gofiber/fiber/v3 v3.4.0 h1:F0aND4vwZF7dR7cbvSwFQQEpBU902XHKWxrLsFBkVqw= +github.com/gofiber/fiber/v3 v3.4.0/go.mod h1:nAhJfdxUIJJph2tPWPmqWf8QDIN2iiqQiQf3lENZpdk= +github.com/gofiber/schema v1.8.2 h1:wq+LO2xEGlsqma/8Akp9PUebQ6vcsYmF0xYQ4F2ijvU= +github.com/gofiber/schema v1.8.2/go.mod h1:iyAMJztdyky7Pk2U7bwUP3EHjzMqUNjZ/hglE0nYO/g= +github.com/gofiber/utils/v2 v2.2.0 h1:YSSmCzQponq/f9uSOg2HtXC5qK1Dmor0o6DqaQVz8GE= +github.com/gofiber/utils/v2 v2.2.0/go.mod h1:Ieopk6sQh7rbhQ12aBNCJtJuG0gxAg0nz63sFCrrOmE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= @@ -165,10 +167,10 @@ github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRg github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= -github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ= +github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -179,8 +181,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= +github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -190,8 +192,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= -github.com/oapi-codegen/runtime v1.4.0 h1:KLOSFOp7UzkbS7Cs1ms6NBEKYr0WmH2wZG0KKbd2er4= -github.com/oapi-codegen/runtime v1.4.0/go.mod h1:5sw5fxCDmnOzKNYmkVNF8d34kyUeejJEY8HNT2WaPec= +github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= +github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= +github.com/oapi-codegen/runtime v1.5.0 h1:aiil4QnH+eiWYSO60eaYZ4aur7sJH3rz6BvT5EBFnxc= +github.com/oapi-codegen/runtime v1.5.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= @@ -219,8 +223,8 @@ github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e h1:zWKUYT07mGmVBH+ github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= -github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= +github.com/shamaton/msgpack/v3 v3.2.0 h1:1q2Ms+MWmuRju+PuDMSFDB7p7621npeX4zprJN5Zck8= +github.com/shamaton/msgpack/v3 v3.2.0/go.mod h1:sgBYvEiyz8JR1NC3yGRoPVME9xXovpnh3l/plW1nfRo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= @@ -254,8 +258,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k= -github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA= +github.com/valyala/fasthttp v1.72.0 h1:R7kYdoWhn1ye1fVpP+cDHDJwYm3NkwLliwgzJ/Abg7M= +github.com/valyala/fasthttp v1.72.0/go.mod h1:zsbLTYqcpIktdQytlVBwIjY9La5d6bs990nBxWg8efk= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -266,29 +270,29 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/arch v0.27.0 h1:0WNVcR8u9yFz8j5FvdHpgwNp3FS5U4guYdzHwEiGjoU= -golang.org/x/arch v0.27.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +golang.org/x/arch v0.29.0 h1:8sSET5wB0+exBm0FGmOtdHMqjlRdV2DRD3/IV6OZgho= +golang.org/x/arch v0.29.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= @@ -307,8 +311,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -327,16 +331,16 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -357,17 +361,17 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94 h1:DddG61lE5LkX6144z22i0gma9BMBs5aZ9B8lZLobxyw= -google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:1dCETSCY2YKZNXQE3h4fun3TYwF5p8jejRKZgfWAgAY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 h1:eZCjr/aAF8c5ccm5pb6T4EXgIei5MlAAPWPJk+5ArfY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260713224248-f5fc221cf8c4 h1:lI0NbdWVmT6lOJJNDd7vyeTdfxP/7ouCLSJUKNNXa0k= +google.golang.org/genproto/googleapis/api v0.0.0-20260713224248-f5fc221cf8c4/go.mod h1:WRrQ7/7N19PypuT0fxLOL5Lq0waoiRri4FbtHDEKrGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260713224248-f5fc221cf8c4 h1:7RtFDizMtT9eZzHzKxifoMGfcDBBy+LYZlgfg24ZmOM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260713224248-f5fc221cf8c4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= +google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -375,8 +379,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= -gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= +gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw= +gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= diff --git a/metadata/meta.go b/metadata/meta.go index 82443e0..1880220 100644 --- a/metadata/meta.go +++ b/metadata/meta.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "log" + "log/slog" "net/http" "os" "path/filepath" @@ -53,7 +54,9 @@ type MetaObject struct { ControlledAccess []string } -func CreateMeta(outPath string, profileName string, gitRemoteName string) error { +// legacyCreateMeta is retained only for a future explicit migration command. +// Normal imports use CreateMeta in reconcile.go and never perform DID/path repair. +func legacyCreateMeta(outPath string, profileName string, gitRemoteName string) error { sc, closer, err := client.NewGen3Client(profileName, g3client.WithClients(g3client.SyfonClient)) if err != nil { return err @@ -476,6 +479,7 @@ func listProjectObjectsByHashes(ctx context.Context, sc *client.ProfileClient, o if end > len(hashes) { end = len(hashes) } + slog.Info("Syfon checksum lookup batch", "start", start+1, "end", end, "total", len(hashes)) records, err := bulkHashRecords(ctx, sc, hashes[start:end]) if err != nil { return nil, fmt.Errorf("failed to bulk lookup records by hash: %w", err) @@ -542,7 +546,7 @@ func listProjectObjectsByPaths(ctx context.Context, sc *client.ProfileClient, or resp, err := sc.Gen3.SyfonClient().Index().List(ctx, syservices.ListRecordsOptions{ Organization: organization, ProjectID: projectID, - Path: candidate, + URL: candidate, Limit: 25, }) if err != nil { diff --git a/metadata/meta_test.go b/metadata/meta_test.go index a814ba0..e85d0a5 100644 --- a/metadata/meta_test.go +++ b/metadata/meta_test.go @@ -15,6 +15,23 @@ import ( cprb "github.com/google/fhir/go/proto/google/fhir/proto/r5/core/resources/bundle_and_contained_resource_go_proto" ) +func TestAppendDocumentReferencesPreservesAuthoredRows(t *testing.T) { + path := filepath.Join(t.TempDir(), DOCUMENT_RESOURCE+NDJSON_EXT) + authored := []byte(`{"resourceType":"DocumentReference", "id":"authored", "identifier":[]}`) + generated := []byte(`{"resourceType":"DocumentReference","id":"generated"}`) + if err := appendDocumentReferences(path, [][]byte{authored}, [][]byte{generated}); err != nil { + t.Fatalf("appendDocumentReferences failed: %v", err) + } + contents, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + want := string(authored) + "\n" + string(generated) + "\n" + if string(contents) != want { + t.Fatalf("authored row changed:\nwant %q\n got %q", want, string(contents)) + } +} + func TestProcessProjectRecordsPreservesExistingAndAddsMissing(t *testing.T) { tmpDir := t.TempDir() diff --git a/metadata/reconcile.go b/metadata/reconcile.go new file mode 100644 index 0000000..54ae5f1 --- /dev/null +++ b/metadata/reconcile.go @@ -0,0 +1,371 @@ +package metadata + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "log" + "log/slog" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/calypr/calypr-cli/g3client" + "github.com/calypr/forge/client" + "github.com/calypr/forge/utils/remoteutil" + gitinventory "github.com/calypr/git-drs/inventory" + fver "github.com/google/fhir/go/fhirversion" + "github.com/google/fhir/go/jsonformat" + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r5/core/datatypes_go_proto" + drpb "github.com/google/fhir/go/proto/google/fhir/proto/r5/core/resources/document_reference_go_proto" +) + +const fileSHA256System = "https://humantumoratlas.org/FILE_SHA256" + +// ReconcileOptions defines one immutable Git snapshot and its authored metadata. +type ReconcileOptions struct { + RepositoryRoot string + GitRef string + FHIRDirectory string + ProfileName string + GitRemoteName string +} + +// ReconcileReport is emitted by normal ETL so operators can see exactly what was +// matched, generated, retained, or rejected. +type ReconcileReport struct { + GitPointers int + AuthoredRows int + MatchedRows int + GeneratedRows int + AuthoredDRSIDsSynced int + MetadataOnlySHA256 []string + AuthoredRowsWithoutSHA int + MissingSyfonRecords []ReconcileIssue + AmbiguousSyfonRecords []ReconcileIssue +} + +// ReconcileIssue identifies a Git checksum that could not be mapped to exactly +// one Syfon record in the configured project scope. +type ReconcileIssue struct { + SHA256 string + Paths []string + SyfonRecords int +} + +// CreateMeta preserves the historical Forge entrypoint while switching normal +// imports to Git-SHA-driven reconciliation of the current checkout. +func CreateMeta(outPath string, profileName string, gitRemoteName string) error { + report, err := ReconcileGitPointers(context.Background(), ReconcileOptions{ + RepositoryRoot: ".", + GitRef: "HEAD", + FHIRDirectory: outPath, + ProfileName: profileName, + GitRemoteName: gitRemoteName, + }) + if err != nil { + return err + } + slog.Info("Forge metadata generation complete", + "git_pointers", report.GitPointers, + "authored_rows", report.AuthoredRows, + "matched_rows", report.MatchedRows, + "generated_rows", report.GeneratedRows, + "missing_syfon", len(report.MissingSyfonRecords), + "metadata_only", len(report.MetadataOnlySHA256), + ) + for i, issue := range report.MissingSyfonRecords { + slog.Warn("Git pointer has no scoped Syfon record; metadata was not generated", + "item", fmt.Sprintf("%d/%d", i+1, len(report.MissingSyfonRecords)), + "sha256", issue.SHA256, + "paths", strings.Join(issue.Paths, ", "), + ) + } + return nil +} + +// ReconcileGitPointers performs the normal metadata import reconciliation. Git +// pointers drive the inventory; Syfon is consulted only for those SHA256 values. +// Existing DocumentReference rows are retained verbatim and only missing rows are +// appended. +func ReconcileGitPointers(ctx context.Context, options ReconcileOptions) (ReconcileReport, error) { + started := time.Now() + var report ReconcileReport + if strings.TrimSpace(options.RepositoryRoot) == "" { + return report, fmt.Errorf("repository root is required") + } + if strings.TrimSpace(options.FHIRDirectory) == "" { + return report, fmt.Errorf("FHIR directory is required") + } + + phaseStarted := time.Now() + slog.Info("Scanning Git snapshot for DRS pointers", "repository", options.RepositoryRoot, "git_ref", options.GitRef) + pointerRecords, err := gitinventory.List(ctx, gitinventory.Options{ + RepositoryRoot: options.RepositoryRoot, + Ref: options.GitRef, + ExcludePrefixes: []string{META_DIR, "CONFIG"}, + }) + if err != nil { + return report, err + } + report.GitPointers = len(pointerRecords) + pointers := make(map[string][]gitinventory.Pointer) + for _, pointer := range pointerRecords { + pointers[pointer.SHA256] = append(pointers[pointer.SHA256], pointer) + } + slog.Info("Git pointer scan complete", "pointers", len(pointerRecords), "unique_sha256", len(pointers), "elapsed", time.Since(phaseStarted)) + + phaseStarted = time.Now() + slog.Info("Connecting to Syfon", "profile", options.ProfileName) + sc, closer, err := client.NewGen3Client(options.ProfileName, g3client.WithClients(g3client.SyfonClient)) + if err != nil { + return report, err + } + defer closer() + slog.Info("Syfon client ready", "endpoint", sc.Credential().APIEndpoint, "elapsed", time.Since(phaseStarted)) + + repoRemote, err := remoteutil.LoadRemoteOrDefault(options.GitRemoteName) + if err != nil { + return report, err + } + slog.Info("Repository scope resolved", "organization", repoRemote.Organization, "project", repoRemote.ProjectID) + + marshaller, err := jsonformat.NewMarshaller(false, "", "", fver.R5) + if err != nil { + return report, err + } + unmarshaller, err := jsonformat.NewUnmarshallerWithoutValidation("America/Los_Angeles", fver.R5) + if err != nil { + return report, err + } + + if err := os.MkdirAll(options.FHIRDirectory, 0o755); err != nil { + return report, fmt.Errorf("create FHIR directory: %w", err) + } + phaseStarted = time.Now() + slog.Info("Preparing authored FHIR metadata", "directory", options.FHIRDirectory) + researchStudyID, err := getResearchStudy(options.FHIRDirectory, repoRemote.ProjectID, sc.Credential().APIEndpoint, marshaller, unmarshaller) + if err != nil { + return report, err + } + + docRefPath := filepath.Join(options.FHIRDirectory, DOCUMENT_RESOURCE+NDJSON_EXT) + authored, authoredSHA256, noSHA, err := readAuthoredDocumentReferences(docRefPath, unmarshaller) + if err != nil { + return report, err + } + report.AuthoredRows = len(authored) + report.AuthoredRowsWithoutSHA = noSHA + slog.Info("Authored FHIR metadata ready", "document_references", len(authored), "without_sha256", noSHA, "elapsed", time.Since(phaseStarted)) + + hashes := make([]string, 0, len(pointers)) + for sha := range pointers { + hashes = append(hashes, sha) + } + sort.Strings(hashes) + phaseStarted = time.Now() + slog.Info("Looking up Git checksums in Syfon", "sha256", len(hashes), "organization", repoRemote.Organization, "project", repoRemote.ProjectID) + objects, err := listProjectObjectsByHashes(ctx, sc, repoRemote.Organization, repoRemote.ProjectID, hashes) + if err != nil { + return report, err + } + slog.Info("Syfon checksum lookup complete", "sha256", len(hashes), "scoped_records", len(objects), "elapsed", time.Since(phaseStarted)) + objectsBySHA := make(map[string][]MetaObject, len(objects)) + for _, object := range objects { + sha := strings.ToLower(checksumValue(object.Checksums, "sha-256", "sha256")) + if sha != "" { + objectsBySHA[sha] = append(objectsBySHA[sha], object) + } + } + + generated := make([][]byte, 0) + for _, sha := range hashes { + if _, exists := authoredSHA256[sha]; exists { + report.MatchedRows++ + continue + } + candidates := objectsBySHA[sha] + switch len(candidates) { + case 0: + report.MissingSyfonRecords = append(report.MissingSyfonRecords, reconcileIssue(sha, pointers[sha], 0)) + continue + case 1: + object := candidates[0] + pointer := pointers[sha][0] + object.Size = pointer.Size + if strings.TrimSpace(object.Name) == "" { + object.Name = filepath.Base(pointer.Path) + } + row := templateDocRef(&object, sc.Credential().APIEndpoint, repoRemote.ProjectID, researchStudyID) + addSHA256Identifier(row.GetDocumentReference(), sha) + encoded, err := marshaller.Marshal(row) + if err != nil { + return report, fmt.Errorf("serialize generated DocumentReference for %s: %w", sha, err) + } + generated = append(generated, encoded) + default: + report.AmbiguousSyfonRecords = append(report.AmbiguousSyfonRecords, reconcileIssue(sha, pointers[sha], len(candidates))) + } + } + if len(report.AmbiguousSyfonRecords) > 0 { + values := make([]string, 0, len(report.AmbiguousSyfonRecords)) + for _, issue := range report.AmbiguousSyfonRecords { + values = append(values, fmt.Sprintf("%s (%d Syfon records)", issueDescription(issue), issue.SyfonRecords)) + } + return report, fmt.Errorf("Git/Syfon reconciliation failed: %d Git SHA256 values are ambiguous (%s)", len(values), summarizeValues(values)) + } + + for sha := range authoredSHA256 { + if _, exists := pointers[sha]; !exists { + report.MetadataOnlySHA256 = append(report.MetadataOnlySHA256, sha) + } + } + sort.Strings(report.MetadataOnlySHA256) + if err := appendDocumentReferences(docRefPath, authored, generated); err != nil { + return report, err + } + // Authored metadata often carries a path or a legacy identifier in its + // first DocumentReference identifier. The Explorer's file-action column + // consumes that primary identifier as the Syfon DRS object ID. Reuse the + // SHA256-scoped Syfon result set above to synchronize only Git-matched rows; + // do not fall back to an unbounded DID/path scan across the project. + repaired, err := repairDocumentReferenceIdentifiersBySHA( + objects, + options.FHIRDirectory, + sc.Credential().APIEndpoint, + repoRemote.ProjectID, + ) + if err != nil { + return report, fmt.Errorf("synchronize authored DocumentReference DRS identifiers: %w", err) + } + report.AuthoredDRSIDsSynced = repaired + report.GeneratedRows = len(generated) + slog.Info("Git/Syfon reconciliation complete", "generated", report.GeneratedRows, "matched", report.MatchedRows, "missing", len(report.MissingSyfonRecords), "repaired", report.AuthoredDRSIDsSynced, "elapsed", time.Since(started)) + if repaired > 0 { + log.Printf("Git/Syfon reconciliation: synchronized %d authored DocumentReference primary DRS identifiers by SHA256", repaired) + } + if len(report.MetadataOnlySHA256) > 0 { + log.Printf("WARNING: retained %d authored DocumentReference SHA256 values not present in Git: %s", len(report.MetadataOnlySHA256), summarizeValues(report.MetadataOnlySHA256)) + } + if report.AuthoredRowsWithoutSHA > 0 { + log.Printf("WARNING: retained %d authored DocumentReference rows without a FILE_SHA256 identifier", report.AuthoredRowsWithoutSHA) + } + return report, nil +} + +func reconcileIssue(sha string, pointers []gitinventory.Pointer, syfonRecords int) ReconcileIssue { + paths := make([]string, 0, len(pointers)) + seen := make(map[string]struct{}, len(pointers)) + for _, pointer := range pointers { + path := strings.TrimSpace(pointer.Path) + if path == "" { + continue + } + if _, ok := seen[path]; ok { + continue + } + seen[path] = struct{}{} + paths = append(paths, path) + } + sort.Strings(paths) + return ReconcileIssue{SHA256: sha, Paths: paths, SyfonRecords: syfonRecords} +} + +func issueDescription(issue ReconcileIssue) string { + if len(issue.Paths) == 0 { + return issue.SHA256 + } + return fmt.Sprintf("%s (%s)", issue.SHA256, strings.Join(issue.Paths, ", ")) +} + +// DiscoverGitPointers reads pointer files from a checkout. META and CONFIG are +// intentionally excluded: they are ETL inputs, not data inventory. +func readAuthoredDocumentReferences(path string, unmarshaller *jsonformat.Unmarshaller) ([][]byte, map[string]struct{}, int, error) { + rows := make([][]byte, 0) + sha256s := make(map[string]struct{}) + withoutSHA := 0 + file, err := os.Open(path) + if os.IsNotExist(err) { + return rows, sha256s, withoutSHA, nil + } + if err != nil { + return nil, nil, 0, err + } + defer file.Close() + scanner := bufio.NewScanner(file) + scanner.Buffer(make([]byte, 64*1024), 10*1024*1024) + for scanner.Scan() { + line := append([]byte(nil), scanner.Bytes()...) + if len(strings.TrimSpace(string(line))) == 0 { + continue + } + rows = append(rows, line) + if !json.Valid(line) { + withoutSHA++ + continue + } + cr, err := parseDocumentReferenceLine(line, unmarshaller) + if err != nil || cr.GetDocumentReference() == nil { + withoutSHA++ + continue + } + sha := strings.ToLower(docRefSHA256(cr.GetDocumentReference())) + if sha == "" { + withoutSHA++ + continue + } + sha256s[sha] = struct{}{} + } + if err := scanner.Err(); err != nil { + return nil, nil, 0, err + } + return rows, sha256s, withoutSHA, nil +} + +func appendDocumentReferences(path string, authored, generated [][]byte) error { + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + for _, row := range append(authored, generated...) { + if _, err := file.Write(row); err != nil { + return err + } + if _, err := file.Write([]byte("\n")); err != nil { + return err + } + } + return nil +} + +func addSHA256Identifier(docRef *drpb.DocumentReference, sha string) { + docRef.Identifier = append(docRef.Identifier, &dtpb.Identifier{ + Use: &dtpb.Identifier_UseCode{}, + System: &dtpb.Uri{Value: fileSHA256System}, + Value: &dtpb.String{Value: sha}, + }) +} + +func pointerDescription(sha string, pointers []gitinventory.Pointer) string { + paths := make([]string, 0, len(pointers)) + for _, pointer := range pointers { + paths = append(paths, pointer.Path) + } + sort.Strings(paths) + return fmt.Sprintf("%s (%s)", sha, summarizeValues(paths)) +} + +func summarizeValues(values []string) string { + if len(values) == 0 { + return "none" + } + const max = 5 + if len(values) <= max { + return strings.Join(values, ", ") + } + return strings.Join(values[:max], ", ") + fmt.Sprintf(" … and %d more", len(values)-max) +} diff --git a/metadata/reconcile_test.go b/metadata/reconcile_test.go new file mode 100644 index 0000000..56d8b4c --- /dev/null +++ b/metadata/reconcile_test.go @@ -0,0 +1,22 @@ +package metadata + +import ( + "reflect" + "testing" + + gitinventory "github.com/calypr/git-drs/inventory" +) + +func TestReconcileIssueSortsAndDeduplicatesPaths(t *testing.T) { + issue := reconcileIssue("abc", []gitinventory.Pointer{ + {Path: "z/file.txt"}, + {Path: "a/file.txt"}, + {Path: "z/file.txt"}, + }, 0) + if issue.SHA256 != "abc" || issue.SyfonRecords != 0 { + t.Fatalf("unexpected issue: %+v", issue) + } + if want := []string{"a/file.txt", "z/file.txt"}; !reflect.DeepEqual(issue.Paths, want) { + t.Fatalf("paths = %#v, want %#v", issue.Paths, want) + } +} diff --git a/metadata/skeleton.go b/metadata/skeleton.go index e39adac..828d246 100644 --- a/metadata/skeleton.go +++ b/metadata/skeleton.go @@ -47,9 +47,16 @@ func CreateResourceReference(resourceId string) *dtpb.Reference { func templateDocRef(obj *MetaObject, endpoint string, project string, rSID string) *cprb.ContainedResource { baseEndpoint := normalizeEndpoint(endpoint) name := obj.Name + identity := "name:" + name + if objectID := strings.TrimSpace(obj.ID); objectID != "" { + identity = "drs:" + objectID + } + if sha256 := strings.ToLower(strings.TrimSpace(checksumValue(obj.Checksums, "sha-256", "sha256"))); sha256 != "" { + identity = "sha256:" + sha256 + } id := uuid.NewSHA1( uuid.NewSHA1(uuid.NameSpaceDNS, []byte(endpoint)), - fmt.Appendf(nil, "%s/%s", project, name), + fmt.Appendf(nil, "%s/%s", project, identity), ).String() var extensions []*dtpb.Extension diff --git a/metadata/skeleton_test.go b/metadata/skeleton_test.go index 49dedbf..785971f 100644 --- a/metadata/skeleton_test.go +++ b/metadata/skeleton_test.go @@ -101,6 +101,39 @@ func TestTemplateDocRef(t *testing.T) { } } +func TestTemplateDocRefIdentityUsesProjectScopedSHA256(t *testing.T) { + first := &MetaObject{ + ID: "drs-a", + Name: "shared-name.json", + Checksums: map[string]string{"sha-256": "AAAAAAAA"}, + } + second := &MetaObject{ + ID: "drs-b", + Name: "shared-name.json", + Checksums: map[string]string{"sha256": "bbbbbbbb"}, + } + renamed := &MetaObject{ + ID: "drs-c", + Name: "renamed.json", + Checksums: map[string]string{"sha-256": "aaaaaaaa"}, + } + + firstID := templateDocRef(first, "localhost", "test-proj", "rs-1").GetDocumentReference().GetId().GetValue() + secondID := templateDocRef(second, "localhost", "test-proj", "rs-1").GetDocumentReference().GetId().GetValue() + renamedID := templateDocRef(renamed, "localhost", "test-proj", "rs-1").GetDocumentReference().GetId().GetValue() + otherProjectID := templateDocRef(first, "localhost", "other-proj", "rs-1").GetDocumentReference().GetId().GetValue() + + if firstID == secondID { + t.Fatal("different SHA256 values with the same basename produced the same DocumentReference ID") + } + if firstID != renamedID { + t.Fatal("the same SHA256 produced different DocumentReference IDs after a filename change") + } + if firstID == otherProjectID { + t.Fatal("the same SHA256 produced the same DocumentReference ID across projects") + } +} + func TestTemplateGitHubDocRef(t *testing.T) { res := templateGitHubDocRef("README.md", 500, "localhost", "test-proj", "rs-1", "github.com/user/repo", "abcd123") dr := res.GetDocumentReference() diff --git a/publish/publish.go b/publish/publish.go index 5495ad9..72c706e 100644 --- a/publish/publish.go +++ b/publish/publish.go @@ -48,6 +48,9 @@ func RunEmpty(projectId string, profileName string) (*sower.StatusResp, error) { } func RunPublish(token string, profileName string, gitRemoteName string) (*sower.StatusResp, error) { + // Project scope comes from the repository's configured git-drs default remote. + // A Git remote is only used to find the repository URL to clone; it must not + // alter the Gen3 project or storage mapping. repoRemoteConfig, err := remoteutil.LoadRemoteOrDefault("") if err != nil { return nil, err @@ -117,7 +120,6 @@ func RunPublish(token string, profileName string, gitRemoteName string) (*sower. } dispatchArgs := &sower.DispatchArgs{ - BucketName: repoRemoteConfig.BucketName, ProjectId: repoRemoteConfig.DispatchProjectID(), APIEndpoint: sc.Credential().APIEndpoint, Profile: sc.Credential().Profile, diff --git a/utils/gitutil/gitutil.go b/utils/gitutil/gitutil.go index 018a662..ea8a841 100644 --- a/utils/gitutil/gitutil.go +++ b/utils/gitutil/gitutil.go @@ -50,7 +50,7 @@ func ResolveGitRemoteName(repo *git.Repository, requested string) (string, error names = append(names, remote.Config().Name) } sort.Strings(names) - return "", fmt.Errorf("multiple git remotes found (%s); specify one with --remote", strings.Join(names, ", ")) + return "", fmt.Errorf("multiple git remotes found (%s); specify one with --git-remote", strings.Join(names, ", ")) } func GetLastLocalCommit(repo *git.Repository) (plumbing.Hash, error) { diff --git a/utils/gitutil/gitutil_test.go b/utils/gitutil/gitutil_test.go index 3e6e6da..db25679 100644 --- a/utils/gitutil/gitutil_test.go +++ b/utils/gitutil/gitutil_test.go @@ -1,9 +1,53 @@ package gitutil import ( + "strings" "testing" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" ) +func TestResolveGitRemoteName(t *testing.T) { + newRepo := func(t *testing.T, remoteNames ...string) *git.Repository { + t.Helper() + repo, err := git.PlainInit(t.TempDir(), false) + if err != nil { + t.Fatalf("init repository: %v", err) + } + for _, name := range remoteNames { + if _, err := repo.CreateRemote(&config.RemoteConfig{ + Name: name, + URLs: []string{"https://github.com/example/" + name + ".git"}, + }); err != nil { + t.Fatalf("create remote %q: %v", name, err) + } + } + return repo + } + + t.Run("prefers origin", func(t *testing.T) { + got, err := ResolveGitRemoteName(newRepo(t, "mirror", "origin"), "") + if err != nil || got != "origin" { + t.Fatalf("ResolveGitRemoteName() = %q, %v; want origin, nil", got, err) + } + }) + + t.Run("uses the only remote", func(t *testing.T) { + got, err := ResolveGitRemoteName(newRepo(t, "upstream"), "") + if err != nil || got != "upstream" { + t.Fatalf("ResolveGitRemoteName() = %q, %v; want upstream, nil", got, err) + } + }) + + t.Run("requires an override for multiple non-origin remotes", func(t *testing.T) { + _, err := ResolveGitRemoteName(newRepo(t, "mirror", "upstream"), "") + if err == nil || !strings.Contains(err.Error(), "specify one with --git-remote") { + t.Fatalf("ResolveGitRemoteName() error = %v; want a remote-selection error", err) + } + }) +} + func TestTrimGitURLPrefix(t *testing.T) { tests := []struct { name string diff --git a/utils/remoteutil/remoteutil.go b/utils/remoteutil/remoteutil.go index 1fb2d48..89eb65b 100644 --- a/utils/remoteutil/remoteutil.go +++ b/utils/remoteutil/remoteutil.go @@ -33,6 +33,9 @@ func (r RemoteConfig) DispatchProjectID() string { if organization == "" { return project } + if strings.HasPrefix(project, organization+"-") { + return project + } return organization + "-" + project } diff --git a/utils/remoteutil/remoteutil_test.go b/utils/remoteutil/remoteutil_test.go index 039b784..fb27f6f 100644 --- a/utils/remoteutil/remoteutil_test.go +++ b/utils/remoteutil/remoteutil_test.go @@ -29,6 +29,29 @@ func TestGetRemoteOrDefault(t *testing.T) { } } +func TestDispatchProjectID(t *testing.T) { + tests := []struct { + name string + organization string + project string + want string + }{ + {name: "qualified from scope", organization: "HTAN_INT", project: "BForePC", want: "HTAN_INT-BForePC"}, + {name: "already qualified", organization: "HTAN_INT", project: "HTAN_INT-BForePC", want: "HTAN_INT-BForePC"}, + {name: "equal organization and project", organization: "gdc_mirror", project: "gdc_mirror", want: "gdc_mirror-gdc_mirror"}, + {name: "no organization", project: "program-project", want: "program-project"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + remote := RemoteConfig{Organization: tt.organization, ProjectID: tt.project} + if got := remote.DispatchProjectID(); got != tt.want { + t.Fatalf("DispatchProjectID() = %q, want %q", got, tt.want) + } + }) + } +} + func TestLoadConfig(t *testing.T) { dir := t.TempDir() _, err := git.PlainInit(dir, false) @@ -45,6 +68,7 @@ func TestLoadConfig(t *testing.T) { for _, kv := range [][2]string{ {defaultRemoteKey, "origin"}, {"drs.remote.origin.project", "proj-a"}, + {"drs.remote.origin.organization", "org-a"}, {"drs.remote.origin.endpoint", "https://example.org"}, {"drs.remote.origin.bucket", "bucket-a"}, } { @@ -64,6 +88,9 @@ func TestLoadConfig(t *testing.T) { if loaded.Remotes["origin"].ProjectID != "proj-a" { t.Fatalf("unexpected project: %+v", loaded.Remotes["origin"]) } + if loaded.Remotes["origin"].Organization != "org-a" { + t.Fatalf("unexpected organization: %+v", loaded.Remotes["origin"]) + } } func TestMissingDefaultRemote(t *testing.T) {