diff --git a/go.mod b/go.mod index 085a326b7..8f2145c9e 100644 --- a/go.mod +++ b/go.mod @@ -28,11 +28,11 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.8.0 github.com/IBM/go-sdk-core/v5 v5.23.2 github.com/aws/amazon-ec2-instance-selector/v3 v3.1.3 - github.com/aws/aws-sdk-go-v2 v1.43.2 - github.com/aws/aws-sdk-go-v2/config v1.32.31 - github.com/aws/aws-sdk-go-v2/service/ec2 v1.318.0 - github.com/aws/aws-sdk-go-v2/service/s3 v1.106.2 - github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 + github.com/aws/aws-sdk-go-v2 v1.43.3 + github.com/aws/aws-sdk-go-v2/config v1.32.34 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.318.1 + github.com/aws/aws-sdk-go-v2/service/s3 v1.106.3 + github.com/aws/aws-sdk-go-v2/service/sts v1.45.3 github.com/pulumi/pulumi-aws-native/sdk v1.73.1 github.com/pulumi/pulumi-aws/sdk/v7 v7.39.0 github.com/pulumi/pulumi-awsx/sdk/v3 v3.8.0 @@ -43,7 +43,7 @@ require ( github.com/pulumi/pulumi-azure-native-sdk/network/v3 v3.24.0 github.com/pulumi/pulumi-azure-native-sdk/resources/v3 v3.24.0 github.com/pulumi/pulumi-azure-native-sdk/storage/v3 v3.24.0 - github.com/pulumi/pulumi-github/sdk/v6 v6.14.0 + github.com/pulumi/pulumi-github/sdk/v6 v6.14.1 github.com/pulumi/pulumi-gitlab/sdk/v9 v9.11.1 github.com/pulumi/pulumi-tls/sdk/v5 v5.5.1 golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 @@ -68,7 +68,7 @@ require ( github.com/gabriel-vasile/mimetype v1.4.15 // indirect github.com/go-git/gcfg/v2 v2.0.2 // indirect github.com/go-git/go-billy/v6 v6.0.0-alpha.2 // indirect - github.com/go-git/go-git/v6 v6.0.0-alpha.4 // indirect + github.com/go-git/go-git/v6 v6.0.0-alpha.5 // indirect github.com/go-openapi/swag/pools v0.28.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect @@ -86,11 +86,11 @@ require ( ) require ( - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.15 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.34 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.26 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.34 // indirect - github.com/aws/aws-sdk-go-v2/service/pricing v1.44.2 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.35 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.27 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.35 // indirect + github.com/aws/aws-sdk-go-v2/service/pricing v1.44.3 // indirect github.com/blang/semver/v4 v4.0.0 // indirect //keep bubble-table v0.19.2 to avoid breaking changes in bubble-table v0.20.0 github.com/hashicorp/hcl/v2 v2.24.0 // indirect @@ -111,16 +111,16 @@ require ( github.com/IBM/platform-services-go-sdk v0.101.0 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/atotto/clipboard v0.1.4 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.30 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.33 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.33 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.14 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.33 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 // indirect - github.com/aws/smithy-go v1.27.5 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.33 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.34 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.34 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.5.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.33.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.3 // indirect + github.com/aws/smithy-go v1.27.6 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect // github.com/charmbracelet/bubbles v1.0.0 // indirect @@ -172,7 +172,7 @@ require ( github.com/klauspost/compress v1.19.1 // indirect github.com/klauspost/cpuid/v2 v2.4.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/lucasb-eyer/go-colorful v1.4.0 // indirect + github.com/lucasb-eyer/go-colorful v1.4.1 // indirect github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect @@ -187,7 +187,7 @@ require ( github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect - github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild v0.0.21 // indirect + github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild v0.0.22 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect @@ -227,8 +227,8 @@ require ( github.com/IBM/vpc-go-sdk v0.87.1 github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.4.1 // indirect - github.com/aws/aws-sdk-go-v2/service/ecs v1.89.2 - github.com/aws/aws-sdk-go-v2/service/iam v1.57.0 + github.com/aws/aws-sdk-go-v2/service/ecs v1.89.3 + github.com/aws/aws-sdk-go-v2/service/iam v1.57.1 github.com/blang/semver v3.5.1+incompatible // indirect github.com/cheggaaa/pb v1.0.29 // indirect github.com/djherbis/times v1.6.0 // indirect diff --git a/go.sum b/go.sum index d9c503064..081595a1c 100644 --- a/go.sum +++ b/go.sum @@ -66,50 +66,50 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/amazon-ec2-instance-selector/v3 v3.1.3 h1:13qtG4reL2+2UiCm2U7gM9QJthDO8hPPzvn7hTjHask= github.com/aws/amazon-ec2-instance-selector/v3 v3.1.3/go.mod h1:wdlMRtz9G4IO6H1yZPsqfGBxR8E6B/bdxHlGkls4kGQ= -github.com/aws/aws-sdk-go-v2 v1.43.2 h1:cl+IXwWb3qazClUcm08tGSsB6OiuV83JVJO9B0jQcPc= -github.com/aws/aws-sdk-go-v2 v1.43.2/go.mod h1:WEzLKBh/mEjXvx1FtQMWgSxMSTVqxQzjkRtk5fa3wkg= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.15 h1:rq/p1VNFfygoKEQ9hHMKsKBE98lspPvT8IxaFs5mFhw= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.15/go.mod h1:bELIhlPfW8OkpDhP1MvCjHDtvv8NhiBTz+K4o26zrXA= -github.com/aws/aws-sdk-go-v2/config v1.32.31 h1:n4nY9O3QKoHIkL85EX+V8RcMFtOhlpTFhGArg915PXk= -github.com/aws/aws-sdk-go-v2/config v1.32.31/go.mod h1:PN0NYDCCoOpGGsZ2+elDUidmHfQBPyYzN2GCgl8HEBs= -github.com/aws/aws-sdk-go-v2/credentials v1.19.30 h1:TTCvvzFU6gXa4iJecNG/0F/B0oYTiazoRECr2XyLHrY= -github.com/aws/aws-sdk-go-v2/credentials v1.19.30/go.mod h1:jKxAp2AEncnliinzpgOSZDFv6+VjvWhjw/AtbfsWT9U= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 h1:kfVL5wAunCJycL6MOQ6aNh6PlAYEymflcjuKmrWUA0o= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31/go.mod h1:nWfRNDAppujCQgOUd43lKT4yeLv9z3nJ3bw1G3BgQKo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.33 h1:HAp1wLFZzch054uh3FK7rcVYg4v7J2FxVf3h3IGNZas= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.33/go.mod h1:mJk5fmqnF+WUlMdPG37pR2Fh3oh6r8F6ZGUgPKvzu0c= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.33 h1:0YA0aCKgsJyno6xkFfaIgjE3/wK08+Qxo9nQfe1UrWM= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.33/go.mod h1:UZqj4WIdTH+ga8Y/DgpAuy/8cGjM3h7gDCliJYGg2SE= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.34 h1:HQYnjFnXpX8EbPW5M1QT8mXzesRPwly0HEPTcFlS02Y= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.34/go.mod h1:tGzj56niKYZBbDIRhwPGDqrULzmWv5b6uBQGqyNaFZw= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.318.0 h1:+jtItWSTMhuHmKKYwdjIZLwyq6mxryj8nE8q9Q5l8Cs= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.318.0/go.mod h1:JZSh40UkVtDPcC6QZ4rxqOTMyC1YtQi9sCrhD0FC9Rk= -github.com/aws/aws-sdk-go-v2/service/ecs v1.89.2 h1:8OQdMdSecpeZECz/zaSmgtsodDkcYmeU1ahY//3qnV0= -github.com/aws/aws-sdk-go-v2/service/ecs v1.89.2/go.mod h1:1AMXbi+8LXR7Ra1g6dQXMKlrRzzdt8wI+IGSkUFntPU= -github.com/aws/aws-sdk-go-v2/service/iam v1.57.0 h1:n8wvZQGLzmMEz1y073wyxdN5uGAGb3fa/k8ZRxpzg4I= -github.com/aws/aws-sdk-go-v2/service/iam v1.57.0/go.mod h1:vXOtv4pXRgGwWMyhblIp2+qI20iajYYlmrzMaIJF86s= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.14 h1:SA43nfaY7+1jjMNIc2ywu99JLJLButtIdLP6j+bT870= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.14/go.mod h1:Du3llKcwbQvHsTXSLzTOGQz0DTDBMEzdg7DAGu7inrY= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.26 h1:Eflerh7atY6HN0yz60peNLOkJA2ZKUyYjZexMbqwMCE= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.26/go.mod h1:dCAXNDmik9NuTjfsvCvW22S6ZFpxmtoliFoQu5XFkh8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.33 h1:mqI7OrxN/DUH85F5OqVn3cIfuZ3+HVcebUm2N8mLlgQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.33/go.mod h1:eZ5jdEpvaaOU8nWWE4cTAJETSEA5FZoWxvNRao4piHY= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.34 h1:Lercr2QB2rOrCwyOusmnQ7IiopfkGcZAgMJbjcSdK/s= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.34/go.mod h1:W0xXPPCb2HAqa3cp2f/nRvE+jGgBmchiuXrfBRlfb1I= -github.com/aws/aws-sdk-go-v2/service/pricing v1.44.2 h1:Uo7traFo3Cb48Idf3tzqjLoQnOmkM0fHbdw51eiOjIg= -github.com/aws/aws-sdk-go-v2/service/pricing v1.44.2/go.mod h1:lQqaP/Lm6OlINrAf+D3OVdA/jhlw9Qqjsu/Cijcpebs= -github.com/aws/aws-sdk-go-v2/service/s3 v1.106.2 h1:lFSYDEyC1JHucMH3fdczMTnDaghqNttyRXKM8JY9EJQ= -github.com/aws/aws-sdk-go-v2/service/s3 v1.106.2/go.mod h1:aw1E7RCjxs5Sd8N6WdICMcMroff12Tzxte+ELXXNqRU= -github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 h1:OHH5iTQvVGmfHjX/5Q+vFuA/Rf2x6/95aJ/75QCQSm4= -github.com/aws/aws-sdk-go-v2/service/signin v1.5.0/go.mod h1:mCF3AK9PpL49oOrhniUXWAfhVBVQ/XbytoE5eccZUIs= -github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 h1:CaJyYhxBE0M/HJX/YvSaSmQlsI91VHB0lKU8LtLxL3A= -github.com/aws/aws-sdk-go-v2/service/sso v1.33.0/go.mod h1:+e6BMRMPjBQoCw/WovYR9GLy2IU0z4Q77smOB1DraSg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 h1:tC323YV77QdafeBr6LUhLDTsboyuyHLNRwAyCP44kGU= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0/go.mod h1:SfLK1sgviHmbI+MozR9iDwDjL4cdCVZtahsjoR+z7wg= -github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 h1:Pd6PNlp4t8PTXxqzstICl52Wsy78vpjFZ7PRUj44mJc= -github.com/aws/aws-sdk-go-v2/service/sts v1.45.0/go.mod h1:rmQ0TnHzuLPmabgjPcsywhsSOmaBDgzR4zvDxSPsGdg= -github.com/aws/smithy-go v1.27.5 h1:d1ro7KpYOYwP6m73YFa+Kc/A130VsAdX68SpsJwARMM= -github.com/aws/smithy-go v1.27.5/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2 v1.43.3 h1:XJIcfv8uDs2ukdQsoAC8/Ebu1ejxwzlayl2ZsiFns2A= +github.com/aws/aws-sdk-go-v2 v1.43.3/go.mod h1:70vwSy16txshwG+g55WkpgPKDIByzHI8ccBsOteo3bQ= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.16 h1:aiuaKlDweRC5qExJondpWjOgyzMHpofpwspGXUtwn4c= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.16/go.mod h1:nG/LOlmox9BDe9HvQnXWzgcK8uKbgBMZ/Hp5pVt/21I= +github.com/aws/aws-sdk-go-v2/config v1.32.34 h1:o+YAizrX562nEZXaB38uYTK8RvIsvW0uuRP+e5e0Pfk= +github.com/aws/aws-sdk-go-v2/config v1.32.34/go.mod h1:wc0zYRChOniiufvdWiRVf3jgXSgbkvaD683IHHHc2ZQ= +github.com/aws/aws-sdk-go-v2/credentials v1.19.33 h1:/e5V3EWfeDiW6cuRxHsC8gbwko4/vvVYPJR2afBKFFY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.33/go.mod h1:ZxAmkcyOM9beY/WO9oxp2oVPXiP3rq5N1/p4NbenJdE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.34 h1:1EsGke6rTD2CG3j2MMVB77n6Q+FlbQWYI/dFdLWBNtM= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.34/go.mod h1:5B1Z/QbaWzqoWRzYxZfmCbDDRcvUHcfAIQw/S+KfDmc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.34 h1:vuIfjzoeqhQMGJyOBU3t0ZEjn2jrN8Bbg1N4CgjzM5Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.34/go.mod h1:hP28cN4CPJLZHirdQPrZR50JcLN4ApRJP2tzG8cRlhY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.34 h1:9faHsnqxJ1vDvB4wMZy/ajIDyz5QhllQjjc72RJpXAw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.34/go.mod h1:Yp6nIyejpa23nzlB/LhT63KTla9Jdi06nv/HH/OkAH8= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.35 h1:Oe8gMKJLO5awqpa5EhAGKVnBv1s+brdWVuxM2mDa7zA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.35/go.mod h1:FZevcG9cOST/FWAAUhHIchjR9fXFXFRCWodOhx+PDLA= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.318.1 h1:h0YI+ocTZTy1QXcTDsKPa+/+kT1mA8z6mc5NZOhXHJw= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.318.1/go.mod h1:LXmcWkgEK0IfPd3dc3DI/bfMaP76AoWWaqYgh76Y2+A= +github.com/aws/aws-sdk-go-v2/service/ecs v1.89.3 h1:iqNL2awSrnAQWX0F0dCfva4pyi5GTy5Kb91eWliTwW8= +github.com/aws/aws-sdk-go-v2/service/ecs v1.89.3/go.mod h1:B6hK6Vdd7SSNFOwJ33qyAAF9+xvBkUzTxIC0ByxFFPM= +github.com/aws/aws-sdk-go-v2/service/iam v1.57.1 h1:frtNexXj45Ko7pFfBKLyxObvNHefmL7vBeZid9uJCfU= +github.com/aws/aws-sdk-go-v2/service/iam v1.57.1/go.mod h1:9SLmFv7Y2prkDI20yPqNj0+YjG885BnqBExw/hekT5g= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.15 h1:JJLBQxwY+AFwuPAi5ivGc1ChnTdUt4cXMv7e76m2c/Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.15/go.mod h1:lQknBIe78MVL0cQOQDlag8KGflMbMEVFx9mB6O8ENvk= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.27 h1:zwB6ltUc0UiyOsRQaMQ8jNLjKECbjhadCyl4hqV0y/c= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.27/go.mod h1:ce9y+Y+hGLUyPKJZZJGoFLuFJNfCNuWZTujUJAsckQA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.34 h1:sYg4qHWLqsjp15PzX7XCOHSOgKEGoZ5vQY43VvZ1pas= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.34/go.mod h1:N58SSz3roKf1HzW5qRaOiyk6MbDLTKgLPvlTfJ90iyI= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.35 h1:ohfdSAm4TA6nryIY7mLqe4mnSIAnAreoAPBM81ZVoIM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.35/go.mod h1:uUjphnxMb3HH3vIiOHl4dH0fGNKL+csjqRQEabbfw5k= +github.com/aws/aws-sdk-go-v2/service/pricing v1.44.3 h1:FlwRvZDV5BHa8v0BCPW8+Za26HobkVYWiuw5aOs/hq8= +github.com/aws/aws-sdk-go-v2/service/pricing v1.44.3/go.mod h1:pmcZWdMf109OWwzIQ7qax7B3BLnuhf6z39+6L3UorEM= +github.com/aws/aws-sdk-go-v2/service/s3 v1.106.3 h1:oSfubHEP3a0nTRAtm99IDaws0f15qwf+fOwS1Esh5jI= +github.com/aws/aws-sdk-go-v2/service/s3 v1.106.3/go.mod h1:lWk6L5Q3YkaC7so1bQUJkvF7hj2KUFzdZ4w15wc2GHY= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.3 h1:togAtAmgV5IGMnQDuBDJeM8z5Y5RN6G7xeOgphWz+Yc= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.3/go.mod h1:T7xKUUUvN7W3RW8UmMvKnD12xqh+Ux2gCPHPhnt64Dg= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.3 h1:YjH64OUytnWZBHUtM9GMyi4ZWBiSQdEJkZuPykOIe44= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.3/go.mod h1:5qoHcDZDTSJotoKk1bvVRPv1MXaL/NhfY9ng8D1g/ig= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.3 h1:A4o1di/XGaqtw6r3toSBrFX2U7mVSLqg7jo9wL4I+cU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.3/go.mod h1:sKuKz2kHtrGVtFu34vbM3LWSA9CKD9YZUmm6e5PPqRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.3 h1:Fi7+DiKN1+QphlajvE6FqeZ8GRbnnRul7zTdUiRpbGc= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.3/go.mod h1:KCc3e27fHZUGtzpek7wZcp6dyCpGkJJo/+3PBujh/yU= +github.com/aws/smithy-go v1.27.6 h1:0zjT8jgK3jbrTT7JJ3EE6JsMhX8JTrZ+f1sEndYDXrA= +github.com/aws/smithy-go v1.27.6/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= @@ -193,8 +193,8 @@ github.com/go-git/go-billy/v6 v6.0.0-alpha.2 h1:1Sv5WemXL8CxKrAx1gioJ+uHNb2bZJhi github.com/go-git/go-billy/v6 v6.0.0-alpha.2/go.mod h1:r/bsv9i/iDyyEU8/Z6mjC+YraOVwie1ddfUqBCElKXQ= github.com/go-git/go-git-fixtures/v6 v6.0.0-alpha.1 h1:gmqi2jvsreu0s8JMLylYDFq4sbjHwwlhktMw0DUg3mA= github.com/go-git/go-git-fixtures/v6 v6.0.0-alpha.1/go.mod h1:ECf1MqJlBdYpKggBrOXjo/0EnvRZx6D++I86UYjPgAQ= -github.com/go-git/go-git/v6 v6.0.0-alpha.4 h1:aDTc2UGanmaE7FkGLSlBEB9nohMnQ+RKXcfq/D+esDQ= -github.com/go-git/go-git/v6 v6.0.0-alpha.4/go.mod h1:4ODa/G7hPWrh4Y+7lmt59Ij3zW38IEfvRoAZxLYYBhc= +github.com/go-git/go-git/v6 v6.0.0-alpha.5 h1:sE+OlkHgYWNMVmN1s9sR7uyFgsWLtxcNWse/vBYKxRE= +github.com/go-git/go-git/v6 v6.0.0-alpha.5/go.mod h1:3IjhiZnM+uBmUrOGSeqrJpsmi4Vd0H2NZO/uK2a7d0s= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -323,8 +323,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0= github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE= -github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= -github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss= +github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mapt-oss/pulumi-ibmcloud/sdk v0.0.12 h1:F+Sr/Q2nZoEeb7epVFmtfkSIeYm59HO3uMJd4qLi6jQ= github.com/mapt-oss/pulumi-ibmcloud/sdk v0.0.12/go.mod h1:U30V8lnPz4X/KybXyAOlz2vHXYcN4ZY/BwgE1XDPt2g= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -370,8 +370,8 @@ github.com/oliveagle/jsonpath v0.1.4 h1:Sr/ffH5YSyQKjSNfvDFkQqAqh3kn/QxF/7j2jjpf github.com/oliveagle/jsonpath v0.1.4/go.mod h1:diWEHhuLqib29heQcHYHyaLcxFC3KpKa/5ihkZBs1Z8= 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.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/opentracing/basictracer-go v1.1.0 h1:Oa1fTSBvAl8pa3U+IJYqrKm0NALwH9OsgwOqDv4xJW0= github.com/opentracing/basictracer-go v1.1.0/go.mod h1:V2HZueSJEp879yv285Aap1BS69fQMD+MNP1mRs6mBQc= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -423,12 +423,12 @@ github.com/pulumi/pulumi-azure-native-sdk/v3 v3.24.0 h1:yN7pGO3lxmblK1ZSzsulPCSm github.com/pulumi/pulumi-azure-native-sdk/v3 v3.24.0/go.mod h1:zy4qrCrgdY+pNrbOtGPBZWJY/Z6gbHsSiN3T+WJGtrY= github.com/pulumi/pulumi-command/sdk v1.2.1 h1:mAziZ91a/9U+5IjZH5Skcar80OSmpBSYljeQNRblTWQ= github.com/pulumi/pulumi-command/sdk v1.2.1/go.mod h1:hQxv9DXg6bFjcd9BEiNdMImQ/V1rnC9D115q5VXYNps= -github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild v0.0.21 h1:OgVpls9YVlZfegXzIxZed5BY1fqP6oYCpLGDXyE9tyM= -github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild v0.0.21/go.mod h1:iRiNhXmySLlREgx1zeoD4Es62WU2jTpd5YXdZ/0AQB4= +github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild v0.0.22 h1:CC/bpL3NHv0Hcw71mRju3HMGgqn1iyB6y3cnGe2Q45I= +github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild v0.0.22/go.mod h1:rWyNO5asSqvmDD9yi6ZoSF3PO93nwdYcqujoxWtSGwI= github.com/pulumi/pulumi-docker/sdk/v4 v4.5.8 h1:rik9L2SIpsoDenY51MkogR6GWgu/0Sy/XmyQmKWNUqU= github.com/pulumi/pulumi-docker/sdk/v4 v4.5.8/go.mod h1:eph7BPNPkEIIK882/Ll4dbeHl5wZEc/UvTcUW0CK1UY= -github.com/pulumi/pulumi-github/sdk/v6 v6.14.0 h1:49TS7PctMDGQpOAD6vu0On+k6L3t0Rtrzmwd/fDcbVk= -github.com/pulumi/pulumi-github/sdk/v6 v6.14.0/go.mod h1:aSCgSWErJwJujq1CKHe71wArYAKjWKHs+REB5GcM0es= +github.com/pulumi/pulumi-github/sdk/v6 v6.14.1 h1:xJxeq/9EhD0kTdAqXi3j+858vHQhoBdVi2vGrjPE0N4= +github.com/pulumi/pulumi-github/sdk/v6 v6.14.1/go.mod h1:tclJs/AnoajeaEW/oCUrE7r2naeE8TkGjO3hhQXxyOQ= github.com/pulumi/pulumi-gitlab/sdk/v9 v9.11.1 h1:DPDQD9FamQ3myy0EQOYuzUFvcJpQDr1H3GNwz/scDgA= github.com/pulumi/pulumi-gitlab/sdk/v9 v9.11.1/go.mod h1:eS9gkxGvdzprE767heyMrPrMY/6FX9UbdPYeI4pbkE8= github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.33.0 h1:fFP72Q6/GfyI7BII3hUS7vn45Eg16zKIIS3I3R9bNEY= diff --git a/oci/Containerfile b/oci/Containerfile index 2f7e54cbb..2cb595ea8 100644 --- a/oci/Containerfile +++ b/oci/Containerfile @@ -34,7 +34,7 @@ ARG PULUMI_RANDOM_VERSION=v4.21.1 # renovate: datasource=github-releases depName=pulumi/pulumi-aws-native ARG PULUMI_AWS_NATIVE_VERSION=v1.73.1 # renovate: datasource=github-releases depName=pulumi/pulumi-github -ARG PULUMI_GITHUB_VERSION=v6.14.0 +ARG PULUMI_GITHUB_VERSION=v6.14.1 # renovate: datasource=github-releases depName=pulumi/pulumi-gitlab ARG PULUMI_GITLAB_VERSION=v10.1.1 # renovate: datasource=github-releases depName=mapt-oss/pulumi-ibmcloud diff --git a/tools/go.mod b/tools/go.mod index 2313b7198..bb82d5c77 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -145,7 +145,7 @@ require ( github.com/ldez/tagliatelle v0.7.2 // indirect github.com/ldez/usetesting v0.5.0 // indirect github.com/leonklingele/grouper v1.1.2 // indirect - github.com/lucasb-eyer/go-colorful v1.4.0 // indirect + github.com/lucasb-eyer/go-colorful v1.4.1 // indirect github.com/macabu/inamedparam v0.2.0 // indirect github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect github.com/manuelarte/funcorder v0.6.0 // indirect diff --git a/tools/go.sum b/tools/go.sum index 368c3dfe1..20d43c641 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -270,8 +270,8 @@ github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ= github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= -github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= -github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss= +github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= diff --git a/tools/vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md b/tools/vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md index 3879f14aa..f349b3357 100644 --- a/tools/vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md +++ b/tools/vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md @@ -8,6 +8,10 @@ but only releases after v1.0.3 properly adhere to it. ## [Unreleased] +## [1.4.1] - 2026-08-02 +### Fixed +- Corrected `D50ToD65` to use the CSS Color 4 matrix inverse of `D65ToD50` (#85). + ## [1.4.0] - 2026-03-28 ### Added - Constructors, decomposers, and blend functions for the CSS Color Level 4 wide-gamut RGB color spaces `DisplayP3`, `A98Rgb`, `ProPhotoRgb`, and `Rec2020` (#81) diff --git a/tools/vendor/github.com/lucasb-eyer/go-colorful/widegamut.go b/tools/vendor/github.com/lucasb-eyer/go-colorful/widegamut.go index 6805a2b96..63c3e878e 100644 --- a/tools/vendor/github.com/lucasb-eyer/go-colorful/widegamut.go +++ b/tools/vendor/github.com/lucasb-eyer/go-colorful/widegamut.go @@ -10,9 +10,9 @@ import "math" // Bradford chromatic adaptation between D50 and D65 illuminants. func D50ToD65(x, y, z float64) (xo, yo, zo float64) { - xo = 0.9555766*x - 0.0230393*y + 0.0631636*z - yo = -0.0282895*x + 1.0099416*y + 0.0210077*z - zo = 0.0122982*x - 0.0204830*y + 1.3299098*z + xo = 0.9554734527042182*x - 0.023098536874261423*y + 0.06325964552894382*z + yo = -0.028369706963208136*x + 1.0099954580058226*y + 0.021041398966943008*z + zo = 0.012314001688319899*x - 0.020507696433477912*y + 1.3303659366080753*z return } diff --git a/tools/vendor/modules.txt b/tools/vendor/modules.txt index 1312fa973..6efa07871 100644 --- a/tools/vendor/modules.txt +++ b/tools/vendor/modules.txt @@ -618,7 +618,7 @@ github.com/leonklingele/grouper/pkg/analyzer/globals github.com/leonklingele/grouper/pkg/analyzer/imports github.com/leonklingele/grouper/pkg/analyzer/types github.com/leonklingele/grouper/pkg/analyzer/vars -# github.com/lucasb-eyer/go-colorful v1.4.0 +# github.com/lucasb-eyer/go-colorful v1.4.1 ## explicit; go 1.12 github.com/lucasb-eyer/go-colorful # github.com/macabu/inamedparam v0.2.0 diff --git a/vendor/github.com/aws/aws-sdk-go-v2/aws/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/aws/go_module_metadata.go index 7222e27d6..5b940e386 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/aws/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/aws/go_module_metadata.go @@ -3,4 +3,4 @@ package aws // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.43.2" +const goModuleVersion = "1.43.3" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/CHANGELOG.md index 7be426c3c..4f4258741 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/CHANGELOG.md @@ -1,3 +1,7 @@ +# v1.7.16 (2026-07-31.2) + +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + # v1.7.15 (2026-07-28) * **Dependency Update**: Update to smithy-go v1.27.5. diff --git a/vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/go_module_metadata.go index db48194cf..e2fe8a2ec 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/go_module_metadata.go @@ -3,4 +3,4 @@ package eventstream // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.7.15" +const goModuleVersion = "1.7.16" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/config/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/config/CHANGELOG.md index 589f82973..3709cc356 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/config/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/config/CHANGELOG.md @@ -1,3 +1,17 @@ +# v1.32.34 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + +# v1.32.33 (2026-07-29) + +* **Dependency Update**: Updated to the latest SDK module versions + +# v1.32.32 (2026-07-28) + +* **Dependency Update**: Update to smithy-go v1.27.5. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.32.31 (2026-07-21) * **Dependency Update**: Updated to the latest SDK module versions diff --git a/vendor/github.com/aws/aws-sdk-go-v2/config/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/config/go_module_metadata.go index 0b48de0f8..2c0f772e5 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/config/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/config/go_module_metadata.go @@ -3,4 +3,4 @@ package config // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.32.31" +const goModuleVersion = "1.32.34" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/credentials/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/credentials/CHANGELOG.md index d26df4886..c72e658ce 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/credentials/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/credentials/CHANGELOG.md @@ -1,3 +1,17 @@ +# v1.19.33 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + +# v1.19.32 (2026-07-29) + +* **Dependency Update**: Updated to the latest SDK module versions + +# v1.19.31 (2026-07-28) + +* **Dependency Update**: Update to smithy-go v1.27.5. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.19.30 (2026-07-21) * **Dependency Update**: Updated to the latest SDK module versions diff --git a/vendor/github.com/aws/aws-sdk-go-v2/credentials/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/credentials/go_module_metadata.go index e75a38fe2..9a265d384 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/credentials/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/credentials/go_module_metadata.go @@ -3,4 +3,4 @@ package credentials // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.19.30" +const goModuleVersion = "1.19.33" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/CHANGELOG.md index 2dc09fe9b..3a1238a86 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/CHANGELOG.md @@ -1,3 +1,17 @@ +# v1.18.34 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + +# v1.18.33 (2026-07-29) + +* **Dependency Update**: Updated to the latest SDK module versions + +# v1.18.32 (2026-07-28) + +* **Dependency Update**: Update to smithy-go v1.27.5. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.18.31 (2026-07-21) * **Dependency Update**: Updated to the latest SDK module versions diff --git a/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/go_module_metadata.go index 38253be79..63d8283bc 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/go_module_metadata.go @@ -3,4 +3,4 @@ package imds // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.18.31" +const goModuleVersion = "1.18.34" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/CHANGELOG.md index afcd57dd3..12fedd913 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.4.34 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + # v1.4.33 (2026-07-29) * **Dependency Update**: Updated to the latest SDK module versions diff --git a/vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/go_module_metadata.go index f38ccca23..1afd96dbf 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/go_module_metadata.go @@ -3,4 +3,4 @@ package configsources // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.4.33" +const goModuleVersion = "1.4.34" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/CHANGELOG.md index ae09aa2d0..5ec077fe2 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/CHANGELOG.md @@ -1,3 +1,8 @@ +# v2.7.34 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + # v2.7.33 (2026-07-29) * **Dependency Update**: Updated to the latest SDK module versions diff --git a/vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/go_module_metadata.go index 98755086c..d1e101dc3 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/go_module_metadata.go @@ -3,4 +3,4 @@ package endpoints // goModuleVersion is the tagged release for this module -const goModuleVersion = "2.7.33" +const goModuleVersion = "2.7.34" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/CHANGELOG.md index 19ff9fc68..e26c72f8b 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.4.35 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + # v1.4.34 (2026-07-29) * **Dependency Update**: Updated to the latest SDK module versions diff --git a/vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/go_module_metadata.go index 1eb67f426..60f66a0ba 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/go_module_metadata.go @@ -3,4 +3,4 @@ package v4a // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.4.34" +const goModuleVersion = "1.4.35" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/ec2/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/ec2/CHANGELOG.md index 2b164ce8d..c2b71c54b 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/ec2/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/ec2/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.318.1 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + # v1.318.0 (2026-07-29) * **Feature**: This release adds support for policy-based routing on AWS Transit Gateway, enabling you to route traffic based on 5-tuple matching (source IP, destination IP, source port, destination port, and protocol) using new policy table entry APIs that direct matching traffic to a target route table. diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/ec2/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/ec2/go_module_metadata.go index 1247cfcaf..997922f17 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/ec2/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/ec2/go_module_metadata.go @@ -3,4 +3,4 @@ package ec2 // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.318.0" +const goModuleVersion = "1.318.1" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/ecs/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/ecs/CHANGELOG.md index a0638d4b2..11279e35e 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/ecs/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/ecs/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.89.3 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + # v1.89.2 (2026-07-29) * **Dependency Update**: Updated to the latest SDK module versions diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/ecs/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/ecs/go_module_metadata.go index b16a71eb3..20abc7bce 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/ecs/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/ecs/go_module_metadata.go @@ -3,4 +3,4 @@ package ecs // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.89.2" +const goModuleVersion = "1.89.3" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/iam/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/iam/CHANGELOG.md index e32142085..f2e0813b2 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/iam/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/iam/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.57.1 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + # v1.57.0 (2026-07-30) * **Feature**: Improved IAM Policy Simulator accuracy. Simulator now evaluates SCP conditions and resource scoping, returns explicitDeny for explicit SCP denials, and reports accurate cross-account decisions. diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/iam/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/iam/go_module_metadata.go index f01b3ec4d..b42d47b64 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/iam/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/iam/go_module_metadata.go @@ -3,4 +3,4 @@ package iam // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.57.0" +const goModuleVersion = "1.57.1" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/CHANGELOG.md index 44b208900..6d861cc10 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/CHANGELOG.md @@ -1,3 +1,7 @@ +# v1.13.15 (2026-07-31.2) + +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + # v1.13.14 (2026-07-28) * **Dependency Update**: Update to smithy-go v1.27.5. diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/go_module_metadata.go index 2898c5e52..9f45b6549 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/go_module_metadata.go @@ -3,4 +3,4 @@ package acceptencoding // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.13.14" +const goModuleVersion = "1.13.15" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/checksum/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/checksum/CHANGELOG.md index 90bc137a6..4e1be6046 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/checksum/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/checksum/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.9.27 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + # v1.9.26 (2026-07-29) * **Dependency Update**: Updated to the latest SDK module versions diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/checksum/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/checksum/go_module_metadata.go index ee0fb6b15..38d710592 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/checksum/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/checksum/go_module_metadata.go @@ -3,4 +3,4 @@ package checksum // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.9.26" +const goModuleVersion = "1.9.27" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/CHANGELOG.md index 90dcc45c9..fb19493e9 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.13.34 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + # v1.13.33 (2026-07-29) * **Dependency Update**: Updated to the latest SDK module versions diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/go_module_metadata.go index 8a073d248..4568b0152 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/go_module_metadata.go @@ -3,4 +3,4 @@ package presignedurl // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.13.33" +const goModuleVersion = "1.13.34" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/s3shared/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/s3shared/CHANGELOG.md index 7f94d7cf7..47b232b64 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/s3shared/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/s3shared/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.19.35 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + # v1.19.34 (2026-07-29) * **Dependency Update**: Updated to the latest SDK module versions diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/s3shared/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/s3shared/go_module_metadata.go index 5a604c778..09153a187 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/s3shared/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/s3shared/go_module_metadata.go @@ -3,4 +3,4 @@ package s3shared // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.19.34" +const goModuleVersion = "1.19.35" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/pricing/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/pricing/CHANGELOG.md index 39a55e62a..8e72d0f0a 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/pricing/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/pricing/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.44.3 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + # v1.44.2 (2026-07-29) * **Dependency Update**: Updated to the latest SDK module versions diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/pricing/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/pricing/go_module_metadata.go index 9cdbbb343..8ad579eac 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/pricing/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/pricing/go_module_metadata.go @@ -3,4 +3,4 @@ package pricing // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.44.2" +const goModuleVersion = "1.44.3" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/CHANGELOG.md index d677f4519..db52d01e8 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.106.3 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + # v1.106.2 (2026-07-29) * **Dependency Update**: Updated to the latest SDK module versions diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/go_module_metadata.go index 0850d2afe..275aa421f 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/go_module_metadata.go @@ -3,4 +3,4 @@ package s3 // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.106.2" +const goModuleVersion = "1.106.3" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/signin/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/signin/CHANGELOG.md index 627ee90ec..2c3936f8b 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/signin/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/signin/CHANGELOG.md @@ -1,3 +1,17 @@ +# v1.5.3 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + +# v1.5.2 (2026-07-29) + +* **Dependency Update**: Updated to the latest SDK module versions + +# v1.5.1 (2026-07-28) + +* **Dependency Update**: Update to smithy-go v1.27.5. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.5.0 (2026-07-21) * **Feature**: Add an option to clients to disable clock skew diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/signin/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/signin/go_module_metadata.go index c430df151..dc620acf4 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/signin/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/signin/go_module_metadata.go @@ -3,4 +3,4 @@ package signin // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.5.0" +const goModuleVersion = "1.5.3" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/sso/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/sso/CHANGELOG.md index f9f09bb01..0a7436c61 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/sso/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/sso/CHANGELOG.md @@ -1,3 +1,17 @@ +# v1.33.3 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + +# v1.33.2 (2026-07-29) + +* **Dependency Update**: Updated to the latest SDK module versions + +# v1.33.1 (2026-07-28) + +* **Dependency Update**: Update to smithy-go v1.27.5. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.33.0 (2026-07-21) * **Feature**: Add an option to clients to disable clock skew diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/sso/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/sso/go_module_metadata.go index 37c3e625a..138dc7a2d 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/sso/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/sso/go_module_metadata.go @@ -3,4 +3,4 @@ package sso // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.33.0" +const goModuleVersion = "1.33.3" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/CHANGELOG.md index f4c8658d9..79593f50e 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/CHANGELOG.md @@ -1,3 +1,17 @@ +# v1.38.3 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + +# v1.38.2 (2026-07-29) + +* **Dependency Update**: Updated to the latest SDK module versions + +# v1.38.1 (2026-07-28) + +* **Dependency Update**: Update to smithy-go v1.27.5. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.38.0 (2026-07-21) * **Feature**: Add an option to clients to disable clock skew diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/go_module_metadata.go index cbe02320a..3253e7f9d 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/go_module_metadata.go @@ -3,4 +3,4 @@ package ssooidc // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.38.0" +const goModuleVersion = "1.38.3" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/sts/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/sts/CHANGELOG.md index 6082149d1..f093f615d 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/sts/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/sts/CHANGELOG.md @@ -1,3 +1,17 @@ +# v1.45.3 (2026-07-31.2) + +* **Dependency Update**: Updated to the latest SDK module versions +* **Dependency Update**: Upgrade to smithy-go v1.27.6 to fix various serde issues in HTTP binding services. + +# v1.45.2 (2026-07-29) + +* **Dependency Update**: Updated to the latest SDK module versions + +# v1.45.1 (2026-07-28) + +* **Dependency Update**: Update to smithy-go v1.27.5. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.45.0 (2026-07-21) * **Feature**: Add an option to clients to disable clock skew diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/sts/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/sts/go_module_metadata.go index fcea01107..5f2e0fad2 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/sts/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/sts/go_module_metadata.go @@ -3,4 +3,4 @@ package sts // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.45.0" +const goModuleVersion = "1.45.3" diff --git a/vendor/github.com/aws/smithy-go/CHANGELOG.md b/vendor/github.com/aws/smithy-go/CHANGELOG.md index 0140cf10e..e727efad5 100644 --- a/vendor/github.com/aws/smithy-go/CHANGELOG.md +++ b/vendor/github.com/aws/smithy-go/CHANGELOG.md @@ -1,3 +1,13 @@ +# Release (2026-07-31) + +## General Highlights +* **Dependency Update**: Updated to the latest SDK module versions + +## Module Highlights +* `github.com/aws/smithy-go`: v1.27.6 + * **Bug Fix**: Fix failure to deserialize any `@httpPayload` struct with a non-string member. + * **Bug Fix**: Fix failure to serialize any `@httpPayload` struct with a nested struct. + # Release (2026-07-27) ## General Highlights diff --git a/vendor/github.com/aws/smithy-go/go_module_metadata.go b/vendor/github.com/aws/smithy-go/go_module_metadata.go index 4277e6869..e9988cb18 100644 --- a/vendor/github.com/aws/smithy-go/go_module_metadata.go +++ b/vendor/github.com/aws/smithy-go/go_module_metadata.go @@ -3,4 +3,4 @@ package smithy // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.27.5" +const goModuleVersion = "1.27.6" diff --git a/vendor/github.com/go-git/go-git/v6/.renovaterc.json5 b/vendor/github.com/go-git/go-git/v6/.renovaterc.json5 index c26b01675..c589f911d 100644 --- a/vendor/github.com/go-git/go-git/v6/.renovaterc.json5 +++ b/vendor/github.com/go-git/go-git/v6/.renovaterc.json5 @@ -15,6 +15,10 @@ }, { "description": "Enable minor and patch updates for go-billy", + "matchBaseBranches": [ + "main", + "releases/v5.x" + ], "matchPackageNames": [ "github.com/go-git/go-billy", ], diff --git a/vendor/github.com/go-git/go-git/v6/config/config.go b/vendor/github.com/go-git/go-git/v6/config/config.go index af07041d9..57bd5a38c 100644 --- a/vendor/github.com/go-git/go-git/v6/config/config.go +++ b/vendor/github.com/go-git/go-git/v6/config/config.go @@ -31,7 +31,7 @@ const ( // should be marshalled or not. // Note that this does not need to align with the default protocol // version from plumbing/protocol. - DefaultProtocolVersion = protocol.V0 // go-git only supports V0 at the moment + DefaultProtocolVersion = protocol.V2 ) // ConfigStorer is a generic storage of Config object. @@ -210,8 +210,8 @@ type Config struct { // equal Branch.Name Branches map[string]*Branch // URLs list of url rewrite rules, if repo url starts with URL.InsteadOf value, it will be replaced with the - // key instead. - URLs map[string]*URL + // URL.Name instead. Ordered by appearance in config file. + URLs []*URL // Raw contains the raw information of a config file. The main goal is // preserve the parsed information from the original format, to avoid // dropping unsupported fields. @@ -326,7 +326,7 @@ func NewConfig() *Config { Remotes: make(map[string]*RemoteConfig), Submodules: make(map[string]*Submodule), Branches: make(map[string]*Branch), - URLs: make(map[string]*URL), + URLs: make([]*URL, 0), Raw: format.New(), } @@ -666,13 +666,14 @@ func (c *Config) unmarshalRemotes() error { func (c *Config) unmarshalURLs() error { s := c.Raw.Section(urlSection) + c.URLs = make([]*URL, 0, len(s.Subsections)) for _, sub := range s.Subsections { r := &URL{} if err := r.unmarshal(sub); err != nil { return err } - c.URLs[r.Name] = r + c.URLs = append(c.URLs, r) } return nil @@ -990,10 +991,23 @@ func (c *Config) marshalURLs() { } func (c *Config) marshalProtocol() { - // Only marshal protocol section if a version was set. if c.Protocol.Version != DefaultProtocolVersion { s := c.Raw.Section(protocolSection) s.SetOption(versionKey, c.Protocol.Version.String()) + return + } + + // The struct holds the default version. Clear any stale protocol.version + // left over in the raw config so switching back to the default persists, + // and drop the section if it becomes empty. Guard on HasSection so a + // non-default round-trip does not introduce an empty [protocol]. + if !c.Raw.HasSection(protocolSection) { + return + } + s := c.Raw.Section(protocolSection) + s.RemoveOption(versionKey) + if len(s.Options) == 0 && len(s.Subsections) == 0 { + c.Raw.RemoveSection(protocolSection) } } @@ -1126,14 +1140,14 @@ func (c *RemoteConfig) IsFirstURLLocal() bool { return url.IsLocalEndpoint(c.URLs[0]) } -func (c *RemoteConfig) applyURLRules(urlRules map[string]*URL) { +func (c *RemoteConfig) applyURLRules(urlRules []*URL) { // save original urls originalURLs := make([]string, len(c.URLs)) copy(originalURLs, c.URLs) for i, url := range c.URLs { - if matchingURLRule := findLongestInsteadOfMatch(url, urlRules); matchingURLRule != nil { - c.URLs[i] = matchingURLRule.ApplyInsteadOf(c.URLs[i]) + if rewrittenURL, matched := applyLongestInsteadOfMatch(url, urlRules); matched { + c.URLs[i] = rewrittenURL c.insteadOfRulesApplied = true } } diff --git a/vendor/github.com/go-git/go-git/v6/config/url.go b/vendor/github.com/go-git/go-git/v6/config/url.go index fb338a73b..66cd8cd92 100644 --- a/vendor/github.com/go-git/go-git/v6/config/url.go +++ b/vendor/github.com/go-git/go-git/v6/config/url.go @@ -54,7 +54,7 @@ func (u *URL) marshal() *format.Subsection { return u.raw } -func findLongestInsteadOfMatch(remoteURL string, urls map[string]*URL) *URL { +func applyLongestInsteadOfMatch(remoteURL string, urls []*URL) (rewrittenURL string, matched bool) { var longestMatch *URL var longestMatchLength int @@ -67,6 +67,7 @@ func findLongestInsteadOfMatch(remoteURL string, urls map[string]*URL) *URL { lengthCurrentInsteadOf := len(currentInsteadOf) // according to spec if there is more than one match, take the longest + // when lengths are equal, use config file order (first match wins) if longestMatch == nil || longestMatchLength < lengthCurrentInsteadOf { longestMatch = u longestMatchLength = lengthCurrentInsteadOf @@ -74,16 +75,16 @@ func findLongestInsteadOfMatch(remoteURL string, urls map[string]*URL) *URL { } } - return longestMatch + if longestMatchLength > 0 { + return longestMatch.Name + remoteURL[longestMatchLength:], true + } + + return remoteURL, false } // ApplyInsteadOf applies the URL rewrite rules to the given URL. +// When multiple insteadOf values match, the longest match is used. func (u *URL) ApplyInsteadOf(url string) string { - for _, j := range u.InsteadOfs { - if strings.HasPrefix(url, j) { - return u.Name + url[len(j):] - } - } - - return url + rewrittenURL, _ := applyLongestInsteadOfMatch(url, []*URL{u}) + return rewrittenURL } diff --git a/vendor/github.com/go-git/go-git/v6/internal/packhandle/cursor_reader.go b/vendor/github.com/go-git/go-git/v6/internal/packhandle/cursor_reader.go new file mode 100644 index 000000000..d2f1fc2e5 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/internal/packhandle/cursor_reader.go @@ -0,0 +1,101 @@ +package packhandle + +import ( + "errors" + "io" + "io/fs" + "sync/atomic" + + "github.com/go-git/go-git/v6/internal/sharedfile" +) + +// ErrInvalidSeekWhence is returned by [cursorReader.Seek] when +// whence is not one of [io.SeekStart], [io.SeekCurrent], or +// [io.SeekEnd]. +var ErrInvalidSeekWhence = errors.New("packhandle: invalid whence") + +// ErrNegativeSeekPosition is returned by [cursorReader.Seek] +// when the resolved absolute offset would be negative. +var ErrNegativeSeekPosition = errors.New("packhandle: negative seek position") + +// cursorReader is the concrete reader returned by both +// [PackHandle.OpenPackReader] and [PackHandle.OpenRandomReader]. +// Each cursor holds its own offset and one [sharedfile.SharedFile] +// reference that Close releases. +// +// Read and Seek mutate the cursor offset and are not safe to call +// concurrently on the same cursor. ReadAt is safe to call +// concurrently with itself. +type cursorReader struct { + sf *sharedfile.SharedFile + file ReadAtCloser + size int64 + offset int64 + closed atomic.Bool +} + +func newCursorReader(sf *sharedfile.SharedFile, size int64) (*cursorReader, error) { + f, err := sf.Acquire() + if err != nil { + return nil, err + } + return &cursorReader{sf: sf, file: f, size: size}, nil +} + +func (c *cursorReader) Read(p []byte) (int, error) { + if c.closed.Load() || c.sf.IsClosed() { + return 0, fs.ErrClosed + } + if c.offset >= c.size { + return 0, io.EOF + } + n, err := c.file.ReadAt(p, c.offset) + c.offset += int64(n) + if errors.Is(err, io.EOF) && n > 0 { + err = nil + } + return n, err +} + +func (c *cursorReader) ReadAt(p []byte, off int64) (int, error) { + if c.closed.Load() || c.sf.IsClosed() { + return 0, fs.ErrClosed + } + return c.file.ReadAt(p, off) +} + +func (c *cursorReader) Seek(offset int64, whence int) (int64, error) { + if c.closed.Load() || c.sf.IsClosed() { + return 0, fs.ErrClosed + } + var abs int64 + switch whence { + case io.SeekStart: + abs = offset + case io.SeekCurrent: + abs = c.offset + offset + case io.SeekEnd: + abs = c.size + offset + default: + return 0, ErrInvalidSeekWhence + } + if abs < 0 { + return 0, ErrNegativeSeekPosition + } + c.offset = abs + return abs, nil +} + +// Close releases the underlying [sharedfile.SharedFile] reference. Idempotent. +func (c *cursorReader) Close() error { + if !c.closed.CompareAndSwap(false, true) { + return nil + } + c.sf.Release() + return nil +} + +var ( + _ PackReader = (*cursorReader)(nil) + _ RandomReader = (*cursorReader)(nil) +) diff --git a/vendor/github.com/go-git/go-git/v6/internal/packhandle/doc.go b/vendor/github.com/go-git/go-git/v6/internal/packhandle/doc.go new file mode 100644 index 000000000..985a72220 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/internal/packhandle/doc.go @@ -0,0 +1,18 @@ +// Package packhandle owns the file descriptors for one pack +// triple (.pack + .idx + .rev) inside go-git. +// +// A [PackHandle] reads from one pack: it produces streaming and +// random-access cursors over the .pack file via +// [PackHandle.OpenPackReader] and [PackHandle.OpenRandomReader], +// a parsed [PackMeta] via [PackHandle.Meta], and an +// [idxfile.Index] over the .idx/.rev pair via [PackHandle.Index]. +// The .pack file descriptor is opened lazily on first cursor +// request, shared across concurrent readers, and closed after a +// one-second idle grace period once no cursors remain. .idx and +// .rev descriptors are owned by the returned [idxfile.Index]. +// +// The package is internal: consumers must not surface any +// packhandle identifier on their own exported APIs; hold +// *PackHandle as a private named field (embedding is forbidden +// because it leaks the method set). +package packhandle diff --git a/vendor/github.com/go-git/go-git/v6/internal/packhandle/errors.go b/vendor/github.com/go-git/go-git/v6/internal/packhandle/errors.go new file mode 100644 index 000000000..71ddef9c8 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/internal/packhandle/errors.go @@ -0,0 +1,20 @@ +package packhandle + +import "errors" + +// Sentinel errors. Callers compare via errors.Is. +var ( + // ErrPackSourceRequired indicates that the [Sources.Pack] source + // was not fully configured: either Open or Size is nil. Returned + // by [New]. + ErrPackSourceRequired = errors.New("packhandle: Sources.Pack.Open and .Size are required") + + // ErrInvalidPackHash indicates that the packHash supplied to + // [New] is the zero hash, which cannot identify a pack. + ErrInvalidPackHash = errors.New("packhandle: packHash must be non-zero") + + // ErrSourceUnconfigured indicates that [Sources.Idx] or + // [Sources.Rev] was left zero-valued at construction. Returned + // by [PackHandle.Index]. + ErrSourceUnconfigured = errors.New("packhandle: source unconfigured") +) diff --git a/vendor/github.com/go-git/go-git/v6/internal/packhandle/index.go b/vendor/github.com/go-git/go-git/v6/internal/packhandle/index.go new file mode 100644 index 000000000..14a8e4705 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/internal/packhandle/index.go @@ -0,0 +1,39 @@ +package packhandle + +import ( + "io/fs" + + "github.com/go-git/go-git/v6/plumbing/format/idxfile" +) + +// Index returns a lazily-constructed [idxfile.Index] backed by +// the Idx and Rev sources. The first successful build is cached; +// transient build failures are not cached and retry on the next +// call. Returns [ErrSourceUnconfigured] if Idx or Rev was not +// configured, or [fs.ErrClosed] if the [PackHandle] is closed. +func (h *PackHandle) Index() (idxfile.Index, error) { + if h.closed.Load() { + return nil, fs.ErrClosed + } + h.indexMu.Lock() + defer h.indexMu.Unlock() + if h.closed.Load() { + return nil, fs.ErrClosed + } + if h.indexVal != nil { + return h.indexVal, nil + } + if h.sources.Idx.Open == nil || h.sources.Rev.Open == nil { + return nil, ErrSourceUnconfigured + } + + idxOpener := func() (idxfile.ReadAtCloser, error) { return h.sources.Idx.Open() } + revOpener := func() (idxfile.ReadAtCloser, error) { return h.sources.Rev.Open() } + + lazy, err := idxfile.NewLazyIndex(idxOpener, revOpener, h.packHash) + if err != nil { + return nil, err + } + h.indexVal = lazy + return lazy, nil +} diff --git a/vendor/github.com/go-git/go-git/v6/internal/packhandle/pack_meta.go b/vendor/github.com/go-git/go-git/v6/internal/packhandle/pack_meta.go new file mode 100644 index 000000000..6105681d2 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/internal/packhandle/pack_meta.go @@ -0,0 +1,60 @@ +package packhandle + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + + "github.com/go-git/go-git/v6/plumbing" +) + +// PackMeta is the parsed pack header plus footer hash. +type PackMeta struct { + Version uint32 // pack format version, validated to be 2 or 3 + Count uint32 // number of objects in the pack + ID plumbing.Hash // pack footer hash +} + +var packMagic = []byte{'P', 'A', 'C', 'K'} + +// parsePackMeta reads and validates the 12-byte pack header and +// the footer hash at the tail. The returned [PackMeta] is +// well-formed only if the footer equals packHash. +func parsePackMeta(src ReadAtCloser, size int64, packHash plumbing.Hash) (PackMeta, error) { + hashSize := int64(packHash.Size()) + if size < 12+hashSize { + return PackMeta{}, fmt.Errorf("packhandle: pack too small: %d bytes", size) + } + + var header [12]byte + if _, err := src.ReadAt(header[:], 0); err != nil { + return PackMeta{}, fmt.Errorf("packhandle: read pack header: %w", err) + } + if !bytes.Equal(header[0:4], packMagic) { + return PackMeta{}, errors.New("packhandle: pack magic mismatch") + } + + version := binary.BigEndian.Uint32(header[4:8]) + if version != 2 && version != 3 { + return PackMeta{}, fmt.Errorf("packhandle: unsupported pack version: %d", version) + } + count := binary.BigEndian.Uint32(header[8:12]) + + footer := make([]byte, hashSize) + if _, err := src.ReadAt(footer, size-hashSize); err != nil { + return PackMeta{}, fmt.Errorf("packhandle: read pack footer: %w", err) + } + + var id plumbing.Hash + id.ResetBySize(int(hashSize)) + if _, err := id.Write(footer); err != nil { + return PackMeta{}, fmt.Errorf("packhandle: write footer to hash: %w", err) + } + + if !bytes.Equal(id.Bytes(), packHash.Bytes()) { + return PackMeta{}, fmt.Errorf("packhandle: pack footer hash %v does not match pinned hash %v", id, packHash) + } + + return PackMeta{Version: version, Count: count, ID: id}, nil +} diff --git a/vendor/github.com/go-git/go-git/v6/internal/packhandle/pack_reader.go b/vendor/github.com/go-git/go-git/v6/internal/packhandle/pack_reader.go new file mode 100644 index 000000000..ee59a9314 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/internal/packhandle/pack_reader.go @@ -0,0 +1,20 @@ +package packhandle + +import "io" + +// PackReader provides streaming access to the .pack file. +// Returned by [PackHandle.OpenPackReader]. Each call returns a +// fresh cursor with its own offset. +type PackReader interface { + io.Reader + io.Seeker + io.Closer +} + +// RandomReader provides random-access reads against the .pack +// file. Returned by [PackHandle.OpenRandomReader]. ReadAt is +// safe to call concurrently with itself and across cursors. +type RandomReader interface { + io.ReaderAt + io.Closer +} diff --git a/vendor/github.com/go-git/go-git/v6/internal/packhandle/packhandle.go b/vendor/github.com/go-git/go-git/v6/internal/packhandle/packhandle.go new file mode 100644 index 000000000..ac15c0f8d --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/internal/packhandle/packhandle.go @@ -0,0 +1,256 @@ +package packhandle + +import ( + "errors" + "fmt" + "io/fs" + "sync" + "sync/atomic" + "time" + + "github.com/go-git/go-git/v6/internal/sharedfile" + "github.com/go-git/go-git/v6/plumbing" + "github.com/go-git/go-git/v6/plumbing/format/idxfile" + "github.com/go-git/go-git/v6/x/fdpool" +) + +// defaultGracePeriod is the idle window after the last cursor +// release before the .pack file descriptor is closed. +const defaultGracePeriod = 1 * time.Second + +// PackHandle reads from one pack triple, owning the .pack file +// descriptor for its lifetime and constructing an [idxfile.Index] +// over the .idx/.rev pair on demand. +// +// The .pack file descriptor is opened lazily on first cursor +// request, shared across concurrent readers, and closed after an +// idle grace period. .idx and .rev descriptors are owned by the +// returned [idxfile.Index]. +// +// Lifecycle contract: +// +// - Each cursor returned by [PackHandle.OpenPackReader] or +// [PackHandle.OpenRandomReader] acquires one reference on the +// underlying [sharedfile.SharedFile]; cursor.Close releases +// it. While at least one cursor is live the .pack FD cannot +// be torn down by the grace timer. +// - [PackHandle.Close] is synchronous: the .pack FD and any +// cached [idxfile.LazyIndex] FDs are closed before the call +// returns. Cursors opened before Close keep working until +// their own Close releases the last reference; calls on a +// cursor whose underlying FD has been closed see +// [fs.ErrClosed]. +// +// All methods are safe for concurrent use. +type PackHandle struct { + sources Sources + packHash plumbing.Hash + pack *sharedfile.SharedFile + + closed atomic.Bool + + metaMu sync.Mutex + metaVal *PackMeta + + indexMu sync.Mutex + indexVal *idxfile.LazyIndex + + sizeVal atomic.Int64 + + closeFn func() error +} + +// New constructs a [PackHandle] over the given sources. packHash +// is pinned to the .pack file's expected footer hash; [PackHandle.Meta] +// verifies the footer against this value. +// +// Returns [ErrPackSourceRequired] if Sources.Pack.Open or +// Sources.Pack.Size is nil, and [ErrInvalidPackHash] if packHash +// is the zero hash. Sources.Idx and Sources.Rev are optional; +// [PackHandle.Index] returns [ErrSourceUnconfigured] when either +// is absent. +func New(sources Sources, packHash plumbing.Hash) (*PackHandle, error) { + return NewWithPool(sources, packHash, nil) +} + +// NewWithPool is like [New] but registers the .pack +// [sharedfile.SharedFile] with the given [*fdpool.Pool]. The pool +// governs LRU eviction of the pack FD across many PackHandles so +// a storage-wide budget bounds the open .pack descriptors. Pass +// nil for pool to disable pooling (equivalent to [New]). +// +// When pool is non-nil the [defaultGracePeriod] timer is inert: +// the FD stays open and registered with the pool until the LRU +// evicts it (or [PackHandle.Close] tears it down). When pool is +// nil the grace timer governs FD lifetime as in [New]. +// +// Neither this constructor nor the cursor entry points +// ([PackHandle.OpenPackReader], [PackHandle.OpenRandomReader]) +// accept a [context.Context]. Pack reads are pure ReadAt I/O +// without cancellation hooks, matching the context-free +// convention of the storage, plumbing/format, and +// plumbing/storer layers; callers requiring cancellation +// enforce it at the call-site in the layer above. +func NewWithPool(sources Sources, packHash plumbing.Hash, pool *fdpool.Pool) (*PackHandle, error) { + if sources.Pack.Open == nil || sources.Pack.Size == nil { + return nil, ErrPackSourceRequired + } + if packHash.IsZero() { + return nil, ErrInvalidPackHash + } + h := &PackHandle{ + sources: sources, + packHash: packHash, + pack: sharedfile.NewWithPool(sources.Pack.Open, defaultGracePeriod, pool), + } + h.closeFn = sync.OnceValue(h.doClose) + return h, nil +} + +// OpenPackReader returns a streaming cursor over the .pack file. +// Each call returns an independent cursor with its own offset. +func (h *PackHandle) OpenPackReader() (PackReader, error) { + if h.closed.Load() { + return nil, fs.ErrClosed + } + size, err := h.packSize() + if err != nil { + return nil, fmt.Errorf("packhandle: pack size: %w", err) + } + return newCursorReader(h.pack, size) +} + +// OpenRandomReader returns a random-access cursor over the .pack +// file. Each call returns an independent cursor. +func (h *PackHandle) OpenRandomReader() (RandomReader, error) { + if h.closed.Load() { + return nil, fs.ErrClosed + } + size, err := h.packSize() + if err != nil { + return nil, fmt.Errorf("packhandle: pack size: %w", err) + } + return newCursorReader(h.pack, size) +} + +// packSize returns the cached .pack file size, consulting +// Sources.Pack.Size only on the first call. The .pack file is +// immutable post-creation and its on-disk identity is pinned via +// packHash, so the size is invariant for the lifetime of this +// handle. Failures are not cached; the next call retries. +// +// The cache uses an [atomic.Int64] with zero as the unset +// sentinel. Pack sizes are never zero — every valid pack carries +// at least a 12-byte header and a footer hash — so a zero load +// unambiguously means "not yet cached." If that invariant ever +// changes, this loop will re-Size on every call and the cache +// becomes dead code. +func (h *PackHandle) packSize() (int64, error) { + if v := h.sizeVal.Load(); v != 0 { + return v, nil + } + size, err := h.sources.Pack.Size() + if err != nil { + return 0, err + } + h.sizeVal.Store(size) + return size, nil +} + +// Close releases the .pack [sharedfile.SharedFile] and closes any +// cached index. Idempotent. +func (h *PackHandle) Close() error { + return h.closeFn() +} + +func (h *PackHandle) doClose() error { + // Set closed before releasing any FDs so a concurrent retry in + // Index or Meta sees the flag and bails with fs.ErrClosed + // instead of reopening idx/rev FDs against a torn-down pack. + h.closed.Store(true) + + packErr := h.pack.Close() + + h.indexMu.Lock() + idx := h.indexVal + h.indexVal = nil + h.indexMu.Unlock() + + var idxErr error + if idx != nil { + idxErr = idx.Close() + } + + return errors.Join(packErr, idxErr) +} + +// Meta reads and verifies the .pack header and footer hash. The +// first successful call is cached; transient open or read +// failures retry on the next call. Returns [fs.ErrClosed] if the +// [PackHandle] is closed. +func (h *PackHandle) Meta() (PackMeta, error) { + if h.closed.Load() { + return PackMeta{}, fs.ErrClosed + } + h.metaMu.Lock() + defer h.metaMu.Unlock() + if h.closed.Load() { + return PackMeta{}, fs.ErrClosed + } + if h.metaVal != nil { + return *h.metaVal, nil + } + + size, err := h.packSize() + if err != nil { + return PackMeta{}, fmt.Errorf("packhandle: pack size: %w", err) + } + src, err := h.pack.Acquire() + if err != nil { + return PackMeta{}, fmt.Errorf("packhandle: acquire pack: %w", err) + } + defer h.pack.Release() + + meta, err := parsePackMeta(src, size, h.packHash) + if err != nil { + return PackMeta{}, err + } + h.metaVal = &meta + return meta, nil +} + +// CloseIdleDescriptors releases the .pack file descriptor and +// the idx/rev descriptors of any cached [idxfile.LazyIndex] +// without marking the [PackHandle] closed. Active acquired +// readers continue to work; FDs held by in-flight readers close +// the instant the last refcount drops to zero. Subsequent +// [PackHandle.OpenPackReader] and [PackHandle.Index] operations +// reopen FDs on demand and resume normal grace-timer behaviour. +// +// Idempotent and safe to call concurrently with the open paths +// and itself. A no-op after [PackHandle.Close]; the closed flag +// short-circuits before touching either [sharedfile.SharedFile]. +// +// PackHandle-level caches survive: the cached [PackMeta] and +// the cached [idxfile.LazyIndex] pointer are not reset. A +// caller that wants to discard the PackHandle entirely uses +// Close; a subsequent Close after CloseIdleDescriptors still +// flips each underlying [sharedfile.SharedFile]'s closed flag +// exactly once via its idempotent Close. +func (h *PackHandle) CloseIdleDescriptors() error { + if h.closed.Load() { + return nil + } + + packErr := h.pack.ReleaseNow() + + h.indexMu.Lock() + idx := h.indexVal + h.indexMu.Unlock() + + var idxErr error + if idx != nil { + idxErr = idx.CloseIdleDescriptors() + } + return errors.Join(packErr, idxErr) +} diff --git a/vendor/github.com/go-git/go-git/v6/internal/packhandle/source.go b/vendor/github.com/go-git/go-git/v6/internal/packhandle/source.go new file mode 100644 index 000000000..d6148131b --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/internal/packhandle/source.go @@ -0,0 +1,50 @@ +package packhandle + +import ( + billy "github.com/go-git/go-billy/v6" + + "github.com/go-git/go-git/v6/internal/sharedfile" +) + +// ReadAtCloser is the file shape returned by Source.Open. +// It is an alias for [sharedfile.ReadAtCloser]; both names +// refer to the same type at compile time. +type ReadAtCloser = sharedfile.ReadAtCloser + +// Source describes how to obtain one file of a pack triple. Open +// is invoked lazily on first need and again after each +// grace-period close; Size is invoked lazily on demand and is +// expected to be cheap (typically backed by an [io/fs.Stat]). +type Source struct { + // Open returns a fresh, independently closeable random-access + // read handle. + Open func() (ReadAtCloser, error) + // Size returns the file's size in bytes. + Size func() (int64, error) +} + +// Sources bundles the three files of one pack. Pack is required. +// Idx and Rev are optional: when left zero, [PackHandle.Index] +// returns [ErrSourceUnconfigured]. +type Sources struct { + Pack Source + Idx Source + Rev Source +} + +// PathSource constructs a [Source] backed by the given path on +// fs. Open delegates to fs.Open; Size delegates to fs.Stat. +func PathSource(fs billy.Basic, path string) Source { + return Source{ + Open: func() (ReadAtCloser, error) { + return fs.Open(path) + }, + Size: func() (int64, error) { + info, err := fs.Stat(path) + if err != nil { + return 0, err + } + return info.Size(), nil + }, + } +} diff --git a/vendor/github.com/go-git/go-git/v6/internal/revision/parser.go b/vendor/github.com/go-git/go-git/v6/internal/revision/parser.go index a36dbd023..aa42fb841 100644 --- a/vendor/github.com/go-git/go-git/v6/internal/revision/parser.go +++ b/vendor/github.com/go-git/go-git/v6/internal/revision/parser.go @@ -565,6 +565,10 @@ func (p *Parser) parseRef() (Revisioner, error) { if endOfRef { p.unscan() + if buf == "@" { + return Ref("HEAD"), nil + } + return Ref(buf), nil } diff --git a/vendor/github.com/go-git/go-git/v6/internal/revision/scanner.go b/vendor/github.com/go-git/go-git/v6/internal/revision/scanner.go index debbf3067..86f523469 100644 --- a/vendor/github.com/go-git/go-git/v6/internal/revision/scanner.go +++ b/vendor/github.com/go-git/go-git/v6/internal/revision/scanner.go @@ -88,7 +88,24 @@ func (s *scanner) scan() (token, string, error) { case '-': return minus, string(ch), nil case '@': - return at, string(ch), nil + next, _, err := s.r.ReadRune() + if err == io.EOF || next == zeroRune { + return at, string(ch), nil + } + + if err != nil { + return tokenError, "", err + } + + if err := s.r.UnreadRune(); err != nil { + return tokenError, "", err + } + + if next == '{' { + return at, string(ch), nil + } + + return word, string(ch), nil case '\\': return aslash, string(ch), nil case '?': diff --git a/vendor/github.com/go-git/go-git/v6/internal/sharedfile/doc.go b/vendor/github.com/go-git/go-git/v6/internal/sharedfile/doc.go new file mode 100644 index 000000000..645dae92d --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/internal/sharedfile/doc.go @@ -0,0 +1,10 @@ +// Package sharedfile provides a refcounted file handle that opens +// lazily on first Acquire, shares the underlying file descriptor +// across concurrent acquirers, and closes the descriptor after a +// configurable grace period once the refcount drops to zero. +// +// This avoids holding file descriptors open indefinitely — which +// causes problems on Windows where open files cannot be deleted — +// while still sharing a single FD across concurrent readers and +// avoiding repeated open/close syscalls for sequential operations. +package sharedfile diff --git a/vendor/github.com/go-git/go-git/v6/internal/sharedfile/sharedfile.go b/vendor/github.com/go-git/go-git/v6/internal/sharedfile/sharedfile.go new file mode 100644 index 000000000..de55cbfbe --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/internal/sharedfile/sharedfile.go @@ -0,0 +1,288 @@ +package sharedfile + +import ( + "io" + "io/fs" + "sync" + "sync/atomic" + "time" + + "github.com/go-git/go-git/v6/x/fdpool" +) + +// ReadAtCloser is the interface a SharedFile manages: any +// ReadAt-capable file with sequential Read+Close support. +type ReadAtCloser interface { + io.ReaderAt + io.ReadCloser +} + +// ErrClosed is returned by Acquire after Close has been called. +// It is an alias for [fs.ErrClosed]; callers may compare against +// either via errors.Is. +var ErrClosed = fs.ErrClosed + +// SharedFile provides refcounted access to a lazily-opened file. +// The underlying [ReadAtCloser] is opened on first Acquire, +// shared across concurrent acquirers, and closed after a grace +// period once the refcount drops to zero. +// +// Lifecycle contract: +// +// - [SharedFile.Acquire] pins the underlying file descriptor +// until the matching [SharedFile.Release]. While at least one +// reference is held the FD cannot be torn down by the grace +// timer or [SharedFile.ReleaseNow]. +// - [SharedFile.Close] is synchronous: it returns only after +// the underlying FD has been closed. Acquires that race a +// Close return [ErrClosed]; ReadAt calls on a descriptor +// handed out before Close see [fs.ErrClosed] on the next +// read, since the OS-level FD has been released. +// +// When constructed with a non-nil [*fdpool.Pool], the grace timer +// is bypassed at refs==0: the FD stays open and registered, and +// the pool decides when to evict via [SharedFile.ReleaseNow]. +// This lets a single pool govern the storage-wide FD budget +// across many SharedFiles. +// +// All methods are safe for concurrent use. +type SharedFile struct { + mu sync.Mutex + open func() (ReadAtCloser, error) + gracePeriod time.Duration + pool *fdpool.Pool + poolHandle fdpool.Handle // pool's per-Member token; zero until first Touch + + file ReadAtCloser + refs int + gen uint64 + timer *time.Timer + closed bool + isClosed atomic.Bool + immediateClose bool // set by ReleaseNow when refs>0; consumed by Release +} + +// New returns a new SharedFile that opens files via open and +// closes the descriptor after gracePeriod of idle time. +func New(open func() (ReadAtCloser, error), gracePeriod time.Duration) *SharedFile { + return NewWithPool(open, gracePeriod, nil) +} + +// NewWithPool returns a SharedFile registered with the given +// [*fdpool.Pool]. The pool governs FD eviction across many +// SharedFiles via [SharedFile.ReleaseNow]; the grace timer is +// bypassed at refs==0 so the FD stays open and registered until +// the pool evicts or [SharedFile.Close] is called. Pass nil for +// pool to disable pooling (equivalent to [New]). +func NewWithPool(open func() (ReadAtCloser, error), gracePeriod time.Duration, pool *fdpool.Pool) *SharedFile { + return &SharedFile{open: open, gracePeriod: gracePeriod, pool: pool} +} + +// Acquire bumps the refcount and returns the underlying file, +// opening it via the constructor's open function on first need. +// Each Acquire must be balanced by exactly one Release. +// +// If a pool is configured, every Acquire calls [fdpool.Pool.Touch] +// after the FD is in hand, which registers the SharedFile on first +// open and refreshes its LRU position on every subsequent acquire. +func (s *SharedFile) Acquire() (ReadAtCloser, error) { + s.mu.Lock() + if s.closed { + s.mu.Unlock() + return nil, ErrClosed + } + + if s.timer != nil { + s.timer.Stop() + s.timer = nil + } + + if s.file == nil { + f, err := s.open() + if err != nil { + s.mu.Unlock() + return nil, err + } + s.file = f + } + s.refs++ + s.gen++ + file := s.file + pool := s.pool + s.mu.Unlock() + + // Touch after releasing s.mu: SharedFile never holds s.mu + // while calling into the pool (see Acquire and Close), so + // the inverse Pool→Member locking via Pinned() during + // eviction is deadlock-free. See fdpool/pool.go's eviction + // comment for the full invariant. + if pool != nil { + pool.Touch(s, &s.poolHandle) + } + return file, nil +} + +// Release decrements the refcount. When it reaches zero the +// grace-period timer is started; the file is closed when the +// timer fires unless another Acquire happens first. +// +// If a pool is configured, the grace timer is skipped at refs==0: +// the FD stays open and registered. The pool drives the eventual +// close via [SharedFile.ReleaseNow] when capacity is exceeded. +func (s *SharedFile) Release() { + s.mu.Lock() + defer s.mu.Unlock() + + if s.refs == 0 { + return + } + s.refs-- + s.gen++ + + if s.refs > 0 || s.closed || s.file == nil { + return + } + + // Soft-close via ReleaseNow latches immediateClose; fire that + // inline now instead of scheduling the grace timer. The flag + // clears on the close transition, restoring normal grace-timer + // behaviour for future Releases. + if s.immediateClose { + s.immediateClose = false + _ = s.file.Close() + s.file = nil + return + } + + // Pool drives eviction: keep the FD open and registered so the + // pool's LRU governs when it closes. No timer. + if s.pool != nil { + return + } + + gen := s.gen + s.timer = time.AfterFunc(s.gracePeriod, func() { + s.mu.Lock() + defer s.mu.Unlock() + // Discard if state advanced since this timer was scheduled. + if s.closed || s.gen != gen || s.refs > 0 || s.file == nil { + return + } + _ = s.file.Close() + s.file = nil + s.timer = nil + }) +} + +// IsClosed reports whether Close has been called. Cursors and +// other downstream readers can use this to short-circuit reads +// after teardown without depending on the underlying +// ReadAtCloser's post-Close error semantics. +func (s *SharedFile) IsClosed() bool { return s.isClosed.Load() } + +// Pinned reports whether the SharedFile has active acquirers +// (refs > 0). Implements [fdpool.Pinnable] so a Pool can prefer +// unpinned victims when capacity is exceeded; pinned SharedFiles +// are still evictable as a fallback when every Member is pinned. +// +// The reported state is observational — refs may transition the +// instant Pinned returns. The pool's eviction policy treats the +// answer as a hint. +func (s *SharedFile) Pinned() bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.refs > 0 +} + +// Compile-time assertion that SharedFile satisfies +// [fdpool.Pinnable]; statically anchors the interface binding +// so a future signature drift on either side breaks the build +// rather than degrading to non-Pinnable fallback at runtime. +var _ fdpool.Pinnable = (*SharedFile)(nil) + +// Close stops any pending grace timer and closes the underlying +// file synchronously. Subsequent Acquire calls return +// [ErrClosed]. Close is idempotent. +// +// If a pool is configured, the SharedFile is forgotten from the +// pool's LRU before Close returns, so a racing eviction cannot +// observe a freed Member. +func (s *SharedFile) Close() error { + s.mu.Lock() + if s.closed { + s.mu.Unlock() + return nil + } + s.closed = true + s.isClosed.Store(true) + s.gen++ + + if s.timer != nil { + s.timer.Stop() + s.timer = nil + } + + var err error + if s.file != nil { + err = s.file.Close() + s.file = nil + } + pool := s.pool + s.mu.Unlock() + + if pool != nil { + pool.Forget(&s.poolHandle) + } + return err +} + +// ReleaseNow closes the underlying file without marking the +// [SharedFile] permanently closed. The next [SharedFile.Acquire] +// reopens via the constructor's open function. +// +// The FD closes inline when refs==0, bypassing the grace timer. +// When refs>0 the SharedFile latches an immediate-close flag: +// in-flight readers complete normally, the FD closes the instant +// the last [SharedFile.Release] drops refs to zero, and +// subsequent Acquires reopen and resume normal grace-timer +// behaviour. +// +// Idempotent and safe to call concurrently. A no-op on a +// SharedFile already permanently closed (returns nil); the +// terminal [SharedFile.Close] path has already disposed the FD. +// ReleaseNow never sets s.closed. +// +// The returned error covers only the inline-close case (refs==0). +// When the latch fires via a subsequent Release, any error from +// the deferred Close is discarded — Release has no return value +// and the original ReleaseNow caller is no longer on the stack. +func (s *SharedFile) ReleaseNow() error { + s.mu.Lock() + defer s.mu.Unlock() + + if s.closed { + return nil + } + + // Cancel any pending grace-period close and invalidate any + // already-queued timer callback via the gen bump. + if s.timer != nil { + s.timer.Stop() + s.timer = nil + } + s.gen++ + + if s.refs == 0 { + if s.file == nil { + return nil + } + err := s.file.Close() + s.file = nil + return err + } + + // refs > 0: latch immediate close for the next refs == 0 + // transition. In-flight readers complete normally. + s.immediateClose = true + return nil +} diff --git a/vendor/github.com/go-git/go-git/v6/internal/transport/transport.go b/vendor/github.com/go-git/go-git/v6/internal/transport/transport.go new file mode 100644 index 000000000..9252b4654 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/internal/transport/transport.go @@ -0,0 +1,39 @@ +// Package transport holds transport internals shared across go-git's transport +// implementations (the public facade lives in plumbing/transport, which aliases +// the exported types here). It is not part of go-git's public API. +package transport + +import ( + "github.com/go-git/go-git/v6/plumbing" + "github.com/go-git/go-git/v6/plumbing/protocol/packp" + "github.com/go-git/go-git/v6/plumbing/protocol/packp/sideband" +) + +// FetchRequest describes a fetch. It is shared by the v0/v1 and v2 fetch paths; +// plumbing/transport aliases it as transport.FetchRequest. +type FetchRequest struct { + // Progress is the progress sideband. + Progress sideband.Progress + + // Wants is the list of object hashes the client wants to fetch. + // The caller selects which remote refs to fetch (refspec matching) + // and extracts their hashes. + Wants []plumbing.Hash + + // Haves is the list of object hashes the client already has. + // TODO: The transport should compute haves internally from the + // storer during pack negotiation, matching how canonical git's + // fetch-pack walks the local object graph to determine common + // ancestors. Once implemented, remove this field. + Haves []plumbing.Hash + + // Depth is the depth of the fetch. + Depth int + + // Filter holds the filters to be applied when deciding what + // objects will be added to the packfile. + Filter packp.Filter + + // IncludeTags indicates whether tags should be fetched. + IncludeTags bool +} diff --git a/vendor/github.com/go-git/go-git/v6/internal/transport/v2.go b/vendor/github.com/go-git/go-git/v6/internal/transport/v2.go new file mode 100644 index 000000000..1b79c9e6b --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/internal/transport/v2.go @@ -0,0 +1,317 @@ +package transport + +import ( + "context" + "errors" + "fmt" + "io" + "slices" + "strings" + + "github.com/go-git/go-git/v6/plumbing" + "github.com/go-git/go-git/v6/plumbing/format/packfile" + "github.com/go-git/go-git/v6/plumbing/protocol/capability" + "github.com/go-git/go-git/v6/plumbing/protocol/packp" + "github.com/go-git/go-git/v6/plumbing/protocol/packp/sideband" + "github.com/go-git/go-git/v6/storage" + "github.com/go-git/go-git/v6/utils/ioutil" +) + +// ErrNoChange is returned by FetchV2 when every wanted object is already present +// in the client's haves and no shallow change was requested, mirroring git's +// everything_local short-circuit in do_fetch_pack_v2. transport.ErrNoChange +// aliases this value. +var ErrNoChange = errors.New("no change") + +// Negotiation pacing, mirroring git's fetch-pack.c (INITIAL_FLUSH, LARGE_FLUSH, +// MAX_IN_VAIN). v2 is stateless per command, so the stateless schedule applies. +const ( + initialFlush = 16 + largeFlush = 16384 + maxInVain = 256 +) + +func nextFlush(count int) int { + if count < largeFlush { + return count << 1 + } + return count * 11 / 10 +} + +// CommandFunc runs a single Protocol v2 command: it encodes req into the +// request and decodes the response via resp. A session's Command method +// satisfies this signature, so the shared v2 helpers stay decoupled from the +// public transport.Commander interface (and the import cycle it would create). +type CommandFunc func(ctx context.Context, cmd string, req packp.CommandArgs, resp packp.Decoder) error + +// ClientCapabilities returns the capabilities a v2 client sends with each +// command: the agent and the server's object-format echoed back so both sides +// agree on the hash algorithm. server is the capability advertisement the +// server sent during the handshake. +// +// Both are gated on the server having advertised them: upstream git only sends +// agent when the server advertised agent, and object-format when the server +// advertised object-format (fetch-pack.c). This keeps the client a conformant +// peer that never sends a capability the server did not offer. +func ClientCapabilities(server capability.List) capability.List { + var caps capability.List + if agent := server.Get(capability.Agent); len(agent) > 0 { + caps.Set(capability.Agent, capability.DefaultAgent()) + } + if of := server.Get(capability.ObjectFormat); len(of) > 0 { + caps.Set(capability.ObjectFormat, of[0]) + } + return caps +} + +// FetchSupports reports whether the server advertised the given fetch feature +// in its v2 capability advertisement (fetch=...). +func FetchSupports(server capability.List, feature string) bool { + for _, v := range server.Get(capability.FetchCmd) { + if slices.Contains(strings.Fields(v), feature) { + return true + } + } + return false +} + +// lsRefsSupportsUnborn reports whether the server advertised the ls-refs +// "unborn" feature (ls-refs=unborn). +func lsRefsSupportsUnborn(server capability.List) bool { + return slices.Contains(server.Get(capability.LsRefs), "unborn") +} + +// LsRefs lists references using the v2 ls-refs command run through cmd. It +// always requests peeled tags and symref targets so HEAD resolves to its +// branch, and requests unborn HEAD reporting when the server advertises it. The +// returned references include a symbolic HEAD (and an unborn HEAD as a symref +// whose target has no hash reference) so callers can detect an unborn branch. +func LsRefs(ctx context.Context, cmd CommandFunc, server capability.List, refPrefixes []string) ([]*plumbing.Reference, error) { + args := &packp.LsRefsArgs{ + Peel: true, + Symrefs: true, + Unborn: lsRefsSupportsUnborn(server), + RefPrefixes: refPrefixes, + } + + out := &packp.LsRefsOutput{} + if err := cmd(ctx, "ls-refs", args, out); err != nil { + return nil, err + } + + return out.References, nil +} + +// HasHashRef reports whether refs contains at least one hash (non-symbolic) +// reference. A v2 ls-refs result with only a symbolic or unborn HEAD and no +// hash references corresponds to an empty repository, so callers treat the +// absence of hash references the same as the v0/v1 empty advertisement. +func HasHashRef(refs []*plumbing.Reference) bool { + for _, r := range refs { + if r.Type() == plumbing.HashReference { + return true + } + } + return false +} + +// wantsLocal reports whether every wanted object is already present in haves, so +// the fetch has nothing to retrieve. +func wantsLocal(wants, haves []plumbing.Hash) bool { + if len(wants) == 0 { + return false + } + have := make(map[plumbing.Hash]struct{}, len(haves)) + for _, h := range haves { + have[h] = struct{}{} + } + for _, w := range wants { + if _, ok := have[w]; !ok { + return false + } + } + return true +} + +// FetchRound runs a single fetch command round. When the returned output has +// Packfile set, packReader is positioned at the first packfile pkt-line so the +// caller can stream it. If packReader implements io.Closer, Fetch closes it +// once the round is done (each HTTP round owns a response body; a stream +// transport returns its persistent reader, which is not a Closer). +type FetchRound func(args *packp.FetchArgs) (out *packp.FetchOutput, packReader io.Reader, err error) + +// FetchV2 drives the v2 fetch negotiation and streams the resulting packfile into +// st. It mirrors git's do_fetch_pack_v2: each round sends the wants, the common +// commits acked so far, and a growing batch of haves, until the server reports +// "ready" (the packfile follows in the same response) or the client runs out of +// haves and sends "done". The packfile (always sideband-64k muxed in v2) is +// streamed here, and any shallow-info from the response is applied to st. +// +// The caller is responsible for validating optional features against the server +// advertisement (see FetchSupports) before requesting Filter or Depth. +func FetchV2(ctx context.Context, st storage.Storer, req *FetchRequest, round FetchRound) error { + // Everything wanted is already local and no shallow change was requested: + // short-circuit before opening negotiation, matching git's everything_local. + if req.Depth == 0 && wantsLocal(req.Wants, req.Haves) { + return ErrNoChange + } + + baseArgs := &packp.FetchArgs{ + Wants: req.Wants, + OFSDelta: true, + NoProgress: req.Progress == nil, + IncludeTag: req.IncludeTags, + } + if req.Filter != "" { + baseArgs.Filter = req.Filter + } + if req.Depth > 0 { + baseArgs.Deepen = req.Depth + shallows, err := st.Shallow() + if err != nil { + return err + } + baseArgs.Shallows = shallows + } + + // Pop haves from a private copy so the caller's slice is left untouched. + remaining := append([]plumbing.Hash(nil), req.Haves...) + var common []plumbing.Hash + seen := make(map[plumbing.Hash]struct{}) + havesToSend := initialFlush + inVain := 0 + seenAck := false + + var shallowInfo *packp.ShallowUpdate + for { + args := *baseArgs + // v2 fetch is stateless per command: re-send every common commit acked + // so far, then a fresh batch of haves. + roundHaves := append([]plumbing.Hash(nil), common...) + batch := 0 + for batch < havesToSend && len(remaining) > 0 { + roundHaves = append(roundHaves, remaining[len(remaining)-1]) + remaining = remaining[:len(remaining)-1] + batch++ + inVain++ + } + havesToSend = nextFlush(havesToSend) + args.Haves = roundHaves + args.Done = batch == 0 || (seenAck && inVain >= maxInVain) + + out, packReader, err := round(&args) + if err != nil { + return err + } + + if out.ShallowInfo != nil { + shallowInfo = &packp.ShallowUpdate{ + Shallows: out.ShallowInfo.Shallows, + Unshallows: out.ShallowInfo.Unshallows, + } + } + + if out.Acknowledgments != nil { + for _, ack := range out.Acknowledgments.ACKs { + if _, ok := seen[ack]; !ok { + seen[ack] = struct{}{} + common = append(common, ack) + } + seenAck = true + inVain = 0 + } + } + + if out.Packfile { + streamErr := streamPackfile(ctx, st, packReader, req.Progress) + // Skip draining/closing on cancellation: streamPackfile wraps + // packReader in a NewContextReader, whose background goroutine + // can still be blocked in the underlying Read after the + // <-ctx.Done() branch returns (see its doc comment). Closing + // here would race that goroutine's in-flight Read -- the same + // class of race fixed in plumbing/transport/http's Fetch/Push + // for the v0/v1 path. On a non-cancellation error or success, + // streamPackfile's last Read has already returned via the + // result channel and the goroutine is quiescent, so closing is + // safe and necessary. On cancellation, a packReader backed by + // a context-bound request (e.g. HTTP) gets torn down by that + // request's own context handling instead, so it is not leaked. + // + // Checked against streamErr itself, not ctx.Err(): ctx can be + // cancelled an instant after a successful/non-cancel return, + // and checking ctx.Err() at that point would skip the close + // for a read that was already fully quiescent. + if !errors.Is(streamErr, context.Canceled) && !errors.Is(streamErr, context.DeadlineExceeded) { + closeReader(packReader) + } + if streamErr != nil { + return streamErr + } + break + } + + closeReader(packReader) + if args.Done { + return fmt.Errorf("transport: server sent no packfile after done") + } + } + + if shallowInfo != nil { + if err := updateShallow(st, shallowInfo); err != nil { + return err + } + } + + return nil +} + +// streamPackfile demultiplexes the sideband-64k packfile stream into st. +func streamPackfile(ctx context.Context, st storage.Storer, packReader io.Reader, progress sideband.Progress) error { + reader := ioutil.NewContextReader(ctx, packReader) + demuxer := sideband.NewDemuxer(sideband.Sideband64k, reader) + if progress != nil { + demuxer.Progress = progress + } + return packfile.UpdateObjectStorage(st, demuxer) +} + +// closeReader drains and closes r when it owns a closable resource (such as an +// HTTP response body). Draining any unread bytes (e.g. the v2 response-end +// pkt-line) before Close lets net/http reuse the connection across negotiation +// rounds. Persistent stream readers do not implement io.Closer and are left +// open for the next round. +func closeReader(r io.Reader) { + if c, ok := r.(io.Closer); ok { + _, _ = io.Copy(io.Discard, r) + _ = c.Close() + } +} + +// updateShallow merges a shallow-info update into st's shallow boundary. +func updateShallow(st storage.Storer, info *packp.ShallowUpdate) error { + shallows, err := st.Shallow() + if err != nil { + return err + } + +outer: + for _, s := range info.Shallows { + for _, old := range shallows { + if s == old { + continue outer + } + } + shallows = append(shallows, s) + } + + for _, s := range info.Unshallows { + for i, old := range shallows { + if s == old { + shallows = append(shallows[:i], shallows[i+1:]...) + break + } + } + } + + return st.SetShallow(shallows) +} diff --git a/vendor/github.com/go-git/go-git/v6/object_walker.go b/vendor/github.com/go-git/go-git/v6/object_walker.go index a770a1328..68904935e 100644 --- a/vendor/github.com/go-git/go-git/v6/object_walker.go +++ b/vendor/github.com/go-git/go-git/v6/object_walker.go @@ -15,10 +15,30 @@ type objectWalker struct { // seen map can become huge if walking over large // repos. Thus using struct{} as the value type. seen map[plumbing.Hash]struct{} + // shallows is the set of shallow roots, loaded lazily + // on the first commit walked. + shallows map[plumbing.Hash]struct{} } func newObjectWalker(s storage.Storer) *objectWalker { - return &objectWalker{s, map[plumbing.Hash]struct{}{}} + return &objectWalker{Storer: s, seen: map[plumbing.Hash]struct{}{}} +} + +// isShallow reports whether hash is a shallow root, meaning its +// parents are not present in the repository. +func (p *objectWalker) isShallow(hash plumbing.Hash) (bool, error) { + if p.shallows == nil { + shallows, err := p.Storer.Shallow() + if err != nil { + return false, err + } + p.shallows = make(map[plumbing.Hash]struct{}, len(shallows)) + for _, h := range shallows { + p.shallows[h] = struct{}{} + } + } + _, ok := p.shallows[hash] + return ok, nil } // walkAllRefs walks all (hash) references from the repo. @@ -60,7 +80,7 @@ func (p *objectWalker) walkObjectTree(hash plumbing.Hash) error { // Fetch the object. obj, err := object.GetObject(p.Storer, hash) if err != nil { - return fmt.Errorf("getting object %s failed: %v", hash, err) + return fmt.Errorf("getting object %s failed: %w", hash, err) } // Walk all children depending on object type. switch obj := obj.(type) { @@ -69,6 +89,15 @@ func (p *objectWalker) walkObjectTree(hash plumbing.Hash) error { if err != nil { return err } + // Parents of a shallow root are not present in the + // repository, so don't attempt to walk them. + shallow, err := p.isShallow(obj.ID()) + if err != nil { + return err + } + if shallow { + break + } for _, h := range obj.ParentHashes { err = p.walkObjectTree(h) if err != nil { diff --git a/vendor/github.com/go-git/go-git/v6/options.go b/vendor/github.com/go-git/go-git/v6/options.go index 1b6691cb8..8ad00669e 100644 --- a/vendor/github.com/go-git/go-git/v6/options.go +++ b/vendor/github.com/go-git/go-git/v6/options.go @@ -464,7 +464,7 @@ type ResetOptions struct { // Validate validates the fields and sets the default values. func (o *ResetOptions) Validate(r *Repository) error { - if o.Commit == plumbing.ZeroHash { + if o.Commit.IsZero() { ref, err := r.Head() if err != nil { return err @@ -813,6 +813,10 @@ type PlainOpenOptions struct { // DetectDotGit defines whether parent directories should be // walked until a .git directory or file is found. DetectDotGit bool + // AlternatesFS provides the billy filesystem to be used for Git Alternates. + // If none is provided, it falls back to the repository's .git directory + // filesystem. + AlternatesFS billy.Filesystem } // Validate validates the fields and sets the default values. diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/filemode/filemode.go b/vendor/github.com/go-git/go-git/v6/plumbing/filemode/filemode.go index 64fa677ce..4dd77ba71 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/filemode/filemode.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/filemode/filemode.go @@ -65,6 +65,26 @@ func New(s string) (FileMode, error) { return FileMode(n), nil } +// FromBytes parses a FileMode from a byte slice containing an octal +// number. This avoids allocating a string when parsing from binary data. +// +// Please note this function does not check if the returned FileMode +// is valid in git or if it is malformed. +func FromBytes(b []byte) (FileMode, error) { + if len(b) == 0 || len(b) > 7 { + return Empty, fmt.Errorf("invalid mode length: %d", len(b)) + } + + var mode uint32 + for _, c := range b { + if c < '0' || c > '7' { + return Empty, fmt.Errorf("invalid octal character: %c", c) + } + mode = mode*8 + uint32(c-'0') + } + return FileMode(mode), nil +} + // NewFromOSFileMode returns the FileMode used by git to represent // the provided file system modes and a nil error on success. If the // file system mode cannot be mapped to any valid git mode (as with diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/diff/unified_encoder.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/diff/unified_encoder.go index 5650bd84e..adf29e77b 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/diff/unified_encoder.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/diff/unified_encoder.go @@ -3,7 +3,7 @@ package diff import ( "fmt" "io" - "regexp" + "slices" "strconv" "strings" @@ -14,8 +14,6 @@ import ( const DefaultContextLines = 3 var ( - splitLinesRegexp = regexp.MustCompile(`[^\n]*(\n|$)`) - operationChar = map[Operation]byte{ Add: '+', Delete: '-', @@ -297,7 +295,11 @@ func (g *hunksGenerator) processEqualsLines(ls []string, i int) { } func splitLines(s string) []string { - out := splitLinesRegexp.FindAllString(s, -1) + // strings.SplitAfter keeps the trailing newline on each line, matching the + // previous `[^\n]*(\n|$)` regexp but without the backtracking cost — line + // splitting dominated the unified-diff encode hot path. A string ending in + // "\n" yields a trailing "" element, which we drop to preserve behaviour. + out := strings.SplitAfter(s, "\n") if out[len(out)-1] == "" { out = out[:len(out)-1] } @@ -366,6 +368,7 @@ func (h *hunk) AddOp(t Operation, ss ...string) { h.fromCount += n } + h.ops = slices.Grow(h.ops, n) for _, s := range ss { h.ops = append(h.ops, &op{s, t}) } diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/gitignore/matcher.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/gitignore/matcher.go index bd1e9e2d4..c9251cba3 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/gitignore/matcher.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/gitignore/matcher.go @@ -1,16 +1,21 @@ package gitignore -// Matcher defines a global multi-pattern matcher for gitignore patterns +// Matcher defines a global multi-pattern matcher for gitignore patterns. type Matcher interface { - // Match matches patterns in the order of priorities. As soon as an inclusion or - // exclusion is found, not further matching is performed. + // Match reports whether path is excluded by the highest-priority matching + // pattern. Path is an ordered sequence of logical path components. Patterns + // created with ParsePattern match only paths beginning with their domain. + // isDir reports whether the final path component is a directory. For a + // pattern ending in "/", isDir only restricts a match at the candidate + // endpoint; descendants of a matched directory may still match. Match(path []string, isDir bool) bool } -// NewMatcher constructs a new global matcher. Patterns must be given in the order of -// increasing priority. That is most generic settings files first, then the content of -// the repo .gitignore, then content of .gitignore down the path or the repo and then -// the content command line arguments. +// NewMatcher constructs a new global matcher from patterns in increasing +// priority order. Match evaluates them from last to first and uses the first +// Exclude or Include result. Generic settings files should come first, followed +// by the repository .gitignore, .gitignore files in successively deeper +// directories, and command-line arguments. func NewMatcher(ps []Pattern) Matcher { return &matcher{ps} } diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/gitignore/pattern.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/gitignore/pattern.go index ff1609b01..105c2f0ec 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/gitignore/pattern.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/gitignore/pattern.go @@ -24,7 +24,12 @@ const ( // Pattern defines a single gitignore pattern. type Pattern interface { - // Match matches the given path to the pattern. + // Match reports how the pattern applies to path. Path is an ordered sequence + // of logical path components. Patterns created with ParsePattern match only + // paths beginning with their domain. isDir reports whether the final path + // component is a directory. For a pattern ending in "/", isDir only + // restricts a match at the candidate endpoint; descendants of a matched + // directory may still match. Match(path []string, isDir bool) MatchResult } @@ -36,7 +41,14 @@ type pattern struct { isGlob bool } -// ParsePattern parses a gitignore pattern string into the Pattern structure. +// ParsePattern parses a gitignore pattern string into a Pattern. The domain is +// an ordered prefix of logical path components that scopes the pattern. +// Matching applies to the components after that prefix. A nil or empty domain +// applies the pattern without a prefix. +// +// ReadPatterns uses the path of the directory containing a .gitignore file as +// its domain. When the filesystem is rooted at a repository, that path is +// repository-relative. func ParsePattern(p string, domain []string) Pattern { // storing domain, copy it to ensure it isn't changed externally domain = append([]string(nil), domain...) @@ -490,11 +502,14 @@ func (p *pattern) globMatch(path []string, isDir bool) bool { } if pattern == zeroToManyDirs { if i == len(p.pattern)-1 { - // Trailing ** matches everything remaining (if there's something left or it's a dir) - if len(path) > 0 || isDir { - matched = true - trailingStar = true - } + // A trailing `**` matches the entries below whatever the + // earlier segments consumed, so it needs either a remaining + // component or a directory candidate standing in for them. + // Assigning matched rather than only raising it stops an + // exhausted path from inheriting the previous segment's + // result, which would make `a/**/*/**` match `a/f.txt`. + matched = len(path) > 0 || isDir + trailingStar = matched break } canTraverse = true @@ -513,9 +528,14 @@ func (p *pattern) globMatch(path []string, isDir bool) bool { if wildmatch(pattern, e) { matched = true break - } else if len(path) == 0 { - // if nothing left then fail - matched = false + } + if len(path) == 0 { + // A `**` that never finds the segment following it is a + // definitive non-match. Returning here rather than + // clearing matched keeps a trailing `**` from reviving + // the pattern once the path is exhausted, which would + // make `**/bar/**` match directories containing no bar. + return false } } } else { diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/idxfile/idxfile.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/idxfile/idxfile.go index b7695b7dc..cbce3210e 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/idxfile/idxfile.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/idxfile/idxfile.go @@ -1,6 +1,7 @@ package idxfile import ( + "bytes" "crypto" encbin "encoding/binary" "fmt" @@ -21,6 +22,14 @@ const ( var idxHeader = []byte{255, 't', 'O', 'c'} // Index represents an index of a packfile. +// +// Implementations satisfy a [io.Closer] contract via [Index.Close]: +// on-disk implementations release file descriptors, pure +// in-memory implementations return nil. Downstream callers +// holding their own concrete [Index] implementations must +// supply a [Close] method to satisfy this interface; a no-op +// `func (*MyIndex) Close() error { return nil }` is sufficient +// for in-memory backends. type Index interface { // Contains checks whether the given hash is in the index. Contains(h plumbing.Hash) (bool, error) @@ -38,6 +47,28 @@ type Index interface { // EntriesByOffset returns an iterator to retrieve all index entries ordered // by offset. EntriesByOffset() (EntryIter, error) + // EntriesWithPrefix returns an iterator over index entries whose + // hashes start with prefix. Implementations use the fanout table + // to bound the search when len(prefix) >= 1; an empty prefix + // returns all entries (equivalent to Entries). The returned + // iterator must be Closed by the caller to release any held + // resources. + EntriesWithPrefix(prefix []byte) (EntryIter, error) + // MayContain reports whether the index might contain h. A false + // return is authoritative ("h is definitely not in this pack") + // based on the idx fanout table; true means the caller should + // call Contains or FindOffset for a definitive answer. + // + // Implementations must be O(1) and I/O-free. Callers route + // every read through MayContain to gate further index work + // (see storage/filesystem.ObjectStorage.findObjectInPackfile); + // an implementation that performs I/O or scales with index + // size silently regresses every storage-level read. + MayContain(h plumbing.Hash) bool + // Close releases any resources held by the index. Implementations + // backed by on-disk files must close their file descriptors; pure + // in-memory implementations must return nil. Close is idempotent. + Close() error } // MemoryIndex is the in memory representation of an idx file. @@ -75,6 +106,9 @@ type MemoryIndex struct { var _ Index = (*MemoryIndex)(nil) +// Close is a no-op. MemoryIndex holds no external resources. +func (idx *MemoryIndex) Close() error { return nil } + // NewMemoryIndex returns an instance of a new MemoryIndex. func NewMemoryIndex(objectIDSize int) *MemoryIndex { m := &MemoryIndex{objectIDSize: objectIDSize} @@ -122,6 +156,13 @@ func (idx *MemoryIndex) findHashIndex(h plumbing.Hash) (int, bool) { return 0, false } +// MayContain implements the Index interface. It reports whether the +// index might contain h using the in-memory fanout mapping. Returns +// false iff h's first byte falls in an empty fanout bucket. +func (idx *MemoryIndex) MayContain(h plumbing.Hash) bool { + return idx.FanoutMapping[h.Bytes()[0]] != noMapping +} + // Contains implements the Index interface. func (idx *MemoryIndex) Contains(h plumbing.Hash) (bool, error) { _, ok := idx.findHashIndex(h) @@ -270,6 +311,57 @@ func (idx *MemoryIndex) Entries() (EntryIter, error) { return &idxfileEntryIter{idx, 0, 0, 0}, nil } +// EntriesWithPrefix implements the Index interface. It returns an +// iterator over entries whose hashes start with prefix. When prefix +// is empty the call is equivalent to Entries; otherwise the +// iterator visits only the fanout bucket selected by prefix[0] and +// stops as soon as the sorted-by-hash bucket walks past prefix. +// +// For a multi-byte prefix the matching entries form a contiguous +// run somewhere within the bucket; binary-search positions the +// iterator at the start of that run so the linear walk only spans +// matches. This mirrors upstream Git's for_each_prefixed_object_in_pack +// which calls bsearch_pack to position before walking forward. +func (idx *MemoryIndex) EntriesWithPrefix(prefix []byte) (EntryIter, error) { + if len(prefix) == 0 { + return idx.Entries() + } + bucket := idx.FanoutMapping[prefix[0]] + if bucket == noMapping { + return &idxfilePrefixIter{done: true}, nil + } + idSize := idx.idSize() + names := idx.Names[bucket] + n := len(names) / idSize + + // Find the leftmost entry whose hash is >= prefix (padded with + // zeros to hash size). All matching entries, if any, start at + // this position; the iterator's stop-on-first-mismatch then + // terminates correctly once the run ends. + target := make([]byte, idSize) + copy(target, prefix) + lo, hi := 0, n + for lo < hi { + mid := (lo + hi) >> 1 + slot := names[mid*idSize : (mid+1)*idSize] + if bytes.Compare(slot, target) < 0 { + lo = mid + 1 + } else { + hi = mid + } + } + + return &idxfilePrefixIter{ + idSize: idSize, + prefix: prefix, + names: names, + offset32: idx.Offset32[bucket], + crc32: idx.CRC32[bucket], + offset64: idx.Offset64, + pos: lo, + }, nil +} + // EntriesByOffset implements the Index interface. func (idx *MemoryIndex) EntriesByOffset() (EntryIter, error) { count, err := idx.Count() @@ -359,6 +451,93 @@ func (i *idxfileEntryIter) Close() error { return nil } +// idxfilePrefixIter walks a single fanout bucket, yielding entries +// whose hash starts with prefix. The bucket is sorted by hash, so +// once a name is read whose first bytes do not match prefix the +// iterator stops. +// +// The iterator references the bucket's per-slot slices directly +// (names, offset32, crc32) plus the shared offset64 table, so it +// does not retain a reference to the parent MemoryIndex. This keeps +// the iterator footprint to just the cursor state and the slice +// headers it actually reads from. +// +// Lifetime: the slice headers are views into the parent +// MemoryIndex's per-bucket storage. The iterator is invalid after +// the parent Index is closed or reindexed — callers must consume +// (or Close) the iterator before discarding the Index. +type idxfilePrefixIter struct { + idSize int + prefix []byte + names []byte // bucket's hash bytes + offset32 []byte // bucket's 32-bit offset table + crc32 []byte // bucket's CRC32 table + offset64 []byte // shared 64-bit offset overflow table + pos int // entries already yielded + done bool +} + +func (i *idxfilePrefixIter) Next() (*Entry, error) { + if i.done { + return nil, io.EOF + } + + offset := i.pos * i.idSize + if offset+i.idSize > len(i.names) { + i.done = true + return nil, io.EOF + } + hashBytes := i.names[offset : offset+i.idSize] + if !bytes.HasPrefix(hashBytes, i.prefix) { + // Bucket is sorted by hash, so the first mismatch ends the run. + i.done = true + return nil, io.EOF + } + + entry := new(Entry) + entry.Hash.ResetBySize(i.idSize) + if _, err := entry.Hash.Write(hashBytes); err != nil { + return nil, fmt.Errorf("cannot write entry hash: %w", err) + } + + o, err := i.bucketOffset(i.pos) + if err != nil { + return nil, err + } + entry.Offset = o + entry.CRC32 = i.bucketCRC32(i.pos) + i.pos++ + return entry, nil +} + +// bucketOffset mirrors MemoryIndex.getOffset using only the per- +// bucket Offset32/Offset64 slices the iterator holds, so callers +// do not need to retain a reference to the parent MemoryIndex. +func (i *idxfilePrefixIter) bucketOffset(pos int) (uint64, error) { + off := pos << 2 + ofs := encbin.BigEndian.Uint32(i.offset32[off : off+4]) + if (uint64(ofs) & isO64Mask) != 0 { + o64 := 8 * (uint64(ofs) & ^isO64Mask) + if l := uint64(len(i.offset64)); l < 8 || o64 > l-8 { + return 0, fmt.Errorf("%w: offset64 index out of range", ErrMalformedIdxFile) + } + return encbin.BigEndian.Uint64(i.offset64[o64 : o64+8]), nil + } + return uint64(ofs), nil +} + +// bucketCRC32 mirrors MemoryIndex.getCRC32 using only the per-bucket +// CRC32 slice the iterator holds. +func (i *idxfilePrefixIter) bucketCRC32(pos int) uint32 { + off := pos << 2 + return encbin.BigEndian.Uint32(i.crc32[off : off+4]) +} + +func (i *idxfilePrefixIter) Close() error { + i.done = true + return nil +} + // Entry is the in memory representation of an object entry in the idx file. type Entry struct { Hash plumbing.Hash diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/idxfile/lazy_index.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/idxfile/lazy_index.go index c5bfb6554..20d464f16 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/idxfile/lazy_index.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/idxfile/lazy_index.go @@ -6,11 +6,16 @@ import ( "errors" "fmt" "io" + "time" + "github.com/go-git/go-git/v6/internal/sharedfile" "github.com/go-git/go-git/v6/plumbing" gsync "github.com/go-git/go-git/v6/utils/sync" + "github.com/go-git/go-git/v6/x/fdpool" ) +const defaultCloseGracePeriod = time.Second + const ( idxHeaderSize = 8 // 4 magic + 4 version idxFanoutSize = 256 * 4 @@ -22,17 +27,9 @@ const ( ) // ReadAtCloser is the interface required for files used by LazyIndex. -// It combines random-access reads with sequential read/seek/close. -// [billy.File] satisfies this interface. -type ReadAtCloser interface { - io.ReaderAt - io.ReadCloser -} - -// openFileFunc opens a file for reading. Each call must return a fresh, -// independently closeable handle. The caller is responsible for closing -// the returned ReadAtCloser. -type openFileFunc func() (ReadAtCloser, error) +// It is an alias for [sharedfile.ReadAtCloser]; both names refer +// to the same type at compile time. +type ReadAtCloser = sharedfile.ReadAtCloser // LazyIndex implements the Index interface by reading directly from // .idx and .rev files via ReadAt, without loading all data into memory. @@ -54,8 +51,8 @@ type LazyIndex struct { off32Start int off64Start int - idx *sharedFile - rev *sharedFile + idx *sharedfile.SharedFile + rev *sharedfile.SharedFile fanout [256]uint32 // cached from idx; small enough to keep in memory } @@ -70,6 +67,29 @@ var _ Index = (*LazyIndex)(nil) // are shared across concurrent readers and released automatically when // idle. func NewLazyIndex(openIdx, openRev func() (ReadAtCloser, error), packHash plumbing.Hash) (*LazyIndex, error) { + return NewLazyIndexWithPool(openIdx, openRev, packHash, nil) +} + +// NewLazyIndexWithPool is like [NewLazyIndex] but registers the +// idx and rev [sharedfile.SharedFile]s with the given +// [*fdpool.Pool]. The pool governs LRU eviction across many +// LazyIndexes so a storage-wide FD budget covers the .idx and +// .rev descriptors. Pass nil to disable pooling (equivalent to +// [NewLazyIndex]). +// +// When pool is non-nil the [defaultCloseGracePeriod] timer is +// inert: each FD stays open and registered with the pool until +// the LRU evicts it (or [LazyIndex.Close] tears it down). When +// pool is nil the grace timer governs FD lifetime as in +// [NewLazyIndex]. +// +// Neither this constructor nor the [Index] methods accept a +// [context.Context]. Index lookups are pure ReadAt I/O without +// cancellation hooks, matching the context-free convention of +// the storage, plumbing/format, and plumbing/storer layers; +// callers requiring cancellation enforce it at the call-site +// in the layer above. +func NewLazyIndexWithPool(openIdx, openRev func() (ReadAtCloser, error), packHash plumbing.Hash, pool *fdpool.Pool) (*LazyIndex, error) { if openIdx == nil { return nil, errors.New("idx opener is nil") } @@ -78,8 +98,8 @@ func NewLazyIndex(openIdx, openRev func() (ReadAtCloser, error), packHash plumbi } s := &LazyIndex{ - idx: newSharedFile(openIdx), - rev: newSharedFile(openRev), + idx: sharedfile.NewWithPool(openIdx, defaultCloseGracePeriod, pool), + rev: sharedfile.NewWithPool(openRev, defaultCloseGracePeriod, pool), } if err := s.init(packHash); err != nil { @@ -94,17 +114,17 @@ func NewLazyIndex(openIdx, openRev func() (ReadAtCloser, error), packHash plumbi // sharedFile so the grace period keeps them warm for the first real // operation. func (s *LazyIndex) init(packHash plumbing.Hash) error { - idxRA, err := s.idx.acquire() + idxRA, err := s.idx.Acquire() if err != nil { return fmt.Errorf("cannot open idx: %w", err) } - defer s.idx.release() + defer s.idx.Release() - revRA, err := s.rev.acquire() + revRA, err := s.rev.Acquire() if err != nil { return fmt.Errorf("cannot open rev: %w", err) } - defer s.rev.release() + defer s.rev.Release() var hdr [idxHeaderSize]byte if _, err := idxRA.ReadAt(hdr[:], 0); err != nil { @@ -181,24 +201,38 @@ func (s *LazyIndex) init(packHash plumbing.Hash) error { // Contains reports whether the given hash exists in the index by // binary-searching the idx names table. func (s *LazyIndex) Contains(h plumbing.Hash) (bool, error) { - idx, err := s.idx.acquire() + idx, err := s.idx.Acquire() if err != nil { return false, err } - defer s.idx.release() + defer s.idx.Release() _, found, err := s.findHashPos(idx, h) return found, err } +// MayContain implements the Index interface. It reports whether the +// index might contain h, using the cached fanout table loaded at +// construction time. No I/O, no lock. False is authoritative ("h is +// not in this pack"); true means call Contains or FindOffset for a +// definitive answer. +func (s *LazyIndex) MayContain(h plumbing.Hash) bool { + first := int(h.Bytes()[0]) + var prev uint32 + if first > 0 { + prev = s.fanout[first-1] + } + return s.fanout[first] > prev +} + // FindOffset returns the packfile offset for the object with the given hash. // It returns plumbing.ErrObjectNotFound if the hash is not in the index. func (s *LazyIndex) FindOffset(h plumbing.Hash) (int64, error) { - idx, err := s.idx.acquire() + idx, err := s.idx.Acquire() if err != nil { return 0, err } - defer s.idx.release() + defer s.idx.Release() pos, found, err := s.findHashPos(idx, h) if err != nil { @@ -219,11 +253,11 @@ func (s *LazyIndex) FindOffset(h plumbing.Hash) (int64, error) { // FindCRC32 returns the CRC32 checksum of the object with the given hash. // It returns plumbing.ErrObjectNotFound if the hash is not in the index. func (s *LazyIndex) FindCRC32(h plumbing.Hash) (uint32, error) { - idx, err := s.idx.acquire() + idx, err := s.idx.Acquire() if err != nil { return 0, err } - defer s.idx.release() + defer s.idx.Release() pos, found, err := s.findHashPos(idx, h) if err != nil { @@ -240,17 +274,17 @@ func (s *LazyIndex) FindCRC32(h plumbing.Hash) (uint32, error) { // by binary-searching the .rev reverse index. // It returns plumbing.ErrObjectNotFound if no object exists at that offset. func (s *LazyIndex) FindHash(o int64) (plumbing.Hash, error) { - idx, err := s.idx.acquire() + idx, err := s.idx.Acquire() if err != nil { return plumbing.ZeroHash, err } - defer s.idx.release() + defer s.idx.Release() - rev, err := s.rev.acquire() + rev, err := s.rev.Acquire() if err != nil { return plumbing.ZeroHash, err } - defer s.rev.release() + defer s.rev.Release() return s.findHashViaRev(idx, rev, o) } @@ -264,13 +298,83 @@ func (s *LazyIndex) Count() (int64, error) { // The caller must call Close on the returned iterator to release the // underlying file reference. func (s *LazyIndex) Entries() (EntryIter, error) { - idx, err := s.idx.acquire() + idx, err := s.idx.Acquire() if err != nil { return nil, err } return &scannerEntryIter{s: s, idx: idx}, nil } +// EntriesWithPrefix implements the Index interface. It returns an +// iterator over entries whose hashes start with prefix. When prefix +// is empty the call is equivalent to Entries; otherwise the +// iterator visits only the fanout-bounded names-table slice +// selected by prefix[0] and stops as soon as a name without prefix +// is read (the names table is sorted by hash). +// +// For a multi-byte prefix the matching entries form a contiguous +// run somewhere within the bucket; binary-search positions the +// iterator at the start of that run so the linear walk only spans +// matches. This mirrors upstream Git's for_each_prefixed_object_in_pack +// which calls bsearch_pack to position before walking forward. +// +// The returned iterator holds an acquired reference to the idx +// SharedFile which is released on Close. +func (s *LazyIndex) EntriesWithPrefix(prefix []byte) (EntryIter, error) { + if len(prefix) == 0 { + return s.Entries() + } + first := int(prefix[0]) + var lo int + if first > 0 { + lo = int(s.fanout[first-1]) + } + hi := int(s.fanout[first]) + if lo >= hi { + return &lazyPrefixIter{}, nil + } + idx, err := s.idx.Acquire() + if err != nil { + return nil, err + } + + // Find the leftmost entry in [lo, hi) whose hash is >= prefix + // (padded with zeros to hash size). All matching entries, if + // any, start at this position; the iterator's + // stop-on-first-mismatch then terminates correctly once the + // run ends. + target := make([]byte, s.hashSize) + copy(target, prefix) + var arr [32]byte + buf := arr[:s.hashSize] + bsLo, bsHi := lo, hi + for bsLo < bsHi { + mid := (bsLo + bsHi) >> 1 + nameOff := int64(s.namesStart + mid*s.hashSize) + if _, err := idx.ReadAt(buf, nameOff); err != nil { + s.idx.Release() + return nil, fmt.Errorf("read name at pos %d: %w", mid, err) + } + if bytes.Compare(buf, target) < 0 { + bsLo = mid + 1 + } else { + bsHi = mid + } + } + if bsLo >= hi { + s.idx.Release() + return &lazyPrefixIter{}, nil + } + + return &lazyPrefixIter{ + s: s, + idx: idx, + prefix: prefix, + pos: bsLo, + end: hi, + }, nil +} + // EntriesByOffset returns an iterator over all index entries sorted by // their packfile offset. It reads positions from the .rev file on each // call to Next, avoiding any up-front allocation or sorting. @@ -278,13 +382,13 @@ func (s *LazyIndex) Entries() (EntryIter, error) { // The caller must call Close on the returned iterator to release the // underlying file references. func (s *LazyIndex) EntriesByOffset() (EntryIter, error) { - idx, err := s.idx.acquire() + idx, err := s.idx.Acquire() if err != nil { return nil, err } - rev, err := s.rev.acquire() + rev, err := s.rev.Acquire() if err != nil { - s.idx.release() + s.idx.Release() return nil, err } return &revEntryIter{s: s, idx: idx, rev: rev}, nil @@ -297,6 +401,20 @@ func (s *LazyIndex) Close() error { return errors.Join(s.idx.Close(), s.rev.Close()) } +// CloseIdleDescriptors releases the idx and rev file descriptors +// without disabling the [LazyIndex]. The FDs close inline when no +// readers are active; otherwise each [sharedfile.SharedFile] +// latches an immediate close on the next refs==0 transition. +// In-flight readers complete normally; subsequent operations +// reopen the FDs on demand and resume normal grace-timer +// behaviour. +// +// Returns the joined error of the inline closes; latched closes +// that fire later are not reported. +func (s *LazyIndex) CloseIdleDescriptors() error { + return errors.Join(s.idx.ReleaseNow(), s.rev.ReleaseNow()) +} + // --- internal helpers; all take an io.ReaderAt so the caller controls // the acquire/release lifecycle. --- @@ -485,7 +603,7 @@ type scannerEntryIter struct { func (it *scannerEntryIter) Next() (*Entry, error) { if it.idx == nil { - return nil, errSharedFileClosed + return nil, sharedfile.ErrClosed } if it.pos >= it.s.count { return nil, io.EOF @@ -502,7 +620,7 @@ func (it *scannerEntryIter) Next() (*Entry, error) { func (it *scannerEntryIter) Close() error { it.pos = it.s.count if it.idx != nil { - it.s.idx.release() + it.s.idx.Release() it.idx = nil } return nil @@ -520,7 +638,7 @@ type revEntryIter struct { func (it *revEntryIter) Next() (*Entry, error) { if it.idx == nil || it.rev == nil { - return nil, errSharedFileClosed + return nil, sharedfile.ErrClosed } if it.pos >= it.s.count { return nil, io.EOF @@ -550,12 +668,61 @@ func (it *revEntryIter) Next() (*Entry, error) { func (it *revEntryIter) Close() error { it.pos = it.s.count if it.idx != nil { - it.s.idx.release() + it.s.idx.Release() it.idx = nil } if it.rev != nil { - it.s.rev.release() + it.s.rev.Release() it.rev = nil } return nil } + +// lazyPrefixIter walks the LazyIndex names table from pos to end, +// yielding entries whose hash starts with prefix. It stops the run +// when a hash without the prefix is read (the table is sorted). It +// holds an acquired reference to the idx SharedFile released on +// Close. +// +// Lifetime: Next may release the iterator's SharedFile reference +// eagerly when the first prefix-mismatched entry is observed — +// further matches are impossible in the sorted table, so holding +// the reference would only add pool pressure. Callers should +// defer Close unconditionally; it is idempotent and the eager +// release is purely an optimisation. +type lazyPrefixIter struct { + s *LazyIndex + idx io.ReaderAt + prefix []byte + pos int + end int +} + +func (it *lazyPrefixIter) Next() (*Entry, error) { + if it.idx == nil { + return nil, io.EOF + } + if it.pos >= it.end { + return nil, io.EOF + } + e, err := it.s.entryAt(it.idx, it.pos) + if err != nil { + return nil, err + } + if !bytes.HasPrefix(e.Hash.Bytes(), it.prefix) { + // Past the prefix in the sorted names table; close out so the + // SharedFile reference is released eagerly. + _ = it.Close() + return nil, io.EOF + } + it.pos++ + return e, nil +} + +func (it *lazyPrefixIter) Close() error { + if it.idx != nil { + it.s.idx.Release() + it.idx = nil + } + return nil +} diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/idxfile/shared_file.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/idxfile/shared_file.go deleted file mode 100644 index fcf2a8913..000000000 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/idxfile/shared_file.go +++ /dev/null @@ -1,139 +0,0 @@ -package idxfile - -import ( - "errors" - "io" - "sync" - "time" -) - -const defaultCloseGracePeriod = time.Second - -// sharedFile provides shared, reference-counted access to a file that is -// opened lazily and closed automatically when no readers remain. -// -// Multiple goroutines can acquire the file concurrently; they all share -// a single underlying file descriptor. The FD is opened on first -// acquire and closed after a grace period once the last active reference -// is released. This avoids holding file descriptors open indefinitely — -// which causes problems on Windows, where open files cannot be deleted — -// while still sharing a single FD across concurrent readers and -// avoiding repeated open/close syscalls for sequential operations. -// -// All methods are safe for concurrent use. -type sharedFile struct { - opener openFileFunc - gracePeriod time.Duration - - mu sync.Mutex - file ReadAtCloser - refs int - closed bool - closeTimer *time.Timer - timerGen uint64 // incremented each time a timer is stopped/replaced -} - -var errSharedFileClosed = errors.New("shared file is closed") - -func newSharedFile(opener openFileFunc) *sharedFile { - return &sharedFile{opener: opener, gracePeriod: defaultCloseGracePeriod} -} - -// acquire increments the reference count and returns the underlying -// file as an io.ReaderAt. If the file is not currently open, it is -// opened via the opener function. -// -// The caller MUST call release() when done reading. Failing to do so -// prevents the file descriptor from ever closing. -func (sf *sharedFile) acquire() (io.ReaderAt, error) { - sf.mu.Lock() - defer sf.mu.Unlock() - - if sf.closed { - return nil, errSharedFileClosed - } - - // Cancel any pending grace-period close. - if sf.closeTimer != nil { - sf.closeTimer.Stop() - sf.closeTimer = nil - sf.timerGen++ // invalidate any already-queued timer callback - } - - if sf.file == nil { - f, err := sf.opener() - if err != nil { - return nil, err - } - sf.file = f - } - - sf.refs++ - return sf.file, nil -} - -// release decrements the reference count. When it reaches zero the -// underlying file is closed after a grace period (or immediately if the -// sharedFile has been permanently closed). -func (sf *sharedFile) release() { - sf.mu.Lock() - defer sf.mu.Unlock() - - if sf.refs <= 0 { - return - } - - sf.refs-- - if sf.refs > 0 { - return - } - - // refs == 0: schedule (or perform) the close. - if sf.closed || sf.gracePeriod == 0 { - sf.closeLocked() - return - } - - gen := sf.timerGen - sf.closeTimer = time.AfterFunc(sf.gracePeriod, func() { - sf.mu.Lock() - defer sf.mu.Unlock() - if sf.timerGen == gen && sf.refs == 0 && sf.file != nil { - sf.closeLocked() - } - }) -} - -// Close marks the sharedFile as permanently closed, preventing future -// acquire calls. If no references are active the underlying file is -// closed immediately; otherwise it closes when the last active -// reference is released. -func (sf *sharedFile) Close() error { - sf.mu.Lock() - defer sf.mu.Unlock() - - sf.closed = true - if sf.closeTimer != nil { - sf.closeTimer.Stop() - sf.closeTimer = nil - sf.timerGen++ // invalidate any already-queued timer callback - } - if sf.refs == 0 && sf.file != nil { - err := sf.file.Close() - sf.file = nil - return err - } - return nil -} - -// closeLocked closes the underlying file. Must be called with mu held. -func (sf *sharedFile) closeLocked() { - if sf.file != nil { - _ = sf.file.Close() - sf.file = nil - } - if sf.closeTimer != nil { - sf.closeTimer.Stop() - sf.closeTimer = nil - } -} diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/index/decoder.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/index/decoder.go index 52a03d867..4cb95c6e4 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/index/decoder.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/index/decoder.go @@ -378,6 +378,15 @@ func (d *Decoder) readChecksum(expected []byte) error { return err } + // A null (all-zero) trailing hash means the checksum was skipped when + // the index was written (git's index.skipHash, 2.40+). Upstream git + // disables verification in this case, so match that even when the + // caller did not opt in via WithSkipHash. + if h.IsZero() { + trace.Internal.Printf("index: null trailing checksum, skipping verification") + return nil + } + if d.skipHash { trace.Internal.Printf("index: skipping checksum verification (skipHash)") return nil diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/index/encoder.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/index/encoder.go index 7e9ad2dee..61316e114 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/index/encoder.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/index/encoder.go @@ -85,7 +85,7 @@ func (e *Encoder) encodeHeader(idx *Index) error { } func (e *Encoder) encodeEntries(idx *Index) error { - sort.Sort(byName(idx.Entries)) + sort.Sort(byNameAndStage(idx.Entries)) for _, entry := range idx.Entries { if err := e.encodeEntry(idx, entry); err != nil { @@ -263,8 +263,13 @@ func (e *Encoder) encodeFooter() error { return binary.Write(e.w, e.hash.Sum(nil)) } -type byName []*Entry +type byNameAndStage []*Entry -func (l byName) Len() int { return len(l) } -func (l byName) Swap(i, j int) { l[i], l[j] = l[j], l[i] } -func (l byName) Less(i, j int) bool { return l[i].Name < l[j].Name } +func (l byNameAndStage) Len() int { return len(l) } +func (l byNameAndStage) Swap(i, j int) { l[i], l[j] = l[j], l[i] } +func (l byNameAndStage) Less(i, j int) bool { + if l[i].Name == l[j].Name { + return l[i].Stage < l[j].Stage + } + return l[i].Name < l[j].Name +} diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/objfile/reader.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/objfile/reader.go index 1df72afa5..4c4280d42 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/objfile/reader.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/objfile/reader.go @@ -15,10 +15,18 @@ import ( var ( ErrClosed = errors.New("objfile: already closed") ErrHeader = errors.New("objfile: invalid header") + ErrHeaderTooLong = errors.New("objfile: header exceeds maximum length") ErrHeaderNotRead = errors.New("objfile: Header must be called before Read") ErrNegativeSize = errors.New("objfile: negative object size") ) +// maxHeaderLen mirrors canonical Git's MAX_HEADER_LEN [1]. The type, +// delimiter, size, and trailing NUL of a loose-object header must fit +// within this many inflated bytes. +// +// [1]: https://github.com/git/git/blob/v2.54.0/object-file.c#L34 +const maxHeaderLen = 32 + // Reader reads and decodes compressed objfile data from a provided io.Reader. // Reader implements io.ReadCloser. Close should be called when finished with // the Reader. Close will not close the underlying io.Reader. @@ -46,8 +54,10 @@ func NewReader(r io.Reader, objectFormat format.ObjectFormat) (*Reader, error) { // Header reads the type and the size of object, and prepares the reader for read func (r *Reader) Header() (t plumbing.ObjectType, size int64, err error) { + budget := maxHeaderLen + var raw []byte - raw, err = r.readUntil(' ') + raw, budget, err = r.readUntil(' ', budget) if err != nil { return t, size, err } @@ -57,7 +67,7 @@ func (r *Reader) Header() (t plumbing.ObjectType, size int64, err error) { return t, size, err } - raw, err = r.readUntil(0) + raw, _, err = r.readUntil(0, budget) if err != nil { return t, size, err } @@ -72,21 +82,27 @@ func (r *Reader) Header() (t plumbing.ObjectType, size int64, err error) { return t, size, err } -// readSlice reads one byte at a time from r until it encounters delim or an -// error. -func (r *Reader) readUntil(delim byte) ([]byte, error) { +// readUntil reads one inflated byte at a time from r.zlib until it encounters +// delim, the budget is exhausted, or an error. budget caps the total number +// of bytes consumed from r.zlib, including delim; it mirrors canonical Git's +// MAX_HEADER_LEN bound applied across the full loose-object header. +func (r *Reader) readUntil(delim byte, budget int) ([]byte, int, error) { var buf [1]byte value := make([]byte, 0, 16) for { + if budget <= 0 { + return nil, 0, ErrHeaderTooLong + } if n, err := r.zlib.Read(buf[:]); err != nil && (err != io.EOF || n == 0) { if err == io.EOF { - return nil, ErrHeader + return nil, 0, ErrHeader } - return nil, err + return nil, 0, err } + budget-- if buf[0] == delim { - return value, nil + return value, budget, nil } value = append(value, buf[0]) diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/objfile/writer.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/objfile/writer.go index 5ec2f5d0e..accaff828 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/objfile/writer.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/objfile/writer.go @@ -43,25 +43,35 @@ func NewWriter(w io.Writer, objectFormat format.ObjectFormat) *Writer { } } -// WriteHeader writes the type and the size and prepares to accept the object's -// contents. If an invalid t is provided, plumbing.ErrInvalidType is returned. If a -// negative size is provided, ErrNegativeSize is returned. +// WriteHeader writes the type and the size and prepares to accept the +// object's contents. If an invalid t is provided, plumbing.ErrInvalidType +// is returned. If a negative size is provided, ErrNegativeSize is +// returned. If the encoded header exceeds maxHeaderLen, +// ErrHeaderTooLong is returned, mirroring the reader's bound. func (w *Writer) WriteHeader(t plumbing.ObjectType, size int64) error { if !t.Valid() { return plumbing.ErrInvalidType } + return w.writeHeader(t, t.Bytes(), size) +} + +func (w *Writer) writeHeader(t plumbing.ObjectType, typeBytes []byte, size int64) error { if size < 0 { return ErrNegativeSize } - b := t.Bytes() + b := make([]byte, 0, maxHeaderLen) + b = append(b, typeBytes...) b = append(b, ' ') - b = append(b, []byte(strconv.FormatInt(size, 10))...) + b = strconv.AppendInt(b, size, 10) b = append(b, 0) + if len(b) > maxHeaderLen { + return ErrHeaderTooLong + } + defer w.prepareForWrite(t, size) _, err := w.zlib.Write(b) - return err } diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/common.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/common.go index 69539696b..fb16202d3 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/common.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/common.go @@ -1,6 +1,7 @@ package packfile import ( + "fmt" "io" "time" @@ -71,3 +72,26 @@ func WritePackfileToObjectStorage( return err } + +// ValidateOFSDeltaBase enforces the canonical-Git invariant on an +// OFS-delta's encoded negative offset: the resolved base offset +// (deltaOffset - negativeOffset) must be strictly positive (past the +// 12-byte pack header) and strictly less than deltaOffset, since an +// OFS-delta can only reference an earlier entry in the same pack. +// +// Mirrors canonical Git's predicate in packfile.c[1]: +// +// base_offset = delta_obj_offset - base_offset; +// if (base_offset <= 0 || base_offset >= delta_obj_offset) +// return 0; /* out of bound */ +// +// Returns a wrapped ErrMalformedPackfile when the bounds are violated; +// returns nil otherwise. +// +// [1]: https://github.com/git/git/blob/v2.54.0/packfile.c#L1289-L1290 +func ValidateOFSDeltaBase(deltaOffset, negativeOffset int64) error { + if negativeOffset <= 0 || negativeOffset >= deltaOffset { + return fmt.Errorf("%w: invalid OFS delta offset", ErrMalformedPackfile) + } + return nil +} diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/delta_selector.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/delta_selector.go index 3f35847ff..79e8a9003 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/delta_selector.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/delta_selector.go @@ -20,12 +20,23 @@ var applyDelta = map[plumbing.ObjectType]bool{ plumbing.TreeObject: true, } -type deltaSelector struct { +// DeltaSelector decides which objects in a pack will be encoded as +// deltas and against which base, using a sliding window over the +// object set. It is the default object selector used by Encoder. +// +// Callers can also run a DeltaSelector ahead of time and feed the +// result back into an Encoder via WithObjectSelector + a passthrough +// ObjectSelector, so the pack-write phase can stream output without +// an internal delay during selection. This is useful when the +// encoder's writer is something like an HTTP request body where +// mid-stream stalls trip server timeouts. +type DeltaSelector struct { storer storer.EncodedObjectStorer } -func newDeltaSelector(s storer.EncodedObjectStorer) *deltaSelector { - return &deltaSelector{s} +// NewDeltaSelector returns a DeltaSelector backed by s. +func NewDeltaSelector(s storer.EncodedObjectStorer) *DeltaSelector { + return &DeltaSelector{s} } // ObjectsToPack creates a list of ObjectToPack from the hashes @@ -33,7 +44,7 @@ func newDeltaSelector(s storer.EncodedObjectStorer) *deltaSelector { // internal logic. `packWindow` specifies the size of the sliding // window used to compare objects for delta compression; 0 turns off // delta compression entirely. -func (dw *deltaSelector) ObjectsToPack( +func (dw *DeltaSelector) ObjectsToPack( hashes []plumbing.Hash, packWindow uint, ) ([]*ObjectToPack, error) { @@ -81,7 +92,7 @@ func (dw *deltaSelector) ObjectsToPack( return otp, nil } -func (dw *deltaSelector) objectsToPack( +func (dw *DeltaSelector) objectsToPack( hashes []plumbing.Hash, packWindow uint, ) ([]*ObjectToPack, error) { @@ -117,7 +128,7 @@ func (dw *deltaSelector) objectsToPack( return objectsToPack, nil } -func (dw *deltaSelector) encodedDeltaObject(h plumbing.Hash) (plumbing.EncodedObject, error) { +func (dw *DeltaSelector) encodedDeltaObject(h plumbing.Hash) (plumbing.EncodedObject, error) { edos, ok := dw.storer.(storer.DeltaObjectStorer) if !ok { return dw.encodedObject(h) @@ -126,11 +137,11 @@ func (dw *deltaSelector) encodedDeltaObject(h plumbing.Hash) (plumbing.EncodedOb return edos.DeltaObject(plumbing.AnyObject, h) } -func (dw *deltaSelector) encodedObject(h plumbing.Hash) (plumbing.EncodedObject, error) { +func (dw *DeltaSelector) encodedObject(h plumbing.Hash) (plumbing.EncodedObject, error) { return dw.storer.EncodedObject(plumbing.AnyObject, h) } -func (dw *deltaSelector) fixAndBreakChains(objectsToPack []*ObjectToPack) error { +func (dw *DeltaSelector) fixAndBreakChains(objectsToPack []*ObjectToPack) error { m := make(map[plumbing.Hash]*ObjectToPack, len(objectsToPack)) for _, otp := range objectsToPack { m[otp.Hash()] = otp @@ -145,7 +156,7 @@ func (dw *deltaSelector) fixAndBreakChains(objectsToPack []*ObjectToPack) error return nil } -func (dw *deltaSelector) fixAndBreakChainsOne(objectsToPack map[plumbing.Hash]*ObjectToPack, otp *ObjectToPack) error { +func (dw *DeltaSelector) fixAndBreakChainsOne(objectsToPack map[plumbing.Hash]*ObjectToPack, otp *ObjectToPack) error { if !otp.Object.Type().IsDelta() { return nil } @@ -179,7 +190,7 @@ func (dw *deltaSelector) fixAndBreakChainsOne(objectsToPack map[plumbing.Hash]*O return nil } -func (dw *deltaSelector) restoreOriginal(otp *ObjectToPack) error { +func (dw *DeltaSelector) restoreOriginal(otp *ObjectToPack) error { if otp.Original != nil { return nil } @@ -200,7 +211,7 @@ func (dw *deltaSelector) restoreOriginal(otp *ObjectToPack) error { // undeltify undeltifies an *ObjectToPack by retrieving the original object from // the storer and resetting it. -func (dw *deltaSelector) undeltify(otp *ObjectToPack) error { +func (dw *DeltaSelector) undeltify(otp *ObjectToPack) error { if err := dw.restoreOriginal(otp); err != nil { return err } @@ -210,11 +221,11 @@ func (dw *deltaSelector) undeltify(otp *ObjectToPack) error { return nil } -func (dw *deltaSelector) sort(objectsToPack []*ObjectToPack) { +func (dw *DeltaSelector) sort(objectsToPack []*ObjectToPack) { sort.Sort(byTypeAndSize(objectsToPack)) } -func (dw *deltaSelector) walk( +func (dw *DeltaSelector) walk( objectsToPack []*ObjectToPack, packWindow uint, ) error { @@ -265,7 +276,7 @@ func (dw *deltaSelector) walk( return nil } -func (dw *deltaSelector) tryToDeltify(indexMap map[plumbing.Hash]*deltaIndex, base, target *ObjectToPack) error { +func (dw *DeltaSelector) tryToDeltify(indexMap map[plumbing.Hash]*deltaIndex, base, target *ObjectToPack) error { // Original object might not be present if we're reusing a delta, so we // ensure it is restored. if err := dw.restoreOriginal(target); err != nil { @@ -316,7 +327,7 @@ func (dw *deltaSelector) tryToDeltify(indexMap map[plumbing.Hash]*deltaIndex, ba return nil } -func (dw *deltaSelector) deltaSizeLimit(targetSize int64, baseDepth int, +func (dw *DeltaSelector) deltaSizeLimit(targetSize int64, baseDepth int, targetDepth int, targetDelta bool, ) int64 { if !targetDelta { diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/diff_delta.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/diff_delta.go index 58af47b8c..96472b719 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/diff_delta.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/diff_delta.go @@ -4,6 +4,7 @@ import ( "bytes" "github.com/go-git/go-git/v6/plumbing" + packutil "github.com/go-git/go-git/v6/plumbing/format/packfile/util" "github.com/go-git/go-git/v6/utils/ioutil" "github.com/go-git/go-git/v6/utils/sync" ) @@ -81,8 +82,8 @@ func DiffDelta(src, tgt []byte) []byte { func diffDelta(index *deltaIndex, src, tgt []byte) []byte { buf := sync.GetBytesBuffer() defer sync.PutBytesBuffer(buf) - buf.Write(deltaEncodeSize(len(src))) - buf.Write(deltaEncodeSize(len(tgt))) + buf.Write(packutil.EncodeLEB128(uint(len(src)))) + buf.Write(packutil.EncodeLEB128(uint(len(tgt)))) if len(index.entries) == 0 { index.init(src) @@ -156,20 +157,6 @@ func encodeInsertOperation(ibuf, buf *bytes.Buffer) { ibuf.Reset() } -func deltaEncodeSize(size int) []byte { - var ret []byte - c := size & 0x7f - size >>= 7 - for size != 0 { - ret = append(ret, byte(c|0x80)) - c = size & 0x7f - size >>= 7 - } - ret = append(ret, byte(c)) - - return ret -} - func encodeCopyOperation(offset, length int) []byte { code := 0x80 var opcodes []byte diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/encoder.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/encoder.go index 5615a1b70..8edada711 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/encoder.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/encoder.go @@ -16,21 +16,64 @@ import ( "github.com/go-git/go-git/v6/utils/sync" ) +// ObjectSelector decides which objects go into a pack and in what +// order, including any delta relationships. The default selector is +// *DeltaSelector. +type ObjectSelector interface { + ObjectsToPack(hashes []plumbing.Hash, packWindow uint) ([]*ObjectToPack, error) +} + // Encoder gets the data from the storage and write it into the writer in PACK -// format +// format. +// +// The encoder has two selector fields: deltaSelector is the +// encoder's own *DeltaSelector, used internally for write-phase +// recovery (e.g. restoreOriginal on cyclic chains). objectSelector is +// what Encode calls to obtain the object list — by default the same +// *DeltaSelector, but a caller can override it via WithObjectSelector. type Encoder struct { - selector *deltaSelector - w *offsetWriter - zw sync.ZlibWriter - hasher hash.Hash + deltaSelector *DeltaSelector + objectSelector ObjectSelector + w *offsetWriter + zw sync.ZlibWriter + hasher hash.Hash useRefDeltas bool } +// EncoderOption configures an Encoder at construction time. +type EncoderOption func(*Encoder) + +// WithObjectSelector overrides the ObjectSelector used by Encode to +// produce the object list. The default is the encoder's own +// *DeltaSelector, which runs delta selection synchronously when +// Encode is called. +// +// Supplying a selector that returns a precomputed []*ObjectToPack +// (typically the result of a prior DeltaSelector.ObjectsToPack call) +// lets Encode skip the selection step and start writing pack bytes +// immediately. This is useful when the encoder's writer is something +// like an HTTP request body where a multi-second mid-stream stall +// trips server timeouts. The encoder still uses its own internal +// *DeltaSelector for recovery operations during the write phase +// (e.g. when a concurrent repack invalidates a chosen delta base), +// so the storer passed to NewEncoder must remain valid. +func WithObjectSelector(s ObjectSelector) EncoderOption { + return func(e *Encoder) { + if s != nil { + e.objectSelector = s + } + } +} + // NewEncoder creates a new packfile encoder using a specific Writer and // EncodedObjectStorer. By default deltas used to generate the packfile will be // OFSDeltaObject. To use Reference deltas, set useRefDeltas to true. -func NewEncoder(w io.Writer, s storer.EncodedObjectStorer, useRefDeltas bool) *Encoder { +// +// Optional EncoderOptions configure encoder behavior; see +// WithObjectSelector for the main use case (precomputed selection for +// streaming output). +func NewEncoder(w io.Writer, s storer.EncodedObjectStorer, useRefDeltas bool, opts ...EncoderOption) *Encoder { var of cfgformat.ObjectFormat if c, ok := s.(config.ConfigStorer); ok { cfg, err := c.Config() @@ -49,24 +92,35 @@ func NewEncoder(w io.Writer, s storer.EncodedObjectStorer, useRefDeltas bool) *E mw := io.MultiWriter(w, h) ow := newOffsetWriter(mw) zw := sync.GetZlibWriter(mw) - return &Encoder{ - selector: newDeltaSelector(s), - w: ow, - zw: zw, - hasher: h, - useRefDeltas: useRefDeltas, + sel := NewDeltaSelector(s) + e := &Encoder{ + deltaSelector: sel, + objectSelector: sel, + w: ow, + zw: zw, + hasher: h, + useRefDeltas: useRefDeltas, + } + for _, opt := range opts { + opt(e) } + return e } // Encode creates a packfile containing all the objects referenced in // hashes and writes it to the writer in the Encoder. `packWindow` // specifies the size of the sliding window used to compare objects // for delta compression; 0 turns off delta compression entirely. +// +// The object set is produced by the configured ObjectSelector (see +// WithObjectSelector). The encoder's internal *DeltaSelector is still +// used for recovery operations during the write phase regardless of +// the configured selector. func (e *Encoder) Encode( hashes []plumbing.Hash, packWindow uint, ) (plumbing.Hash, error) { - objects, err := e.selector.ObjectsToPack(hashes, packWindow) + objects, err := e.objectSelector.ObjectsToPack(hashes, packWindow) if err != nil { return plumbing.ZeroHash, err } @@ -104,7 +158,7 @@ func (e *Encoder) entry(o *ObjectToPack) (err error) { // (for example due to a concurrent repack) and a different base // was chosen, forcing a cycle. Select something other than a // delta, and write this object. - if err := e.selector.restoreOriginal(o); err != nil { + if err := e.deltaSelector.restoreOriginal(o); err != nil { return err } o.BackToOriginal() @@ -162,7 +216,14 @@ func (e *Encoder) writeBaseIfDelta(o *ObjectToPack) error { } func (e *Encoder) writeDeltaHeader(o *ObjectToPack) error { - // Write offset deltas by default + // Every delta in an encoded pack uses the same kind — all OFS_DELTA + // by default, or all REF_DELTA when useRefDeltas is set. The parser + // (see Parser.resolveDeltas) accepts packs that mix OFS_DELTA and + // REF_DELTA in a single chain, because mixed-kind packs occur in + // the wild (repacks across servers with differing + // --delta-base-offset settings, thin-pack splices, third-party + // tooling); the encoder deliberately doesn't introduce that + // complexity on the write side. t := plumbing.OFSDeltaObject if e.useRefDeltas { t = plumbing.REFDeltaObject diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/fsobject.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/fsobject.go index 1bb036b7f..718131581 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/fsobject.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/fsobject.go @@ -6,6 +6,7 @@ import ( "io" "math" "os" + stdsync "sync" billy "github.com/go-git/go-billy/v6" @@ -16,6 +17,46 @@ import ( "github.com/go-git/go-git/v6/utils/sync" ) +// probeSize is the byte count for the closed-FD probe. A one-byte +// ReadAt distinguishes a live descriptor from a closed one without +// mutating the file's seek cursor; a zero-length read is unusable +// because some implementations (e.g. [os.File]) return (0, nil) on +// a closed file. +const probeSize = 1 + +// probeBufPool returns the per-call backing array for [probePack]. +// Pooling keeps the read path allocation-free on what is a very hot +// code path. +var probeBufPool = stdsync.Pool{ + New: func() any { + var buf [probeSize]byte + return &buf + }, +} + +// probePack tests whether pack is still readable at offset by +// issuing a [probeSize]-byte [io.ReaderAt.ReadAt]. The error is +// returned verbatim so any wrapping context (path, syscall) is +// preserved for the caller; classification is left to +// [errors.Is]: +// +// - nil means the descriptor is live; the caller may keep using +// pack. +// - an error matching [os.ErrClosed] means the descriptor has +// been closed and the caller should reopen the file. +// - any other error is propagated, matching the canonical Git +// behaviour in `packfile.c:use_pack`, which does not retry on +// transient I/O errors. +// +// [io.EOF] indicates the offset is at or past end-of-file, which +// implies a truncated pack — propagate rather than masking. +func probePack(pack io.ReaderAt, offset int64) error { + buf := probeBufPool.Get().(*[probeSize]byte) + defer probeBufPool.Put(buf) + _, err := pack.ReadAt(buf[:], offset) + return err +} + // FSObject is an object from the packfile on the filesystem. type FSObject struct { hash plumbing.Hash @@ -27,6 +68,10 @@ type FSObject struct { pack billy.File packPath string cache cache.Object + // acquireRandom, when set, supersedes pack/packPath/fs in + // [FSObject.Reader]: each call yields a fresh cursor that + // Close releases. + acquireRandom func() (RandomReader, error) } // NewFSObject creates a new filesystem object. @@ -56,9 +101,10 @@ func NewFSObject( // Reader implements the plumbing.EncodedObject interface. // -// Reader is safe for concurrent use: it uses ReadAt (which does not modify the -// file's seek cursor) instead of Seek+Read, so multiple goroutines can call -// Reader on FSObjects that share the same underlying packfile handle. +// Reader is safe for concurrent use: it uses ReadAt (which does +// not modify the file's seek cursor) instead of Seek+Read, so +// multiple goroutines can call Reader on FSObjects that share the +// same underlying packfile handle. func (o *FSObject) Reader() (io.ReadCloser, error) { obj, ok := o.cache.Get(o.hash) if ok && obj != o { @@ -70,22 +116,34 @@ func (o *FSObject) Reader() (io.ReadCloser, error) { return reader, nil } - pack := o.pack - var file io.Closer + var ( + pack io.ReaderAt + file io.Closer + ) - // Probe with a 1-byte ReadAt to detect a closed file descriptor without - // modifying any shared state. A zero-length read cannot be used because - // some implementations (e.g. os.File) return (0, nil) for empty reads - // even on closed files. - _, err := pack.ReadAt(make([]byte, 1), o.offset) - if err != nil && errors.Is(err, os.ErrClosed) { - pack, err = o.fs.Open(o.packPath) + if o.acquireRandom != nil { + cur, err := o.acquireRandom() if err != nil { return nil, err } - file = pack - } else if err != nil && !errors.Is(err, io.EOF) { - return nil, err + pack = cur + file = cur + } else { + pack = o.pack + + switch err := probePack(pack, o.offset); { + case err == nil: + // FD is live; keep using pack. + case errors.Is(err, os.ErrClosed): + reopened, oerr := o.fs.Open(o.packPath) + if oerr != nil { + return nil, oerr + } + pack = reopened + file = reopened + default: + return nil, err + } } // SectionReader provides a standalone io.Reader backed by ReadAt. Each diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/pack_handle.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/pack_handle.go new file mode 100644 index 000000000..2c9c07b1f --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/pack_handle.go @@ -0,0 +1,68 @@ +package packfile + +import ( + "io" + + "github.com/go-git/go-git/v6/plumbing" +) + +// PackHandle is the handle [NewPackfile] consumes when +// [WithPackHandle] is supplied. +type PackHandle interface { + // OpenPackReader returns a fresh sequential cursor over the + // .pack file. The cursor is closed by the caller. + OpenPackReader() (io.ReadSeekCloser, error) + // OpenRandomReader returns a fresh random-access cursor over + // the .pack file. The cursor is closed by the caller. + OpenRandomReader() (RandomReader, error) + // PackHash returns the .pack file's trailing checksum, which + // by canonical-Git construction equals the pack's identity + // hash (the hex in pack-.pack). + PackHash() (plumbing.Hash, error) +} + +// RandomReader is the per-read random-access cursor returned by +// [PackHandle.OpenRandomReader]. ReadAt is safe to call +// concurrently with itself; Close releases the cursor's hold on +// the underlying pack file descriptor. +type RandomReader interface { + io.ReaderAt + io.Closer +} + +// PackHandleResolver returns the current [PackHandle] for one +// .pack file. It is invoked on scanner init (once per [Packfile]) +// and on every [FSObject.Reader] call. See [DotGit.PackHandle] +// for the reference implementation. +// +// Contract: +// +// - Every handle returned for the lifetime of a given [Packfile] +// MUST address the same .pack file on disk (same PackHash). +// The handle value MAY change across calls. [Packfile] does +// NOT re-validate identity on re-resolution. +// - Errors propagate to the caller as object-read errors. The +// resolver SHOULD NOT retry internally. +// - The handle returned MUST remain valid until at least one +// cursor obtained from it has been closed by the caller. +type PackHandleResolver func() (PackHandle, error) + +// WithPackHandle injects an externally-owned [PackHandle] resolver. +// The resolved handle is not closed by [Packfile.Close]; its +// lifetime is owned by the resolver. See [PackHandleResolver] for +// the resolver contract. +func WithPackHandle(get PackHandleResolver) PackfileOption { + return func(p *Packfile) { + p.resolveHandle = get + } +} + +// openRandomReader re-resolves the pack handle via resolveHandle +// and returns a fresh random-access cursor. +func (p *Packfile) openRandomReader() (RandomReader, error) { + h, err := p.resolveHandle() + if err != nil { + return nil, err + } + return h.OpenRandomReader() +} diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/packfile.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/packfile.go index d7cb699a5..5570b92ef 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/packfile.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/packfile.go @@ -5,7 +5,9 @@ import ( "crypto" "fmt" "io" + "io/fs" "sync" + "sync/atomic" billy "github.com/go-git/go-billy/v6" @@ -30,9 +32,16 @@ var ( // Packfile allows retrieving information from inside a packfile. type Packfile struct { idxfile.Index - fs billy.Filesystem - file billy.File - scanner *Scanner + fs billy.Filesystem + file billy.File + + // handle is the resolved PackHandle once init has run; nil + // in legacy mode. See NewPackfile for the modes. + handle PackHandle + resolveHandle PackHandleResolver + + scanReader io.ReadSeekCloser + scanner *Scanner cache cache.Object rbuf *bufio.Reader @@ -43,12 +52,19 @@ type Packfile struct { once sync.Once onceErr error + + closed atomic.Bool } -// NewPackfile returns a packfile representation for the given packfile file -// and packfile idx. -// If the filesystem is provided, the packfile will return FSObjects, otherwise -// it will return MemoryObjects. +// NewPackfile returns a packfile representation for the given .pack +// file and idx. If [WithFs] is set the packfile returns [FSObject]s; +// otherwise it returns [plumbing.MemoryObject]s. +// +// When [WithPackHandle] is supplied, the resolver owns the pack +// file descriptor and the file argument is redundant; the +// constructor closes it and [Packfile.Close] does not close the +// resolver-owned handle. Otherwise the file argument is used as-is +// and is closed by [Packfile.Close]. func NewPackfile( file billy.File, opts ...PackfileOption, @@ -61,16 +77,29 @@ func NewPackfile( opt(p) } + if p.resolveHandle != nil && file != nil { + _ = file.Close() + p.file = nil + } + return p } // Get retrieves the encoded object in the packfile with the given hash. func (p *Packfile) Get(h plumbing.Hash) (plumbing.EncodedObject, error) { + if p.closed.Load() { + return nil, fs.ErrClosed + } if err := p.init(); err != nil { return nil, err } p.m.Lock() defer p.m.Unlock() + // Re-check after Lock: Close may have flipped closed and torn + // down the scanner between the early Load and the Lock. + if p.closed.Load() { + return nil, fs.ErrClosed + } return p.get(h) } @@ -78,11 +107,19 @@ func (p *Packfile) Get(h plumbing.Hash) (plumbing.EncodedObject, error) { // GetByOffset retrieves the encoded object from the packfile at the given // offset. func (p *Packfile) GetByOffset(offset int64) (plumbing.EncodedObject, error) { + if p.closed.Load() { + return nil, fs.ErrClosed + } if err := p.init(); err != nil { return nil, err } p.m.Lock() defer p.m.Unlock() + // Re-check after Lock: Close may have flipped closed and torn + // down the scanner between the early Load and the Lock. + if p.closed.Load() { + return nil, fs.ErrClosed + } return p.getByOffset(offset) } @@ -90,6 +127,9 @@ func (p *Packfile) GetByOffset(offset int64) (plumbing.EncodedObject, error) { // GetSizeByOffset retrieves the size of the encoded object from the // packfile with the given offset. func (p *Packfile) GetSizeByOffset(offset int64) (size int64, err error) { + if p.closed.Load() { + return 0, fs.ErrClosed + } if err := p.init(); err != nil { return 0, err } @@ -111,6 +151,9 @@ func (p *Packfile) GetAll() (storer.EncodedObjectIter, error) { // GetByType returns all the objects of the given type. func (p *Packfile) GetByType(typ plumbing.ObjectType) (storer.EncodedObjectIter, error) { + if p.closed.Load() { + return nil, fs.ErrClosed + } if err := p.init(); err != nil { return nil, err } @@ -142,6 +185,9 @@ func (p *Packfile) GetByType(typ plumbing.ObjectType) (storer.EncodedObjectIter, // to avoid exposing the package internals and to improve its thread-safety. // TODO: Remove Scanner method func (p *Packfile) Scanner() (*Scanner, error) { + if p.closed.Load() { + return nil, fs.ErrClosed + } if err := p.init(); err != nil { return nil, err } @@ -198,7 +244,16 @@ func (p *Packfile) getByOffset(offset int64) (plumbing.EncodedObject, error) { func (p *Packfile) init() error { p.once.Do(func() { - if p.file == nil { + if p.handle == nil && p.resolveHandle != nil { + h, err := p.resolveHandle() + if err != nil { + p.onceErr = fmt.Errorf("packfile: resolve pack handle: %w", err) + return + } + p.handle = h + } + + if p.handle == nil && p.file == nil { p.onceErr = fmt.Errorf("file is not set") return } @@ -216,23 +271,44 @@ func (p *Packfile) init() error { opts = append(opts, WithSHA256()) } - p.scanner = NewScanner(p.file, opts...) + var scanSrc io.Reader + if p.handle != nil { + r, err := p.handle.OpenPackReader() + if err != nil { + p.onceErr = fmt.Errorf("packfile: open pack reader: %w", err) + return + } + p.scanReader = r + scanSrc = r + } else { + scanSrc = p.file + } + + p.scanner = NewScanner(scanSrc, opts...) // Validate packfile signature. if !p.scanner.Scan() { p.onceErr = p.scanner.Error() return } - _, err := p.scanner.Seek(-int64(p.objectIDSize), io.SeekEnd) - if err != nil { - p.onceErr = err - return - } - - p.id.ResetBySize(p.objectIDSize) - _, err = p.id.ReadFrom(p.scanner) - if err != nil { - p.onceErr = err + if p.handle != nil { + id, err := p.handle.PackHash() + if err != nil { + p.onceErr = fmt.Errorf("packfile: read pack hash: %w", err) + return + } + p.id = id + } else { + _, err := p.scanner.Seek(-int64(p.objectIDSize), io.SeekEnd) + if err != nil { + p.onceErr = err + return + } + p.id.ResetBySize(p.objectIDSize) + _, err = p.id.ReadFrom(p.scanner) + if err != nil { + p.onceErr = err + } } if p.cache == nil { @@ -260,13 +336,28 @@ func (p *Packfile) headerFromOffset(offset int64) (*ObjectHeader, error) { return &oh, nil } -// Close the packfile and its resources. +// Close the packfile and its resources. Subsequent calls to [Packfile.Get], +// [Packfile.GetByOffset], and the other entry points return [fs.ErrClosed]. +// Close is idempotent. func (p *Packfile) Close() error { + if !p.closed.CompareAndSwap(false, true) { + return nil + } p.m.Lock() defer p.m.Unlock() gogitsync.PutBufioReader(p.rbuf) + if p.handle != nil { + // The resolver owns the handle; close only the scanner cursor. + if p.scanReader != nil { + err := p.scanReader.Close() + p.scanReader = nil + return err + } + return nil + } + closer, ok := p.file.(io.Closer) if !ok { return nil @@ -283,20 +374,34 @@ func (p *Packfile) objectFromHeader(oh *ObjectHeader) (plumbing.EncodedObject, e // If we have filesystem, and the object is not a delta type, return a FSObject. // This avoids having to inflate the object more than once. if !oh.Type.IsDelta() && p.fs != nil { - fs := NewFSObject( - oh.ID(), - oh.Type, - oh.ContentOffset, - oh.Size, - p.Index, - p.fs, - p.file, - p.file.Name(), - p.cache, - ) - - p.cache.Put(fs) - return fs, nil + var fsObj *FSObject + if p.handle != nil { + fsObj = &FSObject{ + hash: oh.ID(), + offset: oh.ContentOffset, + size: oh.Size, + typ: oh.Type, + index: p.Index, + fs: p.fs, + cache: p.cache, + acquireRandom: p.openRandomReader, + } + } else { + fsObj = NewFSObject( + oh.ID(), + oh.Type, + oh.ContentOffset, + oh.Size, + p.Index, + p.fs, + p.file, + p.file.Name(), + p.cache, + ) + } + + p.cache.Put(fsObj) + return fsObj, nil } return p.getMemoryObject(oh) diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/parser.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/parser.go index e7f26fede..c49e2e806 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/parser.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/parser.go @@ -25,6 +25,12 @@ var ( // ErrDeltaNotCached is returned when the delta could not be found in cache. ErrDeltaNotCached = errors.New("delta could not be found in cache") + + // ErrParserConsumed is returned by Parse when called against a Parser + // instance that has already been consumed by a prior Parse call, + // whether that call returned successfully or with an error. Parsers + // are single-shot; construct a new one per pack. + ErrParserConsumed = errors.New("parser already consumed") ) // maxObjectPreallocBytes caps the up-front size hint passed to @@ -52,6 +58,14 @@ func growHint(n int64) int { // Parser decodes a packfile and calls any observer associated to it. Is used // to generate indexes. +// +// A Parser is single-shot: Parse may be called at most once per +// instance. The cache maps and the per-delta parent pointers built up +// during a Parse call are not reset on entry, so a second call would +// observe the prior call's state — successful or not — and produce +// undefined results; the second call therefore returns +// ErrParserConsumed without running. Construct a new Parser for each +// pack you intend to decode. type Parser struct { storage storer.EncodedObjectStorer cache *parserCache @@ -65,6 +79,7 @@ type Parser struct { checksum plumbing.Hash m stdsync.Mutex + parsed bool } // LowMemoryCapable is implemented by storage types that are capable of @@ -161,6 +176,11 @@ func (p *Parser) Parse() (plumbing.Hash, error) { p.m.Lock() defer p.m.Unlock() + if p.parsed { + return plumbing.ZeroHash, ErrParserConsumed + } + p.parsed = true + var pendingDeltas []*ObjectHeader var pendingDeltaREFs []*ObjectHeader @@ -206,18 +226,8 @@ func (p *Parser) Parse() (plumbing.Hash, error) { return plumbing.ZeroHash, err } - for _, oh := range pendingDeltaREFs { - err := p.processDelta(oh) - if err != nil { - return plumbing.ZeroHash, fmt.Errorf("processing ref-delta at offset %v: %w", oh.Offset, err) - } - } - - for _, oh := range pendingDeltas { - err := p.processDelta(oh) - if err != nil { - return plumbing.ZeroHash, fmt.Errorf("processing ofs-delta at offset %v: %w", oh.Offset, err) - } + if err := p.resolveDeltas(pendingDeltas, pendingDeltaREFs); err != nil { + return plumbing.ZeroHash, err } // Return to pool all objects used. @@ -272,6 +282,105 @@ func (p *Parser) ensureContent(oh *ObjectHeader) error { return nil } +// resolveDeltas walks the pack's delta DAG depth-first from each +// non-delta base, processing OFS and REF delta children of every parent +// together. Mirrors canonical Git's threaded_second_pass in +// builtin/index-pack.c[1], which advances both kinds of children from +// each in-progress parent in a single walk. +// +// Splitting REF and OFS resolution into separate passes (REF first, OFS +// second) is incorrect: a REF-delta whose base is an OFS-delta in the +// same pack would look up its base hash before the OFS-delta has been +// applied, since the OFS-delta's resolved hash is unknown at scan time. +// The lookup would then misclassify the in-pack base as a thin-pack +// external reference and the chain would fail to resolve. +// +// Any REF-delta not reached through the depth-first walk has a base +// outside this pack and is processed via the external-reference +// placeholder path. An OFS-delta whose recorded negative offset does +// not match any in-pack object header is rejected as malformed input. +// +// [1]: https://github.com/git/git/blob/v2.54.0/builtin/index-pack.c#L1103 +func (p *Parser) resolveDeltas(ofsDeltas, refDeltas []*ObjectHeader) error { + // Map sizes correspond to the count of distinct parent offsets / + // hashes, not the count of delta entries. Real packs cluster many + // children under one parent (chains and wide trees), so a hint + // sized to len(deltas) consistently overshoots. Let the maps grow. + ofsChildren := map[int64][]*ObjectHeader{} + for _, d := range ofsDeltas { + ofsChildren[d.OffsetReference] = append(ofsChildren[d.OffsetReference], d) + } + refChildren := map[plumbing.Hash][]*ObjectHeader{} + for _, d := range refDeltas { + refChildren[d.Reference] = append(refChildren[d.Reference], d) + } + + var visit func(*ObjectHeader) error + visit = func(parent *ObjectHeader) error { + for _, c := range refChildren[parent.Hash] { + // Two non-delta entries with identical content (or an + // OFS-delta that resolves to the same hash as a non-delta + // elsewhere in the pack) make this child reachable from + // more than one parent; only the first reach resolves it. + if c.parent != nil { + continue + } + if err := p.processDelta(c); err != nil { + return fmt.Errorf("processing ref-delta at offset %v: %w", c.Offset, err) + } + if err := visit(c); err != nil { + return err + } + } + for _, c := range ofsChildren[parent.Offset] { + if c.parent != nil { + continue + } + if err := p.processDelta(c); err != nil { + return fmt.Errorf("processing ofs-delta at offset %v: %w", c.Offset, err) + } + if err := visit(c); err != nil { + return err + } + } + return nil + } + + // Snapshot the non-delta bases before walking, since processDelta + // appends resolved deltas to p.cache.oi via storeOrCache. The + // non-delta fraction of a real pack is small (typical 5-20%), so + // preallocating to len(p.cache.oi) would waste most of the slot. + var bases []*ObjectHeader + for _, oh := range p.cache.oi { + if !oh.Type.IsDelta() { + bases = append(bases, oh) + } + } + for _, base := range bases { + if err := visit(base); err != nil { + return err + } + } + + for _, d := range refDeltas { + if d.parent != nil { + continue + } + if err := p.processDelta(d); err != nil { + return fmt.Errorf("processing ref-delta at offset %v: %w", d.Offset, err) + } + } + + for _, d := range ofsDeltas { + if d.parent != nil { + continue + } + return fmt.Errorf("processing ofs-delta at offset %v: %w", d.Offset, plumbing.ErrObjectNotFound) + } + + return nil +} + func (p *Parser) processDelta(oh *ObjectHeader) error { switch oh.Type { case plumbing.OFSDeltaObject: @@ -295,6 +404,9 @@ func (p *Parser) processDelta(oh *ObjectHeader) error { } else { oh.parent = pa } + // For a thin-pack external reference, store the placeholder so + // subsequent REF-deltas naming the same external hash chain + // through this entry. For an in-pack base the write is a no-op. p.cache.oiByHash[oh.Reference] = oh.parent default: @@ -312,15 +424,34 @@ func (p *Parser) processDelta(oh *ObjectHeader) error { return p.storeOrCache(oh) } +// checkDeltaChainDepth verifies that the delta chain rooted at oh +// stays within [maxDeltaChainDepth] links. The result is cached on +// [ObjectHeader.chainDepth] so a subsequent walk that crosses the +// same parent reuses the work — every entry on the chain ends up +// with its depth set once, which keeps the verification linear in +// the number of distinct objects rather than quadratic in the +// chain length. This mirrors the cached `oe->depth` field that +// upstream Git carries on the object entry in +// `builtin/pack-objects.c`. func checkDeltaChainDepth(oh *ObjectHeader) error { + if oh.chainDepth > 0 { + return nil + } var depth int for current := oh; current != nil && current.isDeltaOnDisk(); current = current.parent { + if current.chainDepth > 0 { + depth += current.chainDepth + if depth > maxDeltaChainDepth { + return fmt.Errorf("%w: delta chain depth exceeds %d", ErrMalformedPackfile, maxDeltaChainDepth) + } + break + } depth++ if depth > maxDeltaChainDepth { return fmt.Errorf("%w: delta chain depth exceeds %d", ErrMalformedPackfile, maxDeltaChainDepth) } } - + oh.chainDepth = depth return nil } @@ -339,7 +470,7 @@ func (p *Parser) parentReader(parent *ObjectHeader) (io.ReaderAt, error) { // from either cache or storage, else we would need to inflate // it to then inflate the current object, which could go on // indefinitely. - if p.storage != nil && parent.Hash != plumbing.ZeroHash { + if p.storage != nil && !parent.Hash.IsZero() { obj, err := p.storage.EncodedObject(parent.Type, parent.Hash) if err == nil { // Ensure that external references have the correct type and size. diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/scanner.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/scanner.go index a44c815d3..ece4f24af 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/scanner.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/scanner.go @@ -481,12 +481,8 @@ func objectEntry(r *Scanner) (stateFn, error) { if err != nil { return nil, err } - // An OFS-delta references a base object that appears - // earlier in the pack; the negative offset must be - // strictly positive and not larger than the current - // object's offset. - if no <= 0 || no > oh.Offset { - return nil, fmt.Errorf("%w: invalid OFS delta offset", ErrMalformedPackfile) + if err := ValidateOFSDeltaBase(oh.Offset, no); err != nil { + return nil, err } oh.OffsetReference = oh.Offset - no } else { diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/types.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/types.go index 6203b337f..448a54b35 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/types.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/types.go @@ -40,6 +40,14 @@ type ObjectHeader struct { parent *ObjectHeader diskType plumbing.ObjectType externalRef bool + + // chainDepth caches the result of [checkDeltaChainDepth] for + // this header. A positive value is the number of delta links + // from this object down to (but not including) the first + // non-delta base. Zero means either "not yet computed" or + // "this header is not a delta"; both cases collapse to a + // constant-time re-check, so the dual meaning is harmless. + chainDepth int } // ID returns the object ID. diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/util/util.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/util/util.go index 3ea8a81b4..e1fef0a61 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/util/util.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/packfile/util/util.go @@ -72,6 +72,27 @@ func ObjectType(b byte) plumbing.ObjectType { return plumbing.ObjectType((b & maskType) >> firstLengthBits) } +// EncodeLEB128 encodes num as an unsigned LEB128 byte sequence and +// returns it. Inverse of DecodeLEB128. +func EncodeLEB128(num uint) []byte { + var out []byte + for { + b := byte(num & maskPayload) + num >>= 7 + if num == 0 { + return append(out, b) + } + out = append(out, b|maskContinue) + } +} + +// EncodeLEB128ToWriter encodes num as an unsigned LEB128 byte sequence +// and writes it to writer. Inverse of DecodeLEB128FromReader. +func EncodeLEB128ToWriter(writer io.Writer, num uint) error { + _, err := writer.Write(EncodeLEB128(num)) + return err +} + // DecodeLEB128 decodes a number encoded as an unsigned LEB128 at the // start of some binary data and returns the decoded number, the rest // of the bytes, and an error if the encoded value does not fit in a diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/format/pktline/scanner.go b/vendor/github.com/go-git/go-git/v6/plumbing/format/pktline/scanner.go index ad6b2a5d6..0acba1314 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/format/pktline/scanner.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/format/pktline/scanner.go @@ -10,9 +10,15 @@ import ( // which then can be tokenized through repeated calls to the Scan // method. // -// After each Scan call, the Bytes method will return the payload of the -// corresponding pkt-line on a shared buffer, which will be 65516 bytes -// or smaller. Flush pkt-lines are represented by empty byte slices. +// After each Scan call, the Bytes method returns the payload of the +// corresponding pkt-line as a slice into the Scanner's internal buffer. +// This buffer is overwritten on the next call to Scan, so callers must +// process or copy the data before the next Scan. For a string copy, use +// Text. +// +// Special pkt-lines ([Flush], [Delim], [ResponseEnd]) return a nil slice +// from Bytes; Len returns the pkt-line length, which equals the +// corresponding constant (Flush=0, Delim=1, ResponseEnd=2). // // Scanning stops at EOF or the first I/O error. type Scanner struct { @@ -51,11 +57,13 @@ func (s *Scanner) Scan() bool { return s.err == nil } -// Bytes returns the most recent packet generated by a call to Scan. -// The underlying array may point to data that will be overwritten by a -// subsequent call to Scan. It does no allocation. -// This returns nil if the last scan was a special packet such as a [Flush], -// [Delim], or [ResponseEnd]. +// Bytes returns the payload of the most recent pkt-line as a slice +// into the Scanner's internal buffer. The slice is valid only until +// the next call to Scan, which overwrites the buffer. Use [Text] or +// copy the data when the payload must outlive the next Scan. +// +// Bytes does no allocation. It returns nil for special pkt-lines +// ([Flush], [Delim], [ResponseEnd]); use [Len] to distinguish them. func (s *Scanner) Bytes() []byte { if s.n >= LenSize { return s.buf[LenSize:s.n] @@ -68,8 +76,10 @@ func (s *Scanner) Text() string { return string(s.Bytes()) } -// Len returns the length of the most recent packet generated by a call to -// Scan. +// Len returns the pkt-line length of the most recent pkt-line. For data +// lines this is the length of the entire pkt-line including the 4-byte +// length prefix. For special pkt-lines, Len returns the corresponding +// constant: [Flush] (0), [Delim] (1), or [ResponseEnd] (2). func (s *Scanner) Len() int { return s.n } diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/object/patch.go b/vendor/github.com/go-git/go-git/v6/plumbing/object/patch.go index 3213aea68..2a7f4444e 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/object/patch.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/object/patch.go @@ -50,6 +50,14 @@ func getPatchContext(ctx context.Context, message string, changes ...*Change) (* } func filePatchWithContext(ctx context.Context, c *Change) (fdiff.FilePatch, error) { + // Submodules (gitlinks) are not blob objects, so their contents cannot be + // read as files. Git represents them in a diff by a single + // "Subproject commit " line, so build that content synthetically + // instead of dropping the change from the patch. + if isSubmodule(c.From) || isSubmodule(c.To) { + return submoduleFilePatch(ctx, c) + } + from, to, err := c.Files() if err != nil { return nil, err @@ -98,6 +106,57 @@ func filePatchWithContext(ctx context.Context, c *Change) (fdiff.FilePatch, erro }, nil } +func isSubmodule(e ChangeEntry) bool { + return e != empty && e.TreeEntry.Mode == filemode.Submodule +} + +// submoduleContent returns the textual representation git uses for a submodule +// (gitlink) in a diff: a single "Subproject commit " line. It returns an +// empty string when the entry does not point to a submodule. +func submoduleContent(e ChangeEntry) string { + if !isSubmodule(e) { + return "" + } + + return fmt.Sprintf("Subproject commit %s\n", e.TreeEntry.Hash) +} + +// submoduleFilePatch builds a file patch for a change that adds, removes or +// updates a submodule. +func submoduleFilePatch(ctx context.Context, c *Change) (fdiff.FilePatch, error) { + fromContent := submoduleContent(c.From) + toContent := submoduleContent(c.To) + + diffs := diff.Do(fromContent, toContent) + + chunks := make([]fdiff.Chunk, 0, len(diffs)) + for _, d := range diffs { + select { + case <-ctx.Done(): + return nil, ErrCanceled + default: + } + + var op fdiff.Operation + switch d.Type { + case dmp.DiffEqual: + op = fdiff.Equal + case dmp.DiffDelete: + op = fdiff.Delete + case dmp.DiffInsert: + op = fdiff.Add + } + + chunks = append(chunks, &textChunk{d.Text, op}) + } + + return &textFilePatch{ + chunks: chunks, + from: c.From, + to: c.To, + }, nil +} + func fileContent(f *File) (content string, isBinary bool, err error) { if f == nil { return content, isBinary, err @@ -157,7 +216,7 @@ type changeEntryWrapper struct { } func (f *changeEntryWrapper) Hash() plumbing.Hash { - if !f.ce.TreeEntry.Mode.IsFile() { + if f.Empty() { return plumbing.ZeroHash } @@ -169,7 +228,7 @@ func (f *changeEntryWrapper) Mode() filemode.FileMode { } func (f *changeEntryWrapper) Path() string { - if !f.ce.TreeEntry.Mode.IsFile() { + if f.Empty() { return "" } @@ -177,7 +236,10 @@ func (f *changeEntryWrapper) Path() string { } func (f *changeEntryWrapper) Empty() bool { - return !f.ce.TreeEntry.Mode.IsFile() + // Submodules (gitlinks) are not files, but they still take part in a diff + // and thus must not be treated as empty entries. + return !f.ce.TreeEntry.Mode.IsFile() && + f.ce.TreeEntry.Mode != filemode.Submodule } // textFilePatch is an implementation of fdiff.FilePatch interface diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/object/tree.go b/vendor/github.com/go-git/go-git/v6/plumbing/object/tree.go index f9b9c77d8..77799ad4c 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/object/tree.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/object/tree.go @@ -1,6 +1,7 @@ package object import ( + "bufio" "context" "errors" "fmt" @@ -300,10 +301,13 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) { var prevSortName string for { - str, err := r.ReadString(' ') + // Use ReadSlice to get a view into bufio's internal buffer, + // avoiding a string allocation for the mode (which is parsed + // into a uint32 immediately and doesn't need to persist). + modeSlice, err := r.ReadSlice(' ') if err != nil { if err == io.EOF { - if len(str) != 0 { + if len(modeSlice) != 0 { return fmt.Errorf("%w: missing mode terminator", ErrMalformedTree) } break @@ -311,24 +315,35 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) { return err } - str = str[:len(str)-1] // strip last byte (' ') + modeSlice = modeSlice[:len(modeSlice)-1] // strip delimiter - mode, err := filemode.New(str) + mode, err := filemode.FromBytes(modeSlice) if err != nil { return fmt.Errorf("%w: malformed mode", ErrMalformedTree) } mode = canonicalTreeMode(mode) - name, err := r.ReadString(0) + nameSlice, err := r.ReadSlice(0) + if err == bufio.ErrBufferFull { + // Rare: name exceeds bufio's buffer. Accumulate the rest. + buf := append([]byte(nil), nameSlice...) + for err == bufio.ErrBufferFull { + var more []byte + more, err = r.ReadSlice(0) + buf = append(buf, more...) + } + nameSlice = buf + } if err != nil { if err == io.EOF { return fmt.Errorf("%w: missing filename terminator", ErrMalformedTree) } return err } - if len(name) == 1 { + if len(nameSlice) == 1 { return fmt.Errorf("%w: empty filename", ErrMalformedTree) } + name := string(nameSlice[:len(nameSlice)-1]) // strip delimiter var hash plumbing.Hash hash.ResetBySize(t.Hash.Size()) @@ -339,11 +354,10 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) { return err } - baseName := name[:len(name)-1] entry := TreeEntry{ Hash: hash, Mode: mode, - Name: baseName, + Name: name, } sortName := treeEntrySortName(&entry) if len(t.Entries) != 0 && prevSortName > sortName { @@ -598,6 +612,13 @@ type TreeWalker struct { recursive bool seen map[plumbing.Hash]bool + // skipPathValidation disables the pathutil.ValidTreePath check in Next. + // It is set by inspection-only callers (e.g. the diff treeNoder) that + // never funnel entry names into the filesystem and must enumerate trees + // faithfully, including entries with names upstream Git accepts but that + // are unsafe to materialise (control characters, `.git`-shaped names). + skipPathValidation bool + s storer.EncodedObjectStorer t *Tree } @@ -630,7 +651,7 @@ func NewTreeWalker(t *Tree, recursive bool, seen map[plumbing.Hash]bool) *TreeWa // HFS+/NTFS variants, Windows reserved names, and traversal sequences. // A malformed entry stops the walk with the validator's error; // inspection-only callers that need to enumerate raw, unvalidated -// names can read Tree.Entries directly. +// names can read Tree.Entries directly or set skipPathValidation. // // In the current implementation any objects which cannot be found in the // underlying repository will be skipped automatically. It is possible that this @@ -668,8 +689,10 @@ func (w *TreeWalker) Next() (name string, entry TreeEntry, err error) { continue } - if err := pathutil.ValidTreePath(entry.Name); err != nil { - return name, entry, err + if !w.skipPathValidation { + if err := pathutil.ValidTreePath(entry.Name); err != nil { + return name, entry, err + } } if entry.Mode == filemode.Dir { diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/object/treenoder.go b/vendor/github.com/go-git/go-git/v6/plumbing/object/treenoder.go index 155fb7356..0284a1416 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/object/treenoder.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/object/treenoder.go @@ -106,6 +106,12 @@ func transformChildren(t *Tree) ([]noder.Noder, error) { ret := make([]noder.Noder, 0, len(t.Entries)) walker := NewTreeWalker(t, false, nil) // don't recurse + // The diff walk is read-only and never materialises entry names into the + // filesystem, so it must enumerate the tree faithfully — including entries + // with names that are unsafe to check out but valid per upstream Git (e.g. + // control characters). Path safety is enforced at materialisation + // boundaries (FindEntry, TreeEntryFile, archive, FileIter), not here. + walker.skipPathValidation = true // don't defer walker.Close() for efficiency reasons. for { _, e, err = walker.Next() diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/objectid.go b/vendor/github.com/go-git/go-git/v6/plumbing/objectid.go index 327e7741c..2697ec7e0 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/objectid.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/objectid.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/binary" "encoding/hex" - "fmt" "io" format "github.com/go-git/go-git/v6/plumbing/format/config" @@ -117,14 +116,16 @@ func (s *ObjectID) Write(in []byte) (int, error) { return n, nil } -// ReadFrom reads the Big Endian representation of the ObjectID from -// reader [r]. +// ReadFrom reads the raw bytes of the ObjectID from reader [r]. +// The number of bytes read is determined by the ObjectID's current size. func (s *ObjectID) ReadFrom(r io.Reader) (int64, error) { - err := binary.Read(r, binary.BigEndian, s.hash[:s.Size()]) + n, err := io.ReadFull(r, s.hash[:s.Size()]) if err != nil { - return 0, fmt.Errorf("read hash from binary: %w", err) + // Clear partial read so the hash remains zero on error. + s.ResetBySize(s.Size()) + return 0, err } - return int64(s.Size()), nil + return int64(n), nil } // WriteTo writes the Big Endian representation of the ObjectID @@ -149,5 +150,5 @@ func (s *ObjectID) ResetBySize(idSize int) { } else { s.format = format.UnsetObjectFormat } - copy(s.hash[:], s.hash[:0]) + clear(s.hash[:]) } diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/capability/capability.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/capability/capability.go index 39a656789..eeb67b87a 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/capability/capability.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/capability/capability.go @@ -266,10 +266,34 @@ const ( // Filter if present, fetch-pack may send "filter" commands to request a // partial clone or partial fetch and request that the server omit various objects from the packfile Filter Capability = "filter" - // SessionID the server may advertise a session ID that can be used to - // identify this process across multiple requests. The client may advertise - // its own session ID back to the server as well. +) + +// V2 command capabilities are advertised by the server in protocol v2 +// capability advertisements. +const ( + // LsRefs is a v2 command capability indicating the server supports + // the ls-refs command for reference discovery. + LsRefs Capability = "ls-refs" + // FetchCmd is a v2 command capability indicating the server supports + // the fetch command. Named FetchCmd to avoid collision with the + // transport-level Fetch method. + FetchCmd Capability = "fetch" + // ObjectInfo is a v2 command capability indicating the server supports + // the object-info command. + ObjectInfo Capability = "object-info" + // BundleURI is a v2 command capability indicating the server supports + // the bundle-uri command. + BundleURI Capability = "bundle-uri" + + // V2 non-command capabilities. + // ServerOption indicates the server supports the server-option capability. + ServerOption Capability = "server-option" + // SessionID indicates the server supports session-id for correlating + // requests and responses in stateless RPC (HTTP). SessionID Capability = "session-id" + // WaitForDone is a v2 fetch sub-feature indicating the server supports + // the wait-for-done argument in the fetch command. + WaitForDone Capability = "wait-for-done" ) const userAgent = "go-git/6.x" diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/capability/list.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/capability/list.go index ef8d13070..fec4fd3ff 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/capability/list.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/capability/list.go @@ -2,14 +2,11 @@ package capability import ( "bytes" - "fmt" - "strings" ) // List represents a list of capabilities. The zero value is safe to use; -// the internal map is lazily initialized on first write. -// -// Note that the List is not thread safe. +// the internal map is lazily initialized on first write. List is not safe for +// concurrent use. type List struct { m map[string]*entry sort []string @@ -37,22 +34,41 @@ func DecodeList(raw []byte, l *List) { } raw = bytes.TrimSpace(raw) - if len(raw) == 0 { return } - for data := range bytes.SplitSeq(raw, []byte{' '}) { - pair := bytes.SplitN(data, []byte{'='}, 2) + for len(raw) > 0 { + var chunk []byte + if i := bytes.IndexByte(raw, ' '); i >= 0 { + chunk = raw[:i] + raw = raw[i+1:] + } else { + chunk = raw + raw = nil + } - c := string(pair[0]) - if len(pair) == 1 { - l.Add(c) + if len(chunk) == 0 { continue } - l.Add(c, string(pair[1])) + if before, after, ok := bytes.Cut(chunk, []byte{'='}); ok { + l.Add(string(before), string(after)) + } else { + l.Add(string(chunk)) + } + } +} + +// EncodeList encodes the List into a v0/v1 space-separated capability string. +// This is the format used in advertise-refs, upload-request, and +// update-request messages. +func EncodeList(l *List) []byte { + if l == nil { + return nil } + b, _ := l.MarshalText() + return b } // Get returns the values for a capability @@ -135,24 +151,50 @@ func (l *List) All() []string { return cs } -// String generates the capabilities strings, the capabilities are sorted in -// insertion order -func (l *List) String() string { - var o []string +// MarshalText implements encoding.TextMarshaler. +func (l *List) MarshalText() ([]byte, error) { + return l.AppendText(nil) +} + +// AppendText implements encoding.TextAppender. +func (l *List) AppendText(b []byte) ([]byte, error) { + first := true for _, key := range l.sort { if l.m == nil { continue } c := l.m[key] if len(c.Values) == 0 { - o = append(o, key) + if !first { + b = append(b, ' ') + } + first = false + b = append(b, key...) continue } for _, value := range c.Values { - o = append(o, fmt.Sprintf("%s=%s", key, value)) + if !first { + b = append(b, ' ') + } + first = false + b = append(b, key...) + b = append(b, '=') + b = append(b, value...) } } + return b, nil +} - return strings.Join(o, " ") +// UnmarshalText implements encoding.TextUnmarshaler. +func (l *List) UnmarshalText(text []byte) error { + DecodeList(text, l) + return nil +} + +// String generates the capabilities strings, the capabilities are sorted in +// insertion order. +func (l *List) String() string { + b, _ := l.MarshalText() + return string(b) } diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/advrefs.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/advrefs.go index e605b6aec..5220acca7 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/advrefs.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/advrefs.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/go-git/go-git/v6/plumbing" + "github.com/go-git/go-git/v6/plumbing/protocol" "github.com/go-git/go-git/v6/plumbing/protocol/capability" ) @@ -14,6 +15,12 @@ import ( // advertised-refs message. The zero value is safe to use; References // and Shallows can be populated via append. type AdvRefs struct { + // Version is the protocol version of the advertisement. The only acceptable + // values are V0 and V1; any other value is invalid. Decode parses it from + // the leading "version" pkt-line (absent for V0) and Encode emits that line + // from it. Within the transport it is set from the version the handshake + // negotiated (DiscoverVersion), which is the single source of truth. + Version protocol.Version // Capabilities are the capabilities. Capabilities capability.List // References are the hash references, including HEAD and peeled refs @@ -136,16 +143,28 @@ func (a *AdvRefs) symRefMap() (map[plumbing.ReferenceName]plumbing.ReferenceName // - If not, scan references in alphabetical order for a matching hash. // - If no match is found, HEAD is returned unchanged. func (a *AdvRefs) resolvedHeadFromHeuristic(head *plumbing.Reference) *plumbing.Reference { + return ResolveHeadFromHashHeuristic(head, a.References) +} + +// ResolveHeadFromHashHeuristic converts a detached HEAD (a HashReference) into a +// SymbolicReference pointing to the branch that shares its hash, scanning refs. +// It is shared by the v0/v1 advertisement resolution and the Protocol v2 ls-refs +// path, so a detached remote HEAD still yields a symbolic local HEAD on clone, +// matching reference git's pre-symref heuristic: +// - Prefer refs/heads/master when it has the same hash as HEAD. +// - Otherwise pick the alphabetically-first non-peeled ref with that hash. +// - If nothing matches, HEAD is returned unchanged. +func ResolveHeadFromHashHeuristic(head *plumbing.Reference, refs []*plumbing.Reference) *plumbing.Reference { headHash := head.Hash() - for _, ref := range a.References { + for _, ref := range refs { if ref.Name() == plumbing.Master && ref.Type() == plumbing.HashReference && ref.Hash() == headHash { return plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.Master) } } - candidates := make([]*plumbing.Reference, 0, len(a.References)) - for _, ref := range a.References { + candidates := make([]*plumbing.Reference, 0, len(refs)) + for _, ref := range refs { if ref.Name() == plumbing.HEAD || ref.Name().IsPeeled() { continue } diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/advrefs_decode.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/advrefs_decode.go index 8b2923d72..1eb90730a 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/advrefs_decode.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/advrefs_decode.go @@ -5,9 +5,11 @@ import ( "errors" "fmt" "io" + "strings" "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/format/pktline" + "github.com/go-git/go-git/v6/plumbing/protocol" "github.com/go-git/go-git/v6/plumbing/protocol/capability" ) @@ -28,28 +30,33 @@ func (a *AdvRefs) Decode(r io.Reader) error { err error ) + s := pktline.NewScanner(r) + nextLine := func() bool { nLine++ - _, p, e := pktline.ReadLine(r) - if e != nil { - if errors.Is(e, io.EOF) { + if !s.Scan() { + if s.Err() == nil { if nLine == 1 { err = ErrEmptyInput } else { - err = NewErrUnexpectedData(fmt.Sprintf("pkt-line %d: unexpected EOF", nLine), line) + err = NewErrUnexpectedData(fmt.Sprintf("pkt-line %d: unexpected EOF", nLine), bytes.Clone(line)) } } else { - err = e + err = s.Err() } return false } - line = bytes.TrimSuffix(p, eol) + if s.Len() == pktline.Flush { + line = nil + return true + } + line = bytes.TrimSuffix(s.Bytes(), eol) return true } decodeError := func(format string, a ...any) error { msg := fmt.Sprintf("pkt-line %d: %s", nLine, fmt.Sprintf(format, a...)) - return NewErrUnexpectedData(msg, line) + return NewErrUnexpectedData(msg, bytes.Clone(line)) } if !nextLine() { @@ -61,6 +68,28 @@ func (a *AdvRefs) Decode(r io.Reader) error { return ErrEmptyAdvRefs } + if line := string(line); strings.HasPrefix(line, "version ") { + v, perr := protocol.Parse(line[len("version "):]) + if perr != nil { + return perr + } + a.Version = v + + if !nextLine() { + return err + } + } + + if a.Version != protocol.V0 && a.Version != protocol.V1 { + return decodeError("unsupported protocol version: %d", a.Version) + } + + // Check for empty repository (flush packet), which may appear + // either as the first line or after the version line. + if isFlush(line) { + return ErrEmptyAdvRefs + } + // Must have at least a hash if len(line) < sha1HexSize { return decodeError("line too short for hash") diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/advrefs_encode.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/advrefs_encode.go index 56425ffe3..b07874ac8 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/advrefs_encode.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/advrefs_encode.go @@ -8,6 +8,7 @@ import ( "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/format/pktline" + "github.com/go-git/go-git/v6/plumbing/protocol" ) // Encode writes the AdvRefs encoding to a writer. @@ -16,6 +17,16 @@ import ( // references and shallows are written in alphabetical order, except for // peeled references that always follow their corresponding references. func (a *AdvRefs) Encode(w io.Writer) error { + switch a.Version { + case protocol.V0: + case protocol.V1: + if _, err := pktline.Writef(w, "version %d\n", a.Version); err != nil { + return err + } + default: + return fmt.Errorf("unsupported protocol version: %d", a.Version) + } + // Find HEAD or use first ref firstName, firstHash := a.firstRef() diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/capability_adv.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/capability_adv.go new file mode 100644 index 000000000..1a0b6ea55 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/capability_adv.go @@ -0,0 +1,96 @@ +package packp + +import ( + "bytes" + "errors" + "fmt" + "io" + + "github.com/go-git/go-git/v6/plumbing/format/pktline" + "github.com/go-git/go-git/v6/plumbing/protocol" + "github.com/go-git/go-git/v6/plumbing/protocol/capability" +) + +// CapabilityAdv represents a protocol v2 server capability advertisement. +// It includes the version line and the capability lines that follow it. +// +// In protocol v2, the server sends: +// +// version 2\n +// agent=git/2.45.0\n +// ls-refs=unborn\n +// fetch=shallow wait-for-done filter\n +// 0000 +// +// Capabilities are one per line in "key" or "key=value" format, +// terminated by a flush packet. This differs from v0/v1 where +// capabilities are space-separated after a NUL byte on the first ref line. +type CapabilityAdv struct { + // Version is the protocol version. Decode sets this to V2. + // Encode writes the version line when Version is V2. + Version protocol.Version + // Capabilities is the parsed list of server capabilities. + Capabilities capability.List +} + +// Decode reads a v2 capability advertisement from a pkt-line stream. +// It expects the stream to start with the "version 2\n" line, +// followed by capability lines (one per line), terminated by a flush packet. +func (ca *CapabilityAdv) Decode(r io.Reader) error { + // Read version line first. + l, line, err := pktline.ReadLine(r) + if err != nil { + return err + } + if l < 4 || line == nil { + return errInvalidVersionLine + } + + line = bytes.TrimSuffix(line, []byte("\n")) + if !bytes.HasPrefix(line, []byte("version ")) { + return errInvalidVersionLine + } + + v, err := protocol.Parse(string(line[8:])) + if err != nil { + return err + } + + if v != protocol.V2 { + return fmt.Errorf("unsupported protocol version in capability advertisement: %s", v) + } + + ca.Version = v + + // Read capability lines until flush. + length, err := DecodeListV2(r, &ca.Capabilities) + if err != nil { + return fmt.Errorf("decoding capability list: %w", err) + } + if length != pktline.Flush { + return fmt.Errorf("expected flush-pkt after capability list, got %04x", length) + } + return nil +} + +// Encode writes a v2 capability advertisement to a pkt-line stream. +// It writes the "version N\n" line (where N is ca.Version), then each +// capability on its own line, and terminates with a flush packet. +// Encode returns an error if ca.Version is not V2. +func (ca *CapabilityAdv) Encode(w io.Writer) error { + if ca.Version != protocol.V2 { + return fmt.Errorf("unsupported protocol version for capability advertisement: %s", ca.Version) + } + + if _, err := pktline.Writef(w, "version %d\n", ca.Version); err != nil { + return err + } + + if err := EncodeListV2(w, &ca.Capabilities); err != nil { + return err + } + + return pktline.WriteFlush(w) +} + +var errInvalidVersionLine = errors.New("capability advertisement must start with version line") diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/command.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/command.go new file mode 100644 index 000000000..71a4aa029 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/command.go @@ -0,0 +1,117 @@ +package packp + +import ( + "bytes" + "errors" + "fmt" + "io" + + "github.com/go-git/go-git/v6/plumbing/format/pktline" + "github.com/go-git/go-git/v6/plumbing/protocol/capability" +) + +// CommandArgs is the interface for v2 command-specific arguments. +type CommandArgs interface { + Encoder + Decoder +} + +// CommandRequest represents a v2 command request. +// +// Wire format: +// +// request = empty-request | command-request +// empty-request = flush-pkt +// command-request = command +// capability-list +// delim-pkt +// command-args +// flush-pkt +// command = PKT-LINE("command=" key LF) +// command-args = *command-specific-arg +// +// An empty Command encodes as an empty request (a single flush-pkt). +// On decode, a flush-pkt as the first packet leaves Command empty. +type CommandRequest struct { + Command string + Capabilities capability.List + Args CommandArgs +} + +// Encode writes the command request to w. +// If Command is empty, it writes a single flush-pkt (empty request). +func (c *CommandRequest) Encode(w io.Writer) error { + if c.Command == "" { + return pktline.WriteFlush(w) + } + + if _, err := pktline.Writef(w, "command=%s\n", c.Command); err != nil { + return err + } + + if err := EncodeListV2(w, &c.Capabilities); err != nil { + return err + } + + if err := pktline.WriteDelim(w); err != nil { + return err + } + + if c.Args != nil { + if err := c.Args.Encode(w); err != nil { + return err + } + } + + return pktline.WriteFlush(w) +} + +// Decode reads a command request from r. +// If the first packet is a flush-pkt, Command is left empty (empty request). +func (c *CommandRequest) Decode(r io.Reader) error { + c.Command = "" + c.Capabilities = capability.List{} + + length, line, err := pktline.ReadLine(r) + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + return err + } + + if length == pktline.Flush { + return nil + } + + line = bytes.TrimSuffix(line, []byte("\n")) + const prefix = "command=" + if !bytes.HasPrefix(line, []byte(prefix)) { + return fmt.Errorf("expected command line, got %q", string(line)) + } + c.Command = string(line[len(prefix):]) + + // Read capabilities until delim-pkt. + length, err = DecodeListV2(r, &c.Capabilities) + if err != nil { + return err + } + if length != pktline.Delim { + return fmt.Errorf("expected delim-pkt after capabilities, got %04x", length) + } + + // Read command args until flush-pkt. + if c.Args != nil { + return c.Args.Decode(r) + } + + // No args decoder — consume the flush-pkt. + length, _, err = pktline.ReadLine(r) + if err != nil { + return err + } + if length != pktline.Flush { + return fmt.Errorf("expected flush-pkt after empty args, got %04x", length) + } + return nil +} diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/fetch.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/fetch.go new file mode 100644 index 000000000..04bbbd42d --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/fetch.go @@ -0,0 +1,794 @@ +package packp + +import ( + "errors" + "fmt" + "io" + "strconv" + "strings" + "time" + + "github.com/go-git/go-git/v6/plumbing" + "github.com/go-git/go-git/v6/plumbing/format/pktline" +) + +// maxSectionLines bounds how many entries a single fetch section (want, have, +// shallow, ACK, wanted-ref, ...) may contribute on decode. It is a defensive +// backstop against a hostile peer streaming unbounded lines into an in-memory +// slice; it sits far above any legitimate request or response (a single +// negotiation round carries at most a flush-batch of haves, and real repos have +// far fewer than four million refs). It is a var only so tests can lower it. +var maxSectionLines = 1 << 22 + +// MalformedResponseError reports a server response that violates the +// gitprotocol-v2 grammar: a malformed pkt-line, an unrecognized line within a +// section, an unexpected/repeated/out-of-order section, or a section terminator +// that contradicts the response shape. It mirrors the situations where upstream +// fetch-pack.c calls die() on the response. +type MalformedResponseError struct { + Reason string +} + +func (e *MalformedResponseError) Error() string { + return "malformed v2 fetch response: " + e.Reason +} + +// FetchArgs represents the arguments for the v2 fetch command. +type FetchArgs struct { + // Wants is the list of object IDs the client wants. + Wants []plumbing.Hash + // Haves is the list of object IDs the client already has. + Haves []plumbing.Hash + // Done indicates the client is done sending wants and haves. + // If false, the client may send additional want/have lines + // in subsequent request rounds (stateful transport only). + Done bool + // ThinPack requests a thin pack if the server supports it. + ThinPack bool + // NoProgress requests that the server suppress progress messages. + NoProgress bool + // IncludeTag requests that the server include tag objects. + IncludeTag bool + // OFSDelta requests that the server use OFS_DELTA objects. + OFSDelta bool + // Shallows is the list of shallow object IDs the client has. + Shallows []plumbing.Hash + // Deepen specifies the number of depth commits to fetch. + Deepen int + // DeepenRelative indicates that deepen is relative to the shallow boundary. + DeepenRelative bool + // DeepenSince specifies a time-based depth constraint. + DeepenSince time.Time + // DeepenNot specifies references to exclude from the shallow boundary. + DeepenNot []string + + // Filter specifies a partial clone filter. + Filter Filter + // WaitForDone indicates that the client will wait for the server to send a + // done acknowledgment before sending additional want/have lines. + WaitForDone bool +} + +// Encode writes the v2 fetch command arguments to a writer. +// Each argument is written as a separate pkt-line. +// The caller is responsible for writing the delim-pkt before and +// the flush-pkt after these arguments. +func (r *FetchArgs) Encode(w io.Writer) error { + if len(r.Wants) == 0 { + return fmt.Errorf("empty wants provided") + } + + wants := append([]plumbing.Hash(nil), r.Wants...) + plumbing.HashesSort(wants) + for _, h := range wants { + if _, err := pktline.Writef(w, "want %s\n", h); err != nil { + return fmt.Errorf("encoding want %q: %w", h, err) + } + } + + haves := append([]plumbing.Hash(nil), r.Haves...) + plumbing.HashesSort(haves) + for _, h := range haves { + if _, err := pktline.Writef(w, "have %s\n", h); err != nil { + return fmt.Errorf("encoding have %q: %w", h, err) + } + } + + if r.Done { + if _, err := pktline.WriteString(w, "done\n"); err != nil { + return fmt.Errorf("encoding done: %w", err) + } + } + + if r.ThinPack { + if _, err := pktline.WriteString(w, "thin-pack\n"); err != nil { + return fmt.Errorf("encoding thin-pack: %w", err) + } + } + + if r.NoProgress { + if _, err := pktline.WriteString(w, "no-progress\n"); err != nil { + return fmt.Errorf("encoding no-progress: %w", err) + } + } + + if r.IncludeTag { + if _, err := pktline.WriteString(w, "include-tag\n"); err != nil { + return fmt.Errorf("encoding include-tag: %w", err) + } + } + + if r.OFSDelta { + if _, err := pktline.WriteString(w, "ofs-delta\n"); err != nil { + return fmt.Errorf("encoding ofs-delta: %w", err) + } + } + + shallows := append([]plumbing.Hash(nil), r.Shallows...) + plumbing.HashesSort(shallows) + for _, h := range shallows { + if _, err := pktline.Writef(w, "shallow %s\n", h); err != nil { + return fmt.Errorf("encoding shallow %q: %w", h, err) + } + } + + if r.Deepen > 0 { + if _, err := pktline.Writef(w, "deepen %d\n", r.Deepen); err != nil { + return fmt.Errorf("encoding deepen %d: %w", r.Deepen, err) + } + } + + if r.DeepenRelative { + // deepen-relative is a flag: the depth is carried by the "deepen " + // line above. Matches git's fetch-pack.c (packet "deepen-relative\n"). + if _, err := pktline.WriteString(w, "deepen-relative\n"); err != nil { + return fmt.Errorf("encoding deepen-relative: %w", err) + } + } + + if !r.DeepenSince.IsZero() { + if _, err := pktline.Writef(w, "deepen-since %d\n", r.DeepenSince.UTC().Unix()); err != nil { + return fmt.Errorf("encoding deepen-since %s: %w", r.DeepenSince, err) + } + } + + for _, ref := range r.DeepenNot { + if _, err := pktline.Writef(w, "deepen-not %s\n", ref); err != nil { + return fmt.Errorf("encoding deepen-not %s: %w", ref, err) + } + } + + if r.Filter != "" { + if _, err := pktline.Writef(w, "filter %s\n", r.Filter); err != nil { + return fmt.Errorf("encoding filter %s: %w", r.Filter, err) + } + } + + if r.WaitForDone { + if _, err := pktline.WriteString(w, "wait-for-done\n"); err != nil { + return fmt.Errorf("encoding wait-for-done: %w", err) + } + } + + return nil +} + +// Decode reads v2 fetch command arguments from a reader until a flush-pkt +// is encountered. The caller is responsible for reading the delim-pkt +// and command header before calling Decode. +func (r *FetchArgs) Decode(rd io.Reader) error { + for { + l, pkt, err := pktline.ReadLine(rd) + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + return err + } + + if l == pktline.Flush || l == pktline.Delim { + return nil + } + + line := strings.TrimSpace(string(pkt)) + if len(line) == 0 { + return nil + } + + switch { + case strings.HasPrefix(line, "want "): + h, ok := parseFullHash(line[5:]) + if !ok { + return fmt.Errorf("malformed want hash: %q", line[5:]) + } + if len(r.Wants) >= maxSectionLines { + return fmt.Errorf("too many want lines (limit %d)", maxSectionLines) + } + r.Wants = append(r.Wants, h) + + case strings.HasPrefix(line, "have "): + h, ok := parseFullHash(line[5:]) + if !ok { + return fmt.Errorf("malformed have hash: %q", line[5:]) + } + if len(r.Haves) >= maxSectionLines { + return fmt.Errorf("too many have lines (limit %d)", maxSectionLines) + } + r.Haves = append(r.Haves, h) + + case line == "done": + r.Done = true + + case line == "thin-pack": + r.ThinPack = true + + case line == "no-progress": + r.NoProgress = true + + case line == "include-tag": + r.IncludeTag = true + + case line == "ofs-delta": + r.OFSDelta = true + + case strings.HasPrefix(line, "shallow "): + h, ok := parseFullHash(line[8:]) + if !ok { + return fmt.Errorf("malformed shallow hash: %q", line[8:]) + } + if len(r.Shallows) >= maxSectionLines { + return fmt.Errorf("too many shallow lines (limit %d)", maxSectionLines) + } + r.Shallows = append(r.Shallows, h) + + case line == "deepen-relative": + r.DeepenRelative = true + + case strings.HasPrefix(line, "deepen-relative "): + // Legacy/lenient: the depth belongs to "deepen "; the argument + // here is ignored. git only ever sends the bare flag. + r.DeepenRelative = true + + case strings.HasPrefix(line, "deepen-since "): + secs, e := strconv.ParseInt(line[13:], 10, 64) + if e != nil { + return fmt.Errorf("malformed deepen-since: %q", line) + } + r.DeepenSince = time.Unix(secs, 0).UTC() + + case strings.HasPrefix(line, "deepen-not "): + if len(r.DeepenNot) >= maxSectionLines { + return fmt.Errorf("too many deepen-not lines (limit %d)", maxSectionLines) + } + r.DeepenNot = append(r.DeepenNot, line[11:]) + + case strings.HasPrefix(line, "deepen "): + n, e := strconv.Atoi(line[7:]) + if e != nil { + return fmt.Errorf("malformed deepen: %q", line) + } + r.Deepen = n + + case strings.HasPrefix(line, "filter "): + r.Filter = Filter(line[7:]) + + case line == "wait-for-done": + r.WaitForDone = true + } + } +} + +// Acknowledgments represents the server response to a v2 fetch command's +// acknowledgments section. It is used by the transport layer to determine +// which objects the server has in common with the client. +type Acknowledgments struct { + // ACKs is the list of common object IDs acknowledged by the server. + // Empty list means the server found no common objects (NAK). + ACKs []plumbing.Hash + // Ready indicates the server is ready to send a packfile after the + // acknowledgments section. For stream transports, ready is implied and + // this field is always true. + Ready bool +} + +// ShallowInfo represents the server response to a v2 fetch command's +// shallow-info section. It is used by the transport layer to update the +// client's shallow boundary after a fetch. +type ShallowInfo struct { + // Shallows is the list of shallow object IDs sent by the server. + Shallows []plumbing.Hash + // Unshallows is the list of object IDs that are no longer shallow. + Unshallows []plumbing.Hash +} + +// WantedRefs represents the server response to a v2 fetch command's +// wanted-refs section. It is used by the transport layer to determine which +// references the server wants the client to have. +type WantedRefs struct { + // Refs is the list of references sent by the server. + Refs []*plumbing.Reference +} + +// PackfileURIs represents the server response to a v2 fetch command's +// packfile-uris section. It is used by the transport layer to determine which +// alternate URIs the server suggests for fetching the packfile. +type PackfileURIs struct { + // URIs is the list of alternate URIs the server suggests for fetching the + // packfile. + URIs []string +} + +// FetchOutput represents the server response to a v2 fetch command. +// +// The response has explicit sections separated by delim-pkt: +// +// acknowledgments\n +// ACK \n +// ready\n +// 0001 +// shallow-info\n +// shallow \n +// 0001 +// packfile\n +// +// 0000 +// +// For HTTP, the transport layer consumes response-end (0002) after Decode returns. +type FetchOutput struct { + // Acknowledgments indicates the server sent an acknowledgments section. + Acknowledgments *Acknowledgments + // ShallowInfo indicates the server sent a shallow-info section. + ShallowInfo *ShallowInfo + // WantedRefs indicates the server sent a wanted-refs section. + WantedRefs *WantedRefs + // PackfileURIs indicates the server sent a packfile-uris section. + PackfileURIs *PackfileURIs + // Packfile reports whether a packfile section follows the metadata + // sections. When true, Decode leaves the reader positioned at the first + // packfile pkt-line so the caller can stream it, and Encode writes the + // "packfile" section header so the caller can write the packfile data. + // When false, the response is a negotiation round + // (acknowledgments flush-pkt) that carries no packfile. + Packfile bool +} + +// Decode reads the v2 fetch response from a reader. The response has +// explicit sections separated by delim-pkt: +// +// acknowledgments\n +// ACK \n +// ready\n +// 0001 +// shallow-info\n +// shallow \n +// 0001 +// packfile\n +// +// 0000 +// +// A response is one of two shapes (gitprotocol-v2): +// +// output = acknowledgments flush-pkt | +// [acknowledgments delim-pkt] [shallow-info delim-pkt] +// [wanted-refs delim-pkt] [packfile-uris delim-pkt] +// packfile flush-pkt +// +// When a metadata section ends with a flush-pkt (the first shape) the +// response is a negotiation round that carries no packfile, and Decode +// returns with Packfile set to false. When Decode reaches the "packfile" +// section header it sets Packfile to true and returns with the reader +// positioned at the first packfile pkt-line; Decode does not read the +// packfile data, leaving the caller to stream it (demultiplexing the +// sideband as needed). +// +// For HTTP, the transport layer consumes response-end (0002) after +// Decode returns. +func (r *FetchOutput) Decode(rd io.Reader) error { + // Sections appear at most once and in the fixed grammar order + // (acknowledgments < shallow-info < wanted-refs < packfile-uris < + // packfile). lastRank enforces both: a header whose rank is not strictly + // greater than the previous one is a repeat or out-of-order, which upstream + // fetch-pack.c rejects via die(). expectPackfile records that a metadata + // section committed the response to the packfile shape, so a premature + // terminator is also rejected. + lastRank := 0 + expectPackfile := false + + for { + l, pkt, err := pktline.ReadLine(rd) + if err != nil { + if errors.Is(err, io.EOF) { + // A premature EOF after a metadata section committed the + // response to the packfile shape is a truncated response, not + // a clean end. Match the flush/response-end handling below. + if expectPackfile { + return &MalformedResponseError{Reason: "expected packfile section"} + } + return nil + } + return err + } + + // A flush-pkt at the top level ends the response. It is only valid + // before the packfile shape was committed to (a negotiation round, an + // empty response, or a clone that turned out to need nothing). + if l == pktline.Flush || l == pktline.ResponseEnd { + if expectPackfile { + return &MalformedResponseError{Reason: "expected packfile section"} + } + return nil + } + + header := strings.TrimSpace(string(pkt)) + rank := fetchSectionRank(header) + if rank == 0 { + return &MalformedResponseError{Reason: fmt.Sprintf("unexpected section %q", header)} + } + if rank <= lastRank { + return &MalformedResponseError{Reason: fmt.Sprintf("section %q is repeated or out of order", header)} + } + lastRank = rank + + switch header { + case "packfile": + // Leave the reader positioned at the packfile data and let + // the caller stream it. Decode never reads packfile bytes. + r.Packfile = true + return nil + + case "acknowledgments": + r.Acknowledgments = &Acknowledgments{} + term, err := r.decodeAcknowledgments(rd) + if err != nil { + return err + } + // ready commits to the packfile shape and must be followed by a + // delim-pkt; otherwise the section is a negotiation round and must + // end the response with a flush-pkt (upstream process_ack). + if r.Acknowledgments.Ready { + if term != pktline.Delim { + return &MalformedResponseError{Reason: "ready acknowledgment must be followed by a delim-pkt"} + } + expectPackfile = true + } else { + if term == pktline.Delim { + return &MalformedResponseError{Reason: "acknowledgments without ready must end the response"} + } + return nil + } + + case "shallow-info": + r.ShallowInfo = &ShallowInfo{} + if err := r.decodeMetadataSection(rd, r.decodeShallowInfo); err != nil { + return err + } + expectPackfile = true + + case "wanted-refs": + r.WantedRefs = &WantedRefs{} + if err := r.decodeMetadataSection(rd, r.decodeWantedRefs); err != nil { + return err + } + expectPackfile = true + + case "packfile-uris": + r.PackfileURIs = &PackfileURIs{} + if err := r.decodeMetadataSection(rd, r.decodePackfileURIs); err != nil { + return err + } + expectPackfile = true + } + } +} + +// fetchSectionRank maps a fetch response section header to its position in the +// gitprotocol-v2 grammar, or 0 for an unrecognized header. +func fetchSectionRank(header string) int { + switch header { + case "acknowledgments": + return 1 + case "shallow-info": + return 2 + case "wanted-refs": + return 3 + case "packfile-uris": + return 4 + case "packfile": + return 5 + default: + return 0 + } +} + +// decodeMetadataSection runs a section decoder and enforces that the section is +// terminated by a delim-pkt, since every metadata section (shallow-info, +// wanted-refs, packfile-uris) precedes the packfile and is delimited from it. +func (r *FetchOutput) decodeMetadataSection(rd io.Reader, decode func(io.Reader) (int, error)) error { + term, err := decode(rd) + if err != nil { + return err + } + if term != pktline.Delim { + return &MalformedResponseError{Reason: "metadata section must be followed by a delim-pkt"} + } + return nil +} + +// Encode writes the v2 fetch response to a writer. +// +// When Packfile is true, Encode writes the present metadata sections +// (acknowledgments, shallow-info, wanted-refs, packfile-uris), each +// terminated by a delim-pkt, followed by the "packfile" section header. +// The caller then streams the packfile data and writes the final +// flush-pkt. +// +// When Packfile is false, the response is a negotiation round: Encode +// writes the acknowledgments section terminated by a flush-pkt and writes +// nothing else. In that case the acknowledgments section must be present +// and must not be ready, and no other metadata sections may be set. +func (r *FetchOutput) Encode(w io.Writer) error { + if !r.Packfile { + if r.Acknowledgments == nil { + return fmt.Errorf("fetch response without a packfile must carry acknowledgments") + } + if r.Acknowledgments.Ready { + return fmt.Errorf("fetch response with ready must carry a packfile") + } + if r.ShallowInfo != nil || r.WantedRefs != nil || r.PackfileURIs != nil { + return fmt.Errorf("fetch response without a packfile cannot carry metadata sections") + } + if _, err := pktline.WriteString(w, "acknowledgments\n"); err != nil { + return err + } + if err := r.encodeAcknowledgments(w); err != nil { + return err + } + return pktline.WriteFlush(w) + } + + if r.Acknowledgments != nil { + if _, err := pktline.WriteString(w, "acknowledgments\n"); err != nil { + return err + } + if err := r.encodeAcknowledgments(w); err != nil { + return err + } + if err := pktline.WriteDelim(w); err != nil { + return err + } + } + + if r.ShallowInfo != nil { + if _, err := pktline.WriteString(w, "shallow-info\n"); err != nil { + return err + } + if err := r.encodeShallowInfo(w); err != nil { + return err + } + if err := pktline.WriteDelim(w); err != nil { + return err + } + } + + if r.WantedRefs != nil { + if _, err := pktline.WriteString(w, "wanted-refs\n"); err != nil { + return err + } + if err := r.encodeWantedRefs(w); err != nil { + return err + } + if err := pktline.WriteDelim(w); err != nil { + return err + } + } + + if r.PackfileURIs != nil { + if _, err := pktline.WriteString(w, "packfile-uris\n"); err != nil { + return err + } + if err := r.encodePackfileURIs(w); err != nil { + return err + } + if err := pktline.WriteDelim(w); err != nil { + return err + } + } + + // Packfile section header. The caller writes the packfile data and the + // final flush-pkt after this. + if _, err := pktline.WriteString(w, "packfile\n"); err != nil { + return err + } + + return nil +} + +func (r *FetchOutput) decodeAcknowledgments(rd io.Reader) (int, error) { + for { + l, pkt, err := pktline.ReadLine(rd) + if err != nil { + return 0, err + } + + if l == pktline.Delim || l == pktline.Flush || l == pktline.ResponseEnd { + return l, nil + } + + line := strings.TrimSpace(string(pkt)) + + switch { + case strings.HasPrefix(line, "ACK "): + parts := strings.SplitN(line, " ", 2) + if len(parts) < 2 { + return 0, &MalformedResponseError{Reason: fmt.Sprintf("malformed ACK line: %q", line)} + } + h, ok := parseFullHash(strings.TrimSpace(parts[1])) + if !ok { + return 0, &MalformedResponseError{Reason: fmt.Sprintf("malformed ACK hash: %q", parts[1])} + } + if len(r.Acknowledgments.ACKs) >= maxSectionLines { + return 0, &MalformedResponseError{Reason: fmt.Sprintf("too many ACK lines (limit %d)", maxSectionLines)} + } + r.Acknowledgments.ACKs = append(r.Acknowledgments.ACKs, h) + + case line == "NAK": + // NAK: no common objects + + case line == "ready": + r.Acknowledgments.Ready = true + + default: + return 0, &MalformedResponseError{Reason: fmt.Sprintf("unexpected acknowledgments line: %q", line)} + } + } +} + +func (r *FetchOutput) decodeShallowInfo(rd io.Reader) (int, error) { + for { + l, pkt, err := pktline.ReadLine(rd) + if err != nil { + return 0, err + } + + if l == pktline.Delim || l == pktline.Flush || l == pktline.ResponseEnd { + return l, nil + } + + line := strings.TrimSpace(string(pkt)) + + switch { + case strings.HasPrefix(line, "shallow "): + h, ok := parseFullHash(line[8:]) + if !ok { + return 0, &MalformedResponseError{Reason: fmt.Sprintf("malformed shallow hash: %q", line)} + } + if len(r.ShallowInfo.Shallows) >= maxSectionLines { + return 0, &MalformedResponseError{Reason: fmt.Sprintf("too many shallow lines (limit %d)", maxSectionLines)} + } + r.ShallowInfo.Shallows = append(r.ShallowInfo.Shallows, h) + + case strings.HasPrefix(line, "unshallow "): + h, ok := parseFullHash(line[10:]) + if !ok { + return 0, &MalformedResponseError{Reason: fmt.Sprintf("malformed unshallow hash: %q", line)} + } + if len(r.ShallowInfo.Unshallows) >= maxSectionLines { + return 0, &MalformedResponseError{Reason: fmt.Sprintf("too many unshallow lines (limit %d)", maxSectionLines)} + } + r.ShallowInfo.Unshallows = append(r.ShallowInfo.Unshallows, h) + + default: + return 0, &MalformedResponseError{Reason: fmt.Sprintf("expected shallow/unshallow, got: %q", line)} + } + } +} + +func (r *FetchOutput) decodeWantedRefs(rd io.Reader) (int, error) { + for { + l, pkt, err := pktline.ReadLine(rd) + if err != nil { + return 0, err + } + + if l == pktline.Delim || l == pktline.Flush || l == pktline.ResponseEnd { + return l, nil + } + + line := strings.TrimSpace(string(pkt)) + + parts := strings.SplitN(line, " ", 2) + if len(parts) < 2 { + return 0, &MalformedResponseError{Reason: fmt.Sprintf("malformed wanted-refs line: %q", line)} + } + + h, ok := parseFullHash(parts[0]) + if !ok { + return 0, &MalformedResponseError{Reason: fmt.Sprintf("malformed wanted-refs hash: %q", parts[0])} + } + + if len(r.WantedRefs.Refs) >= maxSectionLines { + return 0, &MalformedResponseError{Reason: fmt.Sprintf("too many wanted-refs lines (limit %d)", maxSectionLines)} + } + r.WantedRefs.Refs = append(r.WantedRefs.Refs, + plumbing.NewHashReference(plumbing.ReferenceName(parts[1]), h), + ) + } +} + +func (r *FetchOutput) decodePackfileURIs(rd io.Reader) (int, error) { + for { + l, pkt, err := pktline.ReadLine(rd) + if err != nil { + return 0, err + } + + if l == pktline.Delim || l == pktline.Flush || l == pktline.ResponseEnd { + return l, nil + } + + line := strings.TrimSuffix(string(pkt), "\n") + + if len(r.PackfileURIs.URIs) >= maxSectionLines { + return 0, &MalformedResponseError{Reason: fmt.Sprintf("too many packfile-uris lines (limit %d)", maxSectionLines)} + } + r.PackfileURIs.URIs = append(r.PackfileURIs.URIs, line) + } +} + +// encodeAcknowledgments writes the acknowledgments body following upstream +// send_acks (upload-pack.c): the ACK lines first, then a single "ready" when +// the server is ready to send a packfile (and nothing after it), otherwise a +// lone "NAK" when there were no common objects. The grammar is +// (nak | *ack) (ready): NAK is mutually exclusive with ACKs and is suppressed +// once ready is sent, and ready always comes last. +func (r *FetchOutput) encodeAcknowledgments(w io.Writer) error { + for _, h := range r.Acknowledgments.ACKs { + if _, err := pktline.Writef(w, "ACK %s\n", h); err != nil { + return err + } + } + if r.Acknowledgments.Ready { + if _, err := pktline.WriteString(w, "ready\n"); err != nil { + return err + } + return nil + } + if len(r.Acknowledgments.ACKs) == 0 { + if _, err := pktline.WriteString(w, "NAK\n"); err != nil { + return err + } + } + return nil +} + +func (r *FetchOutput) encodeShallowInfo(w io.Writer) error { + for _, h := range r.ShallowInfo.Shallows { + if _, err := pktline.Writef(w, "shallow %s\n", h); err != nil { + return err + } + } + for _, h := range r.ShallowInfo.Unshallows { + if _, err := pktline.Writef(w, "unshallow %s\n", h); err != nil { + return err + } + } + return nil +} + +func (r *FetchOutput) encodeWantedRefs(w io.Writer) error { + for _, ref := range r.WantedRefs.Refs { + if _, err := pktline.Writef(w, "%s %s\n", ref.Hash(), ref.Name()); err != nil { + return err + } + } + return nil +} + +func (r *FetchOutput) encodePackfileURIs(w io.Writer) error { + for _, uri := range r.PackfileURIs.URIs { + if _, err := pktline.WriteString(w, uri+"\n"); err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/gitproto.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/gitproto.go index de1f512a6..4dc3b6db0 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/gitproto.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/gitproto.go @@ -1,7 +1,6 @@ package packp import ( - "errors" "fmt" "io" "strings" @@ -34,6 +33,45 @@ func (g *GitProtoRequest) validate() error { return fmt.Errorf("%w: empty request command", ErrInvalidGitProtoRequest) } + // The request is a single pkt-line whose fields are separated by NUL + // bytes (" \x00host=\x00\x00..."). A + // control byte in any field breaks that framing or splices in extra + // NUL-delimited fields (a second host=, additional parameters) that the + // caller never set. A git:// URL with a percent-encoded NUL, for example + // "git://host/repo%00host=evil", decodes into exactly such a Pathname, so + // refuse control bytes in every field. + if err := validateGitProtoField("request command", g.RequestCommand); err != nil { + return err + } + if err := validateGitProtoField("pathname", g.Pathname); err != nil { + return err + } + if err := validateGitProtoField("host", g.Host); err != nil { + return err + } + for _, p := range g.ExtraParams { + if err := validateGitProtoField("extra parameter", p); err != nil { + return err + } + } + + return nil +} + +// validateGitProtoField rejects a request field containing an ASCII control +// byte (0x00-0x1f or 0x7f). Such a byte would break the NUL-framed pkt-line or +// splice in additional fields. No valid command, path, host, or parameter +// contains one. This matches upstream git, which forbids newlines in the host +// and path of a git:// request (git.git a02ea577, CVE-2021-40330), and extends +// it to the full control range including NUL, which a Go string can carry +// through where a C string cannot. +func validateGitProtoField(name, value string) error { + for i := 0; i < len(value); i++ { + if value[i] < 0x20 || value[i] == 0x7f { + return fmt.Errorf("%w: %s contains control byte %#02x", + ErrInvalidGitProtoRequest, name, value[i]) + } + } return nil } @@ -70,15 +108,19 @@ func (g *GitProtoRequest) Encode(w io.Writer) error { // Decode decodes the request from the reader. func (g *GitProtoRequest) Decode(r io.Reader) error { - _, p, err := pktline.ReadLine(r) - if errors.Is(err, io.EOF) { - return ErrInvalidGitProtoRequest + s := pktline.NewScanner(r) + if !s.Scan() { + if s.Err() == nil { + return ErrInvalidGitProtoRequest + } + return s.Err() } - if err != nil { - return err + + if s.Len() == pktline.Flush { + return io.EOF } - line := string(p) + line := s.Text() if len(line) == 0 { return io.EOF } @@ -111,5 +153,10 @@ func (g *GitProtoRequest) Decode(r io.Reader) error { } } - return nil + // A decoded request comes straight off the wire from an untrusted peer. + // NUL cannot survive here (it delimits the fields split above), but other + // control bytes such as newline or ESC can, and the server forwards these + // fields into URL construction and log lines. Reject them symmetrically + // with Encode. + return g.validate() } diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/list.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/list.go new file mode 100644 index 000000000..0dca78feb --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/list.go @@ -0,0 +1,68 @@ +package packp + +import ( + "bytes" + "errors" + "io" + "strings" + + "github.com/go-git/go-git/v6/plumbing/format/pktline" + "github.com/go-git/go-git/v6/plumbing/protocol/capability" +) + +// EncodeListV2 writes capabilities in v2 format: one capability per pkt-line. +// Each capability is written as "key\n" or "key=value\n" or "key=v1 v2\n". The +// caller is responsible for writing the terminating packet (flush-pkt or +// delim-pkt) after the last capability. +func EncodeListV2(w io.Writer, l *capability.List) error { + for _, key := range l.All() { + values := l.Get(key) + if len(values) == 0 { + if _, err := pktline.Writef(w, "%s\n", key); err != nil { + return err + } + } else { + if _, err := pktline.Writef(w, "%s=%s\n", key, strings.Join(values, " ")); err != nil { + return err + } + } + } + return nil +} + +// DecodeListV2 reads capabilities in v2 format from a pkt-line stream. It +// reads pkt-lines until flush-pkt, delim-pkt, or EOF, appending each parsed +// capability to the list. It returns the terminating packet length +// (pktline.Flush, pktline.Delim, or pktline.ResponseEnd) so the caller knows +// what terminated the capability list. +func DecodeListV2(r io.Reader, l *capability.List) (int, error) { + for { + length, line, err := pktline.ReadLine(r) + if err != nil { + if errors.Is(err, io.EOF) { + return pktline.Flush, nil + } + return 0, err + } + + if length == pktline.Flush || length == pktline.Delim || length == pktline.ResponseEnd { + return length, nil + } + + line = bytes.TrimSuffix(line, []byte("\n")) + if len(line) == 0 { + continue + } + + key, value, hasValue := strings.Cut(string(line), "=") + if hasValue { + for v := range strings.SplitSeq(value, " ") { + if v != "" { + l.Add(key, v) + } + } + } else { + l.Add(key) + } + } +} diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/lsrefs.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/lsrefs.go new file mode 100644 index 000000000..457475e5a --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/lsrefs.go @@ -0,0 +1,290 @@ +package packp + +import ( + "errors" + "fmt" + "io" + "strings" + "unicode" + + "github.com/go-git/go-git/v6/plumbing" + "github.com/go-git/go-git/v6/plumbing/format/pktline" +) + +// LsRefsArgs represents the arguments for the v2 ls-refs command. +// It is encoded as the command-specific arguments and a flush-pkt in a v2 +// command request. +type LsRefsArgs struct { + Peel bool + Symrefs bool + Unborn bool + RefPrefixes []string +} + +// Encode writes the ls-refs arguments to a writer. Each argument is +// written as a separate pkt-line. The caller is responsible for writing +// the delim-pkt before and the flush-pkt after these arguments. +func (r *LsRefsArgs) Encode(w io.Writer) error { + // Validate every ref-prefix before writing anything, so an invalid prefix + // can never leave a partially-written arguments section on the stream + // (Encode is all-or-nothing on a validation error). + for _, p := range r.RefPrefixes { + if err := validateRefPrefix(p); err != nil { + return err + } + } + + if r.Peel { + if _, err := pktline.WriteString(w, "peel\n"); err != nil { + return err + } + } + if r.Symrefs { + if _, err := pktline.WriteString(w, "symrefs\n"); err != nil { + return err + } + } + if r.Unborn { + if _, err := pktline.WriteString(w, "unborn\n"); err != nil { + return err + } + } + for _, p := range r.RefPrefixes { + if _, err := pktline.Writef(w, "ref-prefix %s\n", p); err != nil { + return err + } + } + return nil +} + +// validateRefPrefix rejects a ref-prefix that cannot be safely framed as a +// "ref-prefix

" pkt-line. An empty prefix would emit a stray "ref-prefix " +// argument, and whitespace or control bytes (notably LF and NUL) would break +// the pkt-line framing or let a caller inject extra lines. No valid Git +// reference contains such characters, so this only rejects malformed input. +func validateRefPrefix(p string) error { + if p == "" { + return fmt.Errorf("invalid ref-prefix: empty") + } + for _, c := range p { + if c == 0 || unicode.IsControl(c) || unicode.IsSpace(c) { + return fmt.Errorf("invalid ref-prefix %q: contains whitespace or control character", p) + } + } + return nil +} + +// tooManyRefPrefixes mirrors ls-refs.c TOO_MANY_PREFIXES: past this many +// ref-prefix arguments, upstream clears the list and advertises every ref, both +// to bound memory and because prefix filtering stops paying off. +const tooManyRefPrefixes = 65536 + +// Decode reads ls-refs arguments from a reader until a flush-pkt is encountered. +func (r *LsRefsArgs) Decode(rd io.Reader) error { + tooMany := false + for { + l, pkt, err := pktline.ReadLine(rd) + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + return err + } + + if l == pktline.Flush { + return nil + } + + line := strings.TrimSuffix(string(pkt), "\n") + + if len(line) == 0 { + continue + } + + switch { + case line == "peel": + r.Peel = true + case line == "symrefs": + r.Symrefs = true + case line == "unborn": + r.Unborn = true + case strings.HasPrefix(line, "ref-prefix "): + if tooMany { + continue + } + r.RefPrefixes = append(r.RefPrefixes, line[len("ref-prefix "):]) + if len(r.RefPrefixes) >= tooManyRefPrefixes { + // Too many prefixes: drop them and advertise every ref, as + // upstream ls-refs.c does, instead of growing without bound. + r.RefPrefixes = nil + tooMany = true + } + } + } +} + +// LsRefsOutput represents the server response to an ls-refs command. +// +// Each ref line has the format: +// +// SP [SP symref-target:] [SP peeled:] +// +// or for unborn refs: +// +// unborn SP SP symref-target: +// +// The response ends with a flush-pkt. For HTTP, response-end (0002) is +// consumed by the transport layer and not seen by Decode. +type LsRefsOutput struct { + References []*plumbing.Reference +} + +// Encode writes the ls-refs response lines as pkt-lines following the v2 +// grammar: " SP [SP symref-target:] [SP peeled:]", +// or "unborn SP SP symref-target:" for an unborn HEAD. Peeled +// "^{}" entries are folded into their base ref's line as a peeled attribute, and +// a symbolic ref carries the resolved oid of its target when present. The caller +// is responsible for writing the flush-pkt after these lines. +func (r *LsRefsOutput) Encode(w io.Writer) error { + hashByName := make(map[string]plumbing.Hash, len(r.References)) + for _, ref := range r.References { + if ref.Type() == plumbing.HashReference { + hashByName[ref.Name().String()] = ref.Hash() + } + } + + for _, ref := range r.References { + name := ref.Name().String() + + // Peeled entries are folded into their base ref's line below. + if ref.Name().IsPeeled() { + continue + } + + if ref.Type() == plumbing.SymbolicReference { + oid := "unborn" + if h, ok := hashByName[ref.Target().String()]; ok && !h.IsZero() { + oid = h.String() + } + if _, err := pktline.Writef(w, "%s %s symref-target:%s\n", oid, name, ref.Target()); err != nil { + return err + } + continue + } + + line := fmt.Sprintf("%s %s", ref.Hash(), name) + if peeled, ok := hashByName[name+"^{}"]; ok { + line += " peeled:" + peeled.String() + } + if _, err := pktline.Writef(w, "%s\n", line); err != nil { + return err + } + } + + return nil +} + +// Decode reads ref lines until a flush-pkt. +func (r *LsRefsOutput) Decode(rd io.Reader) error { + for { + l, pkt, err := pktline.ReadLine(rd) + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + return err + } + + if l == pktline.Flush { + return nil + } + + line := strings.TrimSuffix(string(pkt), "\n") + + if len(line) == 0 { + continue + } + + refs, err := parseLsRefsLine(line) + if err != nil { + return err + } + r.References = append(r.References, refs...) + } +} + +// parseLsRefsLine parses a single ref line from ls-refs output. +// Format: SP [SP ...] LF +// Returns one or two references (base + peeled if the peeled attribute is present). +func parseLsRefsLine(line string) ([]*plumbing.Reference, error) { + // Fields tolerates the SP-separated grammar without producing empty tokens + // on repeated spaces: [oid-or-unborn, refname, attr1, attr2, ...]. + parts := strings.Fields(line) + if len(parts) < 2 { + return nil, fmt.Errorf("malformed ref line: %q", line) + } + + oidStr := parts[0] + refName := plumbing.ReferenceName(parts[1]) + + var symrefTarget plumbing.ReferenceName + var peeledHash plumbing.Hash + hasPeeled := false + + for _, attr := range parts[2:] { + if strings.HasPrefix(attr, "symref-target:") { + symrefTarget = plumbing.ReferenceName(attr[len("symref-target:"):]) + } else if strings.HasPrefix(attr, "peeled:") { + h, ok := parseFullHash(attr[len("peeled:"):]) + if !ok { + return nil, fmt.Errorf("malformed peeled hash: %q", attr) + } + peeledHash = h + hasPeeled = true + } + } + + var refs []*plumbing.Reference + + // Handle unborn refs + if oidStr == "unborn" { + if symrefTarget == "" { + return nil, fmt.Errorf("malformed unborn ref line, missing symref-target: %q", line) + } + refs = append(refs, plumbing.NewSymbolicReference(refName, symrefTarget)) + return refs, nil + } + + // Regular hash ref + hash, ok := parseFullHash(oidStr) + if !ok { + return nil, fmt.Errorf("malformed object id: %q", oidStr) + } + + if symrefTarget != "" { + refs = append(refs, plumbing.NewSymbolicReference(refName, symrefTarget)) + } else { + refs = append(refs, plumbing.NewHashReference(refName, hash)) + } + + // If "peeled:" attribute is present, add the peeled ref as a separate entry + if hasPeeled { + refs = append(refs, plumbing.NewHashReference( + plumbing.ReferenceName(refName.String()+"^{}"), + peeledHash, + )) + } + + return refs, nil +} + +// parseFullHash strictly parses a full-length SHA-1 or SHA-256 object id in hex +// form. Object ids on the wire are always full length, so unlike +// plumbing.FromHex (which zero-pads shorter input as a partial SHA-1) it rejects +// anything that is not exactly an object-id length, refusing malformed input. +func parseFullHash(s string) (plumbing.Hash, bool) { + if !plumbing.IsHash(s) { + return plumbing.ZeroHash, false + } + return plumbing.FromHex(s) +} diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/pushopts.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/pushopts.go index 6606fa71d..efddd67b9 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/pushopts.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/pushopts.go @@ -1,11 +1,19 @@ package packp import ( + "errors" + "fmt" "io" + "strings" + "unicode" "github.com/go-git/go-git/v6/plumbing/format/pktline" ) +// ErrInvalidPushOption is returned when a push option contains invalid +// characters. +var ErrInvalidPushOption = errors.New("invalid push option") + // PushOptions represents a list of update request push-options. // // See https://git-scm.com/docs/gitprotocol-pack#_reference_update_request_and_packfile_transfer @@ -15,8 +23,13 @@ type PushOptions struct { // Encode encodes the push options into the given writer. func (opts *PushOptions) Encode(w io.Writer) error { - if len(opts.Options) == 0 { - return nil + for _, opt := range opts.Options { + if strings.ContainsFunc(opt, isNotGraphic) { + return fmt.Errorf("%w: contains invalid character", ErrInvalidPushOption) + } + if len(opt) > pktline.MaxPayloadSize { + return fmt.Errorf("%w: %w", ErrInvalidPushOption, pktline.ErrPayloadTooLong) + } } for _, opt := range opts.Options { @@ -34,17 +47,31 @@ func (opts *PushOptions) Decode(r io.Reader) error { opts.Options = make([]string, 0) } - for { - l, line, err := pktline.ReadLine(r) - if err != nil { - return err - } - if l == pktline.Flush { + s := pktline.NewScanner(r) + flushed := false + for s.Scan() { + if s.Len() == pktline.Flush { + flushed = true break } - opts.Options = append(opts.Options, string(line)) + opt := s.Text() + if strings.ContainsFunc(opt, isNotGraphic) { + return fmt.Errorf("%w: contains invalid character", ErrInvalidPushOption) + } + + opts.Options = append(opts.Options, opt) + } + if err := s.Err(); err != nil { + return err + } + if !flushed { + return io.ErrUnexpectedEOF } return nil } + +func isNotGraphic(r rune) bool { + return !unicode.IsGraphic(r) +} diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/report_status.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/report_status.go index bc0c96686..4b31ed74e 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/report_status.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/report_status.go @@ -2,7 +2,6 @@ package packp import ( "bytes" - "errors" "fmt" "io" "strings" @@ -79,7 +78,9 @@ func (s *ReportStatus) Encode(w io.Writer) error { // Decode reads from the given reader and decodes a report-status message. It // does not read more input than what is needed to fill the report status. func (s *ReportStatus) Decode(r io.Reader) error { - b, err := s.scanFirstLine(r) + sc := pktline.NewScanner(r) + + b, err := s.scanFirstLine(sc) if err != nil { return err } @@ -88,48 +89,37 @@ func (s *ReportStatus) Decode(r io.Reader) error { return err } - var l int flushed := false - for { - l, b, err = pktline.ReadLine(r) - if err != nil { - break - } - - if l == pktline.Flush { + for sc.Scan() { + if sc.Len() == pktline.Flush { flushed = true break } - if err := s.decodeCommandStatus(b); err != nil { + if err := s.decodeCommandStatus(sc.Bytes()); err != nil { return err } } if !flushed { - return fmt.Errorf("missing flush: %w", err) - } - - if err != nil && !errors.Is(err, io.EOF) { - // TODO: We should not ignore EOF errors here. Decoding a report-status - // message ends with a flush-pkt, an EOF indicates that the flush-pkt - // was not received. - return err + if err := sc.Err(); err != nil { + return fmt.Errorf("missing flush: %w", err) + } + return fmt.Errorf("missing flush: %w", io.ErrUnexpectedEOF) } return nil } -func (s *ReportStatus) scanFirstLine(r io.Reader) ([]byte, error) { - _, p, err := pktline.ReadLine(r) - if errors.Is(err, io.EOF) { - return p, io.ErrUnexpectedEOF - } - if err != nil { - return nil, err +func (s *ReportStatus) scanFirstLine(sc *pktline.Scanner) ([]byte, error) { + if !sc.Scan() { + if sc.Err() == nil { + return nil, io.ErrUnexpectedEOF + } + return nil, sc.Err() } - return p, nil + return sc.Bytes(), nil } func (s *ReportStatus) decodeReportStatus(b []byte) error { diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/shallowupd.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/shallowupd.go index f5946d34e..de0b8f758 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/shallowupd.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/shallowupd.go @@ -22,25 +22,19 @@ type ShallowUpdate struct { // Decode parses shallow update information from the reader. func (r *ShallowUpdate) Decode(reader io.Reader) error { - var ( - p []byte - err error - l int - ) - for { - l, p, err = pktline.ReadLine(reader) - if err != nil { - break + s := pktline.NewScanner(reader) + for s.Scan() { + if s.Len() == pktline.Flush { + return nil } - line := bytes.TrimSpace(p) + line := bytes.TrimSpace(s.Bytes()) + var err error switch { case bytes.HasPrefix(line, shallow): err = r.decodeShallowLine(line) case bytes.HasPrefix(line, unshallow): err = r.decodeUnshallowLine(line) - case l == pktline.Flush: - return nil default: err = fmt.Errorf("unexpected shallow line: %q", line) } @@ -50,11 +44,7 @@ func (r *ShallowUpdate) Decode(reader io.Reader) error { } } - if err == io.EOF { - return nil - } - - return err + return s.Err() } func (r *ShallowUpdate) decodeShallowLine(line []byte) error { diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/sideband/demux.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/sideband/demux.go index 1a56c38a5..9df8f5f12 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/sideband/demux.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/sideband/demux.go @@ -1,6 +1,7 @@ package sideband import ( + "bytes" "errors" "fmt" "io" @@ -33,6 +34,7 @@ type Progress interface { type Demuxer struct { t Type r io.Reader + s *pktline.Scanner max int pending []byte @@ -51,6 +53,7 @@ func NewDemuxer(t Type, r io.Reader) *Demuxer { return &Demuxer{ t: t, r: r, + s: pktline.NewScanner(r), max: maxSize, } } @@ -84,7 +87,7 @@ func (d *Demuxer) doRead(b []byte) (int, error) { wanted := len(b) if size > wanted { - d.pending = read[wanted:] + d.pending = bytes.Clone(read[wanted:]) } if wanted > size { @@ -101,20 +104,21 @@ func (d *Demuxer) nextPackData() ([]byte, error) { return content, nil } - l, p, err := pktline.ReadLine(d.r) - if err != nil { - return nil, err + if !d.s.Scan() { + if err := d.s.Err(); err != nil { + return nil, err + } + return nil, io.EOF } - content = p + l := d.s.Len() if l == pktline.Flush { - // Done demultiplex sidebands. Use io.EOF to indicate the end of - // sideband packets. return nil, io.EOF } else if l > d.max { return nil, ErrMaxPackedExceeded } + content = d.s.Bytes() if len(content) < 1 { return nil, fmt.Errorf("invalid sideband pktline %04x %q", l, content) } diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/smart.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/smart.go index b71cbf04c..63c4c9ac3 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/smart.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/smart.go @@ -28,22 +28,28 @@ type SmartReply struct { // Decode decodes a SmartReply from reader. func (s *SmartReply) Decode(r io.Reader) error { - _, p, err := pktline.ReadLine(r) - if err != nil { - return err + sc := pktline.NewScanner(r) + if !sc.Scan() { + if sc.Err() != nil { + return sc.Err() + } + return fmt.Errorf("%w: empty input", ErrInvalidSmartReply) } + p := sc.Bytes() if len(p) == 0 || !bytes.HasPrefix(p, []byte("# service=")) { return fmt.Errorf("%w: %q", ErrInvalidSmartReply, p) } s.Service = strings.TrimSpace(string(p[10:])) - l, _, err := pktline.ReadLine(r) - if err != nil { - return err + if !sc.Scan() { + if sc.Err() != nil { + return sc.Err() + } + return fmt.Errorf("%w: expected flush-pkt", ErrInvalidSmartReply) } - if l != pktline.Flush { + if sc.Len() != pktline.Flush { return fmt.Errorf("%w: expected flush-pkt", ErrInvalidSmartReply) } diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/srvresp.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/srvresp.go index 983502db0..2cf0c6253 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/srvresp.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/srvresp.go @@ -51,22 +51,17 @@ type ACK struct { // Decode decodes the response into the struct. func (r *ServerResponse) Decode(reader io.Reader) error { - var err error - for err == nil { - var p []byte - _, p, err = pktline.ReadLine(reader) - if err != nil { - break + s := pktline.NewScanner(reader) + for s.Scan() { + if err := r.decodeLine(s.Bytes()); err != nil { + if errors.Is(err, io.EOF) { + return nil + } + return err } - - err = r.decodeLine(p) - } - - if errors.Is(err, io.EOF) { - return nil } - return err + return s.Err() } func (r *ServerResponse) decodeLine(line []byte) error { diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/ulreq_decode.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/ulreq_decode.go index 9cc95f64a..019b39f66 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/ulreq_decode.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/ulreq_decode.go @@ -26,25 +26,26 @@ func (req *UploadRequest) Decode(r io.Reader) error { deepenRevList bool ) + s := pktline.NewScanner(r) + nextLine := func() (hasData bool, err error) { nLine++ - l, p, err := pktline.ReadLine(r) - if err == io.EOF { - return false, NewErrUnexpectedData(fmt.Sprintf("pkt-line %d: EOF", nLine), line) - } - if err != nil { - return false, err + if !s.Scan() { + if s.Err() == nil { + return false, NewErrUnexpectedData(fmt.Sprintf("pkt-line %d: EOF", nLine), bytes.Clone(line)) + } + return false, s.Err() } - if l == pktline.Flush { + if s.Len() == pktline.Flush { return false, nil } - line = bytes.TrimSuffix(p, eol) + line = bytes.TrimSuffix(s.Bytes(), eol) return true, nil } decodeError := func(format string, a ...any) error { msg := fmt.Sprintf("pkt-line %d: %s", nLine, fmt.Sprintf(format, a...)) - return NewErrUnexpectedData(msg, line) + return NewErrUnexpectedData(msg, bytes.Clone(line)) } readHash := func() (plumbing.Hash, error) { diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/updreq.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/updreq.go index 6fb1af169..111ebe5fb 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/updreq.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/updreq.go @@ -57,15 +57,20 @@ type Command struct { // Action returns the action type of the command. func (c *Command) Action() Action { - if c.Old == plumbing.ZeroHash && c.New == plumbing.ZeroHash { + // Compare with IsZero rather than == plumbing.ZeroHash: the latter also + // matches on the object-format field, and a zero object id decoded from + // the wire carries the negotiated format (e.g. sha256 for a 64-hex id) + // while plumbing.ZeroHash is format-unset. IsZero looks only at the + // bytes, mirroring Git's is_null_oid. + if c.Old.IsZero() && c.New.IsZero() { return Invalid } - if c.Old == plumbing.ZeroHash { + if c.Old.IsZero() { return Create } - if c.New == plumbing.ZeroHash { + if c.New.IsZero() { return Delete } diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/updreq_decode.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/updreq_decode.go index 11d0fa4fa..824d38465 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/updreq_decode.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/updreq_decode.go @@ -77,16 +77,21 @@ func (req *UpdateRequests) Decode(r io.Reader) error { length int ) + s := pktline.NewScanner(r) + readLine := func(eofErr error) error { - l, p, err := pktline.ReadLine(r) - if errors.Is(err, io.EOF) { - return eofErr + if !s.Scan() { + if s.Err() == nil { + return eofErr + } + return s.Err() } - if err != nil { - return err + length = s.Len() + if length == pktline.Flush { + payload = nil + } else { + payload = s.Bytes() } - payload = p - length = l return nil } @@ -118,6 +123,14 @@ func (req *UpdateRequests) Decode(r io.Reader) error { } } + // A shallow-only no-op push (shallow lines followed immediately by a + // flush, with no commands) is a valid empty request, e.g. from a shallow + // clone with nothing to push. A bare flush with no shallows is still + // treated as malformed. + if length == pktline.Flush && len(req.Shallows) > 0 { + return nil + } + // The first command line must contain capabilities separated by a null byte before, after, ok := bytes.Cut(payload, []byte{0}) if !ok { diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/uphav.go b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/uphav.go index 43e379887..696d5dd33 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/uphav.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/protocol/packp/uphav.go @@ -52,20 +52,13 @@ func (u *UploadHaves) Encode(w io.Writer) error { func (u *UploadHaves) Decode(r io.Reader) error { u.Haves = make([]plumbing.Hash, 0) - for { - l, line, err := pktline.ReadLine(r) - if err != nil { - if err == io.EOF { - break - } - - return fmt.Errorf("decoding haves: %w", err) - } - - if l == pktline.Flush { + s := pktline.NewScanner(r) + for s.Scan() { + if s.Len() == pktline.Flush { break } + line := s.Bytes() if bytes.HasPrefix(line, []byte("done")) { u.Done = true break @@ -79,5 +72,9 @@ func (u *UploadHaves) Decode(r io.Reader) error { u.Haves = append(u.Haves, have) } + if err := s.Err(); err != nil { + return fmt.Errorf("decoding haves: %w", err) + } + return nil } diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/reference.go b/vendor/github.com/go-git/go-git/v6/plumbing/reference.go index 13e4a663c..2f302a2c3 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/reference.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/reference.go @@ -120,6 +120,50 @@ func (r ReferenceName) IsPeeled() bool { return strings.HasSuffix(string(r), "^{}") } +// IsSafe reports whether the reference name can be safely turned into a path +// under the .git directory, mirroring Git's refname_is_safe (refs.c). A name +// is safe when it is either: +// +// - under "refs/", non-empty after the prefix, containing no backslash and +// no empty, "." or ".." path component (so it cannot escape the refs/ +// sub-tree, or alias another name, once turned into a path); or +// - a one-level pseudo-ref whose spelling is restricted to [A-Z_] +// (e.g. HEAD, ORIG_HEAD, FETCH_HEAD). +// +// Everything else — a lowercase or mixed one-level name such as "config" or +// "index", an absolute or drive-prefixed name, or a refs/ name that escapes — +// is unsafe, because it could resolve onto unrelated repository metadata. +// This is a storage-safety check, not full check_refname_format validation; +// see Validate for the latter. +func (r ReferenceName) IsSafe() bool { + s := string(r) + if s == "" { + return false + } + + if rest, ok := strings.CutPrefix(s, refPrefix); ok { + // '\' is a path separator on Windows, so a refs/ name containing one + // could escape the sub-tree or alias another name once turned into a + // path; reject it outright (check_refname_format forbids '\' too). + if rest == "" || strings.Contains(rest, "\\") { + return false + } + for part := range strings.SplitSeq(rest, "/") { + if part == "" || part == "." || part == ".." { + return false + } + } + return true + } + + for i := 0; i < len(s); i++ { + if (s[i] < 'A' || s[i] > 'Z') && s[i] != '_' { + return false + } + } + return true +} + func (r ReferenceName) String() string { return string(r) } diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/storer/storer.go b/vendor/github.com/go-git/go-git/v6/plumbing/storer/storer.go index 7a4190963..51ad5c98b 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/storer/storer.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/storer/storer.go @@ -23,3 +23,15 @@ type Initializer interface { type FilesystemStorer interface { Filesystem() billy.Filesystem } + +// IdleReleaser is implemented by storers that can drop idle +// file descriptors (or other I/O resources) without becoming +// unusable. Callers detect via this interface and call +// [IdleReleaser.CloseIdleDescriptors] at a known quiet point — +// for example between reconcile bursts. +// +// Implementations must remain fully usable after the call; +// subsequent operations reopen resources on demand. +type IdleReleaser interface { + CloseIdleDescriptors() error +} diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/transport/archive.go b/vendor/github.com/go-git/go-git/v6/plumbing/transport/archive.go index dd222be3a..7b158de0e 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/transport/archive.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/transport/archive.go @@ -58,16 +58,19 @@ func Archive(ctx context.Context, w io.WriteCloser, r io.ReadCloser, req *Archiv } rd := bufio.NewReader(r) + sc := pktline.NewScanner(rd) - l, line, err := pktline.ReadLine(rd) - if err != nil { - return nil, fmt.Errorf("archive: reading ACK/NACK: %w", err) + if !sc.Scan() { + if sc.Err() != nil { + return nil, fmt.Errorf("archive: reading ACK/NACK: %w", sc.Err()) + } + return nil, fmt.Errorf("archive: expected ACK/NACK, got EOF") } - if l == pktline.Flush { + if sc.Len() == pktline.Flush { return nil, fmt.Errorf("archive: expected ACK/NACK, got flush") } - resp := strings.TrimSuffix(string(line), "\n") + resp := strings.TrimSuffix(sc.Text(), "\n") switch { case resp == "ACK": case strings.HasPrefix(resp, "NACK "): @@ -76,11 +79,13 @@ func Archive(ctx context.Context, w io.WriteCloser, r io.ReadCloser, req *Archiv return nil, fmt.Errorf("archive: protocol error: %s", resp) } - l, _, err = pktline.ReadLine(rd) - if err != nil { - return nil, fmt.Errorf("archive: reading flush after ACK: %w", err) + if !sc.Scan() { + if sc.Err() != nil { + return nil, fmt.Errorf("archive: reading flush after ACK: %w", sc.Err()) + } + return nil, fmt.Errorf("archive: expected flush after ACK, got EOF") } - if l != pktline.Flush { + if sc.Len() != pktline.Flush { return nil, fmt.Errorf("archive: expected flush after ACK, got data") } diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/transport/common.go b/vendor/github.com/go-git/go-git/v6/plumbing/transport/common.go index 687a65ef6..3d169e0b9 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/transport/common.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/transport/common.go @@ -5,7 +5,7 @@ import ( "io" "net" - "github.com/go-git/go-git/v6/plumbing" + internal "github.com/go-git/go-git/v6/internal/transport" "github.com/go-git/go-git/v6/plumbing/protocol/packp" "github.com/go-git/go-git/v6/plumbing/protocol/packp/sideband" ) @@ -44,32 +44,10 @@ func NewRemoteError(reason string) error { // FetchRequest contains the parameters for a fetch-pack request. // This is used during the pack negotiation phase of the fetch operation. // See https://git-scm.com/docs/pack-protocol#_packfile_negotiation -type FetchRequest struct { - // Progress is the progress sideband. - Progress sideband.Progress - - // Wants is the list of object hashes the client wants to fetch. - // The caller selects which remote refs to fetch (refspec matching) - // and extracts their hashes. - Wants []plumbing.Hash - - // Haves is the list of object hashes the client already has. - // TODO: The transport should compute haves internally from the - // storer during pack negotiation, matching how canonical git's - // fetch-pack walks the local object graph to determine common - // ancestors. Once implemented, remove this field. - Haves []plumbing.Hash - - // Depth is the depth of the fetch. - Depth int - - // Filter holds the filters to be applied when deciding what - // objects will be added to the packfile. - Filter packp.Filter - - // IncludeTags indicates whether tags should be fetched. - IncludeTags bool -} +// FetchRequest is the request sent to the remote to fetch objects. It is an +// alias of the shared internal type so the v0/v1 and v2 fetch paths use the +// exact same request. +type FetchRequest = internal.FetchRequest // PushRequest contains the parameters for a push request. type PushRequest struct { diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/transport/errors.go b/vendor/github.com/go-git/go-git/v6/plumbing/transport/errors.go index 8d6f48790..25e254d5a 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/transport/errors.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/transport/errors.go @@ -1,12 +1,16 @@ package transport -import "errors" +import ( + "errors" + + internal "github.com/go-git/go-git/v6/internal/transport" +) // Transport errors. var ( ErrRepositoryNotFound = errors.New("repository not found") ErrEmptyRemoteRepository = errors.New("remote repository is empty") - ErrNoChange = errors.New("no change") + ErrNoChange = internal.ErrNoChange ErrAuthenticationRequired = errors.New("authentication required") ErrAuthorizationFailed = errors.New("authorization failed") ErrEmptyUploadPackRequest = errors.New("empty git-upload-pack given") diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/transport/http/common.go b/vendor/github.com/go-git/go-git/v6/plumbing/transport/http/common.go index 989760648..aa4630d0b 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/transport/http/common.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/transport/http/common.go @@ -24,9 +24,9 @@ func (e *Err) StatusCode() int { return e.Status } func (e *Err) Error() string { format := "unexpected requesting %q status code: %d" if e.Reason != "" { - return fmt.Sprintf(format+": %s", e.URL, e.Status, e.Reason) + return fmt.Sprintf(format+": %s", redactedURL(e.URL), e.Status, e.Reason) } - return fmt.Sprintf(format, e.URL, e.Status) + return fmt.Sprintf(format, redactedURL(e.URL), e.Status) } // checkError maps HTTP response status codes to typed transport errors. @@ -85,6 +85,13 @@ func applyRedirect(resp *http.Response, baseURL *url.URL) (*url.URL, error) { final := resp.Request.URL if !strings.HasSuffix(final.Path, infoRefsPath) { + // Azure DevOps redirects unauthenticated requests for private repos + // to /_signin. Treat that as an authentication-required condition + // rather than a transport failure so callers can detect it via + // errors.Is(err, transport.ErrAuthenticationRequired). See issue #2200. + if strings.HasSuffix(final.Path, "/_signin") { + return nil, fmt.Errorf("%w: redirect to %q", transport.ErrAuthenticationRequired, final.Path) + } return nil, fmt.Errorf( "http transport: redirect target %q does not end with %s", final.Path, infoRefsPath, diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/transport/http/handshake.go b/vendor/github.com/go-git/go-git/v6/plumbing/transport/http/handshake.go index bbbf6b2e0..127f3e70e 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/transport/http/handshake.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/transport/http/handshake.go @@ -10,7 +10,7 @@ import ( "net/http" "net/url" - "github.com/go-git/go-git/v6/plumbing" + internal "github.com/go-git/go-git/v6/internal/transport" "github.com/go-git/go-git/v6/plumbing/format/pktline" "github.com/go-git/go-git/v6/plumbing/protocol" "github.com/go-git/go-git/v6/plumbing/protocol/capability" @@ -26,12 +26,22 @@ func (t *Transport) Handshake(ctx context.Context, req *transport.Request) (tran baseURL := req.URL forceDumb := t.opts.ForceDumb + // git archive over HTTP discovers protocol support through the upload-pack + // info/refs endpoint and requires Protocol v2 (remote-curl.c). The archive + // request itself is later POSTed to the git-upload-archive endpoint. + discoverService := service + discoverProtocol := req.Protocol + if service == transport.UploadArchiveService { + discoverService = transport.UploadPackService + discoverProtocol = protocol.V2 + } + infoURL, err := url.JoinPath(baseURL.String(), "info/refs") if err != nil { return nil, err } if !forceDumb { - infoURL += "?service=" + service + infoURL += "?service=" + discoverService } // Mark this as the initial request so checkRedirect allows @@ -45,7 +55,7 @@ func (t *Transport) Handshake(ctx context.Context, req *transport.Request) (tran httpReq.Header.Set("User-Agent", capability.DefaultAgent()) if !forceDumb { - if gp := transport.GitProtocolEnv(req.Protocol); gp != "" { + if gp := transport.GitProtocolEnv(discoverProtocol); gp != "" { httpReq.Header.Set("Git-Protocol", gp) } } @@ -99,16 +109,16 @@ func (t *Transport) Handshake(ctx context.Context, req *transport.Request) (tran return handshakeDumb(resp, &sessReq, client, authorizer) } - expected := fmt.Sprintf("application/x-%s-advertisement", service) + expected := fmt.Sprintf("application/x-%s-advertisement", discoverService) isSmart := resp.Header.Get("Content-Type") == expected if isSmart { - return handshakeSmart(resp, &sessReq, client, authorizer) + return handshakeSmart(resp, &sessReq, discoverService, client, authorizer) } return handshakeDumb(resp, &sessReq, client, authorizer) } -func handshakeSmart(resp *http.Response, req *transport.Request, client *http.Client, authorizer func(*http.Request) error) (transport.Session, error) { +func handshakeSmart(resp *http.Response, req *transport.Request, discoverService string, client *http.Client, authorizer func(*http.Request) error) (transport.Session, error) { defer resp.Body.Close() //nolint:errcheck rd := bufio.NewReader(resp.Body) @@ -121,7 +131,7 @@ func handshakeSmart(resp *http.Response, req *transport.Request, client *http.Cl if err := reply.Decode(rd); err != nil { return nil, err } - if reply.Service != req.Command { + if reply.Service != discoverService { return nil, fmt.Errorf("unexpected service name: %w", transport.ErrInvalidResponse) } } @@ -130,10 +140,35 @@ func handshakeSmart(resp *http.Response, req *transport.Request, client *http.Cl if err != nil { return nil, err } - switch ver { - case protocol.V2: - return nil, transport.ErrUnsupportedVersion - case protocol.V1, protocol.V0: + + // git archive over HTTP is only available when the server speaks v2. + if req.Command == transport.UploadArchiveService && ver != protocol.V2 { + return nil, transport.ErrArchiveUnsupported + } + + if ver == protocol.V2 { + // Protocol v2: the server sends a capability advertisement instead of + // the v0/v1 ref advertisement. References are retrieved lazily via the + // ls-refs command, so refs stays nil here. + adv := &packp.CapabilityAdv{} + if err := adv.Decode(rd); err != nil { + return nil, err + } + // Protocol v2 fetch accepts "want " without the server + // advertising allow-*-sha1-in-want, so surface the gate as + // satisfied for exact-SHA1 refspecs (isSupportedRefSpec). The + // v2 client only sends agent/object-format on the wire, so these + // never leak into the request (internal.ClientCapabilities). + adv.Capabilities.Set(capability.AllowReachableSHA1InWant) + adv.Capabilities.Set(capability.AllowTipSHA1InWant) + return &smartPackSession{ + client: client, + baseURL: req.URL, + service: req.Command, + authorizer: authorizer, + version: ver, + caps: adv.Capabilities, + }, nil } ar := &packp.AdvRefs{} @@ -146,6 +181,11 @@ func handshakeSmart(resp *http.Response, req *transport.Request, client *http.Cl return nil, err } + // Source the advertisement's version from the version DiscoverVersion + // already established, keeping the session the single source of truth + // rather than AdvRefs.Decode's independent parse of the same line. + ar.Version = ver + return &smartPackSession{ client: client, baseURL: req.URL, @@ -180,7 +220,11 @@ func handshakeDumb(resp *http.Response, req *transport.Request, client *http.Cli // --- smart HTTP pack session --- -var _ transport.Session = (*smartPackSession)(nil) +var ( + _ transport.Session = (*smartPackSession)(nil) + _ transport.Commander = (*smartPackSession)(nil) + _ transport.Archiver = (*smartPackSession)(nil) +) type smartPackSession struct { client *http.Client @@ -194,11 +238,26 @@ type smartPackSession struct { func (s *smartPackSession) Capabilities() *capability.List { return &s.caps } -func (s *smartPackSession) GetRemoteRefs(_ context.Context) ([]*plumbing.Reference, error) { +func (s *smartPackSession) GetRemoteRefs(ctx context.Context, opts *transport.GetRemoteRefsOptions) (*transport.RemoteRefs, error) { + forPush := s.service == transport.ReceivePackService + if s.version == protocol.V2 { + var prefixes []string + if opts != nil { + prefixes = opts.RefPrefixes + } + refs, err := internal.LsRefs(ctx, s.Command, s.caps, prefixes) + if err != nil { + return nil, err + } + if !forPush && !internal.HasHashRef(refs) { + return nil, transport.ErrEmptyRemoteRepository + } + return transport.NewRemoteRefs(refs), nil + } + if s.refs == nil { return nil, transport.ErrEmptyRemoteRepository } - forPush := s.service == transport.ReceivePackService if !forPush && s.refs.IsEmpty() { return nil, transport.ErrEmptyRemoteRepository } @@ -206,10 +265,50 @@ func (s *smartPackSession) GetRemoteRefs(_ context.Context) ([]*plumbing.Referen if err != nil { return nil, err } - return refs, nil + return transport.NewRemoteRefs(refs), nil +} + +// Command implements transport.Commander. It runs a Protocol v2 command as a +// single stateless HTTP POST: the request envelope is buffered and sent, and +// the response is decoded from the response body. Fetch uses its own round +// instead so it can stream the packfile from the body; Command is for +// non-streaming commands such as ls-refs. +func (s *smartPackSession) Command(ctx context.Context, cmd string, req packp.CommandArgs, resp packp.Decoder) error { + if s.version != protocol.V2 { + return transport.ErrUnsupportedVersion + } + + r := &httpRequester{session: s, ctx: ctx} + cr := &packp.CommandRequest{ + Command: cmd, + Capabilities: internal.ClientCapabilities(s.caps), + Args: req, + } + if err := cr.Encode(r); err != nil { + return err + } + // Command consumes the whole response (it never streams the body out), so + // drain and close it on every path. A bare return on a decode error would + // otherwise leak the response body and its connection. + defer func() { + if r.resp != nil { + _, _ = io.Copy(io.Discard, r.resp.Body) + _ = r.resp.Body.Close() + } + }() + if resp != nil { + if err := resp.Decode(r); err != nil { + return err + } + } + return nil } func (s *smartPackSession) Fetch(ctx context.Context, st storage.Storer, req *transport.FetchRequest) error { + if s.version == protocol.V2 { + return s.fetchV2(ctx, st, req) + } + neg := &httpNegotiator{session: s, ctx: ctx} shallows, err := transport.NegotiatePack(ctx, st, s.caps, true, neg, neg, req) @@ -225,17 +324,84 @@ func (s *smartPackSession) Fetch(ctx context.Context, st storage.Storer, req *tr } } err = transport.FetchPack(ctx, st, s.caps, io.NopCloser(neg), shallows, req) - neg.closeResponse() + // Close the response unless the read itself was a cancellation. The race + // this guards against only exists on cancellation: a ctxReader goroutine + // inside FetchPack can still be blocked in the underlying Read after the + // <-ctx.Done() branch, so niling current.resp here would race it. On a + // non-cancellation error (or success) FetchPack's last Read returned via the + // result channel and its goroutine is quiescent, so closing is safe — and + // necessary, otherwise the response body/connection leaks. On the + // cancellation path the request context unblocks the in-flight read, so the + // body is not leaked. + // + // Classified against err itself via errors.Is, not a fresh ctx.Err() check: + // ctx can turn Err() non-nil an instant after FetchPack already returned + // with its read fully quiescent, and re-checking ctx.Err() at that later, + // independent point would incorrectly skip the close and leak the response + // (mirrors FetchV2's round loop). + if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { + neg.closeResponse() + } return err } +// fetchV2 fetches over Protocol v2. Each negotiation round is a fresh stateless +// POST; internal.FetchV2 decodes the metadata via FetchOutput and, once the +// server commits to a packfile, streams it from that round's response body. +func (s *smartPackSession) fetchV2(ctx context.Context, st storage.Storer, req *transport.FetchRequest) error { + if req.Filter != "" && !internal.FetchSupports(s.caps, "filter") { + return transport.ErrFilterNotSupported + } + if req.Depth > 0 && !internal.FetchSupports(s.caps, "shallow") { + return transport.ErrShallowNotSupported + } + if err := transport.ReconcileObjectFormatV2(st, s.caps); err != nil { + return err + } + + round := func(args *packp.FetchArgs) (*packp.FetchOutput, io.Reader, error) { + r := &httpRequester{session: s, ctx: ctx} + cr := &packp.CommandRequest{ + Command: "fetch", + Capabilities: internal.ClientCapabilities(s.caps), + Args: args, + } + if err := cr.Encode(r); err != nil { + return nil, nil, err + } + out := &packp.FetchOutput{} + if err := out.Decode(r); err != nil { + // The success path returns r.resp.Body for the caller to stream, so + // it must stay open; on a decode error nothing downstream will, so + // release it here rather than leaking the body and its connection. + if r.resp != nil { + _ = r.resp.Body.Close() + } + return nil, nil, err + } + if r.resp == nil { + return nil, nil, fmt.Errorf("http transport: fetch command produced no response") + } + // The response body is positioned at the packfile (when out.Packfile); + // internal.FetchV2 streams it and closes the body via io.Closer. + return out, r.resp.Body, nil + } + + return internal.FetchV2(ctx, st, req, round) +} + func (s *smartPackSession) Push(ctx context.Context, st storage.Storer, req *transport.PushRequest) error { rwc := &httpRequester{session: s, ctx: ctx} err := transport.SendPack(ctx, st, s.caps, rwc, io.NopCloser(rwc), req) - // Only close the response body on success. On error (especially context - // cancellation), context-wrapper goroutines inside SendPack may still - // be reading from it. - if err == nil && rwc.resp != nil { + // Close the response unless the read itself was a cancellation: a ctxReader + // goroutine inside SendPack can still be blocked in the underlying Read + // after the <-ctx.Done() branch, so closing the body here would race it — + // the request context tears the connection down instead. On a + // non-cancellation error (or success) SendPack's last Read returned via the + // result channel and its goroutine is quiescent, so closing is safe — and + // necessary, otherwise the response body/connection leaks (mirrors Fetch + // above and FetchV2's round loop). + if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) && rwc.resp != nil { _ = rwc.resp.Body.Close() } return err @@ -243,6 +409,40 @@ func (s *smartPackSession) Push(ctx context.Context, st storage.Storer, req *tra func (s *smartPackSession) Close() error { return nil } +// Archive implements transport.Archiver. git archive over HTTP is a v2-only, +// stateless operation (remote-curl.c): the archive request is POSTed to the +// git-upload-archive endpoint and the response carries the ACK/NACK and the +// sideband-encoded archive stream. +func (s *smartPackSession) Archive(ctx context.Context, req *transport.ArchiveRequest) (io.ReadCloser, error) { + if s.version != protocol.V2 { + return nil, transport.ErrArchiveUnsupported + } + + rt := &httpRequester{session: s, ctx: ctx} + body := &httpArchiveBody{req: rt} + archive, err := transport.Archive(ctx, rt, body, req) + if err != nil { + _ = body.Close() + return nil, err + } + return archive, nil +} + +// httpArchiveBody adapts an httpRequester to the io.ReadCloser the archive +// client reads from: reads come from the POST response body, and Close closes +// that body. The paired httpRequester is passed to transport.Archive as the +// writer, whose Close fires the POST. +type httpArchiveBody struct{ req *httpRequester } + +func (b *httpArchiveBody) Read(p []byte) (int, error) { return b.req.Read(p) } + +func (b *httpArchiveBody) Close() error { + if b.req.resp != nil { + return b.req.resp.Body.Close() + } + return nil +} + // httpRequester buffers writes and fires a POST on first Read or Close. type httpRequester struct { session *smartPackSession @@ -281,6 +481,9 @@ func (r *httpRequester) doPost() error { httpReq.Header.Set("Content-Type", fmt.Sprintf("application/x-%s-request", r.session.service)) httpReq.Header.Set("Accept", fmt.Sprintf("application/x-%s-result", r.session.service)) httpReq.Header.Set("User-Agent", capability.DefaultAgent()) + if gp := transport.GitProtocolEnv(r.session.version); gp != "" { + httpReq.Header.Set("Git-Protocol", gp) + } if r.session.baseURL.User != nil { password, _ := r.session.baseURL.User.Password() httpReq.SetBasicAuth(r.session.baseURL.User.Username(), password) @@ -296,7 +499,7 @@ func (r *httpRequester) doPost() error { } if r.resp.StatusCode != http.StatusOK { _ = r.resp.Body.Close() - return fmt.Errorf("http transport: POST %s unexpected status %d", serviceURL, r.resp.StatusCode) + return fmt.Errorf("http transport: POST %s unexpected status %d", redactedURL(r.resp.Request.URL), r.resp.StatusCode) } return nil } @@ -360,7 +563,7 @@ type dumbPackSession struct { func (s *dumbPackSession) Capabilities() *capability.List { return &capability.List{} } -func (s *dumbPackSession) GetRemoteRefs(_ context.Context) ([]*plumbing.Reference, error) { +func (s *dumbPackSession) GetRemoteRefs(_ context.Context, _ *transport.GetRemoteRefsOptions) (*transport.RemoteRefs, error) { if s.refs == nil { return nil, transport.ErrEmptyRemoteRepository } @@ -368,7 +571,7 @@ func (s *dumbPackSession) GetRemoteRefs(_ context.Context) ([]*plumbing.Referenc if err != nil { return nil, err } - return refs, nil + return transport.NewRemoteRefs(refs), nil } func (s *dumbPackSession) Fetch(ctx context.Context, st storage.Storer, req *transport.FetchRequest) error { diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/transport/negotiate.go b/vendor/github.com/go-git/go-git/v6/plumbing/transport/negotiate.go index 74f9b9c09..ffa3fbf86 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/transport/negotiate.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/transport/negotiate.go @@ -324,3 +324,72 @@ func readShallows( } return nil } + +// ReconcileObjectFormatV2 aligns the storer's object format with the Protocol +// v2 server's advertised object-format before any packfile is requested. On a +// fresh clone the storer's format is unset (HEAD still points at the +// refs/heads/.invalid placeholder) and the server's sha256 is adopted; +// otherwise a mismatch is a hard error, since indexing a sha256 pack as sha1 +// (or vice versa) corrupts the store and only surfaces later as a checksum +// failure. It mirrors NegotiatePack's v0/v1 object-format handling and git's +// fetch-pack.c, including the case where the server omits object-format (it +// only speaks sha1) but the client repository uses another algorithm. +func ReconcileObjectFormatV2(st storage.Storer, caps capability.List) error { + var clientFormat config.ObjectFormat + if cfg, err := st.Config(); err == nil && cfg != nil { + clientFormat = cfg.Extensions.ObjectFormat + } + + advertised := caps.Get(capability.ObjectFormat) + if len(advertised) == 0 { + // The server advertised no object-format, so it only speaks sha1. + // Upstream errors when the client repo uses a different algorithm + // rather than letting it fail later on a checksum mismatch. + if clientFormat != config.UnsetObjectFormat && clientFormat != config.SHA1 { + return fmt.Errorf("the server does not support algorithm '%s'", clientFormat) + } + return nil + } + + var serverFormat config.ObjectFormat + switch v := config.ObjectFormat(advertised[0]); v { + case config.SHA1, config.SHA256: + serverFormat = v + case config.UnsetObjectFormat: + // An empty value carries no algorithm; treat it exactly as an absent + // object-format (the server only speaks sha1). Apply the same guard as + // the len(advertised)==0 branch so a client repo on a different + // algorithm is rejected rather than slipping past to fail later on a + // checksum mismatch. + if clientFormat != config.UnsetObjectFormat && clientFormat != config.SHA1 { + return fmt.Errorf("the server does not support algorithm '%s'", clientFormat) + } + return nil + default: + // An algorithm go-git does not speak. Fail fast rather than proceed + // with the wrong hash format, matching NegotiatePack's v0/v1 handling. + return fmt.Errorf("server advertised unsupported object-format %q", v) + } + + // Adopt the server format on a fresh clone: unset client + sha256 server, + // with HEAD still at the clone placeholder. + if clientFormat == config.UnsetObjectFormat && serverFormat == config.SHA256 { + if ref, err := st.Reference(plumbing.HEAD); err == nil && ref.Target().String() == "refs/heads/.invalid" { + if setter, ok := st.(xstorage.ObjectFormatSetter); ok { + if err := setter.SetObjectFormat(serverFormat); err != nil { + return fmt.Errorf("unable to set object format: %w", err) + } + clientFormat = serverFormat + } + } + } + + if clientFormat == config.UnsetObjectFormat { + clientFormat = config.SHA1 + } + + if serverFormat != clientFormat { + return fmt.Errorf("mismatched algorithms: client %s; server %s", clientFormat, serverFormat) + } + return nil +} diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/transport/pack_session.go b/vendor/github.com/go-git/go-git/v6/plumbing/transport/pack_session.go index aafe357cc..e8b141f6f 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/transport/pack_session.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/transport/pack_session.go @@ -5,9 +5,26 @@ import ( "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/protocol/capability" + "github.com/go-git/go-git/v6/plumbing/protocol/packp" "github.com/go-git/go-git/v6/storage" ) +// Commander is an optional capability that Protocol v2-capable sessions +// implement. It provides access to arbitrary v2 commands beyond the +// built-in Fetch and Push operations. +// +// Sessions that negotiate Protocol v2 (version 2) implement this interface. +// The Command method executes a named v2 command: req carries the +// command-specific arguments and is encoded into the request, while resp +// decodes the response. For example, GetRemoteRefs runs +// Command(ctx, "ls-refs", lsRefsArgs, lsRefsOutput). The session builds the v2 +// request envelope (command name, the capabilities collected during the +// handshake, delim-pkt, the arguments, and flush-pkt) and, for HTTP, handles +// the response-end packet. +type Commander interface { + Command(ctx context.Context, cmd string, req packp.CommandArgs, resp packp.Decoder) error +} + // Transport is implemented by transports that speak the Git pack // protocol. Each transport implements this directly — stream transports // use the NewStreamSession helper, HTTP handles smart/dumb internally. @@ -18,8 +35,68 @@ type Transport interface { // Session is returned by Transport.Handshake. type Session interface { Capabilities() *capability.List - GetRemoteRefs(ctx context.Context) ([]*plumbing.Reference, error) + GetRemoteRefs(ctx context.Context, opts *GetRemoteRefsOptions) (*RemoteRefs, error) Fetch(ctx context.Context, st storage.Storer, req *FetchRequest) error Push(ctx context.Context, st storage.Storer, req *PushRequest) error Close() error } + +// GetRemoteRefsOptions configures Session.GetRemoteRefs. A nil pointer +// requests all references with default behavior, matching git's +// transport_get_remote_refs(transport, NULL). +type GetRemoteRefsOptions struct { + // RefPrefixes limits the returned references to those matching the + // given prefixes. For Protocol v2 these map directly to ls-refs + // ref-prefix arguments. For v0/v1 the server always advertises every + // reference, so prefixes are ignored. + RefPrefixes []string +} + +// RemoteRefs holds the result of Session.GetRemoteRefs. It is a struct so +// that new output fields can be added without changing the interface. +type RemoteRefs struct { + // References are the advertised references, with HEAD resolved to a + // symbolic reference when the server reports a symref target. + References []*plumbing.Reference + // Unborn is the symref target of HEAD when HEAD points at an unborn + // branch. It is empty when HEAD is not unborn or the server does not + // report it (v0/v1). + Unborn plumbing.ReferenceName +} + +// NewRemoteRefs builds a RemoteRefs from a resolved reference list, +// detecting an unborn HEAD: a symbolic HEAD whose target has no +// corresponding hash reference in the advertisement. +func NewRemoteRefs(refs []*plumbing.Reference) *RemoteRefs { + // A detached remote HEAD is advertised as a bare hash. The v0/v1 + // advertisement resolves it to a symbolic HEAD during decode; the v2 + // ls-refs path does not, so apply the same hash→branch heuristic here so a + // clone records a symbolic HEAD rather than a detached one (matching git). + for i, ref := range refs { + if ref.Name() == plumbing.HEAD && ref.Type() == plumbing.HashReference { + refs[i] = packp.ResolveHeadFromHashHeuristic(ref, refs) + break + } + } + + rr := &RemoteRefs{References: refs} + + var headTarget plumbing.ReferenceName + hashRefs := make(map[plumbing.ReferenceName]struct{}, len(refs)) + for _, ref := range refs { + switch { + case ref.Type() == plumbing.HashReference: + hashRefs[ref.Name()] = struct{}{} + case ref.Name() == plumbing.HEAD && ref.Type() == plumbing.SymbolicReference: + headTarget = ref.Target() + } + } + + if headTarget != "" { + if _, ok := hashRefs[headTarget]; !ok { + rr.Unborn = headTarget + } + } + + return rr +} diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/transport/pack_stream.go b/vendor/github.com/go-git/go-git/v6/plumbing/transport/pack_stream.go index 8685fa3d3..3cdf440fa 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/transport/pack_stream.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/transport/pack_stream.go @@ -7,7 +7,7 @@ import ( "io" "strings" - "github.com/go-git/go-git/v6/plumbing" + internal "github.com/go-git/go-git/v6/internal/transport" "github.com/go-git/go-git/v6/plumbing/protocol" "github.com/go-git/go-git/v6/plumbing/protocol/capability" "github.com/go-git/go-git/v6/plumbing/protocol/packp" @@ -53,11 +53,27 @@ func NewStreamSession(conn Conn, service string) (*StreamSession, error) { return nil, err } - switch ver { - case protocol.V2: - _ = conn.Close() - return nil, ErrUnsupportedVersion - case protocol.V1, protocol.V0: + s.version = ver + + if ver == protocol.V2 { + // Protocol v2: the server sends a capability advertisement + // (version line + capability lines) instead of the v0/v1 ref + // advertisement. References are retrieved lazily via the ls-refs + // command, so nothing is read here beyond the advertisement. + adv := &packp.CapabilityAdv{} + if err := adv.Decode(r); err != nil { + _ = conn.Close() + return nil, err + } + s.caps = adv.Capabilities + // Protocol v2 fetch accepts "want " without the server + // advertising allow-*-sha1-in-want, so surface the gate as + // satisfied for exact-SHA1 refspecs (isSupportedRefSpec). The + // v2 client only sends agent/object-format on the wire, so these + // never leak into the request (internal.ClientCapabilities). + s.caps.Set(capability.AllowReachableSHA1InWant) + s.caps.Set(capability.AllowTipSHA1InWant) + return s, nil } ar := &packp.AdvRefs{} @@ -72,7 +88,10 @@ func NewStreamSession(conn Conn, service string) (*StreamSession, error) { return nil, err } - s.version = ver + // Source the advertisement's version from the version DiscoverVersion + // already established, so s.version is the single source of truth rather + // than relying on AdvRefs.Decode's independent parse of the same line. + ar.Version = ver s.caps = ar.Capabilities s.refs = ar @@ -82,12 +101,30 @@ func NewStreamSession(conn Conn, service string) (*StreamSession, error) { // Capabilities implements PackSession. func (s *StreamSession) Capabilities() *capability.List { return &s.caps } -// GetRemoteRefs implements PackSession. -func (s *StreamSession) GetRemoteRefs(_ context.Context) ([]*plumbing.Reference, error) { +// GetRemoteRefs implements Session. For v0/v1 the server advertises every +// reference during the handshake, so opts is ignored. For v2 the references +// are retrieved on demand via the ls-refs command, honoring the ref-prefix +// filters in opts. +func (s *StreamSession) GetRemoteRefs(ctx context.Context, opts *GetRemoteRefsOptions) (*RemoteRefs, error) { + forPush := s.svc == ReceivePackService + if s.version == protocol.V2 { + var prefixes []string + if opts != nil { + prefixes = opts.RefPrefixes + } + refs, err := internal.LsRefs(ctx, s.Command, s.caps, prefixes) + if err != nil { + return nil, err + } + if !forPush && !internal.HasHashRef(refs) { + return nil, ErrEmptyRemoteRepository + } + return NewRemoteRefs(refs), nil + } + if s.refs == nil { return nil, ErrEmptyRemoteRepository } - forPush := s.svc == ReceivePackService if !forPush && s.refs.IsEmpty() { return nil, ErrEmptyRemoteRepository } @@ -96,11 +133,36 @@ func (s *StreamSession) GetRemoteRefs(_ context.Context) ([]*plumbing.Reference, if err != nil { return nil, err } - return refs, nil + return NewRemoteRefs(refs), nil } // Fetch implements PackSession. func (s *StreamSession) Fetch(ctx context.Context, st storage.Storer, req *FetchRequest) error { + if s.version == protocol.V2 { + if req.Filter != "" && !internal.FetchSupports(s.caps, "filter") { + return ErrFilterNotSupported + } + if req.Depth > 0 && !internal.FetchSupports(s.caps, "shallow") { + return ErrShallowNotSupported + } + if err := ReconcileObjectFormatV2(st, s.caps); err != nil { + return err + } + // Each negotiation round reuses the persistent stream: Command writes + // the request and decodes the metadata, leaving s.r at the packfile. + round := func(args *packp.FetchArgs) (*packp.FetchOutput, io.Reader, error) { + out := &packp.FetchOutput{} + if err := s.Command(ctx, "fetch", args, out); err != nil { + return nil, nil, err + } + return out, s.r, nil + } + if err := internal.FetchV2(ctx, st, req, round); err != nil { + return s.wrapStderr(err) + } + return nil + } + shallows, err := NegotiatePack(ctx, st, s.caps, false, s.r, s.w, req) if err != nil { return s.wrapStderr(err) @@ -119,6 +181,45 @@ func (s *StreamSession) Push(ctx context.Context, st storage.Storer, req *PushRe return nil } +// Command implements Commander. It builds a Protocol v2 request envelope for +// the named command, encodes it, and decodes the response. The request +// carries the capabilities collected during the handshake (the agent and the +// server's object-format), so callers only provide the command arguments. +// +// Command is only valid on a session that negotiated Protocol v2. +func (s *StreamSession) Command(ctx context.Context, cmd string, req packp.CommandArgs, resp packp.Decoder) error { + if s.version != protocol.V2 { + return ErrUnsupportedVersion + } + + cr := &packp.CommandRequest{ + Command: cmd, + Capabilities: s.commandCapabilities(), + Args: req, + } + + if err := cr.Encode(ioutil.NewContextWriter(ctx, s.w)); err != nil { + return s.wrapStderr(err) + } + + if resp != nil { + if err := resp.Decode(ioutil.NewContextReader(ctx, s.r)); err != nil { + return s.wrapStderr(err) + } + } + + return nil +} + +// commandCapabilities returns the capabilities the client sends with each v2 +// command. Both the agent and the object-format are gated on the server having +// advertised them (see internal.ClientCapabilities), so the client never sends +// a capability the server did not offer; the object-format is echoed back so +// both sides agree on the hash algorithm. +func (s *StreamSession) commandCapabilities() capability.List { + return internal.ClientCapabilities(s.caps) +} + // wrapStderr checks if the underlying connection has stderr output and // returns it as a RemoteError so that remote error messages surface at // the operation site rather than at Close time. @@ -158,6 +259,7 @@ func (s *StreamSession) Archive(ctx context.Context, req *ArchiveRequest) (io.Re } var ( - _ Session = (*StreamSession)(nil) - _ Archiver = (*StreamSession)(nil) + _ Session = (*StreamSession)(nil) + _ Archiver = (*StreamSession)(nil) + _ Commander = (*StreamSession)(nil) ) diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/transport/receive_pack.go b/vendor/github.com/go-git/go-git/v6/plumbing/transport/receive_pack.go index bc738c17e..1e4d22c3e 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/transport/receive_pack.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/transport/receive_pack.go @@ -23,10 +23,61 @@ type ReceivePackRequest struct { GitProtocol string AdvertiseRefs bool StatelessRPC bool + + // Hooks are optional server-side callbacks. The zero value installs none. + Hooks ReceivePackHooks +} + +// ReceivePackHooks holds server-side callbacks for ReceivePack. +// +// These are the in-process equivalent of git's pre-receive and post-receive +// hooks. They run after the packfile has been unpacked into the storer but +// before (PreReceive) and after (PostReceive) ref updates, so a server can +// enforce branch protection, signed-commit checks, or other policy without +// reimplementing receive-pack. +type ReceivePackHooks struct { + // PreReceive runs after the packfile is unpacked but before any ref is + // updated. Returning a non-nil error refuses every ref with err.Error() + // as the report-status reason; refs are not updated and PostReceive is + // not run. + PreReceive func(context.Context, *PreReceiveInfo) error + + // PostReceive runs after refs are updated. Any returned error is ignored + // for transport purposes: the refs have already moved and the + // report-status sent to the client reflects the ref-update outcome, not + // this error. The hook itself must handle or log failures it cares about. + PostReceive func(context.Context, *PostReceiveInfo) error +} + +// PreReceiveInfo carries the inputs to a PreReceive hook. +type PreReceiveInfo struct { + // Storer reads the proposed new state: the objects from this push are + // already present alongside the existing repository. + Storer storage.Storer + // Commands are the proposed ref updates. Treat as read-only. + Commands []*packp.Command + // PushOptions are the client's push options (empty if none). + PushOptions []string + // Progress writes to the client's sideband progress channel (band 2) when + // negotiated, or is io.Discard otherwise. Valid only during the call. + Progress io.Writer +} + +// PostReceiveInfo carries the inputs to a PostReceive hook. +type PostReceiveInfo struct { + // Storer reads the committed repository state. + Storer storage.Storer + // Commands are the ref updates that were applied successfully. Refs whose + // update failed are omitted. Treat as read-only. + Commands []*packp.Command + // PushOptions are the client's push options (empty if none). + PushOptions []string + // Progress writes to the client's sideband progress channel (band 2) when + // negotiated, or is io.Discard otherwise. Valid only during the call. + Progress io.Writer } // ReceivePack is a server command that serves the receive-pack service. -// TODO: support hooks func ReceivePack( ctx context.Context, st storage.Storer, @@ -45,18 +96,16 @@ func ReceivePack( } if opts.AdvertiseRefs || !opts.StatelessRPC { - switch version := ProtocolVersion(opts.GitProtocol); version { - case protocol.V1: - if _, err := pktline.Writef(w, "version %d\n", version); err != nil { - return err - } - // TODO: support version 2 - case protocol.V0, protocol.V2: + v := ProtocolVersion(opts.GitProtocol) + switch v { + case protocol.V0, protocol.V1, protocol.V2: + // version emission (if any) is handled inside AdvertiseRefs for correct + // ordering with the HTTP smart-reply prefix when applicable. default: - return fmt.Errorf("%w: %q", ErrUnsupportedVersion, version) + return fmt.Errorf("%w: %q", ErrUnsupportedVersion, v) } - if err := AdvertiseRefs(ctx, st, w, ReceivePackService, opts.StatelessRPC); err != nil { + if err := AdvertiseRefs(ctx, st, w, ReceivePackService, opts.StatelessRPC, v); err != nil { return err } } @@ -95,7 +144,6 @@ func ReceivePack( pushOpts packp.PushOptions ) - // TODO: Pass the options to the server-side hooks. if updreq.Capabilities.Supports(capability.PushOptions) { if err := pushOpts.Decode(rd); err != nil { return fmt.Errorf("decoding push-options: %w", err) @@ -123,20 +171,25 @@ func ReceivePack( } // Report status if the client supports it - if !updreq.Capabilities.Supports(capability.ReportStatus) { + if !updreq.Capabilities.Supports(capability.ReportStatus) && !updreq.Capabilities.Supports(capability.ReportStatusV2) { return unpackErr } var ( useSideband bool writer io.Writer = w + progress = io.Writer(io.Discard) ) if !caps.Supports(capability.NoProgress) { + var mux *sideband.Muxer if caps.Supports(capability.Sideband64k) { - writer = sideband.NewMuxer(sideband.Sideband64k, w) - useSideband = true + mux = sideband.NewMuxer(sideband.Sideband64k, w) } else if caps.Supports(capability.Sideband) { - writer = sideband.NewMuxer(sideband.Sideband, w) + mux = sideband.NewMuxer(sideband.Sideband, w) + } + if mux != nil { + writer = mux + progress = sidebandProgress{mux} useSideband = true } } @@ -148,10 +201,55 @@ func ReceivePack( return res } + if opts.Hooks.PreReceive != nil { + info := &PreReceiveInfo{ + Storer: st, + Commands: updreq.Commands, + PushOptions: pushOpts.Options, + Progress: progress, + } + if hookErr := opts.Hooks.PreReceive(ctx, info); hookErr != nil { + rejected := make(map[plumbing.ReferenceName]error, len(updreq.Commands)) + for _, cmd := range updreq.Commands { + rejected[cmd.Name] = hookErr + } + if err := sendReportStatus(writeCloser, nil, rejected); err != nil { + _ = closeWriter(w) + return err + } + if useSideband { + if err := pktline.WriteFlush(w); err != nil { + _ = closeWriter(w) + return fmt.Errorf("flushing sideband: %w", err) + } + } + if err := closeWriter(w); err != nil { + return err + } + return hookErr + } + } + var firstErr error cmdStatus := make(map[plumbing.ReferenceName]error) updateReferences(st, updreq, cmdStatus, &firstErr) + if opts.Hooks.PostReceive != nil { + applied := make([]*packp.Command, 0, len(updreq.Commands)) + for _, cmd := range updreq.Commands { + if cmdStatus[cmd.Name] == nil { + applied = append(applied, cmd) + } + } + info := &PostReceiveInfo{ + Storer: st, + Commands: applied, + PushOptions: pushOpts.Options, + Progress: progress, + } + _ = opts.Hooks.PostReceive(ctx, info) + } + if err := sendReportStatus(writeCloser, firstErr, cmdStatus); err != nil { return err } @@ -167,6 +265,12 @@ func ReceivePack( return closeWriter(w) } +type sidebandProgress struct{ mux *sideband.Muxer } + +func (p sidebandProgress) Write(b []byte) (int, error) { + return p.mux.WriteChannel(sideband.ProgressMessage, b) +} + func closeWriter(w io.WriteCloser) error { if err := w.Close(); err != nil { return fmt.Errorf("closing writer: %w", err) diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/transport/serve.go b/vendor/github.com/go-git/go-git/v6/plumbing/transport/serve.go index 5b776ee41..91a7e00ee 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/transport/serve.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/transport/serve.go @@ -9,6 +9,7 @@ import ( "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/format/config" "github.com/go-git/go-git/v6/plumbing/object" + "github.com/go-git/go-git/v6/plumbing/protocol" "github.com/go-git/go-git/v6/plumbing/protocol/capability" "github.com/go-git/go-git/v6/plumbing/protocol/packp" "github.com/go-git/go-git/v6/plumbing/storer" @@ -19,13 +20,20 @@ import ( var ErrUpdateReference = errors.New("failed to update ref") // AdvertiseRefs is a server command that implements the reference -// discovery phase of the Git transfer protocol. +// discovery phase of the v0/v1 Git transfer protocol. Protocol v2 advertises +// capabilities only, via [AdvertiseCapabilities]; the sole reason this function +// accepts protocol.V2 is the receive-pack fallback: v2 has no push, so when a +// client requests v2 for receive-pack git ignores it and serves a classic +// advertisement (builtin/receive-pack.c), while http-backend still suppresses +// the "# service=..." smart-reply line for the v2 request (http-backend.c +// get_info_refs). Both behaviours are reproduced below. func AdvertiseRefs( _ context.Context, st storage.Storer, w io.Writer, service string, smart bool, + version protocol.Version, ) error { switch service { case UploadPackService, ReceivePackService: @@ -57,17 +65,7 @@ func AdvertiseRefs( ar.Capabilities.Set(capability.Sideband) ar.Capabilities.Set(capability.NoProgress) ar.Capabilities.Set(capability.Shallow) - - cfg, err := st.Config() - var objectformat config.ObjectFormat - if err == nil && cfg != nil { - objectformat = cfg.Extensions.ObjectFormat - } - - if objectformat == config.UnsetObjectFormat { - objectformat = config.DefaultObjectFormat - } - ar.Capabilities.Set(capability.ObjectFormat, objectformat.String()) + ar.Capabilities.Set(capability.ObjectFormat, objectFormat(st).String()) } // Set references @@ -80,7 +78,10 @@ func AdvertiseRefs( return fmt.Errorf("invalid capabilities: %w", err) } - if smart { + // git's http-backend omits the "# service=..." smart reply whenever the + // requested protocol is v2, even for receive-pack which then falls back to + // a v0 advertisement (http-backend.c get_info_refs). + if smart && version != protocol.V2 { smartReply := packp.SmartReply{ Service: service, } @@ -90,9 +91,76 @@ func AdvertiseRefs( } } + // V1 prefixes the advertisement with an explicit version packet (V0 emits + // none). AdvRefs.Encode writes it from ar.Version, so set the field rather + // than writing the line by hand — a single source for the encoded version. + // A v2 request with no v2 service (e.g. receive-pack) falls back to a v0 + // advertisement, so only V1 sets the field here; V2 stays at the V0 default. + if version == protocol.V1 { + ar.Version = protocol.V1 + } + return ar.Encode(w) } +// AdvertiseCapabilities implements the Protocol v2 capability advertisement for +// the upload-pack service. Unlike the v0/v1 [AdvertiseRefs], it does not list +// references (clients retrieve them with the ls-refs command) and it does not +// emit the smart-HTTP "# service=..." prefix: git omits that line for v2 +// (http-backend.c get_info_refs), the response starts directly with the version +// packet. +func AdvertiseCapabilities(_ context.Context, st storage.Storer, w io.Writer, service string) error { + if service != UploadPackService { + return fmt.Errorf("%w: %s", ErrUnsupportedService, service) + } + + adv := &packp.CapabilityAdv{ + Version: protocol.V2, + Capabilities: serverV2Capabilities(st), + } + return adv.Encode(w) +} + +// serverV2Capabilities builds the v2 capabilities this server implements. Only +// commands and features that are actually handled are advertised: advertising a +// feature that isn't handled makes clients request it and then mis-handle the +// reply. +// +// The fetch "shallow" feature covers the whole deepen family (deepen , +// deepen-since, deepen-not and deepen-relative), all of which are handled, so +// it is advertised as the single token upstream uses. +// +// TODO: advertise these once implemented: +// - ls-refs=unborn report an unborn HEAD on an empty repository +// - fetch=filter partial-clone object filters +// - fetch=ref-in-want want-ref negotiation +// - fetch=sideband-all sideband for the entire response, not just the packfile +// - fetch=packfile-uris offload pack data to out-of-band URIs +// - fetch=wait-for-done negotiate-only fetch (git fetch --negotiate-only) +// - server-option process client "server-option" lines +// - object-info object size/type queries without a fetch +func serverV2Capabilities(st storage.Storer) capability.List { + var caps capability.List + caps.Set(capability.Agent, capability.DefaultAgent()) + caps.Set(capability.LsRefs) + caps.Set(capability.FetchCmd, "shallow") + caps.Set(capability.ObjectFormat, objectFormat(st).String()) + return caps +} + +// objectFormat returns the repository's configured object format, defaulting to +// the package default when the config is missing or unset. +func objectFormat(st storage.Storer) config.ObjectFormat { + cfg, err := st.Config() + if err != nil || cfg == nil { + return config.DefaultObjectFormat + } + if cfg.Extensions.ObjectFormat == config.UnsetObjectFormat { + return config.DefaultObjectFormat + } + return cfg.Extensions.ObjectFormat +} + func addReferences(st storage.Storer, ar *packp.AdvRefs, addHead bool) error { iter, err := st.IterReferences() if err != nil { diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/transport/upload_archive.go b/vendor/github.com/go-git/go-git/v6/plumbing/transport/upload_archive.go index ff4b8a810..3b09f5c2e 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/transport/upload_archive.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/transport/upload_archive.go @@ -151,23 +151,28 @@ const maxArchiveArgs = 64 // readArchiveArgs reads "argument \n" pkt-lines until flush. func readArchiveArgs(r io.Reader) ([]string, error) { var args []string - for { - l, line, err := pktline.ReadLine(r) - if err != nil { - return nil, fmt.Errorf("upload-archive: reading argument: %w", err) - } - if l == pktline.Flush { + sc := pktline.NewScanner(r) + flushed := false + for sc.Scan() { + if sc.Len() == pktline.Flush { + flushed = true break } if len(args) >= maxArchiveArgs { return nil, fmt.Errorf("upload-archive: too many arguments (>%d)", maxArchiveArgs) } - s := strings.TrimSuffix(string(line), "\n") - if !strings.HasPrefix(s, "argument ") { - return nil, fmt.Errorf("upload-archive: expected 'argument' token, got: %s", s) + line := strings.TrimSuffix(sc.Text(), "\n") + if !strings.HasPrefix(line, "argument ") { + return nil, fmt.Errorf("upload-archive: expected 'argument' token, got: %s", line) } - args = append(args, s[len("argument "):]) + args = append(args, line[len("argument "):]) + } + if err := sc.Err(); err != nil { + return nil, fmt.Errorf("upload-archive: reading argument: %w", err) + } + if !flushed { + return nil, fmt.Errorf("upload-archive: reading argument: %w", io.ErrUnexpectedEOF) } return args, nil } diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/transport/upload_pack.go b/vendor/github.com/go-git/go-git/v6/plumbing/transport/upload_pack.go index ee2288d0e..eafecf9f5 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/transport/upload_pack.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/transport/upload_pack.go @@ -3,9 +3,12 @@ package transport import ( "bufio" "context" + "errors" "fmt" "io" "math" + "strings" + "time" "github.com/go-git/go-git/v6/config" "github.com/go-git/go-git/v6/plumbing" @@ -17,6 +20,7 @@ import ( "github.com/go-git/go-git/v6/plumbing/protocol/packp" "github.com/go-git/go-git/v6/plumbing/protocol/packp/sideband" "github.com/go-git/go-git/v6/plumbing/revlist" + "github.com/go-git/go-git/v6/plumbing/storer" "github.com/go-git/go-git/v6/storage" "github.com/go-git/go-git/v6/utils/ioutil" ) @@ -54,18 +58,20 @@ func UploadPack( } if opts.AdvertiseRefs || !opts.StatelessRPC { - switch version := ProtocolVersion(opts.GitProtocol); version { - case protocol.V1: - if _, err := pktline.Writef(w, "version %d\n", version); err != nil { - return err - } - // TODO: support version 2 - case protocol.V0, protocol.V2: + v := ProtocolVersion(opts.GitProtocol) + switch v { + case protocol.V0, protocol.V1, protocol.V2: + // V0/V1 share the classic advertisement; V2 advertises + // capabilities only (refs come via ls-refs). default: - return fmt.Errorf("%w: %q", ErrUnsupportedVersion, version) + return fmt.Errorf("%w: %q", ErrUnsupportedVersion, v) } - if err := AdvertiseRefs(ctx, st, w, UploadPackService, opts.StatelessRPC); err != nil { + if v == protocol.V2 { + if err := AdvertiseCapabilities(ctx, st, w, UploadPackService); err != nil { + return fmt.Errorf("advertising v2 capabilities: %w", err) + } + } else if err := AdvertiseRefs(ctx, st, w, UploadPackService, opts.StatelessRPC, v); err != nil { return fmt.Errorf("advertising references: %w", err) } } @@ -82,6 +88,12 @@ func UploadPack( r = ioutil.NewContextReadCloser(ctx, r) rd := bufio.NewReader(r) + + v := ProtocolVersion(opts.GitProtocol) + if v == protocol.V2 { + return serveUploadPackV2(ctx, st, rd, w, opts) + } + l, _, err := pktline.PeekLine(rd) if err != nil { return fmt.Errorf("peeking line: %w", err) @@ -374,3 +386,758 @@ func getShallowCommits(st storage.Storer, heads []plumbing.Hash, depth int, upd return nil } + +// shallowFrontierDepth returns the depth, counted from the wants (a tip is at +// depth 1), of the closest commit in the client's shallow set, or 0 if none is +// reachable. It mirrors upstream get_shallows_depth (shallow.c): the value +// offsets a deepen-relative request so the new depth is measured from the +// client's existing shallow boundary rather than from the tips. +func shallowFrontierDepth(st storage.Storer, heads, shallows []plumbing.Hash) (int, error) { + shallowSet := make(map[plumbing.Hash]struct{}, len(shallows)) + for _, h := range shallows { + shallowSet[h] = struct{}{} + } + + best := 0 + seen := map[plumbing.Hash]int{} + type frame struct { + hash plumbing.Hash + depth int // depth of this commit's predecessor; the commit sits at depth+1 + } + var stack []frame + for _, h := range heads { + if c, ok := peelToCommit(st, h); ok { + stack = append(stack, frame{c.Hash, 0}) + } + } + for len(stack) > 0 { + f := stack[len(stack)-1] + stack = stack[:len(stack)-1] + if d, ok := seen[f.hash]; ok && d <= f.depth { + continue + } + seen[f.hash] = f.depth + + cur := f.depth + 1 + if _, ok := shallowSet[f.hash]; ok { + if best == 0 || cur < best { + best = cur + } + // A client shallow commit is a normal commit on the server, so the + // walk continues past it, matching upstream get_shallows_or_depth. + } + + c, err := object.GetCommit(st, f.hash) + if err != nil { + continue + } + for _, p := range c.ParentHashes { + stack = append(stack, frame{p, cur}) + } + } + return best, nil +} + +// serveUploadPackV2 handles the git protocol v2 for upload-pack (fetch/ls-refs). +// It is used when the client requests version=2 via GIT_PROTOCOL. +func serveUploadPackV2(ctx context.Context, st storage.Storer, rd *bufio.Reader, w io.WriteCloser, opts *UploadPackRequest) error { + for { + // Peek the command line to choose the argument decoder, then decode the + // whole request envelope through packp.CommandRequest (the same type the + // client encodes). + l, line, err := pktline.PeekLine(rd) + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + return err + } + if l == pktline.Flush { + // A lone flush-pkt ends the request. + _, _, _ = pktline.ReadLine(rd) + return nil + } + + cmd := strings.TrimPrefix(strings.TrimSuffix(string(line), "\n"), "command=") + + req := &packp.CommandRequest{} + switch cmd { + case "ls-refs": + req.Args = &packp.LsRefsArgs{} + case "fetch": + req.Args = &packp.FetchArgs{} + default: + _, _ = pktline.Writef(w, "error unknown-command %s\n", cmd) + _ = pktline.WriteFlush(w) + return fmt.Errorf("unsupported v2 command %q", cmd) + } + + if err := req.Decode(rd); err != nil { + return fmt.Errorf("decoding %s request: %w", cmd, err) + } + + switch cmd { + case "ls-refs": + if err := serveLsRefsV2(ctx, st, w, req.Args.(*packp.LsRefsArgs)); err != nil { + return err + } + // Stateless (HTTP) carries a single command per request; stateful + // transports may continue, but clients typically close after. + if opts.StatelessRPC { + return nil + } + case "fetch": + concluded, err := serveFetchV2(ctx, st, w, req.Args.(*packp.FetchArgs), opts) + if err != nil { + return err + } + if concluded { + return nil + } + // Stateful transport: the round was acknowledgments-only and the + // negotiation continues. Loop to read the client's next command. + } + } +} + +// serveLsRefsV2 responds to a ls-refs command using the decoded arguments. +// +// The reference lines are encoded by writeV2Ref rather than packp.LsRefsOutput: +// a v2 HEAD line carries both a resolved object id and a symref-target +// attribute, which a single plumbing.Reference (hash XOR symbolic) cannot +// represent. writeV2Ref resolves the symref's hash from the storer, matching +// upstream git's send_ref. +func serveLsRefsV2(_ context.Context, st storage.Storer, w io.Writer, args *packp.LsRefsArgs) error { + iter, err := st.IterReferences() + if err != nil { + return err + } + defer iter.Close() + + var refs []*plumbing.Reference + _ = iter.ForEach(func(r *plumbing.Reference) error { + refs = append(refs, r) + return nil + }) + + prefixes := args.RefPrefixes + + // HEAD is emitted first, but only when it passes the ref-prefix filter, + // matching upstream's send_possibly_unborn_head -> send_ref (ls-refs.c), + // where HEAD is subject to ref_match like every other ref. + for _, r := range refs { + if r.Name() == plumbing.HEAD { + if len(prefixes) == 0 || refMatchesAnyPrefix(r.Name().String(), prefixes) { + if err := writeV2Ref(w, st, r, args.Symrefs, args.Peel); err != nil { + return err + } + } + break + } + } + + for _, r := range refs { + if r.Name() == plumbing.HEAD { + continue + } + if len(prefixes) > 0 && !refMatchesAnyPrefix(r.Name().String(), prefixes) { + continue + } + if err := writeV2Ref(w, st, r, args.Symrefs, args.Peel); err != nil { + return err + } + } + + return pktline.WriteFlush(w) +} + +func refMatchesAnyPrefix(name string, prefixes []string) bool { + for _, p := range prefixes { + if strings.HasPrefix(name, p) { + return true + } + } + return false +} + +func writeV2Ref(w io.Writer, st storage.Storer, r *plumbing.Reference, symrefs, peel bool) error { + var hash plumbing.Hash + var target string + if r.Type() == plumbing.SymbolicReference { + ref, err := storer.ResolveReference(st, r.Target()) + if err == nil { + hash = ref.Hash() + } + target = r.Target().String() + } else { + hash = r.Hash() + } + if hash.IsZero() { + return nil + } + // Protocol v2 ls-refs grammar: + // ref = obj-id SP refname *(SP ref-attribute) LF + // ref-attribute = (symref | peeled) + // Both symref-target and peeled are attributes on the ref's own line + // (symref-target first, matching upstream's send_ref ordering), not + // separate lines as in the v0/v1 advertisement format. + line := fmt.Sprintf("%s %s", hash, r.Name()) + if symrefs && target != "" { + line += " symref-target:" + target + } + if peel { + // Peel any ref whose object is (a chain of) annotated tags, not just + // refs/tags/*, and resolve all the way to the underlying non-tag object + // — matching upstream's reference_get_peeled_oid (ls-refs.c). Lightweight + // tags and branches don't point at tag objects, so they emit no attribute. + if peeled, ok := peelToNonTag(st, hash); ok { + line += " peeled:" + peeled.String() + } + } + if _, err := pktline.Writef(w, "%s\n", line); err != nil { + return err + } + return nil +} + +// peelToNonTag follows annotated-tag objects from h down to the first non-tag +// object, mirroring upstream's reference_get_peeled_oid. It returns the peeled +// hash and true when h points at one or more tag objects; false when h is not a +// tag (a lightweight tag, branch, etc.) so no "peeled" attribute is emitted. +func peelToNonTag(st storage.Storer, h plumbing.Hash) (plumbing.Hash, bool) { + tag, err := object.GetTag(st, h) + if err != nil { + return plumbing.ZeroHash, false + } + for { + next := tag.Target + inner, err := object.GetTag(st, next) + if err != nil { + // next is a non-tag object (or missing); return it as the peeled + // value, as upstream's peel does. + return next, true + } + tag = inner + } +} + +// serveFetchV2 handles command=fetch for v2 using the decoded arguments. The +// acknowledgments, shallow-info, and packfile-header sections are emitted +// through packp.FetchOutput; this function streams the packfile data after the +// header, matching the caller-owned streaming on the client side. +// +// It reports whether the fetch concluded. A packfile (or a terminal no-op) +// returns concluded=true and the connection is closed. An acknowledgments-only +// round on a stateful transport returns concluded=false with the connection +// left open, so the caller loops to read the client's next command=fetch round +// (the stateful negotiation continues until the server is ready). A stateless +// (HTTP) round always concludes, since the client re-POSTs each round. +func serveFetchV2(_ context.Context, st storage.Storer, w io.WriteCloser, args *packp.FetchArgs, opts *UploadPackRequest) (concluded bool, err error) { + wants := args.Wants + haves := args.Haves + clientShallows := args.Shallows + depth := args.Deepen + done := args.Done + + // No 'want' lines: the client guessed it didn't want anything. Upstream + // emits no response at all here (upload-pack.c, UPLOAD_DONE), so write + // nothing and just close the stream, no stray flush packet. + if len(wants) == 0 { + return true, w.Close() + } + + out := &packp.FetchOutput{} + + // Negotiation (acknowledgments section), per gitprotocol-v2 "fetch": + // + // - done -> no acknowledgments section; packfile follows. + // - no haves -> clone-like; no acknowledgments section; packfile follows. + // - haves and !done -> emit an acknowledgments section. ACK every common + // object. "ready" is sent only once every want is + // reachable from the common haves (upstream's + // ok_to_give_up); then the packfile follows in the + // same response. Otherwise the section ends without a + // packfile and the client negotiates again with more + // haves (NAK when there is no common object at all). + if !done && len(haves) > 0 { + var common []plumbing.Hash + for _, h := range haves { + if _, err := st.EncodedObject(plumbing.AnyObject, h); err == nil { + common = append(common, h) + } + } + out.Acknowledgments = &packp.Acknowledgments{ACKs: common} + + // "ready" is withheld until every want is reachable from the common + // haves (upstream's ok_to_give_up). Declaring it on the first common + // have would force single-round negotiation and a larger pack. When not + // ready (including no common object at all, which encodes as NAK), the + // acknowledgments section stands alone and the client refines its haves + // in the next request. + if len(common) == 0 || !wantsReachableFromHaves(st, wants, common) { + if err := out.Encode(w); err != nil { + return true, err + } + // Stateless (HTTP) carries one round per request: this response is + // complete and the client re-POSTs the next round. A stateful + // transport keeps the connection open so the client can send its + // next command=fetch with refined haves. + if opts.StatelessRPC { + return true, w.Close() + } + return false, nil + } + out.Acknowledgments.Ready = true + } + + // shallow-info: a shallow fetch bounds the history sent. The boundary forms + // mirror upstream send_shallow_list (upload-pack.c): + // - deepen : a depth boundary from the wants (getShallowCommits). + // - deepen-since / deepen-not: a date/ref boundary (getShallowCommitsByRevList, + // mirroring deepen_by_rev_list). + // Upstream forbids combining deepen with deepen-since/deepen-not, and so do we. + // deepen-relative only changes how the depth is counted: for a fresh fetch + // (no client shallows) relative and absolute depth coincide, and for an + // already-shallow client the depth is offset by the existing boundary's + // distance from the wants (see the deepen-relative handling below). + since := args.DeepenSince + notTips, err := resolveDeepenNot(st, args.DeepenNot) + if err != nil { + _ = w.Close() + return true, fmt.Errorf("resolving deepen-not: %w", err) + } + revList := !since.IsZero() || len(notTips) > 0 + if depth > 0 && revList { + _ = w.Close() + return true, fmt.Errorf("deepen and deepen-since (or deepen-not) cannot be used together") + } + + // A deepen was requested when depth > 0 or a rev-list bound was given. + // haveNewBoundary records that separately from len(newBoundary): a deepen + // that reaches full history yields an empty boundary, which still drives + // shallow-info and the unshallow lines and must not be mistaken for "no + // deepen requested". newBoundary is the grafting boundary for the deepened + // view (nil/empty means graft nothing: full history). + var newBoundary []plumbing.Hash + var haveNewBoundary bool + if depth > 0 || revList { + var shupd packp.ShallowUpdate + computed := true + if revList { + err = getShallowCommitsByRevList(st, wants, since, notTips, &shupd) + } else { + effectiveDepth := depth + if args.DeepenRelative && len(clientShallows) > 0 { + // deepen-relative counts depth from the client's existing + // shallow boundary, not from the wants. Mirror upstream + // get_shallow_commits (shallow.c): offset the absolute depth by + // the depth at which that boundary sits from the wants. + cur, derr := shallowFrontierDepth(st, wants, clientShallows) + if derr != nil { + _ = w.Close() + return true, fmt.Errorf("computing shallow frontier depth: %w", derr) + } + if cur == 0 { + // No client shallow is reachable from the wants; upstream + // computes no new boundary and leaves the client's view + // unchanged. Skip the deepen entirely. + computed = false + } else { + effectiveDepth = depth + cur + } + } + if computed { + err = getShallowCommits(st, wants, effectiveDepth, &shupd) + } + } + if err != nil { + _ = w.Close() + return true, fmt.Errorf("computing shallow commits: %w", err) + } + if computed { + haveNewBoundary = true + newBoundary = shupd.Shallows + } + } + + var objs []plumbing.Hash + if len(clientShallows) > 0 { + // The client already has a shallow view (it sent "shallow" lines). + // A single object walk cannot graft the wanted history at the new + // boundary while also grafting the client's have-history at its existing + // boundary, so compute two views and send their difference: + // newView = objects reachable from the wants, grafted at the new + // boundary (the client's deepened view). + // clientView = objects the client already has, reachable from its haves + // grafted at its existing shallow boundary. + // newView \ clientView is exactly what the client is missing. It never + // omits a needed object; at worst it re-sends one the client has, which + // is harmless. This is what bounds a deepen of an already-shallow clone. + boundary := clientShallows + if haveNewBoundary { + // The deepened boundary, which may be empty: a deepen that reaches + // full history grafts nothing and unshallows the old boundary. + boundary = newBoundary + } + newView, nerr := objectsToUpload(&shallowBoundaryStorer{Storer: st, boundary: boundary}, wants, nil) + if nerr != nil { + _ = w.Close() + return true, fmt.Errorf("getting objects to upload: %w", nerr) + } + clientView, cerr := objectsToUpload(&shallowBoundaryStorer{Storer: st, boundary: clientShallows}, haves, nil) + if cerr != nil { + _ = w.Close() + return true, fmt.Errorf("getting client objects: %w", cerr) + } + objs = hashDifference(newView, clientView) + if haveNewBoundary { + out.ShallowInfo = &packp.ShallowInfo{ + Shallows: newBoundary, + Unshallows: unshallowedCommits(clientShallows, newBoundary, newView), + } + } + } else { + packSt := st + if haveNewBoundary && len(newBoundary) > 0 { + out.ShallowInfo = &packp.ShallowInfo{Shallows: newBoundary} + packSt = &shallowBoundaryStorer{Storer: st, boundary: newBoundary} + } + objs, err = objectsToUpload(packSt, wants, haves) + if err != nil { + _ = w.Close() + return true, fmt.Errorf("getting objects to upload: %w", err) + } + } + + // include-tag: add annotated tags whose target is in the pack (auto-tag + // following), mirroring upstream pack-objects --include-tag. + if args.IncludeTag { + objs, err = includeReachableTags(st, objs) + if err != nil { + _ = w.Close() + return true, fmt.Errorf("collecting include-tag objects: %w", err) + } + } + + // Emit the metadata sections and the "packfile" section header. The client + // switches to sideband demux after seeing the header, matching reference git. + out.Packfile = true + if err := out.Encode(w); err != nil { + return true, err + } + + // The packfile is muxed on sideband-64k band 1. This server never writes the + // progress band (band 2), so the client's no-progress request (args.NoProgress) + // is honored by construction; there is nothing to suppress. + writer := sideband.NewMuxer(sideband.Sideband64k, w) + + var packWindow uint + if opts.SkipDeltaCompression { + packWindow = 0 + } else if cfg, cerr := st.Config(); cerr == nil && cfg != nil { + packWindow = cfg.Pack.Window + } else { + packWindow = config.DefaultPackWindow + } + + e := packfile.NewEncoder(writer, st, false) + if _, err := e.Encode(objs, packWindow); err != nil { + return true, fmt.Errorf("encoding packfile: %w", err) + } + + // Terminate the sideband stream and the v2 fetch response. + if err := pktline.WriteFlush(w); err != nil { + return true, err + } + + return true, w.Close() +} + +// hashDifference returns the elements of a that are not in b, preserving a's +// order. It computes the objects a deepened client is missing (newView minus the +// client's existing view). +func hashDifference(a, b []plumbing.Hash) []plumbing.Hash { + set := make(map[plumbing.Hash]struct{}, len(b)) + for _, h := range b { + set[h] = struct{}{} + } + var out []plumbing.Hash + for _, h := range a { + if _, ok := set[h]; !ok { + out = append(out, h) + } + } + return out +} + +// unshallowedCommits returns the client's shallow commits that the deepened view +// now includes as interior commits (their parents are being sent), so the client +// can clear their shallow mark. Commits still on the new boundary stay shallow. +// Mirrors upstream send_unshallow (upload-pack.c). +func unshallowedCommits(clientShallows, newBoundary, newView []plumbing.Hash) []plumbing.Hash { + inView := make(map[plumbing.Hash]struct{}, len(newView)) + for _, h := range newView { + inView[h] = struct{}{} + } + boundary := make(map[plumbing.Hash]struct{}, len(newBoundary)) + for _, h := range newBoundary { + boundary[h] = struct{}{} + } + var out []plumbing.Hash + for _, cs := range clientShallows { + if _, ok := inView[cs]; !ok { + continue // not part of the deepened view + } + if _, ok := boundary[cs]; ok { + continue // still a boundary commit + } + out = append(out, cs) + } + return out +} + +// resolveDeepenNot resolves each deepen-not argument (a ref name or an object +// id) to a commit hash, peeling annotated tags, mirroring how upstream feeds +// "--not " to rev-list (upload-pack.c send_shallow_list). +func resolveDeepenNot(st storage.Storer, refs []string) ([]plumbing.Hash, error) { + if len(refs) == 0 { + return nil, nil + } + out := make([]plumbing.Hash, 0, len(refs)) + for _, r := range refs { + var h plumbing.Hash + if ref, err := storer.ResolveReference(st, plumbing.ReferenceName(r)); err == nil { + h = ref.Hash() + } else if oid, ok := plumbing.FromHex(r); ok { + if _, err := st.EncodedObject(plumbing.AnyObject, oid); err != nil { + return nil, fmt.Errorf("cannot resolve deepen-not %q", r) + } + h = oid + } else { + return nil, fmt.Errorf("cannot resolve deepen-not %q", r) + } + if peeled, ok := peelToNonTag(st, h); ok { + h = peeled + } + out = append(out, h) + } + return out, nil +} + +// reachableCommits returns the set of commits reachable from tips (inclusive), +// used as the exclusion set for deepen-not. +func reachableCommits(st storage.Storer, tips []plumbing.Hash) (map[plumbing.Hash]struct{}, error) { + seen := make(map[plumbing.Hash]struct{}) + stack := append([]plumbing.Hash(nil), tips...) + for len(stack) > 0 { + h := stack[len(stack)-1] + stack = stack[:len(stack)-1] + if _, ok := seen[h]; ok { + continue + } + seen[h] = struct{}{} + c, err := object.GetCommit(st, h) + if err != nil { + continue + } + stack = append(stack, c.ParentHashes...) + } + return seen, nil +} + +// getShallowCommitsByRevList computes the shallow boundary for a deepen-since +// and/or deepen-not request, mirroring upstream's deepen_by_rev_list +// (upload-pack.c). The included set is every commit reachable from heads that is +// not older than since (when set) and not reachable from any notTips (when set); +// a commit in the set with a parent outside it is a shallow boundary. +// +// Unlike git's rev-list traversal it does not apply the date "slop" used to +// tolerate out-of-order committer timestamps, so under clock skew the boundary +// may differ by a few commits; the resulting shallow clone is still valid. +func getShallowCommitsByRevList(st storage.Storer, heads []plumbing.Hash, since time.Time, notTips []plumbing.Hash, upd *packp.ShallowUpdate) error { + exclude, err := reachableCommits(st, notTips) + if err != nil { + return err + } + + included := make(map[plumbing.Hash]struct{}) + parents := make(map[plumbing.Hash][]plumbing.Hash) + visited := make(map[plumbing.Hash]struct{}) + stack := append([]plumbing.Hash(nil), heads...) + for len(stack) > 0 { + h := stack[len(stack)-1] + stack = stack[:len(stack)-1] + if _, ok := visited[h]; ok { + continue + } + visited[h] = struct{}{} + if _, ex := exclude[h]; ex { + continue + } + c, err := object.GetCommit(st, h) + if err != nil { + continue + } + if !since.IsZero() && c.Committer.When.Before(since) { + continue + } + included[h] = struct{}{} + parents[h] = c.ParentHashes + stack = append(stack, c.ParentHashes...) + } + + for h := range included { + for _, p := range parents[h] { + if _, ok := included[p]; !ok { + upd.Shallows = append(upd.Shallows, h) + break + } + } + } + plumbing.HashesSort(upd.Shallows) + return nil +} + +// includeReachableTags implements the fetch "include-tag" feature: for every +// annotated tag whose (peeled) target is already in objs, it adds the tag +// object and every tag object along the chain, mirroring upstream pack-objects +// --include-tag. Lightweight tags have no tag object and are skipped. +func includeReachableTags(st storage.Storer, objs []plumbing.Hash) ([]plumbing.Hash, error) { + have := make(map[plumbing.Hash]struct{}, len(objs)) + for _, h := range objs { + have[h] = struct{}{} + } + + iter, err := st.IterReferences() + if err != nil { + return objs, err + } + defer iter.Close() + + added := objs + err = iter.ForEach(func(ref *plumbing.Reference) error { + if ref.Type() != plumbing.HashReference || !ref.Name().IsTag() { + return nil + } + var chain []plumbing.Hash + seen := make(map[plumbing.Hash]struct{}) + cur := ref.Hash() + for { + if _, ok := have[cur]; ok { + // Reached an object already in the pack: include the tag + // objects that point at it. + for _, t := range chain { + if _, ok := have[t]; !ok { + have[t] = struct{}{} + added = append(added, t) + } + } + break + } + if _, ok := seen[cur]; ok { + break // defend against a tag cycle in a malformed repo + } + seen[cur] = struct{}{} + tag, terr := object.GetTag(st, cur) + if terr != nil { + break // non-tag object not in the pack: nothing to add + } + chain = append(chain, cur) + cur = tag.Target + } + return nil + }) + if err != nil { + return objs, err + } + return added, nil +} + +// shallowBoundaryStorer reports an additional set of shallow commits (the +// per-request boundary) on top of any the repository already has. revlist's +// object walk stops at shallow commits while still collecting their full trees, +// so wrapping the storer bounds a shallow fetch's packfile to the requested +// depth — the boundary commits ship complete, their ancestors are omitted — +// without the blob loss a plain have-exclusion would cause. +type shallowBoundaryStorer struct { + storage.Storer + boundary []plumbing.Hash +} + +func (s *shallowBoundaryStorer) Shallow() ([]plumbing.Hash, error) { + base, err := s.Storer.Shallow() + if err != nil { + return nil, err + } + if len(s.boundary) == 0 { + return base, nil + } + return append(append([]plumbing.Hash(nil), base...), s.boundary...), nil +} + +// wantsReachableFromHaves reports whether every want is reachable from the set +// of common haves — upstream's ok_to_give_up (upload-pack.c). A want is anchored +// when a common have is the want itself or one of its ancestors, i.e. the want +// can reach a have by walking parents. Tags are peeled to commits first, as the +// ancestry walk operates on commits. Returns false (keep negotiating) if any +// want cannot be resolved to a commit or is not yet anchored. +func wantsReachableFromHaves(st storage.Storer, wants, commonHaves []plumbing.Hash) bool { + haveSet := make(map[plumbing.Hash]struct{}, len(commonHaves)) + haveCommits := make([]*object.Commit, 0, len(commonHaves)) + for _, h := range commonHaves { + haveSet[h] = struct{}{} + if c, ok := peelToCommit(st, h); ok { + haveCommits = append(haveCommits, c) + } + } + + for _, wHash := range wants { + wc, ok := peelToCommit(st, wHash) + if !ok { + return false + } + if _, ok := haveSet[wc.Hash]; ok { + continue + } + anchored := false + for _, hc := range haveCommits { + if hc.Hash == wc.Hash { + anchored = true + break + } + if isAnc, err := hc.IsAncestor(wc); err == nil && isAnc { + anchored = true + break + } + } + if !anchored { + return false + } + } + return true +} + +// peelToCommit resolves h to a commit, following annotated tags. It returns +// false when h is missing or does not peel to a commit. +func peelToCommit(st storage.Storer, h plumbing.Hash) (*object.Commit, bool) { + obj, err := st.EncodedObject(plumbing.AnyObject, h) + if err != nil { + return nil, false + } + switch obj.Type() { + case plumbing.CommitObject: + c, err := object.GetCommit(st, h) + if err != nil { + return nil, false + } + return c, true + case plumbing.TagObject: + tag, err := object.GetTag(st, h) + if err != nil { + return nil, false + } + return peelToCommit(st, tag.Target) + default: + return nil, false + } +} diff --git a/vendor/github.com/go-git/go-git/v6/plumbing/transport/version.go b/vendor/github.com/go-git/go-git/v6/plumbing/transport/version.go index a84e20b9d..d7e879a93 100644 --- a/vendor/github.com/go-git/go-git/v6/plumbing/transport/version.go +++ b/vendor/github.com/go-git/go-git/v6/plumbing/transport/version.go @@ -11,6 +11,9 @@ import ( // DiscoverVersion reads the first pktline from the reader to determine the // protocol version. This is used by the client to determine the protocol // version of the server. +// +// Note that the discovered version is not consumed from the reader, so the +// caller can read it again after discovering the version. func DiscoverVersion(r ioutil.ReadPeeker) (protocol.Version, error) { ver := protocol.V0 _, pktb, err := pktline.PeekLine(r) @@ -20,10 +23,6 @@ func DiscoverVersion(r ioutil.ReadPeeker) (protocol.Version, error) { pkt := strings.TrimSpace(string(pktb)) if strings.HasPrefix(pkt, "version ") { - // Consume the version packet - if _, _, err := pktline.ReadLine(r); err != nil { - return ver, err - } if v, _ := protocol.Parse(pkt[8:]); v > ver { ver = protocol.Version(v) } diff --git a/vendor/github.com/go-git/go-git/v6/remote.go b/vendor/github.com/go-git/go-git/v6/remote.go index 8d7827816..0544266ee 100644 --- a/vendor/github.com/go-git/go-git/v6/remote.go +++ b/vendor/github.com/go-git/go-git/v6/remote.go @@ -15,6 +15,7 @@ import ( "github.com/go-git/go-git/v6/plumbing/client" "github.com/go-git/go-git/v6/plumbing/format/packfile" "github.com/go-git/go-git/v6/plumbing/object" + "github.com/go-git/go-git/v6/plumbing/protocol" "github.com/go-git/go-git/v6/plumbing/protocol/capability" "github.com/go-git/go-git/v6/plumbing/protocol/packp" "github.com/go-git/go-git/v6/plumbing/revlist" @@ -119,12 +120,12 @@ func (r *Remote) PushContext(ctx context.Context, o *PushOptions) (err error) { } defer ioutil.CheckClose(sess, &err) - rRefs, err := sess.GetRemoteRefs(ctx) + rRefs, err := sess.GetRemoteRefs(ctx, nil) if err != nil { return err } - remoteRefs := referenceStorageFromRefs(rRefs, true) + remoteRefs := referenceStorageFromRefs(rRefs.References, true) if err := r.checkRequireRemoteRefs(o.RequireRemoteRefs, remoteRefs); err != nil { return err } @@ -335,6 +336,71 @@ func (r *Remote) FetchContext(ctx context.Context, o *FetchOptions) error { return err } +// fetchRefPrefixes derives the ls-refs "ref-prefix" hints for a fetch from its +// refspecs and tag mode, mirroring canonical git (builtin/fetch.c, +// builtin/clone.c). HEAD is always included so default-branch resolution keeps +// working (e.g. on clone), and refs/tags/ is added when tags are being +// followed. +// +// ref-prefix is purely an optimization, so the returned prefixes must cover +// every ref the fetch could match. When a refspec cannot be safely turned into +// a prefix (an exact-OID source) or there are no refspecs, it returns nil to +// request the full advertisement rather than risk under-scoping it. +func fetchRefPrefixes(specs []config.RefSpec, tags plumbing.TagMode) []string { + if len(specs) == 0 { + return nil + } + + prefixes := make([]string, 0, len(specs)+2) + for _, rs := range specs { + if rs.IsExactSHA1() { + return nil + } + src := rs.Src() + if src == "" { + return nil + } + if prefix, _, found := strings.Cut(src, "*"); found { + // A wildcard: the prefix is the literal part before '*'. A leading + // wildcard trims to an empty prefix, which would emit an invalid + // "ref-prefix " argument, so request the full advertisement instead + // of under-scoping it. + if prefix == "" { + return nil + } + prefixes = append(prefixes, prefix) + continue + } + + // A HEAD source (single-branch clone, "+HEAD:...") resolves through a + // symref to a branch under refs/heads/. HEAD itself is appended + // unconditionally below, so advertise that namespace too; otherwise a + // v2 server that strictly honours ref-prefix omits the resolved branch + // and it cannot be fetched. Matches git clone. + if src == "HEAD" { + prefixes = append(prefixes, "refs/heads/") + continue + } + + // A non-wildcard source may be a short name (e.g. "master"). A v2 server + // prefix-matches ref-prefix against the full refname, so "master" alone + // would never match refs/heads/master. Expand it to every candidate + // full name, mirroring canonical git's refspec_ref_prefixes -> + // expand_ref_prefix (refspec.c, refs.c). + for _, rule := range plumbing.RefRevParseRules { + prefixes = append(prefixes, fmt.Sprintf(rule, src)) + } + } + + // Order matches canonical git: refspec prefixes, then refs/tags/, then + // HEAD last (builtin/clone.c, builtin/fetch.c). + if tags == plumbing.AllTags || tags == plumbing.TagFollowing { + prefixes = append(prefixes, "refs/tags/") + } + prefixes = append(prefixes, "HEAD") + return prefixes +} + // Fetch fetches references along with the objects necessary to complete their // histories. // @@ -378,6 +444,7 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen } req.Command = transport.UploadPackService + req.Protocol = r.transportProtocol() sess, err := cl.Handshake(ctx, req) if err != nil { return nil, err @@ -388,12 +455,14 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen return nil, err } - rRefs, err := sess.GetRemoteRefs(ctx) + rRefs, err := sess.GetRemoteRefs(ctx, &transport.GetRemoteRefsOptions{ + RefPrefixes: fetchRefPrefixes(o.RefSpecs, o.Tags), + }) if err != nil { return nil, err } - remoteRefs := referenceStorageFromRefs(rRefs, true) + remoteRefs := referenceStorageFromRefs(rRefs.References, true) localRefs, err := reference.References(r.s) if err != nil { return nil, err @@ -547,6 +616,21 @@ func newClient(rawURL string, opts []client.Option) (*client.Client, *transport. return cl, &transport.Request{URL: u}, nil } +// transportProtocol returns the wire protocol version configured for this +// remote's repository (the protocol.version setting), defaulting to +// config.DefaultProtocolVersion. It is used for ref discovery and fetch; +// push always uses v0/v1, since protocol v2 has no push. +func (r *Remote) transportProtocol() protocol.Version { + if r.s == nil { + return config.DefaultProtocolVersion + } + cfg, err := r.s.Config() + if err != nil || cfg == nil { + return config.DefaultProtocolVersion + } + return cfg.Protocol.Version +} + func (r *Remote) pruneRemotes(specs []config.RefSpec, localRefs []*plumbing.Reference, remoteRefs storer.ReferenceStorer) (bool, error) { var updatedPrune bool for _, spec := range specs { @@ -704,6 +788,9 @@ func (r *Remote) addObject(rs config.RefSpec, return nil } if !rs.IsForceUpdate() { + if err := checkTagUpdate(cmd); err != nil { + return err + } if err := checkFastForwardUpdate(r.s, remoteRefs, cmd); err != nil { return err } @@ -752,6 +839,9 @@ func (r *Remote) addReferenceIfRefSpecMatches(rs config.RefSpec, return err } } else if !rs.IsForceUpdate() { + if err := checkTagUpdate(cmd); err != nil { + return err + } if err := checkFastForwardUpdate(r.s, remoteRefs, cmd); err != nil { return err } @@ -786,6 +876,14 @@ func (r *Remote) checkForceWithLease(localRef *plumbing.Reference, cmd *packp.Co return nil } +func checkTagUpdate(cmd *packp.Command) error { + if cmd.Name.IsTag() && cmd.Old != plumbing.ZeroHash { + return fmt.Errorf("tag already exists: %s", cmd.Name.String()) + } + + return nil +} + func getRemoteRefsFromStorer(remoteRefStorer storer.ReferenceStorer) ( map[plumbing.Hash]bool, error, ) { @@ -895,7 +993,7 @@ func getHaves( return result, nil } -const refspecAllTags = "+refs/tags/*:refs/tags/*" +const refspecAllTags = "refs/tags/*:refs/tags/*" func calculateRefs( spec []config.RefSpec, @@ -1024,7 +1122,7 @@ func objectExists(s storer.EncodedObjectStorer, h plumbing.Hash) (bool, error) { } func checkFastForwardUpdate(s storer.EncodedObjectStorer, remoteRefs storer.ReferenceStorer, cmd *packp.Command) error { - if cmd.Old == plumbing.ZeroHash { + if cmd.Old.IsZero() { _, err := remoteRefs.Reference(cmd.Name) if errors.Is(err, plumbing.ErrReferenceNotFound) { return nil @@ -1179,6 +1277,11 @@ func (r *Remote) updateLocalReferenceStorage( old, _ := storer.ResolveReference(r.s, localName) newRef := plumbing.NewHashReference(localName, ref.Hash()) + if old != nil && localName.IsTag() && old.Hash() != newRef.Hash() && !force && !spec.IsForceUpdate() { + forceNeeded = true + continue + } + // If the ref exists locally as a non-tag and force is not // specified, only update if the new ref is an ancestor of the old if old != nil && !old.Name().IsTag() && !force && !spec.IsForceUpdate() { @@ -1212,7 +1315,7 @@ func (r *Remote) updateLocalReferenceStorage( if isWildcard { tags = remoteRefs } - tagUpdated, err := r.buildFetchedTags(tags) + tagUpdated, tagForceNeeded, err := r.buildFetchedTags(tags, tagMode == plumbing.AllTags, force) if err != nil { return updated, err } @@ -1220,6 +1323,9 @@ func (r *Remote) updateLocalReferenceStorage( if tagUpdated { updated = true } + if tagForceNeeded { + forceNeeded = true + } if forceNeeded { err = ErrForceNeeded @@ -1228,7 +1334,7 @@ func (r *Remote) updateLocalReferenceStorage( return updated, err } -func (r *Remote) buildFetchedTags(refs memory.ReferenceStorage) (updated bool, err error) { +func (r *Remote) buildFetchedTags(refs memory.ReferenceStorage, allTags, force bool) (updated, forceNeeded bool, err error) { for _, ref := range refs { if !ref.Name().IsTag() { continue @@ -1240,12 +1346,28 @@ func (r *Remote) buildFetchedTags(refs memory.ReferenceStorage) (updated bool, e } if err != nil { - return false, err + return updated, forceNeeded, err + } + + old, err := r.s.Reference(ref.Name()) + if err != nil && !errors.Is(err, plumbing.ErrReferenceNotFound) { + return updated, forceNeeded, err + } + if err == nil && old.Hash() != ref.Hash() { + if !allTags { + // An auto-followed tag only creates one that is missing locally; it + // never moves a tag that already points elsewhere. + continue + } + if !force { + forceNeeded = true + continue + } } refUpdated, err := updateReferenceStorerIfNeeded(r.s, ref) if err != nil { - return updated, err + return updated, forceNeeded, err } if refUpdated { @@ -1253,7 +1375,7 @@ func (r *Remote) buildFetchedTags(refs memory.ReferenceStorage) (updated bool, e } } - return updated, err + return updated, forceNeeded, err } // ListContext lists the references on the remote repository. @@ -1290,6 +1412,7 @@ func (r *Remote) list(ctx context.Context, o *ListOptions) (rfs []*plumbing.Refe } req.Command = transport.UploadPackService + req.Protocol = r.transportProtocol() sess, err := cl.Handshake(ctx, req) if err != nil { return nil, err @@ -1297,13 +1420,13 @@ func (r *Remote) list(ctx context.Context, o *ListOptions) (rfs []*plumbing.Refe defer ioutil.CheckClose(sess, &err) - allRefs, err := sess.GetRemoteRefs(ctx) + allRefs, err := sess.GetRemoteRefs(ctx, nil) if err != nil { return nil, err } var resultRefs []*plumbing.Reference - for _, ref := range allRefs { + for _, ref := range allRefs.References { isPeeled := strings.HasSuffix(ref.Name().String(), peeledSuffix) switch o.PeelingOption { case IgnorePeeled: @@ -1325,7 +1448,7 @@ func (r *Remote) list(ctx context.Context, o *ListOptions) (rfs []*plumbing.Refe func objectsToPush(commands []*packp.Command) []plumbing.Hash { objects := make([]plumbing.Hash, 0, len(commands)) for _, cmd := range commands { - if cmd.New == plumbing.ZeroHash { + if cmd.New.IsZero() { continue } objects = append(objects, cmd.New) diff --git a/vendor/github.com/go-git/go-git/v6/repository.go b/vendor/github.com/go-git/go-git/v6/repository.go index 8d366fe46..9976ae11d 100644 --- a/vendor/github.com/go-git/go-git/v6/repository.go +++ b/vendor/github.com/go-git/go-git/v6/repository.go @@ -454,7 +454,9 @@ func PlainOpenWithOptions(path string, o *PlainOpenOptions) (*Repository, error) } repositoryFs = dotgit.NewRepositoryFilesystem(dot, dotGitCommon) - s := filesystem.NewStorage(repositoryFs, cache.NewObjectLRUDefault()) + s := filesystem.NewStorageWithOptions(repositoryFs, cache.NewObjectLRUDefault(), filesystem.Options{ + AlternatesFS: o.AlternatesFS, + }) r, err := Open(s, wt) if err != nil { @@ -999,7 +1001,8 @@ func (r *Repository) buildTagSignature(tag *object.Tag, signer Signer) (string, return "", err } - b, err := signer.Sign(rdr) + // TODO: thread a caller-supplied context once CreateTag accepts one. + b, err := signer.Sign(context.TODO(), rdr) if err != nil { return "", err } @@ -1575,7 +1578,7 @@ func (r *Repository) Log(o *LogOptions) (object.CommitIter, error) { func (r *Repository) log(from plumbing.Hash, commitIterFunc func(*object.Commit) object.CommitIter) (object.CommitIter, error) { h := from - if from == plumbing.ZeroHash { + if from.IsZero() { head, err := r.Head() if err != nil { return nil, err diff --git a/vendor/github.com/go-git/go-git/v6/signer.go b/vendor/github.com/go-git/go-git/v6/signer.go index b4f2b514a..5f0fe0e6a 100644 --- a/vendor/github.com/go-git/go-git/v6/signer.go +++ b/vendor/github.com/go-git/go-git/v6/signer.go @@ -1,6 +1,7 @@ package git import ( + "context" "io" "github.com/go-git/go-git/v6/plumbing" @@ -13,10 +14,12 @@ type signableObject interface { // Signer is an interface for signing git objects. // message is a reader containing the encoded object to be signed. +// ctx cancels signers that perform external or remote work; purely local +// signers may ignore it. // Implementors should return the encoded signature and an error if any. // See https://git-scm.com/docs/gitformat-signature for more information. type Signer interface { - Sign(message io.Reader) ([]byte, error) + Sign(ctx context.Context, message io.Reader) ([]byte, error) } func signObject(signer Signer, obj signableObject) ([]byte, error) { @@ -29,5 +32,6 @@ func signObject(signer Signer, obj signableObject) ([]byte, error) { return nil, err } - return signer.Sign(r) + // TODO: thread a caller-supplied context once Worktree.Commit accepts one. + return signer.Sign(context.TODO(), r) } diff --git a/vendor/github.com/go-git/go-git/v6/storage/filesystem/dotgit/dotgit.go b/vendor/github.com/go-git/go-git/v6/storage/filesystem/dotgit/dotgit.go index 367042513..e9469c643 100644 --- a/vendor/github.com/go-git/go-git/v6/storage/filesystem/dotgit/dotgit.go +++ b/vendor/github.com/go-git/go-git/v6/storage/filesystem/dotgit/dotgit.go @@ -10,10 +10,12 @@ import ( "fmt" "hash" "io" + "maps" "os" "path" "path/filepath" "runtime" + "slices" "sort" "strings" "sync" @@ -21,13 +23,17 @@ import ( "github.com/go-git/go-billy/v6" + "github.com/go-git/go-git/v6/internal/packhandle" + "github.com/go-git/go-git/v6/internal/pathutil" "github.com/go-git/go-git/v6/plumbing" formatcfg "github.com/go-git/go-git/v6/plumbing/format/config" "github.com/go-git/go-git/v6/plumbing/format/idxfile" + "github.com/go-git/go-git/v6/plumbing/format/packfile" "github.com/go-git/go-git/v6/plumbing/format/revfile" plumbhash "github.com/go-git/go-git/v6/plumbing/hash" "github.com/go-git/go-git/v6/storage" "github.com/go-git/go-git/v6/utils/ioutil" + "github.com/go-git/go-git/v6/x/fdpool" ) const ( @@ -83,16 +89,57 @@ var ( // resolve outside the modules/ subtree, mirroring canonical Git's // "ignoring suspicious submodule name" defence. ErrModuleNameEscape = errors.New("submodule name escapes modules/ directory") + // ErrReferenceNameEscape is returned when a reference name would + // resolve outside its reference sub-tree once turned into a path + // under the .git directory (e.g. a name with a ".." component). + ErrReferenceNameEscape = errors.New("reference name escapes the reference storage") ) +func isPathSep(r rune) bool { return r == '/' || r == '\\' } + +// validReferenceName rejects reference names that cannot be safely turned into +// a path under the .git directory. A loose reference (and its reflog) is stored +// verbatim at ".git/", so a crafted name — for instance one advertised by +// a malicious remote — could climb out of its reference sub-tree and read, +// overwrite, or delete unrelated metadata such as .git/config. +// +// The storage-safety gate is plumbing.ReferenceName.IsSafe, mirroring Git's +// refname_is_safe: a name must be under refs/ without escaping it, or be a +// [A-Z_] pseudo-ref. This alone rejects absolute, drive-prefixed, escaping and +// single-level metadata names. On top of it, this adds filesystem-specific +// hardening that IsSafe's literal check does not cover: control characters, and +// components a case-insensitive/NTFS/HFS+ filesystem would fold back to "." or +// ".." (trailing dots/spaces, Alternate Data Streams, ignorable Unicode code +// points). The per-component check is delegated to pathutil.IsHFSDot and +// pathutil.IsNTFSDot with "." as the needle, exactly as validSubmoduleName +// does, and runs regardless of host OS because a name can be authored on one OS +// and reach this layer on another. +func validReferenceName(name plumbing.ReferenceName) error { + if !name.IsSafe() { + return fmt.Errorf("%w: %q is not under refs/ nor a valid pseudo-ref", ErrReferenceNameEscape, string(name)) + } + + s := string(name) + for i := 0; i < len(s); i++ { + if s[i] < 0x20 || s[i] == 0x7f { + return fmt.Errorf("%w: %q", ErrReferenceNameEscape, s) + } + } + for _, part := range strings.FieldsFunc(s, isPathSep) { + // IsNTFSDot/IsHFSDot with a "." needle match ".." and its disguises + // but not a bare ".", so reject that component explicitly too. + if part == "." || pathutil.IsHFSDot(part, ".") || pathutil.IsNTFSDot(part, ".", "") { + return fmt.Errorf("%w: %q", ErrReferenceNameEscape, s) + } + } + return nil +} + // Options holds configuration for the storage. type Options struct { // ExclusiveAccess means that the filesystem is not modified externally // while the repo is open. ExclusiveAccess bool - // KeepDescriptors makes the file descriptors to be reused but they will - // need to be manually closed calling Close(). - KeepDescriptors bool // AlternatesFS provides the billy filesystem to be used for Git Alternates. // If none is provided, it falls back to using the underlying instance used for // DotGit. @@ -107,6 +154,17 @@ type Options struct { // WriteReverseIndex controls whether .rev files are written when // creating new packfiles. Defaults to true. WriteReverseIndex bool + // Pool, when non-nil, governs the LRU eviction of file + // descriptors held by [packhandle.PackHandle] instances + // constructed by this DotGit. Nil disables pooling for the + // pack-FD lifecycle (grace-period close on quiescence). + // + // The Pool field's API stability tracks [fdpool.Pool]'s, not + // this package's. Per the x/ package policy, the fdpool API + // may change without following semantic versioning; consumers + // reading this field should treat it as experimental on the + // same timeline. + Pool *fdpool.Pool } // The DotGit type represents a local git repository on disk. This @@ -126,7 +184,10 @@ type DotGit struct { packList []plumbing.Hash packMap map[plumbing.Hash]struct{} - files map[plumbing.Hash]billy.File + // packHandles caches one [*packhandle.PackHandle] per pack hash, + // guarded by packHandlesMu. Lazy-initialised on first use. + packHandlesMu sync.Mutex + packHandles map[plumbing.Hash]*packhandle.PackHandle } // New returns a DotGit value ready to be used. The path argument must @@ -178,24 +239,21 @@ func (d *DotGit) Initialize() error { // Close closes all opened files. func (d *DotGit) Close() error { - var firstError error - if d.files != nil { - for _, f := range d.files { - err := f.Close() - if err != nil && firstError == nil { - firstError = err - continue - } + d.packHandlesMu.Lock() + handles := d.packHandles + d.packHandles = nil + d.packHandlesMu.Unlock() + + var phErrs []error + for _, h := range handles { + if err := h.Close(); err != nil { + phErrs = append(phErrs, err) } - - d.files = nil } - if firstError != nil { - return firstError - } + d.packMap = nil - return nil + return errors.Join(phErrs...) } // ConfigWriter returns a file pointer for write to the config file @@ -255,6 +313,9 @@ func (d *DotGit) Shallow() (billy.File, error) { // ReflogReader returns a file pointer for reading the reflog for the given reference. // Returns nil, nil if the reflog file does not exist. func (d *DotGit) ReflogReader(name plumbing.ReferenceName) (billy.File, error) { + if err := validReferenceName(name); err != nil { + return nil, err + } p := d.fs.Join(logsPath, string(name)) f, err := d.fs.Open(p) if err != nil { @@ -269,6 +330,9 @@ func (d *DotGit) ReflogReader(name plumbing.ReferenceName) (billy.File, error) { // ReflogWriter returns a file pointer for appending to the reflog for the given reference. // It creates the file and any necessary parent directories if they don't exist. func (d *DotGit) ReflogWriter(name plumbing.ReferenceName) (billy.File, error) { + if err := validReferenceName(name); err != nil { + return nil, err + } p := d.fs.Join(logsPath, string(name)) if err := d.fs.MkdirAll(filepath.Dir(p), os.ModePerm); err != nil { return nil, err @@ -278,6 +342,9 @@ func (d *DotGit) ReflogWriter(name plumbing.ReferenceName) (billy.File, error) { // DeleteReflog removes the reflog file for the given reference. func (d *DotGit) DeleteReflog(name plumbing.ReferenceName) error { + if err := validReferenceName(name); err != nil { + return err + } p := d.fs.Join(logsPath, string(name)) err := d.fs.Remove(p) if os.IsNotExist(err) { @@ -289,8 +356,15 @@ func (d *DotGit) DeleteReflog(name plumbing.ReferenceName) error { // NewObjectPack return a writer for a new packfile, it saves the packfile to // disk and also generates and save the index for the given packfile. func (d *DotGit) NewObjectPack() (*PackWriter, error) { - d.cleanPackList() - return newPackWrite(d.fs, d.options.ObjectFormat, d.options.WriteReverseIndex) + cleanErr := d.cleanPackList() + pw, err := newPackWrite(d.fs, d.options.ObjectFormat, d.options.WriteReverseIndex) + if err != nil { + return nil, errors.Join(cleanErr, err) + } + if cleanErr != nil { + return nil, cleanErr + } + return pw, nil } // ObjectPacks returns the list of availables packfiles @@ -341,17 +415,6 @@ func (d *DotGit) objectPackPath(hash plumbing.Hash, extension string) string { } func (d *DotGit) objectPackOpen(hash plumbing.Hash, extension string) (billy.File, error) { - if d.options.KeepDescriptors && extension == "pack" { - if d.files == nil { - d.files = make(map[plumbing.Hash]billy.File) - } - - f, ok := d.files[hash] - if ok { - return f, nil - } - } - err := d.hasPack(hash) if err != nil { return nil, err @@ -367,10 +430,6 @@ func (d *DotGit) objectPackOpen(hash plumbing.Hash, extension string) (billy.Fil return nil, err } - if d.options.KeepDescriptors && extension == "pack" { - d.files[hash] = pack - } - return pack, nil } @@ -462,26 +521,198 @@ func newBytesReadAtCloser(data []byte) *bytesReadAtCloser { func (b *bytesReadAtCloser) Close() error { return nil } +// packHandle returns the cached [packhandle.PackHandle] for the +// given pack hash, constructing one on first access. Rev is +// served through [DotGit.OpenPackRev] so the in-memory fallback +// applies when the .rev file is absent or [Options.ReadReverseIndex] +// is false. Returns [ErrPackfileNotFound] when the .pack file +// cannot be located. +func (d *DotGit) packHandle(hash plumbing.Hash) (*packhandle.PackHandle, error) { + d.packHandlesMu.Lock() + defer d.packHandlesMu.Unlock() + + if h, ok := d.packHandles[hash]; ok { + return h, nil + } + + if err := d.hasPack(hash); err != nil { + return nil, err + } + + packPath := d.objectPackPath(hash, "pack") + idxPath := d.objectPackPath(hash, "idx") + + sources := packhandle.Sources{ + Pack: packhandle.Source{ + Open: func() (packhandle.ReadAtCloser, error) { + return d.ObjectPack(hash) + }, + Size: func() (int64, error) { + fi, err := d.fs.Stat(packPath) + if err != nil { + return 0, err + } + return fi.Size(), nil + }, + }, + Idx: packhandle.Source{ + Open: func() (packhandle.ReadAtCloser, error) { + return d.ObjectPackIdx(hash) + }, + Size: func() (int64, error) { + fi, err := d.fs.Stat(idxPath) + if err != nil { + return 0, err + } + return fi.Size(), nil + }, + }, + Rev: packhandle.Source{ + Open: func() (packhandle.ReadAtCloser, error) { + return d.OpenPackRev(hash) + }, + // Rev may be served from an in-memory buffer with no + // stat-able size. [idxfile.LazyIndex] does not consult + // Rev.Size, so a zero stub is correct here. + Size: func() (int64, error) { return 0, nil }, + }, + } + + ph, err := packhandle.NewWithPool(sources, hash, d.options.Pool) + if err != nil { + return nil, err + } + + if d.packHandles == nil { + d.packHandles = make(map[plumbing.Hash]*packhandle.PackHandle) + } + d.packHandles[hash] = ph + return ph, nil +} + +// walkPackHandles snapshots the [packhandle.PackHandle] catalog +// under packHandlesMu and invokes fn on each entry with the lock +// released. The snapshot shortens the critical section so +// concurrent [DotGit.packHandle] lookups do not block on slow +// per-handle work such as file-close syscalls. +// +// The catalog is left intact; fn observes the same PackHandle +// pointers that subsequent lookups return. Callers that want to +// drain the catalog must do so separately — Close and +// cleanPackList keep their inline snapshot-with-clear because +// they need the drain to be atomic with the snapshot. +// +// Errors returned by fn are joined; the walk continues past a +// failing entry so one bad pack does not strand FDs in others. +func (d *DotGit) walkPackHandles(fn func(*packhandle.PackHandle) error) error { + d.packHandlesMu.Lock() + handles := slices.Collect(maps.Values(d.packHandles)) + d.packHandlesMu.Unlock() + + var errs []error + for _, h := range handles { + if err := fn(h); err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + +// CloseIdleDescriptors releases the FDs held by every cached +// [packhandle.PackHandle] without evicting them from the +// catalog. The catalog and each PackHandle's cached state +// (`PackMeta`, `LazyIndex`) survive the call; subsequent +// operations reopen FDs on demand. +// +// Idempotent and safe to call concurrently with other DotGit +// operations. After [DotGit.Close] the catalog is empty, so the +// call is a no-op. +func (d *DotGit) CloseIdleDescriptors() error { + return d.walkPackHandles(func(ph *packhandle.PackHandle) error { + return ph.CloseIdleDescriptors() + }) +} + +// PackHandle returns the cached [packfile.PackHandle] for the +// given pack hash, constructing one on first access. The returned +// interface does not expose Close; dotgit owns the handle's +// lifetime and tears it down via [DotGit.Close] or +// [DotGit.cleanPackList]. +func (d *DotGit) PackHandle(hash plumbing.Hash) (packfile.PackHandle, error) { + ph, err := d.packHandle(hash) + if err != nil { + return nil, err + } + return packHandleAdapter{ph}, nil +} + +// packHandleAdapter satisfies [packfile.PackHandle] by delegating +// to the dotgit-owned [packhandle.PackHandle]. Unexported so +// callers cannot type-assert back to the concrete type and reach +// [packhandle.PackHandle.Close]. +type packHandleAdapter struct{ ph *packhandle.PackHandle } + +func (a packHandleAdapter) OpenPackReader() (io.ReadSeekCloser, error) { + return a.ph.OpenPackReader() +} + +func (a packHandleAdapter) OpenRandomReader() (packfile.RandomReader, error) { + return a.ph.OpenRandomReader() +} + +func (a packHandleAdapter) PackHash() (plumbing.Hash, error) { + m, err := a.ph.Meta() + if err != nil { + return plumbing.ZeroHash, err + } + return m.ID, nil +} + // DeleteOldObjectPackAndIndex removes a pack and its index if older than t. +// The .pack, .idx and .rev files are each attempted independently; any +// failures are joined into the returned error so a partial failure cannot +// leave orphaned siblings on disk. A missing .rev is not an error — the +// reverse index is optional and may have been generated only in memory. func (d *DotGit) DeleteOldObjectPackAndIndex(hash plumbing.Hash, t time.Time) error { - d.cleanPackList() + var errs []error + if err := d.cleanPackList(); err != nil { + errs = append(errs, err) + } - path := d.objectPackPath(hash, `pack`) + packPath := d.objectPackPath(hash, `pack`) if !t.IsZero() { - fi, err := d.fs.Stat(path) + fi, err := d.fs.Stat(packPath) if err != nil { - return err + errs = append(errs, err) + return errors.Join(errs...) } // too new, skip deletion. if !fi.ModTime().Before(t) { - return nil + return errors.Join(errs...) } } - err := d.fs.Remove(path) - if err != nil { - return err + + for _, ext := range []string{`pack`, `idx`, `rev`} { + if err := d.fs.Remove(d.objectPackPath(hash, ext)); err != nil { + if ext == `rev` && os.IsNotExist(err) { + continue + } + errs = append(errs, err) + } } - return d.fs.Remove(d.objectPackPath(hash, `idx`)) + + d.packHandlesMu.Lock() + ph, ok := d.packHandles[hash] + if ok { + delete(d.packHandles, hash) + } + d.packHandlesMu.Unlock() + if ok { + if err := ph.Close(); err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) } // NewObject return a writer for a new object file. @@ -660,9 +891,32 @@ func (d *DotGit) hasObject(h plumbing.Hash) error { return nil } -func (d *DotGit) cleanPackList() { +// cleanPackList resets the pack catalog and drops every cached +// [packhandle.PackHandle]. A pack-set mutation may have +// invalidated the handles' underlying files, so they cannot +// safely be reused. Idle readers finish normally; their cursor +// Close becomes a no-op release. +// +// PackHandle.Close owns the underlying sharedfile and LazyIndex +// FDs, so a close failure here means an FD has not been released. +// The errors are joined and returned so callers can surface them +// rather than silently masking I/O failures during cleanup. +func (d *DotGit) cleanPackList() error { d.packMap = nil d.packList = nil + + d.packHandlesMu.Lock() + handles := d.packHandles + d.packHandles = nil + d.packHandlesMu.Unlock() + + var errs []error + for _, h := range handles { + if err := h.Close(); err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) } func (d *DotGit) genPackList() error { @@ -845,6 +1099,10 @@ func (d *DotGit) checkReferenceAndTruncate(f billy.File, old *plumbing.Reference // SetRef stores a reference, optionally checking that old matches the current value. func (d *DotGit) SetRef(r, old *plumbing.Reference) error { + if err := validReferenceName(r.Name()); err != nil { + return err + } + var content string switch r.Type() { case plumbing.SymbolicReference: @@ -880,6 +1138,10 @@ func (d *DotGit) Refs() ([]*plumbing.Reference, error) { // Ref returns the reference for a given reference name. func (d *DotGit) Ref(name plumbing.ReferenceName) (*plumbing.Reference, error) { + if err := validReferenceName(name); err != nil { + return nil, err + } + ref, err := d.readReferenceFile(".", name.String()) if err == nil { return ref, nil @@ -943,6 +1205,10 @@ func (d *DotGit) packedRef(name plumbing.ReferenceName) (*plumbing.Reference, er // RemoveRef removes a reference by name. func (d *DotGit) RemoveRef(name plumbing.ReferenceName) error { + if err := validReferenceName(name); err != nil { + return err + } + path := d.fs.Join(".", name.String()) _, err := d.fs.Stat(path) if err == nil { @@ -1377,7 +1643,17 @@ func (d *DotGit) Alternates() ([]*DotGit, error) { if err != nil { return nil, fmt.Errorf("cannot chroot %q: %w", path, err) } - alternates = append(alternates, New(afs)) + // Inherit the parent DotGit's ObjectFormat so the alternate + // reads and writes hashes at the same width as the + // repository it serves. Inherit the FD pool too so the + // storage-wide budget covers alternate packs and idxes. + // Other Options keep their defaults from New. + alternates = append(alternates, NewWithOptions(afs, Options{ + ObjectFormat: d.options.ObjectFormat, + ReadReverseIndex: true, + WriteReverseIndex: true, + Pool: d.options.Pool, + })) } if err = scanner.Err(); err != nil { diff --git a/vendor/github.com/go-git/go-git/v6/storage/filesystem/dotgit/openpack.go b/vendor/github.com/go-git/go-git/v6/storage/filesystem/dotgit/openpack.go new file mode 100644 index 000000000..db4ee3059 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/storage/filesystem/dotgit/openpack.go @@ -0,0 +1,102 @@ +package dotgit + +import ( + "errors" + "io" + "io/fs" + + "github.com/go-git/go-billy/v6" + + "github.com/go-git/go-git/v6/internal/packhandle" + "github.com/go-git/go-git/v6/plumbing" +) + +// errReadOnlyPack is returned by write-side methods of the handle +// returned by [DotGit.OpenPackForReading]. +var errReadOnlyPack = errors.New("dotgit: pack file is read-only") + +// OpenPackForReading returns a read-only handle on the .pack file +// for the given pack hash, backed by the internal FD pool. The +// returned [billy.File] grace-closes on idle and shares its +// descriptor across concurrent callers; closing the handle +// releases this caller's lease without affecting the pool. +// +// Write-side methods (Write, WriteAt, Lock, Unlock, Truncate) +// return an error. Stat resolves through the filesystem on each +// call. +func (d *DotGit) OpenPackForReading(hash plumbing.Hash) (billy.File, error) { + ph, err := d.packHandle(hash) + if err != nil { + return nil, err + } + + pr, err := ph.OpenPackReader() + if err != nil { + return nil, err + } + // [packhandle.PackReader] is declared as + // Reader+Seeker+Closer; ReadAt is not in the interface, so + // the assertion is a real runtime check rather than a static + // guarantee. The current concrete (cursorReader) satisfies + // it; the dynamic assert exists to catch a future PackReader + // implementation that does not. + ra, ok := pr.(io.ReaderAt) + if !ok { + _ = pr.Close() + return nil, errors.New("dotgit: pack reader does not support ReadAt") + } + + return &readOnlyPackFile{ + cursor: pr, + ra: ra, + name: d.objectPackPath(hash, "pack"), + fs: d.fs, + }, nil +} + +// readOnlyPackFile adapts a packhandle cursor into a [billy.File]. +// +// The embedded cursor pins one [sharedfile.SharedFile] reference +// for the lifetime of this handle: Read, ReadAt, and Seek route +// through the cursor, so the .pack FD stays live until Close. +// [readOnlyPackFile.Stat] is the exception — it goes through the +// filesystem on each call and may report a result that diverges +// from the cursor's view if the underlying pack was mutated or +// deleted out from under the handle. Callers that need a +// snapshot consistent with the cursor's reads should derive size +// from prior reads rather than re-Stat through this method. +// +// The handle holds a [billy.Filesystem] reference for Stat +// rather than a back-pointer to [DotGit]; this avoids a +// DotGit → cache → PackHandle → readOnlyPackFile reference cycle. +type readOnlyPackFile struct { + cursor packhandle.PackReader + ra io.ReaderAt + name string + fs billy.Filesystem +} + +func (f *readOnlyPackFile) Read(p []byte) (int, error) { return f.cursor.Read(p) } +func (f *readOnlyPackFile) Close() error { return f.cursor.Close() } +func (f *readOnlyPackFile) Name() string { return f.name } +func (f *readOnlyPackFile) Seek(o int64, w int) (int64, error) { + return f.cursor.Seek(o, w) +} + +func (f *readOnlyPackFile) ReadAt(p []byte, off int64) (int, error) { + return f.ra.ReadAt(p, off) +} + +func (f *readOnlyPackFile) Stat() (fs.FileInfo, error) { + return f.fs.Stat(f.name) +} + +func (f *readOnlyPackFile) Write(_ []byte) (int, error) { return 0, errReadOnlyPack } +func (f *readOnlyPackFile) WriteAt(_ []byte, _ int64) (int, error) { + return 0, errReadOnlyPack +} +func (f *readOnlyPackFile) Lock() error { return errReadOnlyPack } +func (f *readOnlyPackFile) Unlock() error { return errReadOnlyPack } +func (f *readOnlyPackFile) Truncate(int64) error { return errReadOnlyPack } + +var _ billy.File = (*readOnlyPackFile)(nil) diff --git a/vendor/github.com/go-git/go-git/v6/storage/filesystem/object.go b/vendor/github.com/go-git/go-git/v6/storage/filesystem/object.go index 5bd8c4f07..3d38b9d1b 100644 --- a/vendor/github.com/go-git/go-git/v6/storage/filesystem/object.go +++ b/vendor/github.com/go-git/go-git/v6/storage/filesystem/object.go @@ -9,9 +9,11 @@ import ( "os" "runtime" "sync" + "sync/atomic" "time" "golang.org/x/sync/errgroup" + "golang.org/x/sync/singleflight" "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/cache" @@ -24,6 +26,27 @@ import ( "github.com/go-git/go-git/v6/utils/ioutil" ) +// indexSFKey is the singleflight key used by [ObjectStorage.requireIndex] +// to coalesce concurrent first-readers around a single populateIndex +// scan. The literal value is opaque; only its uniqueness within +// indexSF matters. +const indexSFKey = "populate" + +// reindexSFKey is the singleflight key used by [ObjectStorage.Reindex] +// to collapse concurrent rescans. Kept distinct from indexSFKey so a +// cold first-load and an externally-driven rescan do not deduplicate +// against each other. +const reindexSFKey = "reindex" + +// packEntry pairs a pack hash with its idxfile.Index for inclusion +// in ObjectStorage.packs. The slice is always reassigned (never +// modified in place) so readers can hold a stable snapshot of the +// slice header after releasing muI.RLock. +type packEntry struct { + h plumbing.Hash + idx idxfile.Index +} + // ObjectStorage implements object storage backed by the filesystem. type ObjectStorage struct { options Options @@ -34,12 +57,28 @@ type ObjectStorage struct { dir *dotgit.DotGit index map[plumbing.Hash]idxfile.Index - - packList []plumbing.Hash - packListIdx int - packfiles map[plumbing.Hash]*packfile.Packfile - muI sync.RWMutex - muP sync.RWMutex + // packs mirrors s.index as a slice of (hash, idx) pairs, + // written in lockstep with s.index under muI.Lock and always + // reassigned (never modified in place). Readers can snapshot + // the slice header under RLock and release the lock before + // any per-pack I/O — the backing array and the embedded idx + // pointers stay valid for the snapshot's lifetime, so slow + // LazyIndex FindOffset calls do not block a concurrent + // Reindex on muI.Lock. + packs []packEntry + muI sync.RWMutex + + // indexSF coalesces concurrent first-readers so populateIndex + // runs once per cold-load even under thundering-herd contention. + indexSF singleflight.Group + + // lastHitPackIdx records the s.packs index that served the most + // recent successful findObjectInPackfile probe, encoded as the + // slice position plus one (0 = no hint). Storing an Int32 instead + // of a *plumbing.Hash eliminates the per-call escape-to-heap of + // the previous MRU pointer design. Stale entries cost one + // MayContain + FindOffset but never misroute lookups. + lastHitPackIdx atomic.Int32 oh *plumbing.ObjectHasher @@ -179,6 +218,11 @@ func findInAlternates[T any](s *ObjectStorage, fn func(*ObjectStorage) (T, error return foundVal, nil } +// requireIndex ensures s.index is populated, performing a cold-load +// on first access. Concurrent first-readers are coalesced via +// singleflight so populateIndex runs once per thundering herd; the +// winning goroutine publishes the local map under s.muI.Lock and +// joiners block in singleflight.Do until the in-flight call returns. func (s *ObjectStorage) requireIndex() error { s.muI.RLock() if s.index != nil { @@ -187,48 +231,141 @@ func (s *ObjectStorage) requireIndex() error { } s.muI.RUnlock() - s.muI.Lock() - defer s.muI.Unlock() - - s.index = make(map[plumbing.Hash]idxfile.Index) - packs, err := s.dir.ObjectPacks() - if err != nil { - return err - } + _, err, _ := s.indexSF.Do(indexSFKey, func() (any, error) { + // Re-check inside the singleflight window: a racing winner + // may have already published, in which case there is + // nothing for this caller to do. + s.muI.RLock() + if s.index != nil { + s.muI.RUnlock() + return nil, nil + } + s.muI.RUnlock() - for _, h := range packs { - if err := s.loadIdxFile(h); err != nil { - return err + local, entries, err := s.populateIndex() + if err != nil { + return nil, err } - } - return nil + s.muI.Lock() + if s.index == nil { + s.index = local + s.packs = entries + } else { + // A racing winner published while we were loading. + // Close any indexes we built so SharedFile refcounts + // (held by LazyIndex) do not leak. + for _, idx := range local { + _ = idx.Close() + } + } + s.muI.Unlock() + return nil, nil + }) + return err } -// Reindex indexes again all packfiles. Useful if git changed packfiles externally -func (s *ObjectStorage) Reindex() { - s.index = nil +// Reindex re-populates s.index from disk and atomically swaps the +// freshly-loaded map and packs slice in, so the next read is a +// hot-cache hit. Call when the on-disk pack inventory has changed +// externally. +// +// Concurrent Reindex calls coalesce through indexSF on reindexSFKey: +// one goroutine performs populateIndex and the swap, the rest block +// in singleflight.Do and observe the same outcome. Without this the +// callers would each scan the disk and race to publish, producing +// duplicate I/O. +// +// populateIndex runs before the swap so s.index is never nil during +// Reindex: a concurrent PackfileWriter.Notify can rely on s.index +// being a writable map under muI.Lock. Previously-cached LazyIndex +// entries are not closed here; in-flight readers that borrowed an +// entry before the swap keep using it, and the underlying +// SharedFile FDs close via the grace timer (no-pool mode) or fdpool +// LRU eviction (pool mode) once they fall out of use. This mirrors +// canonical Git's reprepare model where packfile_store_reprepare +// leaves existing packs in place and only the FD-level LRU evicts. +// +// The MRU hint indexes into the previous packs slice and is reset +// after the swap so a stale hint cannot misroute a probe against +// the new slice. +func (s *ObjectStorage) Reindex() error { + _, err, _ := s.indexSF.Do(reindexSFKey, func() (any, error) { + local, entries, err := s.populateIndex() + if err != nil { + return nil, err + } + + s.muI.Lock() + s.index = local + s.packs = entries + s.lastHitPackIdx.Store(0) + s.muI.Unlock() + + return nil, nil + }) + return err } -func (s *ObjectStorage) loadIdxFile(h plumbing.Hash) error { - if s.options.UseInMemoryIdx { - return s.loadMemoryIndex(h) +// populateIndex loads every pack's idx in parallel and returns the +// resulting map. The caller is responsible for publishing the map +// into s.index under s.muI.Lock; populateIndex itself takes no +// locks on s.muI, so callers must not hold it while invoking. +func (s *ObjectStorage) populateIndex() (map[plumbing.Hash]idxfile.Index, []packEntry, error) { + packHashes, err := s.dir.ObjectPacks() + if err != nil { + return nil, nil, err } - // Use LazyIndex on a best-effort basis. - if idx, err := s.loadLazyIndex(h); err == nil { - // If an index already exists, and implements io.Closer, try to close it. - if i, found := s.index[h]; found && i != nil { - if closer, ok := i.(io.Closer); ok { - _ = closer.Close() + // Per-pack writes target disjoint slice positions, so no mutex + // is needed across the errgroup workers. + entries := make([]packEntry, len(packHashes)) + g := new(errgroup.Group) + g.SetLimit(runtime.GOMAXPROCS(0)) + + for i, h := range packHashes { + g.Go(func() error { + idx, err := s.loadIdx(h) + if err != nil { + return err + } + entries[i] = packEntry{h: h, idx: idx} + return nil + }) + } + if err := g.Wait(); err != nil { + // Best-effort cleanup of indexes that did finish before + // the failing one: close any that hold descriptors so we + // do not leak SharedFile refcounts on the error path. + for _, e := range entries { + if e.idx == nil { + continue } + _ = e.idx.Close() } + return nil, nil, err + } - s.index[h] = idx - return nil + local := make(map[plumbing.Hash]idxfile.Index, len(entries)) + for _, e := range entries { + local[e.h] = e.idx } + return local, entries, nil +} - return s.loadMemoryIndex(h) +// loadIdx loads a single pack's idx and returns the constructed +// idxfile.Index. It does not mutate s.index; callers are +// responsible for installation. +func (s *ObjectStorage) loadIdx(h plumbing.Hash) (idxfile.Index, error) { + if !s.options.UseInMemoryIdx { + // Use LazyIndex on a best-effort basis; fall through to + // MemoryIndex if construction fails (e.g. a malformed + // .rev file), matching the legacy loadIdxFile path. + if idx, err := s.loadLazyIndex(h); err == nil { + return idx, nil + } + } + return s.loadMemoryIndexValue(h) } func (s *ObjectStorage) loadLazyIndex(h plumbing.Hash) (*idxfile.LazyIndex, error) { @@ -239,13 +376,16 @@ func (s *ObjectStorage) loadLazyIndex(h plumbing.Hash) (*idxfile.LazyIndex, erro return s.dir.OpenPackRev(h) } - return idxfile.NewLazyIndex(openIdx, openRev, h) + return idxfile.NewLazyIndexWithPool(openIdx, openRev, h, s.options.Pool) } -func (s *ObjectStorage) loadMemoryIndex(h plumbing.Hash) (err error) { +// loadMemoryIndexValue decodes a pack's idx into a MemoryIndex and +// returns it. Unlike the now-removed loadMemoryIndex helper it does +// not write into s.index; the caller installs the returned value. +func (s *ObjectStorage) loadMemoryIndexValue(h plumbing.Hash) (idx idxfile.Index, err error) { f, err := s.dir.ObjectPackIdx(h) if err != nil { - return err + return nil, err } defer ioutil.CheckClose(f, &err) @@ -260,16 +400,15 @@ func (s *ObjectStorage) loadMemoryIndex(h plumbing.Hash) (err error) { idxf := idxfile.NewMemoryIndex(h.Size()) d := idxfile.NewDecoder(f, hasher) if err = d.Decode(idxf); err != nil { - return err + return nil, err } if idxf.PackfileChecksum != h { - return fmt.Errorf("%w: packfile mismatch: target is %q not %q", + return nil, fmt.Errorf("%w: packfile mismatch: target is %q not %q", idxfile.ErrMalformedIdxFile, idxf.PackfileChecksum.String(), h.String()) } - s.index[h] = idxf - return err + return idxf, err } // RawObjectWriter returns a writer for a new loose object of the given type and size. @@ -305,9 +444,21 @@ func (s *ObjectStorage) PackfileWriter() (io.WriteCloser, error) { w.Notify = func(h plumbing.Hash, writer *idxfile.Writer) { index, err := writer.Index() - if err == nil { - s.index[h] = index + if err != nil { + return + } + s.muI.Lock() + if _, existed := s.index[h]; !existed { + // Copy-on-grow rather than append-in-place so any + // reader that snapshotted the old slice header keeps + // seeing a stable backing array. + next := make([]packEntry, len(s.packs)+1) + copy(next, s.packs) + next[len(s.packs)] = packEntry{h: h, idx: index} + s.packs = next } + s.index[h] = index + s.muI.Unlock() } return w, nil @@ -360,25 +511,29 @@ func (s *ObjectStorage) LazyWriter() (w io.WriteCloser, wh func(typ plumbing.Obj // HasEncodedObject returns nil if the object exists, without actually // reading the object data from storage. func (s *ObjectStorage) HasEncodedObject(h plumbing.Hash) (err error) { - // Check unpacked objects - f, err := s.dir.Object(h) - if err != nil { - if !os.IsNotExist(err) { - return err + // Pack-membership-first when the index is healthy: a hit on + // the in-memory fanout shortcut avoids a loose Stat. If the + // index fails to load (e.g. a corrupt .idx on disk), fall + // through so a loose object can still answer the probe and + // the lookup degrades to loose-only — mirrors canonical Git's + // tolerance of partial pack-store corruption. + idxErr := s.requireIndex() + if idxErr == nil { + if _, _, offset := s.findObjectInPackfile(h); offset != -1 { + return nil } - // Fall through to check packed objects. - } else { - defer ioutil.CheckClose(f, &err) - return nil } - // Check packed objects. - if err := s.requireIndex(); err != nil { - return err - } - _, _, offset := s.findObjectInPackfile(h) - if offset != -1 { + // Existence-only on the loose path: Stat instead of Open + // avoids a per-call open()+close() pair when the object lives + // in loose. + if _, statErr := s.dir.ObjectStat(h); statErr == nil { return nil + } else if !os.IsNotExist(statErr) { + return statErr + } + if idxErr != nil { + return idxErr } _, err = findInAlternates(s, func(alt *ObjectStorage) (struct{}, error) { @@ -408,128 +563,57 @@ func (s *ObjectStorage) encodedObjectSizeFromUnpacked(h plumbing.Hash) (size int } func (s *ObjectStorage) packfile(idx idxfile.Index, pack plumbing.Hash) (*packfile.Packfile, error) { - if p := s.packfileFromCache(pack); p != nil { - return p, nil - } - - f, err := s.dir.ObjectPack(pack) - if err != nil { - return nil, err - } - - p := packfile.NewPackfile(f, + return packfile.NewPackfile(nil, + packfile.WithPackHandle(func() (packfile.PackHandle, error) { + return s.dir.PackHandle(pack) + }), packfile.WithIdx(idx), packfile.WithFs(s.dir.Fs()), packfile.WithCache(s.objectCache), packfile.WithObjectIDSize(pack.Size()), - ) - return p, s.storePackfileInCache(pack, p) -} - -func (s *ObjectStorage) packfileFromCache(hash plumbing.Hash) *packfile.Packfile { - s.muP.Lock() - defer s.muP.Unlock() - - if s.packfiles == nil { - if s.options.KeepDescriptors { - s.packfiles = make(map[plumbing.Hash]*packfile.Packfile) - } else if s.options.MaxOpenDescriptors > 0 { - s.packList = make([]plumbing.Hash, s.options.MaxOpenDescriptors) - s.packfiles = make(map[plumbing.Hash]*packfile.Packfile, s.options.MaxOpenDescriptors) - } - } - - return s.packfiles[hash] -} - -func (s *ObjectStorage) storePackfileInCache(hash plumbing.Hash, p *packfile.Packfile) error { - s.muP.Lock() - defer s.muP.Unlock() - - if s.options.KeepDescriptors { - s.packfiles[hash] = p - return nil - } - - if s.options.MaxOpenDescriptors <= 0 { - return nil - } - - // start over as the limit of packList is hit - if s.packListIdx >= len(s.packList) { - s.packListIdx = 0 - } - - // close the existing packfile if open - if next := s.packList[s.packListIdx]; !next.IsZero() { - open := s.packfiles[next] - delete(s.packfiles, next) - if open != nil { - if err := open.Close(); err != nil { - return err - } - } - } - - // cache newly open packfile - s.packList[s.packListIdx] = hash - s.packfiles[hash] = p - s.packListIdx++ - - return nil -} - -func (s *ObjectStorage) encodedObjectSizeFromPackfile(h plumbing.Hash) (size int64, err error) { - if err := s.requireIndex(); err != nil { - return 0, err - } - - pack, _, offset := s.findObjectInPackfile(h) - if offset == -1 { - return 0, plumbing.ErrObjectNotFound - } - - idx := s.index[pack] - hash, err := idx.FindHash(offset) - if err == nil { - obj, ok := s.objectCache.Get(hash) - if ok { - return obj.Size(), nil - } - } else if err != nil && !errors.Is(err, plumbing.ErrObjectNotFound) { - return 0, err - } - - p, err := s.packfile(idx, pack) - if err != nil { - return 0, err - } - - if !s.options.KeepDescriptors && s.options.MaxOpenDescriptors == 0 { - defer ioutil.CheckClose(p, &err) - } - - return p.GetSizeByOffset(offset) + ), nil } // EncodedObjectSize returns the plaintext size of the given object, // without actually reading the full object data from storage. func (s *ObjectStorage) EncodedObjectSize(h plumbing.Hash) (size int64, err error) { - size, err = s.encodedObjectSizeFromUnpacked(h) - if err != nil && !errors.Is(err, plumbing.ErrObjectNotFound) { - return 0, err - } else if err == nil { - return size, nil + // Pack-membership-first when the index is healthy: a single + // in-memory fanout probe routes packed reads through the pack + // reader and skips the loose Stat. If the index fails to load + // (e.g. corrupt .idx), fall through to loose so the lookup + // degrades gracefully — same shape as HasEncodedObject. + idxErr := s.requireIndex() + if idxErr == nil { + if pack, idx, offset := s.findObjectInPackfile(h); !pack.IsZero() { + if cached, ok := s.objectCache.Get(h); ok { + return cached.Size(), nil + } + p, perr := s.packfile(idx, pack) + if perr != nil { + return 0, perr + } + size, err = p.GetSizeByOffset(offset) + if err == nil { + return size, nil + } + if !errors.Is(err, plumbing.ErrObjectNotFound) { + return 0, err + } + // Membership claimed the hash but the pack lost it — + // fall through to loose and alternates. + } } - size, err = s.encodedObjectSizeFromPackfile(h) + size, err = s.encodedObjectSizeFromUnpacked(h) if err == nil { return size, nil } - if !errors.Is(err, plumbing.ErrObjectNotFound) { return 0, err } + if idxErr != nil { + return 0, idxErr + } return findInAlternates(s, func(alt *ObjectStorage) (int64, error) { return alt.EncodedObjectSize(h) @@ -542,22 +626,39 @@ func (s *ObjectStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (p var obj plumbing.EncodedObject var err error - if s.index != nil { - obj, err = s.getFromPackfile(h, false) - if errors.Is(err, plumbing.ErrObjectNotFound) { - obj, err = s.getFromUnpacked(h) + // Pack-membership-first when the index is healthy: see + // EncodedObjectSize for the routing rationale. The shared + // object cache is keyed by hash only — gating the cache + // check on findObjectInPackfile keeps reads safe when callers + // share a cache across ObjectStorages (see + // TestGetFromObjectFileSharedCache). A failed requireIndex + // (corrupt .idx) degrades to loose-only rather than failing + // the whole read. + idxErr := s.requireIndex() + routed := false + if idxErr == nil { + if pack, idx, offset := s.findObjectInPackfile(h); !pack.IsZero() { + routed = true + if cached, ok := s.objectCache.Get(h); ok { + if t == plumbing.AnyObject || cached.Type() == t { + return cached, nil + } + return nil, plumbing.ErrObjectNotFound + } + obj, err = s.getFromPackfileAt(pack, idx, h, offset, false) } - } else { + } + if !routed { obj, err = s.getFromUnpacked(h) - if errors.Is(err, plumbing.ErrObjectNotFound) { - obj, err = s.getFromPackfile(h, false) - } } if errors.Is(err, plumbing.ErrObjectNotFound) { obj, err = findInAlternates(s, func(alt *ObjectStorage) (plumbing.EncodedObject, error) { return alt.EncodedObject(t, h) }) + if errors.Is(err, plumbing.ErrObjectNotFound) && idxErr != nil { + return nil, idxErr + } } if err != nil { @@ -643,35 +744,34 @@ func (s *ObjectStorage) getFromUnpacked(h plumbing.Hash) (obj plumbing.EncodedOb return obj, nil } -// Get returns the object with the given hash, by searching for it in -// the packfile. +// getFromPackfile resolves h via the packfile path: cheap pack- +// membership probe, then fetch from the located pack. func (s *ObjectStorage) getFromPackfile(h plumbing.Hash, canBeDelta bool) (plumbing.EncodedObject, error) { if err := s.requireIndex(); err != nil { return nil, err } - pack, hash, offset := s.findObjectInPackfile(h) + pack, idx, offset := s.findObjectInPackfile(h) if offset == -1 { return nil, plumbing.ErrObjectNotFound } + return s.getFromPackfileAt(pack, idx, h, offset, canBeDelta) +} - s.muI.RLock() - idx := s.index[pack] - s.muI.RUnlock() - +// getFromPackfileAt fetches the object at a pre-located pack +// position, skipping the membership probe. Used by callers that +// already ran findObjectInPackfile (e.g. EncodedObject's pack- +// membership-first fast path) so the lookup is not repeated. +func (s *ObjectStorage) getFromPackfileAt(pack plumbing.Hash, idx idxfile.Index, h plumbing.Hash, offset int64, canBeDelta bool) (plumbing.EncodedObject, error) { p, err := s.packfile(idx, pack) if err != nil { return nil, err } - - if !s.options.KeepDescriptors && s.options.MaxOpenDescriptors == 0 { - defer ioutil.CheckClose(p, &err) - } + defer ioutil.CheckClose(p, &err) if canBeDelta { - return s.decodeDeltaObjectAt(p, offset, hash) + return s.decodeDeltaObjectAt(p, offset, h) } - return p.GetByOffset(offset) } @@ -724,18 +824,71 @@ func (s *ObjectStorage) decodeDeltaObjectAt( return newDeltaObject(obj, hash, base, header.Size), nil } -func (s *ObjectStorage) findObjectInPackfile(h plumbing.Hash) (plumbing.Hash, plumbing.Hash, int64) { - defer s.muI.Unlock() - s.muI.Lock() +// findObjectInPackfile locates h across the storage's packs and +// returns (pack-hash, idx, offset). offset == -1 means not found +// in any pack; the returned idx is then nil. Snapshots s.packs +// under RLock and releases the lock before calling MayContain / +// FindOffset, so slow LazyIndex I/O does not block concurrent +// Reindex / requireIndex publish on muI.Lock. +// +// MRU policy diverges from canonical Git's find_pack_entry +// (packfile.c), which moves the hit pack to the head of the +// packed_git linked list so every subsequent walk starts there. +// go-git stores a single-slot atomic hint instead. A true reorder +// would require write-locking s.packs on every successful find, +// defeating the snapshot-under-RLock pattern that the rest of the +// read path relies on. The hint can go stale under concurrent +// Reindex / PackfileWriter.Notify; staleness costs at most one +// extra MayContain + FindOffset probe and never misroutes, since +// FindOffset's contract returns an offset only for the hash it +// was asked about. +func (s *ObjectStorage) findObjectInPackfile(h plumbing.Hash) (plumbing.Hash, idxfile.Index, int64) { + s.muI.RLock() + packs := s.packs + s.muI.RUnlock() - for packfile, index := range s.index { - offset, err := index.FindOffset(h) + if len(packs) == 0 { + return plumbing.ZeroHash, nil, -1 + } + + // MRU: probe the last successfully-hit pack first. The hint is + // encoded as packs index + 1; 0 means no hint. A stale entry + // costs one MayContain + FindOffset but never misroutes. + hint := int(s.lastHitPackIdx.Load()) - 1 + if hint >= 0 && hint < len(packs) { + pe := packs[hint] + if pe.idx != nil && pe.idx.MayContain(h) { + if offset, err := pe.idx.FindOffset(h); err == nil { + return pe.h, pe.idx, offset + } + } + } else { + hint = -1 + } + + for i, pe := range packs { + if i == hint { + // Skip the MRU pack — we already tried it above. + continue + } + if !pe.idx.MayContain(h) { + continue + } + offset, err := pe.idx.FindOffset(h) if err == nil { - return packfile, h, offset + // Update the hint only when it actually changed. + // Saves a no-op atomic Store on the hot stay-on-pack + // pattern (caught by the MRU probe above) and avoids + // any allocation — the encoded value is a small int. + next := int32(i + 1) + if s.lastHitPackIdx.Load() != next { + s.lastHitPackIdx.Store(next) + } + return pe.h, pe.idx, offset } } - return plumbing.ZeroHash, plumbing.ZeroHash, -1 + return plumbing.ZeroHash, nil, -1 } // HashesWithPrefix returns all objects with a hash that starts with a prefix by searching for @@ -748,13 +901,24 @@ func (s *ObjectStorage) HashesWithPrefix(prefix []byte) ([]plumbing.Hash, error) seen := hashListAsMap(hashes) - // TODO: This could be faster with some idxfile changes, - // or diving into the packfile. if err := s.requireIndex(); err != nil { return nil, err } - for _, index := range s.index { - ei, err := index.Entries() + // Snapshot the index map under muI.RLock so the iteration + // below is safe against a concurrent Reindex swap or a + // PackfileWriter.Notify insert. The borrowed LazyIndex values + // stay alive for the duration of the loop via this slice; the + // underlying SharedFile FDs are governed by their refcount and + // the fdpool, not by removal from s.index. + s.muI.RLock() + indexes := make([]idxfile.Index, 0, len(s.index)) + for _, idx := range s.index { + indexes = append(indexes, idx) + } + s.muI.RUnlock() + + for _, index := range indexes { + ei, err := index.EntriesWithPrefix(prefix) if err != nil { return nil, err } @@ -766,13 +930,11 @@ func (s *ObjectStorage) HashesWithPrefix(prefix []byte) ([]plumbing.Hash, error) _ = ei.Close() return nil, err } - if e.Hash.HasPrefix(prefix) { - if _, ok := seen[e.Hash]; ok { - continue - } - seen[e.Hash] = struct{}{} - hashes = append(hashes, e.Hash) + if _, ok := seen[e.Hash]; ok { + continue } + seen[e.Hash] = struct{}{} + hashes = append(hashes, e.Hash) } _ = ei.Close() } @@ -838,13 +1000,16 @@ func (s *ObjectStorage) buildPackfileIters( return &lazyPackfilesIter{ hashes: packs, open: func(h plumbing.Hash) (storer.EncodedObjectIter, error) { - pack, err := s.dir.ObjectPack(h) + pack, err := s.dir.OpenPackForReading(h) if err != nil { return nil, err } + s.muI.RLock() + idx := s.index[h] + s.muI.RUnlock() return newPackfileIter( - s.dir.Fs(), pack, t, seen, s.index[h], - s.objectCache, s.options.KeepDescriptors, h.Size(), + s.dir.Fs(), pack, t, seen, idx, + s.objectCache, false, h.Size(), ) }, }, nil @@ -862,36 +1027,78 @@ func (s *ObjectStorage) Close() error { } s.muA.RUnlock() - s.muP.RLock() - defer s.muP.RUnlock() - - if s.options.KeepDescriptors || s.options.MaxOpenDescriptors > 0 { - for _, packfile := range s.packfiles { - err := packfile.Close() - if firstError == nil && err != nil { - firstError = err - } + // Close each cached Index. LazyIndex.Close releases idle file + // descriptors and permanently disables the index; MemoryIndex's + // Close is a no-op. + s.muI.RLock() + for _, idx := range s.index { + if err := idx.Close(); firstError == nil && err != nil { + firstError = err } } + s.muI.RUnlock() + + _ = s.dir.Close() + + return firstError +} - // If the index being used implements io.Closer, make sure we call it. - // LazyIndex.Close permanently disables the index and releases any - // idle file descriptors. The same pattern applies to other Index - // implementations that hold resources. +// CloseIdleDescriptors releases the FDs held by this +// [ObjectStorage] and every cached alternate. The object cache, +// the packfile cache, the alternates cache, and the `s.index` +// map (with the [idxfile.LazyIndex] entries inside it) all +// survive — only the file descriptors backing those LazyIndex +// entries are released. +// +// The call fans out across three independent FD owners: the +// [dotgit.DotGit] `PackHandle` catalog (.pack and the +// `LazyIndex` inside each `PackHandle`), the ObjectStorage-level +// idx map (which holds its own LazyIndex per pack, distinct +// from the one inside the `PackHandle`), and any cached +// alternate `ObjectStorage`. +// +// Idempotent and safe to call concurrently with reads. In-flight +// reads complete normally; the FDs they hold refcounts on close +// the instant the last reader releases. After +// [ObjectStorage.Close] the call is a no-op. +// +// Parent storage is released before alternates — a deliberate +// divergence from Close, which goes alternates-first. The +// parent-first order favours OS reclaim of this storage's FDs +// when an alternate's release is slow (e.g. a network FS). +func (s *ObjectStorage) CloseIdleDescriptors() error { + var errs []error + + if err := s.dir.CloseIdleDescriptors(); err != nil { + errs = append(errs, err) + } + + // ObjectStorage maintains a separate idxfile cache in s.index + // (populated by loadIdxFile → loadLazyIndex). LazyIndex + // entries own .idx/.rev FDs distinct from those inside the + // dotgit PackHandle catalog; they need their own soft-close + // fan-out. The storer.IdleReleaser assertion picks up + // LazyIndex automatically and silently skips index + // implementations that hold no FDs (notably MemoryIndex). s.muI.RLock() for _, idx := range s.index { - if closer, ok := idx.(io.Closer); ok { - if err := closer.Close(); firstError == nil && err != nil { - firstError = err + if r, ok := idx.(storer.IdleReleaser); ok { + if err := r.CloseIdleDescriptors(); err != nil { + errs = append(errs, err) } } } s.muI.RUnlock() - s.packfiles = nil - _ = s.dir.Close() + s.muA.RLock() + for _, alt := range s.alternates { + if err := alt.CloseIdleDescriptors(); err != nil { + errs = append(errs, err) + } + } + s.muA.RUnlock() - return firstError + return errors.Join(errs...) } func hashListAsMap(l []plumbing.Hash) map[plumbing.Hash]struct{} { @@ -931,6 +1138,45 @@ func (s *ObjectStorage) ObjectPacks() ([]plumbing.Hash, error) { } // DeleteOldObjectPackAndIndex removes a pack and its index if older than t. +// Also drops the in-memory s.index map entry and the matching s.packs +// slice entry for the removed pack so subsequent routing decisions in +// findObjectInPackfile no longer claim membership for a hash that +// lives only in the now-deleted pack. If the MRU hint pointed at the +// deleted slot, invalidate it. func (s *ObjectStorage) DeleteOldObjectPackAndIndex(h plumbing.Hash, t time.Time) error { - return s.dir.DeleteOldObjectPackAndIndex(h, t) + if err := s.dir.DeleteOldObjectPackAndIndex(h, t); err != nil { + return err + } + s.muI.Lock() + defer s.muI.Unlock() + + idx, ok := s.index[h] + if !ok { + return nil + } + delete(s.index, h) + + // Drop the matching s.packs entry. Allocate a fresh slice and + // copy the rest (mirror of PackfileWriter.Notify's copy-on-grow) + // so readers holding the old slice header keep a stable view. + for i, pe := range s.packs { + if pe.h != h { + continue + } + next := make([]packEntry, 0, len(s.packs)-1) + next = append(next, s.packs[:i]...) + next = append(next, s.packs[i+1:]...) + s.packs = next + // Invalidate the MRU hint if it pointed at the deleted slot. + // findObjectInPackfile's bounds check + FindOffset contract + // make a stale hint safe, but resetting under the lock here + // removes the staleness window entirely. + if s.lastHitPackIdx.Load() == int32(i+1) { + s.lastHitPackIdx.Store(0) + } + break + } + + _ = idx.Close() + return nil } diff --git a/vendor/github.com/go-git/go-git/v6/storage/filesystem/storage.go b/vendor/github.com/go-git/go-git/v6/storage/filesystem/storage.go index 291d2d4fc..774c2c52e 100644 --- a/vendor/github.com/go-git/go-git/v6/storage/filesystem/storage.go +++ b/vendor/github.com/go-git/go-git/v6/storage/filesystem/storage.go @@ -11,9 +11,16 @@ import ( "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/cache" formatcfg "github.com/go-git/go-git/v6/plumbing/format/config" + "github.com/go-git/go-git/v6/plumbing/storer" "github.com/go-git/go-git/v6/storage/filesystem/dotgit" + "github.com/go-git/go-git/v6/x/fdpool" ) +// defaultPoolCapacity is the FD pool capacity selected when +// [Options.Pool] is nil. It accommodates roughly 85 concurrently-hot +// packs (3 FDs per pack: pack + idx + rev). +const defaultPoolCapacity = 256 + // Storage is an implementation of git.Storer that stores data on disk in the // standard git format (this is, the .git directory). Zero values of this type // are not safe to use, see the NewStorage function below. @@ -22,7 +29,7 @@ type Storage struct { dir *dotgit.DotGit hasher plumbing.Hasher - ObjectStorage + *ObjectStorage ReferenceStorage IndexStorage ShallowStorage @@ -31,17 +38,22 @@ type Storage struct { ReflogStorage } +// Compile-time assertions pin both *Storage and *dotgit.DotGit to +// [storer.IdleReleaser]. *Storage promotes CloseIdleDescriptors +// from the embedded [ObjectStorage] via Go's method-set promotion +// rules; *dotgit.DotGit defines the method directly. A future +// rename or signature change on either side breaks the build +// immediately. +var ( + _ storer.IdleReleaser = (*Storage)(nil) + _ storer.IdleReleaser = (*dotgit.DotGit)(nil) +) + // Options holds configuration for the storage. type Options struct { // ExclusiveAccess means that the filesystem is not modified externally // while the repo is open. ExclusiveAccess bool - // KeepDescriptors makes the file descriptors to be reused but they will - // need to be manually closed calling Close(). - KeepDescriptors bool - // MaxOpenDescriptors is the max number of file descriptors to keep - // open. If KeepDescriptors is true, all file descriptors will remain open. - MaxOpenDescriptors int // LargeObjectThreshold maximum object size (in bytes) that will be read in to memory. // If left unset or set to 0 there is no limit LargeObjectThreshold int64 @@ -68,15 +80,69 @@ type Options struct { // IndexCache provides an optional cache implementation for index data. // If left as nil, a default stat-based implementation is created automatically. IndexCache IndexCache + + // Pool governs LRU eviction of the read-side .pack/.idx/.rev + // file descriptors that this Storage opens. When a Storage + // exceeds the pool capacity the least-recently-used SharedFile + // is closed (via ReleaseNow) and reopens on the next read. + // Pack-write FDs ([PackWriter]) are short-lived and are not + // pooled. + // + // A nil Pool selects a per-Storage pool sized at the package + // default (currently 256 entries, accommodating roughly 85 + // concurrently-hot packs at 3 FDs per pack). The default + // assumes go-git owns the dominant share of FDs in the + // process; applications running other FD-heavy subsystems + // (network servers, large connection pools) should construct + // a pool sized against their full FD budget and pass it here + // rather than rely on the default. + // + // Sharing one Pool across multiple Storages bounds the FD + // budget process-wide rather than per Storage — useful for + // servers handling concurrent per-request repositories or + // batch tools iterating many repos. Construct the pool with + // [fdpool.New], pass it via this field to + // [NewStorageWithOptions], then open repositories with + // [git.Open], [git.Clone], or [git.Init] using the resulting + // Storer. The path-based wrappers (all Plain* functions: + // PlainOpen, PlainClone, PlainInit) construct their own + // Storage internally and so do not accept an injected pool; + // that is by design. + // + // To disable pooling, pass [fdpool.New](0) (or any non-positive + // capacity): the resulting no-op Pool causes pool-less + // SharedFiles to fall back to their grace-period close on + // quiescence. + // + // To request mmap-backed read FDs (read-only, where the + // platform supports it) construct the underlying billy + // filesystem with [github.com/go-git/go-billy/v6/osfs.WithMmap] + // before handing it to [NewStorageWithOptions]. The pool + // governs that file equally whether it is FD- or mmap-backed. + // + // The Pool field's API stability tracks [fdpool.Pool]'s, not + // this package's. Per the x/ package policy, the fdpool API + // may change without following semantic versioning; consumers + // reading this field should treat it as experimental on the + // same timeline. + Pool *fdpool.Pool } // NewStorage returns a new Storage backed by a given `fs.Filesystem` and cache. +// +// The cache is keyed by object hash only; passing the same +// [cache.Object] across multiple Storage instances is safe because +// every storage-level read gates its cache lookup on a successful +// pack-membership probe, so a hit from another storage's pack cannot +// be served here. See [ObjectStorage.EncodedObject] and +// TestGetFromObjectFileSharedCache. func NewStorage(fs billy.Filesystem, cache cache.Object) *Storage { return NewStorageWithOptions(fs, cache, Options{}) } // NewStorageWithOptions returns a new Storage with extra options, -// backed by a given `fs.Filesystem` and cache. +// backed by a given `fs.Filesystem` and cache. See [NewStorage] for +// the cross-Storage cache safety contract. // Returns an error if an explicit ObjectFormat is provided via options // but conflicts with an existing config in the filesystem. func NewStorageWithOptions(fs billy.Filesystem, c cache.Object, ops Options) *Storage { @@ -100,13 +166,21 @@ func NewStorageWithOptions(fs billy.Filesystem, c cache.Object, ops Options) *St hasher := plumbing.NewHasher(ops.ObjectFormat, plumbing.AnyObject, 0) + // Construct a per-Storage FD pool at the package default + // capacity unless the caller injected one. Pass a non-positive- + // capacity Pool via Options.Pool to disable pooling. + pool := ops.Pool + if pool == nil { + pool = fdpool.New(defaultPoolCapacity) + } + dirOps := dotgit.Options{ ExclusiveAccess: ops.ExclusiveAccess, AlternatesFS: ops.AlternatesFS, - KeepDescriptors: ops.KeepDescriptors, ObjectFormat: ops.ObjectFormat, ReadReverseIndex: readRevIdx, WriteReverseIndex: writeRevIdx, + Pool: pool, } dir := dotgit.NewWithOptions(fs, dirOps) @@ -123,7 +197,7 @@ func NewStorageWithOptions(fs billy.Filesystem, c cache.Object, ops Options) *St dir: dir, hasher: hasher, - ObjectStorage: *NewObjectStorageWithOptions(dir, c, ops), + ObjectStorage: NewObjectStorageWithOptions(dir, c, ops), ReferenceStorage: ReferenceStorage{dir: dir}, IndexStorage: IndexStorage{dir: dir, h: hasher.Hash, cache: ops.IndexCache, skipHash: skipHash}, ShallowStorage: ShallowStorage{dir: dir}, diff --git a/vendor/github.com/go-git/go-git/v6/submodule.go b/vendor/github.com/go-git/go-git/v6/submodule.go index a708d572e..d28eb153e 100644 --- a/vendor/github.com/go-git/go-git/v6/submodule.go +++ b/vendor/github.com/go-git/go-git/v6/submodule.go @@ -247,6 +247,16 @@ func (s *Submodule) UpdateContext(ctx context.Context, o *SubmoduleUpdateOptions return s.update(ctx, o, plumbing.ZeroHash) } +// indexEntry returns the index entry recorded for the submodule path. +func (s *Submodule) indexEntry() (*index.Entry, error) { + idx, err := s.w.r.Storer.Index() + if err != nil { + return nil, err + } + + return idx.Entry(s.c.Path) +} + func (s *Submodule) update(ctx context.Context, o *SubmoduleUpdateOptions, forceHash plumbing.Hash) error { if !s.initialized && !o.Init { return ErrSubmoduleNotInitialized @@ -258,16 +268,11 @@ func (s *Submodule) update(ctx context.Context, o *SubmoduleUpdateOptions, force } } - idx, err := s.w.r.Storer.Index() - if err != nil { - return err - } - hash := forceHash if hash.IsZero() { - e, err := idx.Entry(s.c.Path) + e, err := s.indexEntry() if err != nil { - return err + return fmt.Errorf("submodule %q: %w", s.c.Name, err) } hash = e.Hash @@ -364,18 +369,33 @@ func (s Submodules) Init() error { return nil } -// Update updates all the submodules in this list. +// Update updates all the submodules in this list. Submodules declared in +// .gitmodules but missing from the index are skipped, matching the behaviour +// of the git CLI. func (s Submodules) Update(o *SubmoduleUpdateOptions) error { return s.UpdateContext(context.Background(), o) } -// UpdateContext updates all the submodules in this list. +// UpdateContext updates all the submodules in this list. Submodules declared +// in .gitmodules but missing from the index are skipped, matching the +// behaviour of the git CLI. // // The provided Context must be non-nil. If the context expires before the // operation is complete, an error is returned. The context only affects the // transport operations. func (s Submodules) UpdateContext(ctx context.Context, o *SubmoduleUpdateOptions) error { for _, sub := range s { + // A .gitmodules entry whose path has no gitlink in the index is not + // a submodule from git's point of view: the git CLI silently ignores + // such stale entries on clone, update and status, so skip them here + // instead of failing the whole operation. + if _, err := sub.indexEntry(); err != nil { + if errors.Is(err, index.ErrEntryNotFound) { + continue + } + return err + } + if err := sub.UpdateContext(ctx, o); err != nil { return err } diff --git a/vendor/github.com/go-git/go-git/v6/utils/binary/read.go b/vendor/github.com/go-git/go-git/v6/utils/binary/read.go index 9d6ed3d75..7e6f51361 100644 --- a/vendor/github.com/go-git/go-git/v6/utils/binary/read.go +++ b/vendor/github.com/go-git/go-git/v6/utils/binary/read.go @@ -4,10 +4,12 @@ package binary import ( "bufio" + "bytes" "encoding/binary" "errors" "io" "math" + "sync" ) // ErrIntegerOverflow is returned when a Git-format variable-width integer @@ -153,21 +155,50 @@ func ReadUint16(r io.Reader) (uint16, error) { const sniffLen = 8000 +// sniffPool reuses sniff-window buffers across IsBinary calls so the hot diff +// path (one call per file, per side) does not allocate one per invocation. +var sniffPool = sync.Pool{ + New: func() any { + b := make([]byte, sniffLen) + return &b + }, +} + // IsBinary detects if data is a binary value based on: // http://git.kernel.org/cgit/git/git.git/tree/xdiff-interface.c?id=HEAD#n198 func IsBinary(r io.Reader) (bool, error) { - reader := bufio.NewReader(r) - for range sniffLen { - b, err := reader.ReadByte() - if err == io.EOF { - break + // Scan up to sniffLen bytes for a NUL, reading in chunks and checking each + // with bytes.IndexByte. Returning as soon as a NUL is found preserves the + // early-exit of the previous byte-at-a-time loop — a binary blob with an + // early NUL is not forced to read (or block on) the rest of the window — + // while bytes.IndexByte avoids that loop's per-byte overhead. The buffer + // comes from a pool, so there is no per-call allocation. + bufp := sniffPool.Get().(*[]byte) + defer sniffPool.Put(bufp) + buf := *bufp + + for remaining := sniffLen; remaining > 0; { + chunk := buf + if len(chunk) > remaining { + chunk = chunk[:remaining] + } + + n, err := r.Read(chunk) + if bytes.IndexByte(chunk[:n], 0) >= 0 { + return true, nil } + remaining -= n + if err != nil { + if errors.Is(err, io.EOF) { + return false, nil + } return false, err } - - if b == byte(0) { - return true, nil + if n == 0 { + // A compliant io.Reader should not return (0, nil); treat it as + // "no more data" rather than spinning. + return false, nil } } diff --git a/vendor/github.com/go-git/go-git/v6/utils/ioutil/context.go b/vendor/github.com/go-git/go-git/v6/utils/ioutil/context.go index 94e02dd0e..c7a39f48b 100644 --- a/vendor/github.com/go-git/go-git/v6/utils/ioutil/context.go +++ b/vendor/github.com/go-git/go-git/v6/utils/ioutil/context.go @@ -24,15 +24,16 @@ type ctxWriter struct { } // NewContextWriter wraps a writer to make it respect the given Context. -// If there is a blocking write, the returned Writer will return -// whenever the context is cancelled (the return values are n=0 -// and err=ctx.Err().) +// When the context is cancelled the returned Writer returns ctx.Err(), but +// only once the underlying write that is already in flight has completed — so +// the underlying writer is guaranteed quiescent after Write returns, letting +// callers close it without racing the in-flight write. // -// Note that this wrapper DOES NOT ACTUALLY cancel the underlying -// write, as there is no way to do that with the standard Go io -// interface. So the read and write _will_ happen or hang. Use -// this sparingly, make sure to cancel the read or write as necessary -// (e.g. closing a connection whose context is up, etc.) +// Note that this wrapper DOES NOT ACTUALLY cancel the underlying write, as +// there is no way to do that with the standard Go io interface: the in-flight +// write _will_ happen or hang. Use this sparingly, and make sure the underlying +// write can be unblocked some other way (e.g. closing a connection whose +// context is up) so the cancel path it waits on can return. // // Furthermore, in order to protect your memory from being read // _after_ you've cancelled the context, this io.Writer will @@ -81,6 +82,8 @@ func (w *ctxWriter) Write(buf []byte) (int, error) { select { case <-w.ctx.Done(): + // Wait for the in-flight write to complete. + <-ret return total, w.ctx.Err() case write := <-ret: if err := w.ctx.Err(); err != nil { diff --git a/vendor/github.com/go-git/go-git/v6/worktree.go b/vendor/github.com/go-git/go-git/v6/worktree.go index a1ad097eb..c030eadd6 100644 --- a/vendor/github.com/go-git/go-git/v6/worktree.go +++ b/vendor/github.com/go-git/go-git/v6/worktree.go @@ -14,6 +14,7 @@ import ( "time" "github.com/go-git/go-billy/v6" + "github.com/go-git/go-billy/v6/osfs" "github.com/go-git/go-billy/v6/util" "github.com/go-git/go-git/v6/config" @@ -66,6 +67,30 @@ func (w *Worktree) Filesystem() billy.Filesystem { return w.filesystem.Filesystem } +// reusableRootFS returns a worktree filesystem to use for a single bulk +// checkout/reset. When the worktree lives on an OS-backed billy filesystem, +// it opens one *os.Root for the whole operation and wraps it in the same +// validating worktreeFilesystem, so each file is opened relative to a +// reused directory instead of opening and closing a fresh root per call. +// It falls back to the default filesystem when that is not possible. +func (w *Worktree) reusableRootFS() (*worktreeFilesystem, func()) { + bos, ok := w.filesystem.Filesystem.(*osfs.BoundOS) + if !ok { + return w.filesystem, func() {} + } + root, err := os.OpenRoot(bos.Root()) + if err != nil { + return w.filesystem, func() {} + } + rfs, err := osfs.FromRoot(root) + if err != nil { + _ = root.Close() + return w.filesystem, func() {} + } + return newWorktreeFilesystem(rfs, w.filesystem.protectNTFS, w.filesystem.protectHFS), + func() { _ = root.Close() } +} + // Pull incorporates changes from a remote repository into the current branch. // Returns nil if the operation is successful, NoErrAlreadyUpToDate if there are // no changes to be fetched, or an error. @@ -320,8 +345,13 @@ func (w *Worktree) Reset(opts *ResetOptions) error { return err } + cfg, err := w.r.Config() + if err != nil { + return err + } + if opts.Mode == MergeReset { - unstaged, err := w.containsUnstagedChanges() + unstaged, err := w.containsUnstagedChanges(cfg) if err != nil { return err } @@ -376,13 +406,13 @@ func (w *Worktree) Reset(opts *ResetOptions) error { } if opts.Mode == MergeReset && len(removedFiles) > 0 { - if err := w.resetWorktree(t, removedFiles); err != nil { + if err := w.resetWorktree(cfg, t, removedFiles); err != nil { return err } } if opts.Mode == HardReset || opts.Mode == KeepReset { - if err := w.resetWorktreeToTree(prevTree, t, opts.Files); err != nil { + if err := w.resetWorktreeToTree(cfg, prevTree, t, opts.Files); err != nil { return err } } @@ -659,9 +689,12 @@ func (w *Worktree) checkKeepResetConflicts(fromTree, toTree *object.Tree, sparse // file with SkipWorktree=true must not exist in the worktree. // // files optionally restricts the operation to a specific subset of paths. -func (w *Worktree) resetWorktreeToTree(fromTree, toTree *object.Tree, files []string) error { +func (w *Worktree) resetWorktreeToTree(cfg *config.Config, fromTree, toTree *object.Tree, files []string) error { filesMap := buildFilePathMap(files) + fs, closeFS := w.reusableRootFS() + defer closeFS() + // Step 1: delete files removed from the tracked tree. treeChanges, err := diffTrees(fromTree, toTree) if err != nil { @@ -679,7 +712,7 @@ func (w *Worktree) resetWorktreeToTree(fromTree, toTree *object.Tree, files []st if len(files) > 0 && !inFiles(filesMap, name) { continue } - if err := rmFileAndDirsIfEmpty(w.filesystem, name); err != nil { + if err := rmFileAndDirsIfEmpty(fs, name); err != nil { return err } } @@ -697,7 +730,7 @@ func (w *Worktree) resetWorktreeToTree(fromTree, toTree *object.Tree, files []st // Delete actions. The observable result is unchanged because Delete // actions are skipped by the loop below; the matcher only avoids the // pointless lstat of every file under directories like node_modules. - worktreeChanges, err := w.diffStagingWithWorktree(true, true) + worktreeChanges, err := w.diffStagingWithWorktree(cfg, true, true) if err != nil { return err } @@ -729,7 +762,7 @@ func (w *Worktree) resetWorktreeToTree(fromTree, toTree *object.Tree, files []st } } - if err := w.checkoutChange(ch, toTree, b); err != nil { + if err := w.checkoutChange(cfg, fs, ch, toTree, b); err != nil { return err } } @@ -745,10 +778,10 @@ func (w *Worktree) resetWorktreeToTree(fromTree, toTree *object.Tree, files []st if len(files) > 0 && !inFiles(filesMap, e.Name) { continue } - if _, statErr := w.filesystem.Lstat(e.Name); os.IsNotExist(statErr) { + if _, statErr := fs.Lstat(e.Name); os.IsNotExist(statErr) { continue } - if err := rmFileAndDirsIfEmpty(w.filesystem, e.Name); err != nil { + if err := rmFileAndDirsIfEmpty(fs, e.Name); err != nil { return err } } @@ -766,8 +799,8 @@ func (w *Worktree) resetWorktreeToTree(fromTree, toTree *object.Tree, files []st // from the index in this same Reset is no longer in idxMap, so the // noder's IgnoreMatcher would prune it from the walk and the Delete // action needed to remove it from disk would never be emitted. -func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { - changes, err := w.diffStagingWithWorktree(true, false) +func (w *Worktree) resetWorktree(cfg *config.Config, t *object.Tree, files []string) error { + changes, err := w.diffStagingWithWorktree(cfg, true, false) if err != nil { return err } @@ -778,6 +811,9 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { } b := newIndexBuilder(idx) + fs, closeFS := w.reusableRootFS() + defer closeFS() + filesMap := buildFilePathMap(files) for _, ch := range changes { if len(files) > 0 { @@ -798,7 +834,7 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { } } - if err := w.checkoutChange(ch, t, b); err != nil { + if err := w.checkoutChange(cfg, fs, ch, t, b); err != nil { return err } } @@ -807,7 +843,7 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { return w.r.Storer.SetIndex(idx) } -func (w *Worktree) checkoutChange(ch merkletrie.Change, t *object.Tree, idx *indexBuilder) error { +func (w *Worktree) checkoutChange(cfg *config.Config, fs *worktreeFilesystem, ch merkletrie.Change, t *object.Tree, idx *indexBuilder) error { a, err := ch.Action() if err != nil { return err @@ -838,18 +874,18 @@ func (w *Worktree) checkoutChange(ch merkletrie.Change, t *object.Tree, idx *ind // fit: we want to be able to clean up legitimately-tracked // shapes like "submodule/.git" rather than abort the whole // reset on a single weird untracked file. - return rmFileAndDirsIfEmpty(w.filesystem, ch.From.String()) + return rmFileAndDirsIfEmpty(fs, ch.From.String()) } if isSubmodule { - return w.checkoutChangeSubmodule(name, a, e, idx) + return w.checkoutChangeSubmodule(fs, name, a, e, idx) } - return w.checkoutChangeRegularFile(name, a, t, e, idx) + return w.checkoutChangeRegularFile(cfg, fs, name, a, t, e, idx) } -func (w *Worktree) containsUnstagedChanges() (bool, error) { - ch, err := w.diffStagingWithWorktree(false, true) +func (w *Worktree) containsUnstagedChanges(cfg *config.Config) (bool, error) { + ch, err := w.diffStagingWithWorktree(cfg, false, true) if err != nil { return false, err } @@ -894,7 +930,8 @@ func (w *Worktree) setHEADCommit(commit plumbing.Hash) error { return w.r.Storer.SetReference(branch) } -func (w *Worktree) checkoutChangeSubmodule(name string, +func (w *Worktree) checkoutChangeSubmodule(fs *worktreeFilesystem, + name string, a merkletrie.Action, e *object.TreeEntry, idx *indexBuilder, @@ -917,7 +954,11 @@ func (w *Worktree) checkoutChangeSubmodule(name string, return err } - if err := w.filesystem.MkdirAll(name, mode); err != nil { + if err := w.clearBlockingSymlinks(fs, name); err != nil { + return err + } + + if err := fs.MkdirAll(name, mode); err != nil { return err } @@ -927,7 +968,9 @@ func (w *Worktree) checkoutChangeSubmodule(name string, return nil } -func (w *Worktree) checkoutChangeRegularFile(name string, +func (w *Worktree) checkoutChangeRegularFile(cfg *config.Config, + fs *worktreeFilesystem, + name string, a merkletrie.Action, t *object.Tree, e *object.TreeEntry, @@ -939,7 +982,7 @@ func (w *Worktree) checkoutChangeRegularFile(name string, // to apply perm changes the file is deleted, billy doesn't implement // chmod - if err := w.filesystem.Remove(name); err != nil { + if err := fs.Remove(name); err != nil { return err } @@ -950,41 +993,85 @@ func (w *Worktree) checkoutChangeRegularFile(name string, return err } - if err := w.checkoutFile(f); err != nil { + if err := w.checkoutFile(cfg, fs, f); err != nil { return err } - return w.addIndexFromFile(name, e.Hash, idx) + return w.addIndexFromFile(fs, name, e.Hash, idx) } return nil } -func (w *Worktree) checkoutFile(f *object.File) (err error) { +// clearBlockingSymlinks removes a symlink that is in the way of +// materialising name, so the checkout writes a real entry in its place +// instead of following the link out of the worktree. Two cases: +// +// - a leading directory component that is a symlink (e.g. "s" while +// writing "s/config", where "s" links to ".git"): OpenFile/MkdirAll +// would traverse it, so the write would land under the link's target. +// - the final component itself being a symlink (e.g. writing "s" while +// "s" links to ".git/config"): OpenFile with O_TRUNC, or Symlink, +// would follow/replace through it and clobber the target. +// +// A symlink can never be a legitimate parent of, or the destination for, +// a tracked entry, so removing it is always correct. This mirrors upstream +// Git's forced checkout, which unlinks a blocking symlink in the leading +// path (create_directories) and unlinks an existing entry before +// write_entry. +// https://github.com/git/git/blob/v2.54.0/entry.c#L50 +func (w *Worktree) clearBlockingSymlinks(fs *worktreeFilesystem, name string) error { + var dirs []string + for dir := filepath.Dir(name); dir != "." && dir != "" && dir != string(filepath.Separator); dir = filepath.Dir(dir) { + dirs = append(dirs, dir) + } + // Leading components, shallowest-first: removing the shallowest symlink + // invalidates every component beneath it, so a single removal is enough. + for i := len(dirs) - 1; i >= 0; i-- { + fi, err := fs.Lstat(dirs[i]) + if err != nil { + continue + } + if fi.Mode()&os.ModeSymlink != 0 { + return fs.Remove(dirs[i]) + } + } + // Final component: an existing symlink here would be followed by the + // subsequent OpenFile/Symlink/MkdirAll, so replace it. + if fi, err := fs.Lstat(name); err == nil && fi.Mode()&os.ModeSymlink != 0 { + return fs.Remove(name) + } + return nil +} + +func (w *Worktree) checkoutFile(cfg *config.Config, fs *worktreeFilesystem, f *object.File) (err error) { + // checkoutFile is the materialisation boundary for tracked entries. + // Remove any blocking symlink first so the subsequent OpenFile or + // Symlink call writes the entry itself instead of following a planted + // final-component link in the underlying filesystem. + if err := w.clearBlockingSymlinks(fs, f.Name); err != nil { + return err + } + mode, err := f.Mode.ToOSFileMode() if err != nil { return err } if mode&os.ModeSymlink != 0 { - return w.checkoutFileSymlink(f) + return w.checkoutFileSymlink(fs, f) } - dstFile, err := w.filesystem.OpenFile(f.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode.Perm()) + dstFile, err := fs.OpenFile(f.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode.Perm()) if err != nil { return err } defer ioutil.CheckClose(dstFile, &err) - return w.copyObjectToWorktree(f, dstFile) + return w.copyObjectToWorktree(cfg, f, dstFile) } -func (w *Worktree) copyObjectToWorktree(object *object.File, file billy.File) (err error) { - cfg, err := w.r.Config() - if err != nil { - return err - } - +func (w *Worktree) copyObjectToWorktree(cfg *config.Config, object *object.File, file billy.File) (err error) { var src io.ReadCloser var dst io.Writer = file @@ -1018,7 +1105,7 @@ func (w *Worktree) copyObjectToWorktree(object *object.File, file billy.File) (e return err } -func (w *Worktree) checkoutFileSymlink(f *object.File) (err error) { +func (w *Worktree) checkoutFileSymlink(fs *worktreeFilesystem, f *object.File) (err error) { // .gitmodules symlink rejection (and its NTFS / HFS variants) is // enforced by the worktreeFilesystem wrapper's Symlink method via // validSymlinkName. See https://github.com/git/git/commit/10ecfa7 @@ -1036,14 +1123,14 @@ func (w *Worktree) checkoutFileSymlink(f *object.File) (err error) { return err } - err = w.filesystem.Symlink(string(bytes), f.Name) + err = fs.Symlink(string(bytes), f.Name) // On windows, this might fail. // Follow Git on Windows behavior by writing the link as it is. if err != nil && isSymlinkWindowsNonAdmin(err) { mode, _ := f.Mode.ToOSFileMode() - to, err := w.filesystem.OpenFile(f.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode.Perm()) + to, err := fs.OpenFile(f.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode.Perm()) if err != nil { return err } @@ -1066,9 +1153,9 @@ func (w *Worktree) addIndexFromTreeEntry(name string, f *object.TreeEntry, idx * return nil } -func (w *Worktree) addIndexFromFile(name string, h plumbing.Hash, idx *indexBuilder) error { +func (w *Worktree) addIndexFromFile(fs *worktreeFilesystem, name string, h plumbing.Hash, idx *indexBuilder) error { idx.Remove(name) - fi, err := w.filesystem.Lstat(name) + fi, err := fs.Lstat(name) if err != nil { return err } @@ -1151,17 +1238,22 @@ func resolveModuleURL(originURL, moduleURL string) (string, error) { // Submodules returns all the available submodules func (w *Worktree) Submodules() (Submodules, error) { + cfg, err := w.r.Config() + if err != nil { + return nil, err + } + + return w.submodulesWithConfig(cfg) +} + +// submodulesWithConfig returns all the available submodules using an already-loaded config. +func (w *Worktree) submodulesWithConfig(cfg *config.Config) (Submodules, error) { l := make(Submodules, 0) m, err := w.readGitmodulesFile() if err != nil || m == nil { return l, err } - c, err := w.r.Config() - if err != nil { - return nil, err - } - var originURL string if origin, err := w.r.Remote(DefaultRemoteName); err == nil { if origin.c != nil && len(origin.c.URLs) > 0 { @@ -1170,13 +1262,13 @@ func (w *Worktree) Submodules() (Submodules, error) { } for _, s := range m.Submodules { - sub := w.newSubmodule(s, c.Submodules[s.Name]) - cfg := sub.Config() - resolvedURL, err := resolveModuleURL(originURL, cfg.URL) + sub := w.newSubmodule(s, cfg.Submodules[s.Name]) + subCfg := sub.Config() + resolvedURL, err := resolveModuleURL(originURL, subCfg.URL) if err != nil { return nil, fmt.Errorf("failed to resolve submodule URL %q: %w", s.URL, err) } - cfg.URL = resolvedURL + subCfg.URL = resolvedURL l = append(l, sub) } diff --git a/vendor/github.com/go-git/go-git/v6/worktree_commit.go b/vendor/github.com/go-git/go-git/v6/worktree_commit.go index 03564d898..9d2916681 100644 --- a/vendor/github.com/go-git/go-git/v6/worktree_commit.go +++ b/vendor/github.com/go-git/go-git/v6/worktree_commit.go @@ -116,6 +116,18 @@ func (w *Worktree) CherryPick(commitOpts *CommitOptions, ortStrategyOption OrtMe return ErrCannotCherryPickWithoutCommitOptions } + cfg, err := w.r.Config() + if err != nil { + return err + } + + // Materialise changes through the same validating filesystem and + // checkout path as reset/checkout, so cherry-pick shares their + // leading-symlink handling, mode awareness (symlinks, exec bits, + // CRLF) and root reuse instead of writing raw bytes via Create. + fs, closeFS := w.reusableRootFS() + defer closeFS() + for _, commit := range commits { var changes object.Changes headRef, err := w.r.Head() @@ -165,20 +177,14 @@ func (w *Worktree) CherryPick(commitOpts *CommitOptions, ortStrategyOption OrtMe if to == nil { continue } - content, err := to.Contents() - if err != nil { - return err - } - name := change.To.Name - dstFile, err := w.filesystem.Create(name) - if err != nil { - return err - } - _, err = dstFile.Write([]byte(content)) - if err != nil { + // change.Files names the *File after the tree leaf. The + // worktree write needs the full path so it lands at the + // right location and is validated by the wrapper. + to.Name = change.To.Name + if err := w.checkoutFile(cfg, fs, to); err != nil { return err } - if _, err := w.Add(name); err != nil { + if _, err := w.Add(to.Name); err != nil { return err } } @@ -197,6 +203,11 @@ func (w *Worktree) CherryPick(commitOpts *CommitOptions, ortStrategyOption OrtMe } func (w *Worktree) autoAddModifiedAndDeleted() error { + cfg, err := w.r.Config() + if err != nil { + return err + } + s, err := w.Status() if err != nil { return err @@ -212,7 +223,7 @@ func (w *Worktree) autoAddModifiedAndDeleted() error { continue } - if _, _, err := w.doAddFile(idx, s, path, nil); err != nil { + if _, _, err := w.doAddFile(cfg, idx, s, path, nil); err != nil { return err } } diff --git a/vendor/github.com/go-git/go-git/v6/worktree_fs.go b/vendor/github.com/go-git/go-git/v6/worktree_fs.go index ebdf2602c..0ee29133b 100644 --- a/vendor/github.com/go-git/go-git/v6/worktree_fs.go +++ b/vendor/github.com/go-git/go-git/v6/worktree_fs.go @@ -36,10 +36,25 @@ func defaultProtectNTFS() bool { return true } -// worktreeFilesystem wraps a billy.Filesystem and validates every path passed -// to a mutating operation. This prevents writing to, or deleting from, -// dangerous locations (e.g. .git/*, ../) regardless of which worktree -// code path triggers the operation. +// worktreeFilesystem wraps a billy.Filesystem and validates every path it +// is handed, so worktree operations cannot use dangerous paths at the +// boundary. Two layers apply: +// +// - validPath rejects dangerous path *strings*: .git and its HFS+/NTFS +// variants, "..", control characters, volume names. +// - validNoLeadingSymlink rejects paths whose leading directories +// already exist on disk as symlinks, so a write or delete cannot +// follow a planted link out of the tree. +// +// Both layers run on every mutating operation (validWritePath) and every +// read (validReadPath). Chroot additionally refuses a symlink as the final +// component, so a sub-filesystem such as a submodule worktree cannot be +// scoped to a redirected target. +// +// The wrapper intentionally stops at leading-component traversal. Callers +// that need final-component no-follow semantics for materialisation +// (checkoutFile) enforce that directly by removing the blocking symlink +// before opening the destination path. type worktreeFilesystem struct { billy.Filesystem protectNTFS bool @@ -51,7 +66,7 @@ func newWorktreeFilesystem(fs billy.Filesystem, protectNTFS, protectHFS bool) *w } func (sfs *worktreeFilesystem) Create(filename string) (billy.File, error) { - if err := sfs.validPath(filename); err != nil { + if err := sfs.validWritePath(filename); err != nil { return nil, fmt.Errorf("create: %w", err) } return sfs.Filesystem.Create(filename) @@ -65,7 +80,7 @@ func (sfs *worktreeFilesystem) Open(filename string) (billy.File, error) { } func (sfs *worktreeFilesystem) OpenFile(filename string, flag int, perm fs.FileMode) (billy.File, error) { - if err := sfs.validPath(filename); err != nil { + if err := sfs.validWritePath(filename); err != nil { return nil, fmt.Errorf("openfile: %w", err) } return sfs.Filesystem.OpenFile(filename, flag, perm) @@ -79,14 +94,14 @@ func (sfs *worktreeFilesystem) Stat(filename string) (os.FileInfo, error) { } func (sfs *worktreeFilesystem) Remove(filename string) error { - if err := sfs.validPath(filename); err != nil { + if err := sfs.validWritePath(filename); err != nil { return fmt.Errorf("remove: %w", err) } return sfs.Filesystem.Remove(filename) } func (sfs *worktreeFilesystem) Rename(from, to string) error { - if err := sfs.validPath(from, to); err != nil { + if err := sfs.validWritePath(from, to); err != nil { return fmt.Errorf("rename: %w", err) } return sfs.Filesystem.Rename(from, to) @@ -107,7 +122,7 @@ func (sfs *worktreeFilesystem) Lstat(filename string) (os.FileInfo, error) { } func (sfs *worktreeFilesystem) Symlink(target, link string) error { - if err := sfs.validPath(link); err != nil { + if err := sfs.validWritePath(link); err != nil { return fmt.Errorf("symlink: %w", err) } if err := sfs.validSymlinkName(link); err != nil { @@ -132,7 +147,7 @@ func (sfs *worktreeFilesystem) MkdirAll(path string, perm fs.FileMode) error { if path == "" || path == "." || path == "/" { return nil } - if err := sfs.validPath(path); err != nil { + if err := sfs.validWritePath(path); err != nil { return fmt.Errorf("mkdirall: %w", err) } return sfs.Filesystem.MkdirAll(path, perm) @@ -142,21 +157,34 @@ func (sfs *worktreeFilesystem) TempFile(_, _ string) (billy.File, error) { return nil, fmt.Errorf("tempfile: %w", errUnsupportedOperation) } -// validReadPath is like validPath but treats the empty string and "." as -// valid references to the worktree root. Read-side operations on the root -// (e.g. ReadDir(""), Lstat(".")) are legitimate; mutating the root itself -// is not, so write-side operations continue to use validPath directly. +// validReadPath is like validWritePath but treats the empty string and "." +// as valid references to the worktree root. Read-side operations on the +// root (e.g. ReadDir(""), Lstat(".")) are legitimate. Mutating the root +// itself is not, so write-side operations reject it via validPath. Reads +// are still refused through a leading symlink, so the wrapper never +// follows a planted link even on the read surface. func (sfs *worktreeFilesystem) validReadPath(p string) error { if p == "" || p == "." || p == "/" { return nil } - return sfs.validPath(p) + if err := sfs.validPath(p); err != nil { + return err + } + return sfs.validNoLeadingSymlink(p) } func (sfs *worktreeFilesystem) Chroot(path string) (billy.Filesystem, error) { if err := sfs.validReadPath(path); err != nil { return nil, fmt.Errorf("chroot: %w", err) } + // Chroot scopes a sub-filesystem to path, so the final component must + // be a real directory too: a symlink there would silently redirect the + // scope (e.g. a submodule worktree) to a target outside the tree. This + // is the "valid path, wrong target" case that validNoLeadingSymlink, + // which only inspects leading components, does not cover. + if fi, err := sfs.Filesystem.Lstat(path); err == nil && fi.Mode()&os.ModeSymlink != 0 { + return nil, fmt.Errorf("chroot: invalid path %q: is a symlink", path) + } return sfs.Filesystem.Chroot(path) } @@ -232,6 +260,50 @@ func (sfs *worktreeFilesystem) validPath(paths ...string) error { return nil } +// validWritePath validates paths for mutating operations. It layers the +// filesystem-state check validNoLeadingSymlink on top of the string-only +// checks in validPath, so a write can neither name a dangerous path nor +// reach one by traversing an existing symlink. Every mutating method on +// the wrapper funnels through here, so the leading-symlink invariant holds +// for all worktree writers without each call site having to remember it. +func (sfs *worktreeFilesystem) validWritePath(paths ...string) error { + if err := sfs.validPath(paths...); err != nil { + return err + } + return sfs.validNoLeadingSymlink(paths...) +} + +// validNoLeadingSymlink rejects paths whose leading directory components +// resolve through a symlink that already exists on the underlying +// filesystem. validPath guards the path string. This guards the on-disk +// state, so a write or delete cannot reach outside the worktree by +// traversing a symlink that a tree or an earlier step left in place. +// +// This is the fail-closed backstop for the whole class. Callers that want +// upstream's replace-and-continue behaviour (checkout) remove the blocking +// symlink first via clearBlockingSymlinks, so no symlink remains when the +// write reaches the wrapper. Callers that do not get a safe error, +// matching upstream Git refusing rather than following the link. See +// has_symlink_leading_path (symlinks.c) and the check_leading_path guard +// in unlink_entry (entry.c). +func (sfs *worktreeFilesystem) validNoLeadingSymlink(paths ...string) error { + for _, p := range paths { + for dir := filepath.Dir(p); dir != "." && dir != "" && dir != string(filepath.Separator); dir = filepath.Dir(dir) { + fi, err := sfs.Filesystem.Lstat(dir) + if err != nil { + // A missing ancestor is materialised as a real directory. + // Any other Lstat error is left for the operation itself + // to surface. + continue + } + if fi.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("invalid path %q: leading component %q is a symlink", p, dir) + } + } + } + return nil +} + // validSymlinkName checks the per-component name of a symlink for // dotfile names that attackers can use to trick a checkout into // writing a dangerous symlink. Each path component is compared diff --git a/vendor/github.com/go-git/go-git/v6/worktree_status.go b/vendor/github.com/go-git/go-git/v6/worktree_status.go index 4515ae763..3be2499b5 100644 --- a/vendor/github.com/go-git/go-git/v6/worktree_status.go +++ b/vendor/github.com/go-git/go-git/v6/worktree_status.go @@ -13,6 +13,7 @@ import ( "github.com/go-git/go-billy/v6/util" + "github.com/go-git/go-git/v6/config" "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/filemode" "github.com/go-git/go-git/v6/plumbing/format/gitignore" @@ -63,10 +64,15 @@ func (w *Worktree) StatusWithOptions(o StatusOptions) (Status, error) { hash = ref.Hash() } - return w.status(o.Strategy, hash) + cfg, err := w.r.Config() + if err != nil { + return nil, err + } + + return w.status(cfg, o.Strategy, hash) } -func (w *Worktree) status(ss StatusStrategy, commit plumbing.Hash) (Status, error) { +func (w *Worktree) status(cfg *config.Config, ss StatusStrategy, commit plumbing.Hash) (Status, error) { s, err := ss.new(w) if err != nil { return nil, err @@ -96,7 +102,7 @@ func (w *Worktree) status(ss StatusStrategy, commit plumbing.Hash) (Status, erro } } - right, err := w.diffStagingWithWorktree(false, true) + right, err := w.diffStagingWithWorktree(cfg, false, true) if err != nil { return nil, err } @@ -135,21 +141,16 @@ func nameFromAction(ch *merkletrie.Change) string { return name } -func (w *Worktree) diffStagingWithWorktree(reverse, excludeIgnoredChanges bool) (merkletrie.Changes, error) { +func (w *Worktree) diffStagingWithWorktree(cfg *config.Config, reverse, excludeIgnoredChanges bool) (merkletrie.Changes, error) { idx, err := w.r.Storer.Index() if err != nil { return nil, err } - cfg, err := w.r.Config() - if err != nil { - return nil, err - } - from := mindex.NewRootNodeWithOptions(idx, mindex.RootNodeOptions{ UpholdExecutableBit: cfg.Core.FileMode, }) - submodules, err := w.getSubmodulesStatus() + submodules, err := w.getSubmodulesStatus(cfg) if err != nil { return nil, err } @@ -188,10 +189,10 @@ func (w *Worktree) collectIgnorePatterns() []gitignore.Pattern { return append(patterns, w.Excludes...) } -func (w *Worktree) getSubmodulesStatus() (map[string]plumbing.Hash, error) { +func (w *Worktree) getSubmodulesStatus(cfg *config.Config) (map[string]plumbing.Hash, error) { o := map[string]plumbing.Hash{} - sub, err := w.Submodules() + sub, err := w.submodulesWithConfig(cfg) if err != nil { return nil, err } @@ -293,7 +294,7 @@ func (w *Worktree) Add(path string) (plumbing.Hash, error) { return w.doAdd(path, make([]gitignore.Pattern, 0), false) } -func (w *Worktree) doAddDirectory(idx *index.Index, s Status, directory string, ignorePattern []gitignore.Pattern) (added bool, err error) { +func (w *Worktree) doAddDirectory(cfg *config.Config, idx *index.Index, s Status, directory string, ignorePattern []gitignore.Pattern) (added bool, err error) { if len(ignorePattern) > 0 { m := gitignore.NewMatcher(ignorePattern) matchPath := strings.Split(directory, string(os.PathSeparator)) @@ -311,7 +312,7 @@ func (w *Worktree) doAddDirectory(idx *index.Index, s Status, directory string, } var a bool - a, _, err = w.doAddFile(idx, s, name, ignorePattern) + a, _, err = w.doAddFile(cfg, idx, s, name, ignorePattern) if err != nil { return added, err } @@ -360,6 +361,11 @@ func (w *Worktree) doAdd(path string, ignorePattern []gitignore.Pattern, skipSta }() } + cfg, err := w.r.Config() + if err != nil { + return plumbing.ZeroHash, err + } + idx, err := w.r.Storer.Index() if err != nil { return plumbing.ZeroHash, err @@ -392,11 +398,12 @@ func (w *Worktree) doAdd(path string, ignorePattern []gitignore.Pattern, skipSta } path = relPath } + path = filepath.ToSlash(path) if err != nil || !fi.IsDir() { - added, h, err = w.doAddFile(idx, s, path, ignorePattern) + added, h, err = w.doAddFile(cfg, idx, s, path, ignorePattern) } else { - added, err = w.doAddDirectory(idx, s, path, ignorePattern) + added, err = w.doAddDirectory(cfg, idx, s, path, ignorePattern) } if err != nil { @@ -431,6 +438,11 @@ func (w *Worktree) AddGlob(pattern string) error { return ErrGlobNoMatches } + cfg, err := w.r.Config() + if err != nil { + return err + } + s, err := w.Status() if err != nil { return err @@ -450,9 +462,9 @@ func (w *Worktree) AddGlob(pattern string) error { var added bool if fi.IsDir() { - added, err = w.doAddDirectory(idx, s, file, make([]gitignore.Pattern, 0)) + added, err = w.doAddDirectory(cfg, idx, s, file, make([]gitignore.Pattern, 0)) } else { - added, _, err = w.doAddFile(idx, s, file, make([]gitignore.Pattern, 0)) + added, _, err = w.doAddFile(cfg, idx, s, file, make([]gitignore.Pattern, 0)) } if err != nil { @@ -474,7 +486,7 @@ func (w *Worktree) AddGlob(pattern string) error { // doAddFile create a new blob from path and update the index, added is true if // the file added is different from the index. // if s status is nil will skip the status check and update the index anyway -func (w *Worktree) doAddFile(idx *index.Index, s Status, path string, ignorePattern []gitignore.Pattern) (added bool, h plumbing.Hash, err error) { +func (w *Worktree) doAddFile(cfg *config.Config, idx *index.Index, s Status, path string, ignorePattern []gitignore.Pattern) (added bool, h plumbing.Hash, err error) { if s != nil && s.File(path).Worktree == Unmodified { return false, h, nil } @@ -487,7 +499,7 @@ func (w *Worktree) doAddFile(idx *index.Index, s Status, path string, ignorePatt } } - h, err = w.copyFileToStorage(path) + h, err = w.copyFileToStorage(cfg, path) if err != nil { if os.IsNotExist(err) { added = true @@ -504,7 +516,7 @@ func (w *Worktree) doAddFile(idx *index.Index, s Status, path string, ignorePatt return true, h, err } -func (w *Worktree) copyFileToStorage(path string) (hash plumbing.Hash, err error) { +func (w *Worktree) copyFileToStorage(cfg *config.Config, path string) (hash plumbing.Hash, err error) { fi, err := w.filesystem.Lstat(path) if err != nil { return plumbing.ZeroHash, err @@ -524,7 +536,7 @@ func (w *Worktree) copyFileToStorage(path string) (hash plumbing.Hash, err error if fi.Mode()&os.ModeSymlink != 0 { err = w.fillEncodedObjectFromSymlink(writer, path, fi) } else { - err = w.fillEncodedObjectFromFile(writer, path, fi) + err = w.fillEncodedObjectFromFile(cfg, writer, path, fi) } if err != nil { @@ -534,18 +546,13 @@ func (w *Worktree) copyFileToStorage(path string) (hash plumbing.Hash, err error return w.r.Storer.SetEncodedObject(obj) } -func (w *Worktree) fillEncodedObjectFromFile(dst io.Writer, path string, _ os.FileInfo) (err error) { +func (w *Worktree) fillEncodedObjectFromFile(cfg *config.Config, dst io.Writer, path string, _ os.FileInfo) (err error) { file, err := w.filesystem.Open(path) if err != nil { return err } defer ioutil.CheckClose(file, &err) - cfg, err := w.r.Config() - if err != nil { - return err - } - switch cfg.Core.AutoCRLF { case "true", "input": br := sync.GetBufioReader(file) @@ -743,8 +750,10 @@ func (w *Worktree) RemoveGlob(pattern string) error { } dir, _ := filepath.Split(file) - if err := w.removeEmptyDirectory(dir); err != nil { - return err + if dir != "" { + if err := w.removeEmptyDirectory(dir); err != nil { + return err + } } } diff --git a/vendor/github.com/go-git/go-git/v6/x/fdpool/pool.go b/vendor/github.com/go-git/go-git/v6/x/fdpool/pool.go new file mode 100644 index 000000000..b7eccbbb9 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v6/x/fdpool/pool.go @@ -0,0 +1,256 @@ +// Package fdpool implements a fixed-capacity LRU cache of +// resources that own a file descriptor. The pool is used by +// storage/filesystem to bound the number of open .pack/.idx/.rev +// descriptors across a Storage. Members register on first FD +// acquire and report subsequent acquires via Touch; when the LRU +// length exceeds the configured capacity, the least-recently- +// touched Member is evicted via Member.ReleaseNow. +// +// A zero or negative capacity yields a no-op pool: Touch and +// Forget do nothing, never evict, and Stats reports the raw +// capacity value. This lets callers wire the pool unconditionally +// even when pooling is disabled. +package fdpool + +import ( + "container/list" + "sync" +) + +// Member is the interface a pool entry must implement. On eviction +// the Pool calls ReleaseNow on the Member; the per-Member +// registration token is the [Handle] the caller passes alongside. +// +// ReleaseNow must close the underlying FD without permanently +// invalidating the Member: a subsequent acquire from the Member's +// owner should reopen and re-register (via Touch) automatically. +// Returning an error is fine; the pool discards it and continues, +// since eviction is best-effort. +type Member interface { + ReleaseNow() error +} + +// Pinnable is an optional Member-side interface that the Pool +// consults during eviction. When evicting because capacity is +// exceeded, the Pool walks the LRU back-to-front and skips +// Members whose Pinned reports true; if every Member is pinned, +// the Pool falls back to evicting the LRU tail unconditionally. +// This matches canonical Git's find_lru_pack policy +// (packfile.c:482-530), where the in-use preference is a hint +// rather than a guarantee. +// +// Members that do not implement Pinnable are treated as unpinned, +// preserving the unconditional-LRU behaviour from the pool's +// first release. +type Pinnable interface { + Pinned() bool +} + +// Handle is the per-Member registration token that the Pool uses +// to locate a Member in its internal LRU. Each Member instance +// owns exactly one Handle and passes a pointer to it alongside +// the Member on every [Pool.Touch] and [Pool.Forget] call. +// +// The zero value is valid and reports "not yet registered". The +// Pool reads and writes Handle fields exclusively under its +// internal mutex; callers must not touch them directly. +// +// Using a per-Member Handle (rather than the Member itself as a +// map key) removes the implicit "Member must be comparable" +// requirement that an internal map[Member]*list.Element would +// impose — a Member with a non-comparable concrete type would +// otherwise panic on registration. +type Handle struct { + elem *list.Element +} + +// entry is the per-Member record stored in the LRU list. Holding +// a back-pointer to the Member's Handle lets eviction clear +// h.elem before invoking ReleaseNow, so a concurrent re-Touch on +// the evicted Member observes h.elem == nil and re-registers +// cleanly. +type entry struct { + m Member + h *Handle +} + +// Stats captures a snapshot of the pool's runtime state. The +// values are observational — they may change immediately after +// Stats returns. +type Stats struct { + // Capacity is the configured maximum number of Members the + // pool will keep without evicting. <= 0 means the pool is a + // no-op. + Capacity int + // Active is the current number of registered Members. + Active int + // Hits is the cumulative count of Touch calls that targeted + // an already-registered Member (cache hit). + Hits uint64 + // Evictions is the cumulative count of Members evicted via + // ReleaseNow because capacity was exceeded. + Evictions uint64 + // EvictionFailures is the cumulative count of evictions whose + // Member.ReleaseNow returned a non-nil error. The eviction + // itself still completes (the Member is removed from the LRU + // regardless); the counter exists so operators can distinguish + // clean evictions from those that hit a Close error. + EvictionFailures uint64 + // PinnedSkips is the cumulative count of Pinnable Members the + // eviction walk skipped because Pinned() reported true. The + // counter is incremented in both cases: when an unpinned + // victim was eventually found further forward, and when every + // Member was pinned and the walk fell back to evicting the + // LRU tail anyway. Useful for spotting churn under sustained + // concurrent load. + PinnedSkips uint64 +} + +// Pool is a fixed-capacity LRU of Members. The zero value is not +// usable; construct via New. +type Pool struct { + mu sync.Mutex + capacity int + lru *list.List // front = MRU, back = LRU; values are *entry + hits uint64 + evictions uint64 + evictionFailures uint64 + pinnedSkips uint64 +} + +// New constructs a Pool with the given capacity. capacity <= 0 +// returns a Pool whose Touch and Forget are no-ops and that never +// evicts; callers may use New(0) to disable pooling without +// special-casing the call sites. +func New(capacity int) *Pool { + if capacity <= 0 { + return &Pool{capacity: capacity} + } + return &Pool{ + capacity: capacity, + lru: list.New(), + } +} + +// Touch reports an FD-active transition on m: either the first +// acquire (m is newly registered) or a subsequent acquire on an +// already-registered Member (m moves to MRU front). The Handle h +// identifies m in the LRU; the same h must be passed on every +// Touch/Forget for the same Member. If the resulting active count +// exceeds the capacity, the LRU tail is evicted via +// Member.ReleaseNow. Eviction never targets m itself. +// +// Touch is safe to call from multiple goroutines. +func (p *Pool) Touch(m Member, h *Handle) { + if p.capacity <= 0 || m == nil || h == nil { + return + } + p.mu.Lock() + defer p.mu.Unlock() + + if h.elem != nil { + p.lru.MoveToFront(h.elem) + p.hits++ + return + } + h.elem = p.lru.PushFront(&entry{m: m, h: h}) + + // Evict if we exceeded capacity. Two-pass victim selection: + // walk the LRU back-to-front and prefer the LRU-most Member + // whose Pinnable.Pinned reports false; fall back to evicting + // the LRU tail unconditionally if every Member is pinned. + // Matches canonical Git's find_lru_pack (packfile.c:482-530). + // Members that do not implement Pinnable are treated as + // unpinned, preserving the unconditional-LRU behaviour from + // the pool's first release. The eviction target cannot be m + // (we just inserted it at the front) so this never evicts + // the caller's own Member. + if p.lru.Len() > p.capacity { + var victimElem *list.Element + for e := p.lru.Back(); e != nil && e != h.elem; e = e.Prev() { + ent := e.Value.(*entry) + if pn, ok := ent.m.(Pinnable); !ok || !pn.Pinned() { + victimElem = e + break + } + p.pinnedSkips++ + } + if victimElem == nil { + victimElem = p.lru.Back() + } + if victimElem != nil { + victimEnt := victimElem.Value.(*entry) + p.lru.Remove(victimElem) + // Clear the victim's Handle before dropping p.mu so a + // concurrent re-Touch on the same Member observes + // h.elem == nil and re-registers cleanly. Without + // this clear the re-Touch would call MoveToFront on + // the removed element (a no-op in container/list), + // silently leaving the Member unregistered while its + // Handle still claimed it was. + victimEnt.h.elem = nil + p.evictions++ + // Release the lock while calling ReleaseNow to avoid + // a lock-ordering hazard against the Member's own + // mutex (SharedFile.mu in the real wiring). The rule + // is: Member calls into Pool only after releasing + // Member.mu (see SharedFile.Acquire and Close). The + // Pool→Member call through Pinned above is + // deadlock-free because no Member call holds s.mu + // while waiting for p.mu. + // + // ReleaseNow's error is intentionally discarded per + // the Member contract above: eviction is best-effort. + // This contrasts with the errors.Join pattern used + // elsewhere (e.g. packhandle.doClose) and is recorded + // here so the asymmetry doesn't read as an oversight. + p.mu.Unlock() + err := victimEnt.m.ReleaseNow() + p.mu.Lock() + if err != nil { + p.evictionFailures++ + } + } + } +} + +// Forget removes the Handle's Member from the LRU without invoking +// ReleaseNow. Used when the Member is permanently closed by its +// owner. +// +// Forget is idempotent: calling it with a Handle that was never +// registered, or that has already been Forgotten or evicted, is a +// no-op. +func (p *Pool) Forget(h *Handle) { + if p.capacity <= 0 || h == nil { + return + } + p.mu.Lock() + defer p.mu.Unlock() + + if h.elem == nil { + return + } + p.lru.Remove(h.elem) + h.elem = nil +} + +// Stats returns a snapshot of the pool's current statistics. +// Counters are monotonic; subtraction across two Stats snapshots +// yields per-interval rates. +func (p *Pool) Stats() Stats { + p.mu.Lock() + defer p.mu.Unlock() + var active int + if p.lru != nil { + active = p.lru.Len() + } + return Stats{ + Capacity: p.capacity, + Active: active, + Hits: p.hits, + Evictions: p.evictions, + EvictionFailures: p.evictionFailures, + PinnedSkips: p.pinnedSkips, + } +} diff --git a/vendor/github.com/go-git/go-git/v6/x/plugin/config/static.go b/vendor/github.com/go-git/go-git/v6/x/plugin/config/static.go index 2ede5bb1d..fc081c434 100644 --- a/vendor/github.com/go-git/go-git/v6/x/plugin/config/static.go +++ b/vendor/github.com/go-git/go-git/v6/x/plugin/config/static.go @@ -66,19 +66,19 @@ func cloneRemotes(m map[string]*config.RemoteConfig) map[string]*config.RemoteCo return cp } -func cloneURLs(m map[string]*config.URL) map[string]*config.URL { - if m == nil { +func cloneURLs(s []*config.URL) []*config.URL { + if s == nil { return nil } - cp := make(map[string]*config.URL, len(m)) - for k, v := range m { + cp := make([]*config.URL, len(s)) + for i, v := range s { if v == nil { - cp[k] = nil + cp[i] = nil continue } cloned := *v cloned.InsteadOfs = cloneSlice(v.InsteadOfs) - cp[k] = &cloned + cp[i] = &cloned } return cp } diff --git a/vendor/github.com/go-git/go-git/v6/x/plugin/plugin_signer.go b/vendor/github.com/go-git/go-git/v6/x/plugin/plugin_signer.go index 0e0fc05c4..4c186f9ad 100644 --- a/vendor/github.com/go-git/go-git/v6/x/plugin/plugin_signer.go +++ b/vendor/github.com/go-git/go-git/v6/x/plugin/plugin_signer.go @@ -1,14 +1,19 @@ package plugin -import "io" +import ( + "context" + "io" +) const objectSignerPlugin Name = "object-signer" var objectSigner = newKey[Signer](objectSignerPlugin) // Signer signs arbitrary data and returns the detached signature bytes. +// ctx cancels signers that perform external or remote work; purely local +// signers may ignore it. type Signer interface { - Sign(message io.Reader) ([]byte, error) + Sign(ctx context.Context, message io.Reader) ([]byte, error) } // ObjectSigner returns the key used to register an object-signing plugin. diff --git a/vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md b/vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md index 3879f14aa..f349b3357 100644 --- a/vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md +++ b/vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md @@ -8,6 +8,10 @@ but only releases after v1.0.3 properly adhere to it. ## [Unreleased] +## [1.4.1] - 2026-08-02 +### Fixed +- Corrected `D50ToD65` to use the CSS Color 4 matrix inverse of `D65ToD50` (#85). + ## [1.4.0] - 2026-03-28 ### Added - Constructors, decomposers, and blend functions for the CSS Color Level 4 wide-gamut RGB color spaces `DisplayP3`, `A98Rgb`, `ProPhotoRgb`, and `Rec2020` (#81) diff --git a/vendor/github.com/lucasb-eyer/go-colorful/widegamut.go b/vendor/github.com/lucasb-eyer/go-colorful/widegamut.go index 6805a2b96..63c3e878e 100644 --- a/vendor/github.com/lucasb-eyer/go-colorful/widegamut.go +++ b/vendor/github.com/lucasb-eyer/go-colorful/widegamut.go @@ -10,9 +10,9 @@ import "math" // Bradford chromatic adaptation between D50 and D65 illuminants. func D50ToD65(x, y, z float64) (xo, yo, zo float64) { - xo = 0.9555766*x - 0.0230393*y + 0.0631636*z - yo = -0.0282895*x + 1.0099416*y + 0.0210077*z - zo = 0.0122982*x - 0.0204830*y + 1.3299098*z + xo = 0.9554734527042182*x - 0.023098536874261423*y + 0.06325964552894382*z + yo = -0.028369706963208136*x + 1.0099954580058226*y + 0.021041398966943008*z + zo = 0.012314001688319899*x - 0.020507696433477912*y + 1.3303659366080753*z return } diff --git a/vendor/github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild/internal/pulumiUtilities.go b/vendor/github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild/internal/pulumiUtilities.go index 931641d0e..cba09323f 100644 --- a/vendor/github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild/internal/pulumiUtilities.go +++ b/vendor/github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild/internal/pulumiUtilities.go @@ -165,7 +165,7 @@ func callPlainInner( func PkgResourceDefaultOpts(opts []pulumi.ResourceOption) []pulumi.ResourceOption { defaults := []pulumi.ResourceOption{} - version := semver.MustParse("0.0.21") + version := semver.MustParse("0.0.22") if !version.Equals(semver.Version{}) { defaults = append(defaults, pulumi.Version(version.String())) } @@ -176,7 +176,7 @@ func PkgResourceDefaultOpts(opts []pulumi.ResourceOption) []pulumi.ResourceOptio func PkgInvokeDefaultOpts(opts []pulumi.InvokeOption) []pulumi.InvokeOption { defaults := []pulumi.InvokeOption{} - version := semver.MustParse("0.0.21") + version := semver.MustParse("0.0.22") if !version.Equals(semver.Version{}) { defaults = append(defaults, pulumi.Version(version.String())) } diff --git a/vendor/github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild/pulumi-plugin.json b/vendor/github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild/pulumi-plugin.json index f95a6988f..460ba6fc7 100644 --- a/vendor/github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild/pulumi-plugin.json +++ b/vendor/github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild/pulumi-plugin.json @@ -1,5 +1,5 @@ { "resource": true, "name": "docker-build", - "version": "0.0.21" + "version": "0.0.22" } diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/actionsEnvironmentVariable.go b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/actionsEnvironmentVariable.go index 363a52e1c..632e43dd5 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/actionsEnvironmentVariable.go +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/actionsEnvironmentVariable.go @@ -63,14 +63,14 @@ import ( // return err // } // exampleRepositoryEnvironment, err := github.NewRepositoryEnvironment(ctx, "example", &github.RepositoryEnvironmentArgs{ -// Repository: pulumi.String(pulumi.String(example.Name)), +// Repository: pulumi.String(example.Name), // Environment: pulumi.String("example_environment"), // }) // if err != nil { // return err // } // _, err = github.NewActionsEnvironmentVariable(ctx, "example", &github.ActionsEnvironmentVariableArgs{ -// Repository: pulumi.String(pulumi.String(example.Name)), +// Repository: pulumi.String(example.Name), // Environment: exampleRepositoryEnvironment.Environment, // VariableName: pulumi.String("example_variable_name"), // Value: pulumi.String("example-value"), diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/actionsOrganizationSecret.go b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/actionsOrganizationSecret.go index 6cdaa1c80..543509df8 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/actionsOrganizationSecret.go +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/actionsOrganizationSecret.go @@ -83,7 +83,7 @@ import ( // Visibility: pulumi.String("selected"), // Value: pulumi.Any(someSecretString), // SelectedRepositoryIds: pulumi.IntArray{ -// pulumi.Int(pulumi.Int(repo.RepoId)), +// pulumi.Int(repo.RepoId), // }, // }) // if err != nil { @@ -94,7 +94,7 @@ import ( // Visibility: pulumi.String("selected"), // ValueEncrypted: pulumi.Any(someEncryptedSecretString), // SelectedRepositoryIds: pulumi.IntArray{ -// pulumi.Int(pulumi.Int(repo.RepoId)), +// pulumi.Int(repo.RepoId), // }, // }) // if err != nil { diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/actionsOrganizationVariable.go b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/actionsOrganizationVariable.go index a8934cff1..c368f00fe 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/actionsOrganizationVariable.go +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/actionsOrganizationVariable.go @@ -66,7 +66,7 @@ import ( // Visibility: pulumi.String("selected"), // Value: pulumi.String("example_variable_value"), // SelectedRepositoryIds: pulumi.IntArray{ -// pulumi.Int(pulumi.Int(repo.RepoId)), +// pulumi.Int(repo.RepoId), // }, // }) // if err != nil { diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/branchProtection.go b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/branchProtection.go index d4c25d8ff..6de29c8f7 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/branchProtection.go +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/branchProtection.go @@ -88,7 +88,7 @@ import ( // }, // }, // ForcePushBypassers: pulumi.StringArray{ -// pulumi.String(pulumi.String(example.NodeId)), +// pulumi.String(example.NodeId), // pulumi.String("/exampleuser"), // pulumi.String("exampleorganization/exampleteam"), // }, diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/codespacesOrganizationSecretRepositories.go b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/codespacesOrganizationSecretRepositories.go index 92b486599..d3348cbbe 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/codespacesOrganizationSecretRepositories.go +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/codespacesOrganizationSecretRepositories.go @@ -41,7 +41,7 @@ import ( // _, err = github.NewCodespacesOrganizationSecretRepositories(ctx, "org_secret_repos", &github.CodespacesOrganizationSecretRepositoriesArgs{ // SecretName: pulumi.String("existing_secret_name"), // SelectedRepositoryIds: pulumi.IntArray{ -// pulumi.Int(pulumi.Int(repo.RepoId)), +// pulumi.Int(repo.RepoId), // }, // }) // if err != nil { diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/dependabotOrganizationSecret.go b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/dependabotOrganizationSecret.go index 8308af2a2..9e7c006c8 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/dependabotOrganizationSecret.go +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/dependabotOrganizationSecret.go @@ -83,7 +83,7 @@ import ( // Visibility: pulumi.String("selected"), // Value: pulumi.Any(someSecretString), // SelectedRepositoryIds: pulumi.IntArray{ -// pulumi.Int(pulumi.Int(repo.RepoId)), +// pulumi.Int(repo.RepoId), // }, // }) // if err != nil { @@ -94,7 +94,7 @@ import ( // Visibility: pulumi.String("selected"), // ValueEncrypted: pulumi.Any(someEncryptedSecretString), // SelectedRepositoryIds: pulumi.IntArray{ -// pulumi.Int(pulumi.Int(repo.RepoId)), +// pulumi.Int(repo.RepoId), // }, // }) // if err != nil { diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/enterpriseActionsPermissions.go b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/enterpriseActionsPermissions.go index d889a778f..2b3af4255 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/enterpriseActionsPermissions.go +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/enterpriseActionsPermissions.go @@ -49,7 +49,7 @@ import ( // }, // EnabledOrganizationsConfig: &github.EnterpriseActionsPermissionsEnabledOrganizationsConfigArgs{ // OrganizationIds: pulumi.IntArray{ -// pulumi.Int(pulumi.String(example_org.Id)), +// pulumi.String(example_org.Id), // }, // }, // }) diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/enterpriseActionsRunnerGroup.go b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/enterpriseActionsRunnerGroup.go index cfc6b60ed..74cb5f84a 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/enterpriseActionsRunnerGroup.go +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/enterpriseActionsRunnerGroup.go @@ -36,7 +36,7 @@ import ( // return err // } // enterpriseOrganization, err := github.NewEnterpriseOrganization(ctx, "enterprise_organization", &github.EnterpriseOrganizationArgs{ -// EnterpriseId: pulumi.String(pulumi.String(enterprise.Id)), +// EnterpriseId: pulumi.String(enterprise.Id), // Name: pulumi.String("my-organization"), // BillingEmail: pulumi.String("octocat@octo.cat"), // AdminLogins: pulumi.StringArray{ @@ -48,7 +48,7 @@ import ( // } // _, err = github.NewEnterpriseActionsRunnerGroup(ctx, "example", &github.EnterpriseActionsRunnerGroupArgs{ // Name: pulumi.String("my-awesome-runner-group"), -// EnterpriseSlug: pulumi.String(pulumi.String(enterprise.Slug)), +// EnterpriseSlug: pulumi.String(enterprise.Slug), // AllowsPublicRepositories: pulumi.Bool(true), // Visibility: pulumi.String("selected"), // SelectedOrganizationIds: pulumi.IntArray{ diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/getAppToken.go b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/getAppToken.go index 8d247d8bf..4207135e7 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/getAppToken.go +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/getAppToken.go @@ -28,12 +28,16 @@ import ( // // func main() { // pulumi.Run(func(ctx *pulumi.Context) error { +// invokeFile, err := std.File(ctx, &std.FileArgs{ +// Input: "foo/bar.pem", +// }, nil) +// if err != nil { +// return err +// } // _, err := github.GetAppToken(ctx, &github.GetAppTokenArgs{ // AppId: "123456", // InstallationId: "78910", -// PemFile: std.File(ctx, &std.FileArgs{ -// Input: "foo/bar.pem", -// }, nil).Result, +// PemFile: invokeFile.Result, // }, nil) // if err != nil { // return err diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/internal/pulumiUtilities.go b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/internal/pulumiUtilities.go index e41a5e6ee..7f0621616 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/internal/pulumiUtilities.go +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/internal/pulumiUtilities.go @@ -165,7 +165,7 @@ func callPlainInner( func PkgResourceDefaultOpts(opts []pulumi.ResourceOption) []pulumi.ResourceOption { defaults := []pulumi.ResourceOption{} - version := semver.MustParse("6.14.0") + version := semver.MustParse("6.14.1") if !version.Equals(semver.Version{}) { defaults = append(defaults, pulumi.Version(version.String())) } @@ -176,7 +176,7 @@ func PkgResourceDefaultOpts(opts []pulumi.ResourceOption) []pulumi.ResourceOptio func PkgInvokeDefaultOpts(opts []pulumi.InvokeOption) []pulumi.InvokeOption { defaults := []pulumi.InvokeOption{} - version := semver.MustParse("6.14.0") + version := semver.MustParse("6.14.1") if !version.Equals(semver.Version{}) { defaults = append(defaults, pulumi.Version(version.String())) } diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/organizationProject.go b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/organizationProject.go index 2a5ee8c6b..b9ec82e0d 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/organizationProject.go +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/organizationProject.go @@ -11,7 +11,7 @@ import ( "github.com/pulumi/pulumi/sdk/v3/go/pulumi" ) -// !> **Warning:** This resource no longer works as the [Projects (classic) REST API](https://docs.github.com/en/rest/projects/projects?apiVersion=2022-11-28) has been [removed](https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/) and as such has been deprecated. It will be removed in a future release. +// > **Warning:** This resource no longer works as the [Projects (classic) REST API](https://docs.github.com/en/rest/projects/projects?apiVersion=2022-11-28) has been [removed](https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/) and as such has been deprecated. It will be removed in a future release. // // This resource allows you to create and manage projects for GitHub organization. // diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/projectCard.go b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/projectCard.go index ad7c22e79..b27f96f3a 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/projectCard.go +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/projectCard.go @@ -12,7 +12,7 @@ import ( "github.com/pulumi/pulumi/sdk/v3/go/pulumi" ) -// !> **Warning:** This resource no longer works as the [Projects (classic) REST API](https://docs.github.com/en/rest/projects/projects?apiVersion=2022-11-28) has been [removed](https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/) and as such has been deprecated. It will be removed in a future release. +// > **Warning:** This resource no longer works as the [Projects (classic) REST API](https://docs.github.com/en/rest/projects/projects?apiVersion=2022-11-28) has been [removed](https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/) and as such has been deprecated. It will be removed in a future release. // // This resource allows you to create and manage cards for GitHub projects. // diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/projectColumn.go b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/projectColumn.go index 246d06110..ee6555beb 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/projectColumn.go +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/projectColumn.go @@ -12,7 +12,7 @@ import ( "github.com/pulumi/pulumi/sdk/v3/go/pulumi" ) -// !> **Warning:** This resource no longer works as the [Projects (classic) REST API](https://docs.github.com/en/rest/projects/projects?apiVersion=2022-11-28) has been [removed](https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/) and as such has been deprecated. It will be removed in a future release. +// > **Warning:** This resource no longer works as the [Projects (classic) REST API](https://docs.github.com/en/rest/projects/projects?apiVersion=2022-11-28) has been [removed](https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/) and as such has been deprecated. It will be removed in a future release. // // This resource allows you to create and manage columns for GitHub projects. // diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/pulumi-plugin.json b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/pulumi-plugin.json index be88203dd..95f4ad9c9 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/pulumi-plugin.json +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/pulumi-plugin.json @@ -1,5 +1,5 @@ { "resource": true, "name": "github", - "version": "6.14.0" + "version": "6.14.1" } diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/repositoryEnvironment.go b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/repositoryEnvironment.go index 421a03ec9..f382b831e 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/repositoryEnvironment.go +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/repositoryEnvironment.go @@ -48,7 +48,7 @@ import ( // Reviewers: github.RepositoryEnvironmentReviewerArray{ // &github.RepositoryEnvironmentReviewerArgs{ // Users: pulumi.IntArray{ -// pulumi.Int(pulumi.String(current.Id)), +// pulumi.String(current.Id), // }, // }, // }, diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/repositoryEnvironmentDeploymentPolicy.go b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/repositoryEnvironmentDeploymentPolicy.go index b813078ed..769227a9c 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/repositoryEnvironmentDeploymentPolicy.go +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/repositoryEnvironmentDeploymentPolicy.go @@ -49,7 +49,7 @@ import ( // Reviewers: github.RepositoryEnvironmentReviewerArray{ // &github.RepositoryEnvironmentReviewerArgs{ // Users: pulumi.IntArray{ -// pulumi.Int(pulumi.String(current.Id)), +// pulumi.String(current.Id), // }, // }, // }, @@ -108,7 +108,7 @@ import ( // Reviewers: github.RepositoryEnvironmentReviewerArray{ // &github.RepositoryEnvironmentReviewerArgs{ // Users: pulumi.IntArray{ -// pulumi.Int(pulumi.String(current.Id)), +// pulumi.String(current.Id), // }, // }, // }, diff --git a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/repositoryProject.go b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/repositoryProject.go index 5f8227c7d..4b4427674 100644 --- a/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/repositoryProject.go +++ b/vendor/github.com/pulumi/pulumi-github/sdk/v6/go/github/repositoryProject.go @@ -12,7 +12,7 @@ import ( "github.com/pulumi/pulumi/sdk/v3/go/pulumi" ) -// !> **Warning:** This resource no longer works as the [Projects (classic) REST API](https://docs.github.com/en/rest/projects/projects?apiVersion=2022-11-28) has been [removed](https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/) and as such has been deprecated. It will be removed in a future release. +// > **Warning:** This resource no longer works as the [Projects (classic) REST API](https://docs.github.com/en/rest/projects/projects?apiVersion=2022-11-28) has been [removed](https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/) and as such has been deprecated. It will be removed in a future release. // // This resource allows you to create and manage projects for GitHub repository. // diff --git a/vendor/modules.txt b/vendor/modules.txt index b9c177448..180df9138 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -237,7 +237,7 @@ github.com/aws/amazon-ec2-instance-selector/v3/pkg/instancetypes github.com/aws/amazon-ec2-instance-selector/v3/pkg/selector github.com/aws/amazon-ec2-instance-selector/v3/pkg/selector/outputs github.com/aws/amazon-ec2-instance-selector/v3/pkg/sorter -# github.com/aws/aws-sdk-go-v2 v1.43.2 +# github.com/aws/aws-sdk-go-v2 v1.43.3 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/aws github.com/aws/aws-sdk-go-v2/aws/arn @@ -264,15 +264,15 @@ github.com/aws/aws-sdk-go-v2/internal/shareddefaults github.com/aws/aws-sdk-go-v2/internal/strings github.com/aws/aws-sdk-go-v2/internal/sync/singleflight github.com/aws/aws-sdk-go-v2/internal/timeconv -# github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.15 +# github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.16 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/eventstreamapi -# github.com/aws/aws-sdk-go-v2/config v1.32.31 +# github.com/aws/aws-sdk-go-v2/config v1.32.34 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/config github.com/aws/aws-sdk-go-v2/config/internal/ini -# github.com/aws/aws-sdk-go-v2/credentials v1.19.30 +# github.com/aws/aws-sdk-go-v2/credentials v1.19.33 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/credentials github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds @@ -282,85 +282,85 @@ github.com/aws/aws-sdk-go-v2/credentials/logincreds github.com/aws/aws-sdk-go-v2/credentials/processcreds github.com/aws/aws-sdk-go-v2/credentials/ssocreds github.com/aws/aws-sdk-go-v2/credentials/stscreds -# github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 +# github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.34 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/feature/ec2/imds github.com/aws/aws-sdk-go-v2/feature/ec2/imds/internal/config -# github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.33 +# github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.34 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/internal/configsources -# github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.33 +# github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.34 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 -# github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.34 +# github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.35 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/internal/v4a github.com/aws/aws-sdk-go-v2/internal/v4a/internal/crypto github.com/aws/aws-sdk-go-v2/internal/v4a/internal/v4 -# github.com/aws/aws-sdk-go-v2/service/ec2 v1.318.0 +# github.com/aws/aws-sdk-go-v2/service/ec2 v1.318.1 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/ec2 github.com/aws/aws-sdk-go-v2/service/ec2/internal/endpoints github.com/aws/aws-sdk-go-v2/service/ec2/types -# github.com/aws/aws-sdk-go-v2/service/ecs v1.89.2 +# github.com/aws/aws-sdk-go-v2/service/ecs v1.89.3 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/ecs github.com/aws/aws-sdk-go-v2/service/ecs/document github.com/aws/aws-sdk-go-v2/service/ecs/internal/document github.com/aws/aws-sdk-go-v2/service/ecs/internal/endpoints github.com/aws/aws-sdk-go-v2/service/ecs/types -# github.com/aws/aws-sdk-go-v2/service/iam v1.57.0 +# github.com/aws/aws-sdk-go-v2/service/iam v1.57.1 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/iam github.com/aws/aws-sdk-go-v2/service/iam/internal/endpoints github.com/aws/aws-sdk-go-v2/service/iam/types -# github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.14 +# github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.15 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding -# github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.26 +# github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.27 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/internal/checksum -# github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.33 +# github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.34 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url -# github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.34 +# github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.35 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/internal/s3shared github.com/aws/aws-sdk-go-v2/service/internal/s3shared/arn github.com/aws/aws-sdk-go-v2/service/internal/s3shared/config -# github.com/aws/aws-sdk-go-v2/service/pricing v1.44.2 +# github.com/aws/aws-sdk-go-v2/service/pricing v1.44.3 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/pricing github.com/aws/aws-sdk-go-v2/service/pricing/internal/endpoints github.com/aws/aws-sdk-go-v2/service/pricing/types -# github.com/aws/aws-sdk-go-v2/service/s3 v1.106.2 +# github.com/aws/aws-sdk-go-v2/service/s3 v1.106.3 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/s3 github.com/aws/aws-sdk-go-v2/service/s3/internal/arn github.com/aws/aws-sdk-go-v2/service/s3/internal/customizations github.com/aws/aws-sdk-go-v2/service/s3/internal/endpoints github.com/aws/aws-sdk-go-v2/service/s3/types -# github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 +# github.com/aws/aws-sdk-go-v2/service/signin v1.5.3 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/signin github.com/aws/aws-sdk-go-v2/service/signin/internal/endpoints github.com/aws/aws-sdk-go-v2/service/signin/types -# github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 +# github.com/aws/aws-sdk-go-v2/service/sso v1.33.3 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/sso github.com/aws/aws-sdk-go-v2/service/sso/internal/endpoints github.com/aws/aws-sdk-go-v2/service/sso/types -# github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 +# github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.3 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/ssooidc github.com/aws/aws-sdk-go-v2/service/ssooidc/internal/endpoints github.com/aws/aws-sdk-go-v2/service/ssooidc/types -# github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 +# github.com/aws/aws-sdk-go-v2/service/sts v1.45.3 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/sts github.com/aws/aws-sdk-go-v2/service/sts/internal/endpoints github.com/aws/aws-sdk-go-v2/service/sts/types -# github.com/aws/smithy-go v1.27.5 +# github.com/aws/smithy-go v1.27.6 ## explicit; go 1.24 github.com/aws/smithy-go github.com/aws/smithy-go/auth @@ -519,15 +519,18 @@ github.com/go-git/go-billy/v6/helper/polyfill github.com/go-git/go-billy/v6/memfs github.com/go-git/go-billy/v6/osfs github.com/go-git/go-billy/v6/util -# github.com/go-git/go-git/v6 v6.0.0-alpha.4 +# github.com/go-git/go-git/v6 v6.0.0-alpha.5 ## explicit; go 1.25.0 github.com/go-git/go-git/v6 github.com/go-git/go-git/v6/config github.com/go-git/go-git/v6/internal/archive +github.com/go-git/go-git/v6/internal/packhandle github.com/go-git/go-git/v6/internal/pathutil github.com/go-git/go-git/v6/internal/reference github.com/go-git/go-git/v6/internal/repository github.com/go-git/go-git/v6/internal/revision +github.com/go-git/go-git/v6/internal/sharedfile +github.com/go-git/go-git/v6/internal/transport github.com/go-git/go-git/v6/internal/url github.com/go-git/go-git/v6/plumbing github.com/go-git/go-git/v6/plumbing/cache @@ -575,6 +578,7 @@ github.com/go-git/go-git/v6/utils/merkletrie/internal/frame github.com/go-git/go-git/v6/utils/merkletrie/noder github.com/go-git/go-git/v6/utils/sync github.com/go-git/go-git/v6/utils/trace +github.com/go-git/go-git/v6/x/fdpool github.com/go-git/go-git/v6/x/plugin github.com/go-git/go-git/v6/x/plugin/config github.com/go-git/go-git/v6/x/plugin/zlib @@ -766,7 +770,7 @@ github.com/kylelemons/godebug/pretty ## explicit; go 1.24.0 github.com/leodido/go-urn github.com/leodido/go-urn/scim/schema -# github.com/lucasb-eyer/go-colorful v1.4.0 +# github.com/lucasb-eyer/go-colorful v1.4.1 ## explicit; go 1.12 github.com/lucasb-eyer/go-colorful # github.com/mapt-oss/pulumi-ibmcloud/sdk v0.0.12 @@ -922,16 +926,16 @@ github.com/pulumi/pulumi-azure-native-sdk/v3/utilities ## explicit; go 1.24.7 github.com/pulumi/pulumi-command/sdk/go/command/internal github.com/pulumi/pulumi-command/sdk/go/command/remote -# github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild v0.0.21 -## explicit; go 1.25.8 +# github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild v0.0.22 +## explicit; go 1.26.0 github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild github.com/pulumi/pulumi-docker-build/sdk/go/dockerbuild/internal # github.com/pulumi/pulumi-docker/sdk/v4 v4.5.8 ## explicit; go 1.21 github.com/pulumi/pulumi-docker/sdk/v4/go/docker github.com/pulumi/pulumi-docker/sdk/v4/go/docker/internal -# github.com/pulumi/pulumi-github/sdk/v6 v6.14.0 -## explicit; go 1.25.8 +# github.com/pulumi/pulumi-github/sdk/v6 v6.14.1 +## explicit; go 1.25.11 github.com/pulumi/pulumi-github/sdk/v6/go/github github.com/pulumi/pulumi-github/sdk/v6/go/github/internal # github.com/pulumi/pulumi-gitlab/sdk/v9 v9.11.1