~edwargix/git.sr.ht

eb6daa29f0a20325f10cccad1625dc2e29d39da0 — Drew DeVault 3 years ago 379557d
API: Implement mutation { createArtifact }

Can you believe that this function worked on the first try?
M api/go.mod => api/go.mod +1 -0
@@ 21,6 21,7 @@ require (
	github.com/lib/pq v1.8.0
	github.com/martinlindhe/base36 v1.1.0
	github.com/matryer/moq v0.0.0-20200310130814-7721994d1b54 // indirect
	github.com/minio/minio-go/v7 v7.0.5
	github.com/mitchellh/mapstructure v1.3.2 // indirect
	github.com/pkg/errors v0.9.1 // indirect
	github.com/prometheus/client_golang v1.8.0 // indirect

M api/go.sum => api/go.sum +24 -0
@@ 13,6 13,7 @@ git.sr.ht/~sircmpwn/dowork v0.0.0-20201121170652-c2a771442daf/go.mod h1:8neHEO35
git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3 h1:4wDp4BKF7NQqoh73VXpZsB/t1OEhDpz/zEpmdQfbjDk=
git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw=
git.sr.ht/~sircmpwn/git.sr.ht v0.0.0-20201126161813-85c7338ae566 h1:4MRSyIEfnNfGsLl/R0IjZV7JgYJctGacBMwYJV5RkZE=
git.sr.ht/~sircmpwn/git.sr.ht v0.0.0-20201128022450-379557dc4278 h1:sC44HSgIiwjZOouW07tYRkokvPbswTQ7cUn6IbEeOEU=
git.sr.ht/~sircmpwn/go-bare v0.0.0-20200623145341-debb068b456a h1:bqT/ygbVtD6b/B/skCQ+hZI4OtwuyKFQJxwS3z1lY3g=
git.sr.ht/~sircmpwn/go-bare v0.0.0-20200623145341-debb068b456a/go.mod h1:BVJwbDfVjCjoFiKrhkei6NdGcZYpkDkdyCdg1ukytRA=
git.sr.ht/~sircmpwn/go-bare v0.0.0-20200812160916-d2c72e1a5018 h1:89QMorzx6ML69PKPoayL3HuSfb7WqAlxD1dZ7DyzD0k=


@@ 174,6 175,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=


@@ 241,6 244,7 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=


@@ 251,6 255,9 @@ github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s=
github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=


@@ 289,6 296,14 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4=
github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw=
github.com/minio/minio-go v1.0.0 h1:ooSujki+Z1PRGZsYffJw5jnF5eMBvzMVV86TLAlM0UM=
github.com/minio/minio-go v3.0.2+incompatible h1:TgjYEriSwSpcawLVPw4LpkENIJdqqtVwkyMEnNapZ8w=
github.com/minio/minio-go/v7 v7.0.5 h1:I2NIJ2ojwJqD/YByemC1M59e1b4FW9kS7NlOar7HPV4=
github.com/minio/minio-go/v7 v7.0.5/go.mod h1:TA0CQCjJZHM5SJj9IjqR0NmpmQJ6bCbXifAJ3mUU6Hw=
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=


@@ 303,8 318,10 @@ github.com/mitchellh/mapstructure v1.3.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg=
github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=


@@ 392,6 409,8 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=


@@ 408,6 427,7 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=


@@ 558,6 578,7 @@ golang.org/x/sys v0.0.0-20201126144705-a4b67b81d3d2 h1:WFCmm2Hi9I2gYf1kv7LQ8ajKA
golang.org/x/sys v0.0.0-20201126144705-a4b67b81d3d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4-0.20201021145329-22f1617af38e h1:0kyKOEC0chG7FKmnf/1uNwvDLc3NtNTRip2rXAN9nwI=
golang.org/x/text v0.3.4-0.20201021145329-22f1617af38e/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=


@@ 629,6 650,8 @@ gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qS
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=
gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=
gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=


@@ 645,6 668,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

M api/graph/api/generated.go => api/graph/api/generated.go +140 -0
@@ 108,6 108,10 @@ type ComplexityRoot struct {
		Results func(childComplexity int) int
	}

	Features struct {
		Artifacts func(childComplexity int) int
	}

	Mutation struct {
		CreateRepository func(childComplexity int, name string, visibility model.Visibility, description *string) int
		DeleteACL        func(childComplexity int, id int) int


@@ 225,6 229,7 @@ type ComplexityRoot struct {

	Version struct {
		DeprecationDate func(childComplexity int) int
		Features        func(childComplexity int) int
		Major           func(childComplexity int) int
		Minor           func(childComplexity int) int
		Patch           func(childComplexity int) int


@@ 519,6 524,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.CommitCursor.Results(childComplexity), true

	case "Features.artifacts":
		if e.complexity.Features.Artifacts == nil {
			break
		}

		return e.complexity.Features.Artifacts(childComplexity), true

	case "Mutation.createRepository":
		if e.complexity.Mutation.CreateRepository == nil {
			break


@@ 1168,6 1180,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.Version.DeprecationDate(childComplexity), true

	case "Version.features":
		if e.complexity.Version.Features == nil {
			break
		}

		return e.complexity.Version.Features(childComplexity), true

	case "Version.major":
		if e.complexity.Version.Major == nil {
			break


@@ 1283,10 1302,19 @@ type Version {
  major: Int!
  minor: Int!
  patch: Int!

  # If this API version is scheduled for deprecation, this is the date on which
  # it will stop working; or null if this API version is not scheduled for
  # deprecation.
  deprecationDate: Time

  # Optional features
  features: Features!
}

# Describes the status of optional features
type Features {
  artifacts: Boolean!
}

enum AccessMode {


@@ 3301,6 3329,41 @@ func (ec *executionContext) _CommitCursor_cursor(ctx context.Context, field grap
	return ec.marshalOCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋcoreᚑgoᚋmodelᚐCursor(ctx, field.Selections, res)
}

func (ec *executionContext) _Features_artifacts(ctx context.Context, field graphql.CollectedField, obj *model.Features) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	fc := &graphql.FieldContext{
		Object:     "Features",
		Field:      field,
		Args:       nil,
		IsMethod:   false,
		IsResolver: false,
	}

	ctx = graphql.WithFieldContext(ctx, fc)
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return obj.Artifacts, nil
	})
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	if resTmp == nil {
		if !graphql.HasFieldError(ctx, fc) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	res := resTmp.(bool)
	fc.Result = res
	return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}

func (ec *executionContext) _Mutation_createRepository(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {


@@ 6879,6 6942,41 @@ func (ec *executionContext) _Version_deprecationDate(ctx context.Context, field 
	return ec.marshalOTime2ᚖtimeᚐTime(ctx, field.Selections, res)
}

func (ec *executionContext) _Version_features(ctx context.Context, field graphql.CollectedField, obj *model.Version) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	fc := &graphql.FieldContext{
		Object:     "Version",
		Field:      field,
		Args:       nil,
		IsMethod:   false,
		IsResolver: false,
	}

	ctx = graphql.WithFieldContext(ctx, fc)
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return obj.Features, nil
	})
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	if resTmp == nil {
		if !graphql.HasFieldError(ctx, fc) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	res := resTmp.(*model.Features)
	fc.Result = res
	return ec.marshalNFeatures2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐFeatures(ctx, field.Selections, res)
}

func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {


@@ 8427,6 8525,33 @@ func (ec *executionContext) _CommitCursor(ctx context.Context, sel ast.Selection
	return out
}

var featuresImplementors = []string{"Features"}

func (ec *executionContext) _Features(ctx context.Context, sel ast.SelectionSet, obj *model.Features) graphql.Marshaler {
	fields := graphql.CollectFields(ec.OperationContext, sel, featuresImplementors)

	out := graphql.NewFieldSet(fields)
	var invalids uint32
	for i, field := range fields {
		switch field.Name {
		case "__typename":
			out.Values[i] = graphql.MarshalString("Features")
		case "artifacts":
			out.Values[i] = ec._Features_artifacts(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
			}
		default:
			panic("unknown field " + strconv.Quote(field.Name))
		}
	}
	out.Dispatch()
	if invalids > 0 {
		return graphql.Null
	}
	return out
}

var mutationImplementors = []string{"Mutation"}

func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {


@@ 9226,6 9351,11 @@ func (ec *executionContext) _Version(ctx context.Context, sel ast.SelectionSet, 
			}
		case "deprecationDate":
			out.Values[i] = ec._Version_deprecationDate(ctx, field, obj)
		case "features":
			out.Values[i] = ec._Version_features(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
			}
		default:
			panic("unknown field " + strconv.Quote(field.Name))
		}


@@ 9765,6 9895,16 @@ func (ec *executionContext) marshalNEntity2gitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsr
	return ec._Entity(ctx, sel, v)
}

func (ec *executionContext) marshalNFeatures2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐFeatures(ctx context.Context, sel ast.SelectionSet, v *model.Features) graphql.Marshaler {
	if v == nil {
		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	return ec._Features(ctx, sel, v)
}

func (ec *executionContext) unmarshalNID2string(ctx context.Context, v interface{}) (string, error) {
	res, err := graphql.UnmarshalID(v)
	return res, graphql.ErrorOnPath(ctx, err)

M api/graph/model/artifact.go => api/graph/model/artifact.go +3 -2
@@ 19,8 19,9 @@ type Artifact struct {
	Checksum   string      `json:"checksum"`
	Size       int         `json:"size"`

	Commit string

	alias  string
	commit string
	fields *database.ModelFields
}



@@ 51,7 52,7 @@ func (a *Artifact) Fields() *database.ModelFields {

			// Always fetch:
			{ "id", "", &a.ID },
			{ "commit", "", &a.commit },
			{ "commit", "", &a.Commit },
			{ "filename", "", &a.Filename },
		},
	}

M api/graph/model/models_gen.go => api/graph/model/models_gen.go +5 -0
@@ 34,6 34,10 @@ type CommitCursor struct {
	Cursor  *model.Cursor `json:"cursor"`
}

type Features struct {
	Artifacts bool `json:"artifacts"`
}

type ReferenceCursor struct {
	Results []*Reference  `json:"results"`
	Cursor  *model.Cursor `json:"cursor"`


@@ 73,6 77,7 @@ type Version struct {
	Minor           int        `json:"minor"`
	Patch           int        `json:"patch"`
	DeprecationDate *time.Time `json:"deprecationDate"`
	Features        *Features  `json:"features"`
}

type AccessKind string

M api/graph/schema.graphqls => api/graph/schema.graphqls +9 -0
@@ 28,10 28,19 @@ type Version {
  major: Int!
  minor: Int!
  patch: Int!

  # If this API version is scheduled for deprecation, this is the date on which
  # it will stop working; or null if this API version is not scheduled for
  # deprecation.
  deprecationDate: Time

  # Optional features
  features: Features!
}

# Describes the status of optional features
type Features {
  artifacts: Boolean!
}

enum AccessMode {

M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +152 -1
@@ 4,10 4,16 @@ package graph
// will be copied through when generating and any unknown code will be moved to the end.

import (
	"bytes"
	"context"
	"crypto/md5"
	"crypto/sha256"
	"database/sql"
	"encoding/base64"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"os"
	"path"
	"sort"


@@ 28,6 34,8 @@ import (
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/go-git/go-git/v5/plumbing/storer"
	minio "github.com/minio/minio-go/v7"
	"github.com/minio/minio-go/v7/pkg/credentials"
)

func (r *aCLResolver) Repository(ctx context.Context, obj *model.ACL) (*model.Repository, error) {


@@ 385,7 393,139 @@ func (r *mutationResolver) DeleteACL(ctx context.Context, id int) (*model.ACL, e
}

func (r *mutationResolver) UploadArtifact(ctx context.Context, repoID int, revspec string, file graphql.Upload) (*model.Artifact, error) {
	panic(fmt.Errorf("uploadArtifact: not implemented"))
	conf := config.ForContext(ctx)
	upstream, _ := conf.Get("objects", "s3-upstream")
	accessKey, _ := conf.Get("objects", "s3-access-key")
	secretKey, _ := conf.Get("objects", "s3-secret-key")
	bucket, _ := conf.Get("git.sr.ht", "s3-bucket")
	prefix, _ := conf.Get("git.sr.ht", "s3-prefix")
	if upstream == "" || accessKey == "" || secretKey == "" || bucket == "" {
		return nil, fmt.Errorf("Object storage is not enabled for this server")
	}

	mc, err := minio.New(upstream, &minio.Options{
		Creds:  credentials.NewStaticV4(accessKey, secretKey, ""),
		Secure: true,
	})
	if err != nil {
		panic(err)
	}

	repo, err := loaders.ForContext(ctx).RepositoriesByID.Load(repoID)
	if err != nil {
		return nil, fmt.Errorf("Repository %d not found", repoID)
	}
	if repo.OwnerID != auth.ForContext(ctx).UserID {
		return nil, fmt.Errorf("Access denied for repo %d", repoID)
	}

	gitRepo := repo.Repo()
	gitRepo.Lock()
	hash, err := gitRepo.ResolveRevision(plumbing.Revision(revspec))
	gitRepo.Unlock()
	if err != nil {
		return nil, err
	}

	s3path := path.Join(prefix, "artifacts",
		"~" + auth.ForContext(ctx).Username, repo.Name, file.Filename)

	err = mc.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
	if s3err, ok := err.(minio.ErrorResponse); !ok ||
		(s3err.Code != "BucketAlreadyExists" && s3err.Code != "BucketAlreadyOwnedByYou") {
		panic(err)
	}

	core := minio.Core{mc}
	uid, err := core.NewMultipartUpload(ctx, bucket,
		s3path, minio.PutObjectOptions{
			ContentType: "application/octet-stream",
		})
	if err != nil {
		return nil, err
	}

	defer func() {
		if err := recover(); err != nil {
			core.AbortMultipartUpload(context.Background(), bucket, s3path, uid)
			panic(err)
		}
	}()

	var artifact model.Artifact
	if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
		// To guarantee atomicity, we do the following:
		// 1. Upload all parts as an S3 multipart upload, but don't complete
		//    it. This computes the SHA-256 as a side-effect.
		// 2. Insert row into the database and let PostgreSQL prevent
		//    duplicates with constraints. Once this finishes we know we are the
		//    exclusive writer of this artifact.
		// 3. Complete the S3 multi-part upload.
		sha := sha256.New()
		reader := io.TeeReader(file.File, sha)

		var (
			partNum int = 1
			parts   []minio.CompletePart
		)
		for {
			var data [16777216]byte // 16 MiB
			n, err := reader.Read(data[:])
			if errors.Is(err, io.EOF) {
				if n == 0 {
					break
				}
				// Write data[:n] and go for another loop before quitting
			} else if err != nil {
				return err
			}

			md5sum := md5.Sum(data[:n])
			md5b64 := base64.StdEncoding.EncodeToString(md5sum[:])
			shasum := sha256.Sum256(data[:n])
			shahex := hex.EncodeToString(shasum[:])

			opart, err := core.PutObjectPart(ctx, bucket, s3path,
				uid, partNum, bytes.NewReader(data[:n]), int64(n),
				md5b64, shahex, nil)
			if err != nil {
				return err
			}
			parts = append(parts, minio.CompletePart{
				PartNumber: partNum,
				ETag:       opart.ETag,
			})
			partNum++
		}

		checksum := "sha256:" + hex.EncodeToString(sha.Sum(nil))
		row := tx.QueryRowContext(ctx, `
			INSERT INTO artifacts (
				created, user_id, repo_id, commit, filename, checksum, size
			) VALUES (
				NOW() at time zone 'utc',
				$1, $2, $3, $4, $5, $6
			) RETURNING id, created, filename, checksum, size, commit`,
			repo.OwnerID, repo.ID, hash.String(), file.Filename,
			checksum, file.Size)
		if err := row.Scan(&artifact.ID, &artifact.Created, &artifact.Filename,
			&artifact.Checksum, &artifact.Size, &artifact.Commit); err != nil {
			if err.Error() == "pq: duplicate key value violates unique constraint \"repo_artifact_filename_unique\"" {
				return fmt.Errorf("An artifact by this name already exists for this repository (artifact names must be unique for within each repository)")
			}
			return err
		}

		_, err := core.CompleteMultipartUpload(ctx, bucket, s3path, uid, parts)
		return err
	}); err != nil {
		if err != nil {
			core.AbortMultipartUpload(context.Background(), bucket, s3path, uid)
		}
		return nil, err
	}

	return &artifact, nil
}

func (r *mutationResolver) DeleteArtifact(ctx context.Context, id int) (*model.Artifact, error) {


@@ 393,11 533,22 @@ func (r *mutationResolver) DeleteArtifact(ctx context.Context, id int) (*model.A
}

func (r *queryResolver) Version(ctx context.Context) (*model.Version, error) {
	conf := config.ForContext(ctx)
	upstream, _ := conf.Get("objects", "s3-upstream")
	accessKey, _ := conf.Get("objects", "s3-access-key")
	secretKey, _ := conf.Get("objects", "s3-secret-key")
	bucket, _ := conf.Get("git.sr.ht", "s3-bucket")
	artifacts := upstream != "" && accessKey != "" && secretKey != "" && bucket != ""

	return &model.Version{
		Major:           0,
		Minor:           0,
		Patch:           0,
		DeprecationDate: nil,

		Features: &model.Features{
			Artifacts: artifacts,
		},
	}, nil
}