Detailed changes
@@ -11,7 +11,7 @@ jobs:
with-go:
strategy:
matrix:
- go-version: [1.24.2]
+ go-version: [1.25.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
@@ -30,7 +30,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
- go-version: 1.24.2
+ go-version: 1.25.x
- name: Build
run: make
@@ -33,7 +33,7 @@ jobs:
- uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
- go-version: 1.24.2
+ go-version: 1.25.x
- name: Run benchmark
run: go test -v ./... -bench=. -run=xxx -benchmem | tee output.txt
@@ -2,6 +2,14 @@ package connections
import "github.com/git-bug/git-bug/entity"
+// CursorEdge is a minimal edge carrying only a cursor. Use it with
+// connections.Connection when the edge type needs no additional fields.
+type CursorEdge struct {
+ Cursor string
+}
+
+func (e CursorEdge) GetCursor() string { return e.Cursor }
+
// LazyBugEdge is a special relay edge used to implement a lazy loading connection
type LazyBugEdge struct {
Id entity.Id
@@ -0,0 +1,3998 @@
+// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
+
+package graph
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strconv"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/99designs/gqlgen/graphql"
+ "github.com/git-bug/git-bug/api/graphql/models"
+ "github.com/git-bug/git-bug/repository"
+ "github.com/vektah/gqlparser/v2/ast"
+)
+
+// region ************************** generated!.gotpl **************************
+
+type GitCommitResolver interface {
+ ShortHash(ctx context.Context, obj *models.GitCommitMeta) (string, error)
+
+ FullMessage(ctx context.Context, obj *models.GitCommitMeta) (string, error)
+
+ Parents(ctx context.Context, obj *models.GitCommitMeta) ([]string, error)
+ Files(ctx context.Context, obj *models.GitCommitMeta, after *string, before *string, first *int, last *int) (*models.GitChangedFileConnection, error)
+ Diff(ctx context.Context, obj *models.GitCommitMeta, path string) (*repository.FileDiff, error)
+}
+
+// endregion ************************** generated!.gotpl **************************
+
+// region ***************************** args.gotpl *****************************
+
+func (ec *executionContext) field_GitCommit_diff_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_GitCommit_diff_argsPath(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["path"] = arg0
+ return args, nil
+}
+func (ec *executionContext) field_GitCommit_diff_argsPath(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (string, error) {
+ if _, ok := rawArgs["path"]; !ok {
+ var zeroVal string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("path"))
+ if tmp, ok := rawArgs["path"]; ok {
+ return ec.unmarshalNString2string(ctx, tmp)
+ }
+
+ var zeroVal string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_GitCommit_files_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_GitCommit_files_argsAfter(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["after"] = arg0
+ arg1, err := ec.field_GitCommit_files_argsBefore(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["before"] = arg1
+ arg2, err := ec.field_GitCommit_files_argsFirst(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["first"] = arg2
+ arg3, err := ec.field_GitCommit_files_argsLast(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["last"] = arg3
+ return args, nil
+}
+func (ec *executionContext) field_GitCommit_files_argsAfter(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*string, error) {
+ if _, ok := rawArgs["after"]; !ok {
+ var zeroVal *string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after"))
+ if tmp, ok := rawArgs["after"]; ok {
+ return ec.unmarshalOString2ᚖstring(ctx, tmp)
+ }
+
+ var zeroVal *string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_GitCommit_files_argsBefore(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*string, error) {
+ if _, ok := rawArgs["before"]; !ok {
+ var zeroVal *string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before"))
+ if tmp, ok := rawArgs["before"]; ok {
+ return ec.unmarshalOString2ᚖstring(ctx, tmp)
+ }
+
+ var zeroVal *string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_GitCommit_files_argsFirst(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*int, error) {
+ if _, ok := rawArgs["first"]; !ok {
+ var zeroVal *int
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("first"))
+ if tmp, ok := rawArgs["first"]; ok {
+ return ec.unmarshalOInt2ᚖint(ctx, tmp)
+ }
+
+ var zeroVal *int
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_GitCommit_files_argsLast(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*int, error) {
+ if _, ok := rawArgs["last"]; !ok {
+ var zeroVal *int
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("last"))
+ if tmp, ok := rawArgs["last"]; ok {
+ return ec.unmarshalOInt2ᚖint(ctx, tmp)
+ }
+
+ var zeroVal *int
+ return zeroVal, nil
+}
+
+// endregion ***************************** args.gotpl *****************************
+
+// region ************************** directives.gotpl **************************
+
+// endregion ************************** directives.gotpl **************************
+
+// region **************************** field.gotpl *****************************
+
+func (ec *executionContext) _GitBlob_path(ctx context.Context, field graphql.CollectedField, obj *models.GitBlob) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitBlob_path(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Path, 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.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitBlob_path(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitBlob",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitBlob_hash(ctx context.Context, field graphql.CollectedField, obj *models.GitBlob) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitBlob_hash(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Hash, 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.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitBlob_hash(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitBlob",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitBlob_text(ctx context.Context, field graphql.CollectedField, obj *models.GitBlob) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitBlob_text(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Text, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ return graphql.Null
+ }
+ res := resTmp.(*string)
+ fc.Result = res
+ return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitBlob_text(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitBlob",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitBlob_size(ctx context.Context, field graphql.CollectedField, obj *models.GitBlob) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitBlob_size(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Size, 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.(int)
+ fc.Result = res
+ return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitBlob_size(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitBlob",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Int does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitBlob_isBinary(ctx context.Context, field graphql.CollectedField, obj *models.GitBlob) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitBlob_isBinary(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.IsBinary, 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) fieldContext_GitBlob_isBinary(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitBlob",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Boolean does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitBlob_isTruncated(ctx context.Context, field graphql.CollectedField, obj *models.GitBlob) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitBlob_isTruncated(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.IsTruncated, 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) fieldContext_GitBlob_isTruncated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitBlob",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Boolean does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitChangedFile_path(ctx context.Context, field graphql.CollectedField, obj *repository.ChangedFile) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitChangedFile_path(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Path, 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.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitChangedFile_path(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitChangedFile",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitChangedFile_oldPath(ctx context.Context, field graphql.CollectedField, obj *repository.ChangedFile) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitChangedFile_oldPath(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.OldPath, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ return graphql.Null
+ }
+ res := resTmp.(*string)
+ fc.Result = res
+ return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitChangedFile_oldPath(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitChangedFile",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitChangedFile_status(ctx context.Context, field graphql.CollectedField, obj *repository.ChangedFile) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitChangedFile_status(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Status, 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.(repository.ChangeStatus)
+ fc.Result = res
+ return ec.marshalNGitChangeStatus2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐChangeStatus(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitChangedFile_status(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitChangedFile",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type GitChangeStatus does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitChangedFileConnection_nodes(ctx context.Context, field graphql.CollectedField, obj *models.GitChangedFileConnection) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitChangedFileConnection_nodes(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Nodes, 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.([]*repository.ChangedFile)
+ fc.Result = res
+ return ec.marshalNGitChangedFile2ᚕᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐChangedFileᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitChangedFileConnection_nodes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitChangedFileConnection",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "path":
+ return ec.fieldContext_GitChangedFile_path(ctx, field)
+ case "oldPath":
+ return ec.fieldContext_GitChangedFile_oldPath(ctx, field)
+ case "status":
+ return ec.fieldContext_GitChangedFile_status(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type GitChangedFile", field.Name)
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitChangedFileConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *models.GitChangedFileConnection) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitChangedFileConnection_pageInfo(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.PageInfo, 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.(*models.PageInfo)
+ fc.Result = res
+ return ec.marshalNPageInfo2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐPageInfo(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitChangedFileConnection_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitChangedFileConnection",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "hasNextPage":
+ return ec.fieldContext_PageInfo_hasNextPage(ctx, field)
+ case "hasPreviousPage":
+ return ec.fieldContext_PageInfo_hasPreviousPage(ctx, field)
+ case "startCursor":
+ return ec.fieldContext_PageInfo_startCursor(ctx, field)
+ case "endCursor":
+ return ec.fieldContext_PageInfo_endCursor(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type PageInfo", field.Name)
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitChangedFileConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *models.GitChangedFileConnection) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitChangedFileConnection_totalCount(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.TotalCount, 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.(int)
+ fc.Result = res
+ return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitChangedFileConnection_totalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitChangedFileConnection",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Int does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitCommit_hash(ctx context.Context, field graphql.CollectedField, obj *models.GitCommitMeta) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitCommit_hash(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Hash, 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.(repository.Hash)
+ fc.Result = res
+ return ec.marshalNString2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐHash(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitCommit_hash(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitCommit",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitCommit_shortHash(ctx context.Context, field graphql.CollectedField, obj *models.GitCommitMeta) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitCommit_shortHash(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.GitCommit().ShortHash(rctx, obj)
+ })
+ 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.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitCommit_shortHash(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitCommit",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitCommit_message(ctx context.Context, field graphql.CollectedField, obj *models.GitCommitMeta) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitCommit_message(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Message, 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.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitCommit_message(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitCommit",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitCommit_fullMessage(ctx context.Context, field graphql.CollectedField, obj *models.GitCommitMeta) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitCommit_fullMessage(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.GitCommit().FullMessage(rctx, obj)
+ })
+ 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.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitCommit_fullMessage(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitCommit",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitCommit_authorName(ctx context.Context, field graphql.CollectedField, obj *models.GitCommitMeta) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitCommit_authorName(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.AuthorName, 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.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitCommit_authorName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitCommit",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitCommit_authorEmail(ctx context.Context, field graphql.CollectedField, obj *models.GitCommitMeta) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitCommit_authorEmail(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.AuthorEmail, 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.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitCommit_authorEmail(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitCommit",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitCommit_date(ctx context.Context, field graphql.CollectedField, obj *models.GitCommitMeta) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitCommit_date(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Date, 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.(time.Time)
+ fc.Result = res
+ return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitCommit_date(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitCommit",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Time does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitCommit_parents(ctx context.Context, field graphql.CollectedField, obj *models.GitCommitMeta) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitCommit_parents(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.GitCommit().Parents(rctx, obj)
+ })
+ 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.([]string)
+ fc.Result = res
+ return ec.marshalNString2ᚕstringᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitCommit_parents(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitCommit",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitCommit_files(ctx context.Context, field graphql.CollectedField, obj *models.GitCommitMeta) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitCommit_files(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.GitCommit().Files(rctx, obj, fc.Args["after"].(*string), fc.Args["before"].(*string), fc.Args["first"].(*int), fc.Args["last"].(*int))
+ })
+ 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.(*models.GitChangedFileConnection)
+ fc.Result = res
+ return ec.marshalNGitChangedFileConnection2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitChangedFileConnection(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitCommit_files(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitCommit",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "nodes":
+ return ec.fieldContext_GitChangedFileConnection_nodes(ctx, field)
+ case "pageInfo":
+ return ec.fieldContext_GitChangedFileConnection_pageInfo(ctx, field)
+ case "totalCount":
+ return ec.fieldContext_GitChangedFileConnection_totalCount(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type GitChangedFileConnection", field.Name)
+ },
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_GitCommit_files_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitCommit_diff(ctx context.Context, field graphql.CollectedField, obj *models.GitCommitMeta) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitCommit_diff(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.GitCommit().Diff(rctx, obj, fc.Args["path"].(string))
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ return graphql.Null
+ }
+ res := resTmp.(*repository.FileDiff)
+ fc.Result = res
+ return ec.marshalOGitFileDiff2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐFileDiff(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitCommit_diff(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitCommit",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "path":
+ return ec.fieldContext_GitFileDiff_path(ctx, field)
+ case "oldPath":
+ return ec.fieldContext_GitFileDiff_oldPath(ctx, field)
+ case "isBinary":
+ return ec.fieldContext_GitFileDiff_isBinary(ctx, field)
+ case "isNew":
+ return ec.fieldContext_GitFileDiff_isNew(ctx, field)
+ case "isDelete":
+ return ec.fieldContext_GitFileDiff_isDelete(ctx, field)
+ case "hunks":
+ return ec.fieldContext_GitFileDiff_hunks(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type GitFileDiff", field.Name)
+ },
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_GitCommit_diff_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitCommitConnection_nodes(ctx context.Context, field graphql.CollectedField, obj *models.GitCommitConnection) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitCommitConnection_nodes(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Nodes, 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.([]*models.GitCommitMeta)
+ fc.Result = res
+ return ec.marshalNGitCommit2ᚕᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitCommitMetaᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitCommitConnection_nodes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitCommitConnection",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "hash":
+ return ec.fieldContext_GitCommit_hash(ctx, field)
+ case "shortHash":
+ return ec.fieldContext_GitCommit_shortHash(ctx, field)
+ case "message":
+ return ec.fieldContext_GitCommit_message(ctx, field)
+ case "fullMessage":
+ return ec.fieldContext_GitCommit_fullMessage(ctx, field)
+ case "authorName":
+ return ec.fieldContext_GitCommit_authorName(ctx, field)
+ case "authorEmail":
+ return ec.fieldContext_GitCommit_authorEmail(ctx, field)
+ case "date":
+ return ec.fieldContext_GitCommit_date(ctx, field)
+ case "parents":
+ return ec.fieldContext_GitCommit_parents(ctx, field)
+ case "files":
+ return ec.fieldContext_GitCommit_files(ctx, field)
+ case "diff":
+ return ec.fieldContext_GitCommit_diff(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type GitCommit", field.Name)
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitCommitConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *models.GitCommitConnection) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitCommitConnection_pageInfo(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.PageInfo, 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.(*models.PageInfo)
+ fc.Result = res
+ return ec.marshalNPageInfo2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐPageInfo(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitCommitConnection_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitCommitConnection",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "hasNextPage":
+ return ec.fieldContext_PageInfo_hasNextPage(ctx, field)
+ case "hasPreviousPage":
+ return ec.fieldContext_PageInfo_hasPreviousPage(ctx, field)
+ case "startCursor":
+ return ec.fieldContext_PageInfo_startCursor(ctx, field)
+ case "endCursor":
+ return ec.fieldContext_PageInfo_endCursor(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type PageInfo", field.Name)
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitCommitConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *models.GitCommitConnection) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitCommitConnection_totalCount(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.TotalCount, 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.(int)
+ fc.Result = res
+ return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitCommitConnection_totalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitCommitConnection",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Int does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitDiffHunk_oldStart(ctx context.Context, field graphql.CollectedField, obj *repository.DiffHunk) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitDiffHunk_oldStart(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.OldStart, 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.(int)
+ fc.Result = res
+ return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitDiffHunk_oldStart(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitDiffHunk",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Int does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitDiffHunk_oldLines(ctx context.Context, field graphql.CollectedField, obj *repository.DiffHunk) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitDiffHunk_oldLines(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.OldLines, 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.(int)
+ fc.Result = res
+ return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitDiffHunk_oldLines(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitDiffHunk",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Int does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitDiffHunk_newStart(ctx context.Context, field graphql.CollectedField, obj *repository.DiffHunk) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitDiffHunk_newStart(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.NewStart, 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.(int)
+ fc.Result = res
+ return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitDiffHunk_newStart(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitDiffHunk",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Int does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitDiffHunk_newLines(ctx context.Context, field graphql.CollectedField, obj *repository.DiffHunk) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitDiffHunk_newLines(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.NewLines, 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.(int)
+ fc.Result = res
+ return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitDiffHunk_newLines(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitDiffHunk",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Int does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitDiffHunk_lines(ctx context.Context, field graphql.CollectedField, obj *repository.DiffHunk) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitDiffHunk_lines(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Lines, 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.([]repository.DiffLine)
+ fc.Result = res
+ return ec.marshalNGitDiffLine2ᚕgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐDiffLineᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitDiffHunk_lines(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitDiffHunk",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "type":
+ return ec.fieldContext_GitDiffLine_type(ctx, field)
+ case "content":
+ return ec.fieldContext_GitDiffLine_content(ctx, field)
+ case "oldLine":
+ return ec.fieldContext_GitDiffLine_oldLine(ctx, field)
+ case "newLine":
+ return ec.fieldContext_GitDiffLine_newLine(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type GitDiffLine", field.Name)
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitDiffLine_type(ctx context.Context, field graphql.CollectedField, obj *repository.DiffLine) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitDiffLine_type(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Type, 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.(repository.DiffLineType)
+ fc.Result = res
+ return ec.marshalNGitDiffLineType2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐDiffLineType(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitDiffLine_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitDiffLine",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type GitDiffLineType does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitDiffLine_content(ctx context.Context, field graphql.CollectedField, obj *repository.DiffLine) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitDiffLine_content(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Content, 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.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitDiffLine_content(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitDiffLine",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitDiffLine_oldLine(ctx context.Context, field graphql.CollectedField, obj *repository.DiffLine) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitDiffLine_oldLine(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.OldLine, 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.(int)
+ fc.Result = res
+ return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitDiffLine_oldLine(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitDiffLine",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Int does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitDiffLine_newLine(ctx context.Context, field graphql.CollectedField, obj *repository.DiffLine) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitDiffLine_newLine(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.NewLine, 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.(int)
+ fc.Result = res
+ return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitDiffLine_newLine(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitDiffLine",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Int does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitFileDiff_path(ctx context.Context, field graphql.CollectedField, obj *repository.FileDiff) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitFileDiff_path(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Path, 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.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitFileDiff_path(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitFileDiff",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitFileDiff_oldPath(ctx context.Context, field graphql.CollectedField, obj *repository.FileDiff) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitFileDiff_oldPath(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.OldPath, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ return graphql.Null
+ }
+ res := resTmp.(*string)
+ fc.Result = res
+ return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitFileDiff_oldPath(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitFileDiff",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitFileDiff_isBinary(ctx context.Context, field graphql.CollectedField, obj *repository.FileDiff) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitFileDiff_isBinary(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.IsBinary, 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) fieldContext_GitFileDiff_isBinary(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitFileDiff",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Boolean does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitFileDiff_isNew(ctx context.Context, field graphql.CollectedField, obj *repository.FileDiff) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitFileDiff_isNew(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.IsNew, 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) fieldContext_GitFileDiff_isNew(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitFileDiff",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Boolean does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitFileDiff_isDelete(ctx context.Context, field graphql.CollectedField, obj *repository.FileDiff) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitFileDiff_isDelete(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.IsDelete, 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) fieldContext_GitFileDiff_isDelete(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitFileDiff",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Boolean does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitFileDiff_hunks(ctx context.Context, field graphql.CollectedField, obj *repository.FileDiff) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitFileDiff_hunks(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Hunks, 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.([]repository.DiffHunk)
+ fc.Result = res
+ return ec.marshalNGitDiffHunk2ᚕgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐDiffHunkᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitFileDiff_hunks(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitFileDiff",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "oldStart":
+ return ec.fieldContext_GitDiffHunk_oldStart(ctx, field)
+ case "oldLines":
+ return ec.fieldContext_GitDiffHunk_oldLines(ctx, field)
+ case "newStart":
+ return ec.fieldContext_GitDiffHunk_newStart(ctx, field)
+ case "newLines":
+ return ec.fieldContext_GitDiffHunk_newLines(ctx, field)
+ case "lines":
+ return ec.fieldContext_GitDiffHunk_lines(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type GitDiffHunk", field.Name)
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitLastCommit_name(ctx context.Context, field graphql.CollectedField, obj *models.GitLastCommit) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitLastCommit_name(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Name, 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.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitLastCommit_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitLastCommit",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitLastCommit_commit(ctx context.Context, field graphql.CollectedField, obj *models.GitLastCommit) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitLastCommit_commit(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Commit, 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.(*models.GitCommitMeta)
+ fc.Result = res
+ return ec.marshalNGitCommit2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitCommitMeta(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitLastCommit_commit(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitLastCommit",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "hash":
+ return ec.fieldContext_GitCommit_hash(ctx, field)
+ case "shortHash":
+ return ec.fieldContext_GitCommit_shortHash(ctx, field)
+ case "message":
+ return ec.fieldContext_GitCommit_message(ctx, field)
+ case "fullMessage":
+ return ec.fieldContext_GitCommit_fullMessage(ctx, field)
+ case "authorName":
+ return ec.fieldContext_GitCommit_authorName(ctx, field)
+ case "authorEmail":
+ return ec.fieldContext_GitCommit_authorEmail(ctx, field)
+ case "date":
+ return ec.fieldContext_GitCommit_date(ctx, field)
+ case "parents":
+ return ec.fieldContext_GitCommit_parents(ctx, field)
+ case "files":
+ return ec.fieldContext_GitCommit_files(ctx, field)
+ case "diff":
+ return ec.fieldContext_GitCommit_diff(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type GitCommit", field.Name)
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitRef_name(ctx context.Context, field graphql.CollectedField, obj *models.GitRef) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitRef_name(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Name, 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.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitRef_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitRef",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitRef_shortName(ctx context.Context, field graphql.CollectedField, obj *models.GitRef) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitRef_shortName(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.ShortName, 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.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitRef_shortName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitRef",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitRef_type(ctx context.Context, field graphql.CollectedField, obj *models.GitRef) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitRef_type(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Type, 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.(models.GitRefType)
+ fc.Result = res
+ return ec.marshalNGitRefType2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefType(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitRef_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitRef",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type GitRefType does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitRef_hash(ctx context.Context, field graphql.CollectedField, obj *models.GitRef) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitRef_hash(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Hash, 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.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitRef_hash(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitRef",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitRef_isDefault(ctx context.Context, field graphql.CollectedField, obj *models.GitRef) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitRef_isDefault(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.IsDefault, 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) fieldContext_GitRef_isDefault(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitRef",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Boolean does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitRefConnection_nodes(ctx context.Context, field graphql.CollectedField, obj *models.GitRefConnection) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitRefConnection_nodes(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Nodes, 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.([]*models.GitRef)
+ fc.Result = res
+ return ec.marshalNGitRef2ᚕᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitRefConnection_nodes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitRefConnection",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "name":
+ return ec.fieldContext_GitRef_name(ctx, field)
+ case "shortName":
+ return ec.fieldContext_GitRef_shortName(ctx, field)
+ case "type":
+ return ec.fieldContext_GitRef_type(ctx, field)
+ case "hash":
+ return ec.fieldContext_GitRef_hash(ctx, field)
+ case "isDefault":
+ return ec.fieldContext_GitRef_isDefault(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type GitRef", field.Name)
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitRefConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *models.GitRefConnection) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitRefConnection_pageInfo(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.PageInfo, 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.(*models.PageInfo)
+ fc.Result = res
+ return ec.marshalNPageInfo2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐPageInfo(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitRefConnection_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitRefConnection",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "hasNextPage":
+ return ec.fieldContext_PageInfo_hasNextPage(ctx, field)
+ case "hasPreviousPage":
+ return ec.fieldContext_PageInfo_hasPreviousPage(ctx, field)
+ case "startCursor":
+ return ec.fieldContext_PageInfo_startCursor(ctx, field)
+ case "endCursor":
+ return ec.fieldContext_PageInfo_endCursor(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type PageInfo", field.Name)
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitRefConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *models.GitRefConnection) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitRefConnection_totalCount(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.TotalCount, 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.(int)
+ fc.Result = res
+ return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitRefConnection_totalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitRefConnection",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Int does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitTreeEntry_name(ctx context.Context, field graphql.CollectedField, obj *repository.TreeEntry) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitTreeEntry_name(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Name, 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.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitTreeEntry_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitTreeEntry",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitTreeEntry_type(ctx context.Context, field graphql.CollectedField, obj *repository.TreeEntry) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitTreeEntry_type(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.ObjectType, 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.(repository.ObjectType)
+ fc.Result = res
+ return ec.marshalNGitObjectType2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐObjectType(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitTreeEntry_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitTreeEntry",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type GitObjectType does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _GitTreeEntry_hash(ctx context.Context, field graphql.CollectedField, obj *repository.TreeEntry) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_GitTreeEntry_hash(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Hash, 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.(repository.Hash)
+ fc.Result = res
+ return ec.marshalNString2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐHash(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_GitTreeEntry_hash(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "GitTreeEntry",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+// endregion **************************** field.gotpl *****************************
+
+// region **************************** input.gotpl *****************************
+
+// endregion **************************** input.gotpl *****************************
+
+// region ************************** interface.gotpl ***************************
+
+// endregion ************************** interface.gotpl ***************************
+
+// region **************************** object.gotpl ****************************
+
+var gitBlobImplementors = []string{"GitBlob"}
+
+func (ec *executionContext) _GitBlob(ctx context.Context, sel ast.SelectionSet, obj *models.GitBlob) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, gitBlobImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("GitBlob")
+ case "path":
+ out.Values[i] = ec._GitBlob_path(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "hash":
+ out.Values[i] = ec._GitBlob_hash(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "text":
+ out.Values[i] = ec._GitBlob_text(ctx, field, obj)
+ case "size":
+ out.Values[i] = ec._GitBlob_size(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "isBinary":
+ out.Values[i] = ec._GitBlob_isBinary(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "isTruncated":
+ out.Values[i] = ec._GitBlob_isTruncated(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
+var gitChangedFileImplementors = []string{"GitChangedFile"}
+
+func (ec *executionContext) _GitChangedFile(ctx context.Context, sel ast.SelectionSet, obj *repository.ChangedFile) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, gitChangedFileImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("GitChangedFile")
+ case "path":
+ out.Values[i] = ec._GitChangedFile_path(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "oldPath":
+ out.Values[i] = ec._GitChangedFile_oldPath(ctx, field, obj)
+ case "status":
+ out.Values[i] = ec._GitChangedFile_status(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
+var gitChangedFileConnectionImplementors = []string{"GitChangedFileConnection"}
+
+func (ec *executionContext) _GitChangedFileConnection(ctx context.Context, sel ast.SelectionSet, obj *models.GitChangedFileConnection) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, gitChangedFileConnectionImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("GitChangedFileConnection")
+ case "nodes":
+ out.Values[i] = ec._GitChangedFileConnection_nodes(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "pageInfo":
+ out.Values[i] = ec._GitChangedFileConnection_pageInfo(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "totalCount":
+ out.Values[i] = ec._GitChangedFileConnection_totalCount(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
+var gitCommitImplementors = []string{"GitCommit"}
+
+func (ec *executionContext) _GitCommit(ctx context.Context, sel ast.SelectionSet, obj *models.GitCommitMeta) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, gitCommitImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("GitCommit")
+ case "hash":
+ out.Values[i] = ec._GitCommit_hash(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "shortHash":
+ field := field
+
+ innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._GitCommit_shortHash(ctx, field, obj)
+ if res == graphql.Null {
+ atomic.AddUint32(&fs.Invalids, 1)
+ }
+ return res
+ }
+
+ if field.Deferrable != nil {
+ dfs, ok := deferred[field.Deferrable.Label]
+ di := 0
+ if ok {
+ dfs.AddField(field)
+ di = len(dfs.Values) - 1
+ } else {
+ dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+ deferred[field.Deferrable.Label] = dfs
+ }
+ dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+ return innerFunc(ctx, dfs)
+ })
+
+ // don't run the out.Concurrently() call below
+ out.Values[i] = graphql.Null
+ continue
+ }
+
+ out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+ case "message":
+ out.Values[i] = ec._GitCommit_message(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "fullMessage":
+ field := field
+
+ innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._GitCommit_fullMessage(ctx, field, obj)
+ if res == graphql.Null {
+ atomic.AddUint32(&fs.Invalids, 1)
+ }
+ return res
+ }
+
+ if field.Deferrable != nil {
+ dfs, ok := deferred[field.Deferrable.Label]
+ di := 0
+ if ok {
+ dfs.AddField(field)
+ di = len(dfs.Values) - 1
+ } else {
+ dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+ deferred[field.Deferrable.Label] = dfs
+ }
+ dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+ return innerFunc(ctx, dfs)
+ })
+
+ // don't run the out.Concurrently() call below
+ out.Values[i] = graphql.Null
+ continue
+ }
+
+ out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+ case "authorName":
+ out.Values[i] = ec._GitCommit_authorName(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "authorEmail":
+ out.Values[i] = ec._GitCommit_authorEmail(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "date":
+ out.Values[i] = ec._GitCommit_date(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "parents":
+ field := field
+
+ innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._GitCommit_parents(ctx, field, obj)
+ if res == graphql.Null {
+ atomic.AddUint32(&fs.Invalids, 1)
+ }
+ return res
+ }
+
+ if field.Deferrable != nil {
+ dfs, ok := deferred[field.Deferrable.Label]
+ di := 0
+ if ok {
+ dfs.AddField(field)
+ di = len(dfs.Values) - 1
+ } else {
+ dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+ deferred[field.Deferrable.Label] = dfs
+ }
+ dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+ return innerFunc(ctx, dfs)
+ })
+
+ // don't run the out.Concurrently() call below
+ out.Values[i] = graphql.Null
+ continue
+ }
+
+ out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+ case "files":
+ field := field
+
+ innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._GitCommit_files(ctx, field, obj)
+ if res == graphql.Null {
+ atomic.AddUint32(&fs.Invalids, 1)
+ }
+ return res
+ }
+
+ if field.Deferrable != nil {
+ dfs, ok := deferred[field.Deferrable.Label]
+ di := 0
+ if ok {
+ dfs.AddField(field)
+ di = len(dfs.Values) - 1
+ } else {
+ dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+ deferred[field.Deferrable.Label] = dfs
+ }
+ dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+ return innerFunc(ctx, dfs)
+ })
+
+ // don't run the out.Concurrently() call below
+ out.Values[i] = graphql.Null
+ continue
+ }
+
+ out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+ case "diff":
+ field := field
+
+ innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._GitCommit_diff(ctx, field, obj)
+ return res
+ }
+
+ if field.Deferrable != nil {
+ dfs, ok := deferred[field.Deferrable.Label]
+ di := 0
+ if ok {
+ dfs.AddField(field)
+ di = len(dfs.Values) - 1
+ } else {
+ dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+ deferred[field.Deferrable.Label] = dfs
+ }
+ dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+ return innerFunc(ctx, dfs)
+ })
+
+ // don't run the out.Concurrently() call below
+ out.Values[i] = graphql.Null
+ continue
+ }
+
+ out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
+var gitCommitConnectionImplementors = []string{"GitCommitConnection"}
+
+func (ec *executionContext) _GitCommitConnection(ctx context.Context, sel ast.SelectionSet, obj *models.GitCommitConnection) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, gitCommitConnectionImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("GitCommitConnection")
+ case "nodes":
+ out.Values[i] = ec._GitCommitConnection_nodes(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "pageInfo":
+ out.Values[i] = ec._GitCommitConnection_pageInfo(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "totalCount":
+ out.Values[i] = ec._GitCommitConnection_totalCount(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
+var gitDiffHunkImplementors = []string{"GitDiffHunk"}
+
+func (ec *executionContext) _GitDiffHunk(ctx context.Context, sel ast.SelectionSet, obj *repository.DiffHunk) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, gitDiffHunkImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("GitDiffHunk")
+ case "oldStart":
+ out.Values[i] = ec._GitDiffHunk_oldStart(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "oldLines":
+ out.Values[i] = ec._GitDiffHunk_oldLines(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "newStart":
+ out.Values[i] = ec._GitDiffHunk_newStart(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "newLines":
+ out.Values[i] = ec._GitDiffHunk_newLines(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "lines":
+ out.Values[i] = ec._GitDiffHunk_lines(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
+var gitDiffLineImplementors = []string{"GitDiffLine"}
+
+func (ec *executionContext) _GitDiffLine(ctx context.Context, sel ast.SelectionSet, obj *repository.DiffLine) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, gitDiffLineImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("GitDiffLine")
+ case "type":
+ out.Values[i] = ec._GitDiffLine_type(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "content":
+ out.Values[i] = ec._GitDiffLine_content(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "oldLine":
+ out.Values[i] = ec._GitDiffLine_oldLine(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "newLine":
+ out.Values[i] = ec._GitDiffLine_newLine(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
+var gitFileDiffImplementors = []string{"GitFileDiff"}
+
+func (ec *executionContext) _GitFileDiff(ctx context.Context, sel ast.SelectionSet, obj *repository.FileDiff) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, gitFileDiffImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("GitFileDiff")
+ case "path":
+ out.Values[i] = ec._GitFileDiff_path(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "oldPath":
+ out.Values[i] = ec._GitFileDiff_oldPath(ctx, field, obj)
+ case "isBinary":
+ out.Values[i] = ec._GitFileDiff_isBinary(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "isNew":
+ out.Values[i] = ec._GitFileDiff_isNew(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "isDelete":
+ out.Values[i] = ec._GitFileDiff_isDelete(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "hunks":
+ out.Values[i] = ec._GitFileDiff_hunks(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
+var gitLastCommitImplementors = []string{"GitLastCommit"}
+
+func (ec *executionContext) _GitLastCommit(ctx context.Context, sel ast.SelectionSet, obj *models.GitLastCommit) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, gitLastCommitImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("GitLastCommit")
+ case "name":
+ out.Values[i] = ec._GitLastCommit_name(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "commit":
+ out.Values[i] = ec._GitLastCommit_commit(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
+var gitRefImplementors = []string{"GitRef"}
+
+func (ec *executionContext) _GitRef(ctx context.Context, sel ast.SelectionSet, obj *models.GitRef) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, gitRefImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("GitRef")
+ case "name":
+ out.Values[i] = ec._GitRef_name(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "shortName":
+ out.Values[i] = ec._GitRef_shortName(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "type":
+ out.Values[i] = ec._GitRef_type(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "hash":
+ out.Values[i] = ec._GitRef_hash(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "isDefault":
+ out.Values[i] = ec._GitRef_isDefault(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
+var gitRefConnectionImplementors = []string{"GitRefConnection"}
+
+func (ec *executionContext) _GitRefConnection(ctx context.Context, sel ast.SelectionSet, obj *models.GitRefConnection) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, gitRefConnectionImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("GitRefConnection")
+ case "nodes":
+ out.Values[i] = ec._GitRefConnection_nodes(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "pageInfo":
+ out.Values[i] = ec._GitRefConnection_pageInfo(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "totalCount":
+ out.Values[i] = ec._GitRefConnection_totalCount(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
+var gitTreeEntryImplementors = []string{"GitTreeEntry"}
+
+func (ec *executionContext) _GitTreeEntry(ctx context.Context, sel ast.SelectionSet, obj *repository.TreeEntry) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, gitTreeEntryImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("GitTreeEntry")
+ case "name":
+ out.Values[i] = ec._GitTreeEntry_name(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "type":
+ out.Values[i] = ec._GitTreeEntry_type(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "hash":
+ out.Values[i] = ec._GitTreeEntry_hash(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
+// endregion **************************** object.gotpl ****************************
+
+// region ***************************** type.gotpl *****************************
+
+func (ec *executionContext) unmarshalNGitChangeStatus2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐChangeStatus(ctx context.Context, v any) (repository.ChangeStatus, error) {
+ var res repository.ChangeStatus
+ err := res.UnmarshalGQL(v)
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNGitChangeStatus2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐChangeStatus(ctx context.Context, sel ast.SelectionSet, v repository.ChangeStatus) graphql.Marshaler {
+ return v
+}
+
+func (ec *executionContext) marshalNGitChangedFile2ᚕᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐChangedFileᚄ(ctx context.Context, sel ast.SelectionSet, v []*repository.ChangedFile) graphql.Marshaler {
+ ret := make(graphql.Array, len(v))
+ var wg sync.WaitGroup
+ isLen1 := len(v) == 1
+ if !isLen1 {
+ wg.Add(len(v))
+ }
+ for i := range v {
+ i := i
+ fc := &graphql.FieldContext{
+ Index: &i,
+ Result: &v[i],
+ }
+ ctx := graphql.WithFieldContext(ctx, fc)
+ f := func(i int) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = nil
+ }
+ }()
+ if !isLen1 {
+ defer wg.Done()
+ }
+ ret[i] = ec.marshalNGitChangedFile2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐChangedFile(ctx, sel, v[i])
+ }
+ if isLen1 {
+ f(i)
+ } else {
+ go f(i)
+ }
+
+ }
+ wg.Wait()
+
+ for _, e := range ret {
+ if e == graphql.Null {
+ return graphql.Null
+ }
+ }
+
+ return ret
+}
+
+func (ec *executionContext) marshalNGitChangedFile2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐChangedFile(ctx context.Context, sel ast.SelectionSet, v *repository.ChangedFile) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._GitChangedFile(ctx, sel, v)
+}
+
+func (ec *executionContext) marshalNGitChangedFileConnection2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitChangedFileConnection(ctx context.Context, sel ast.SelectionSet, v models.GitChangedFileConnection) graphql.Marshaler {
+ return ec._GitChangedFileConnection(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNGitChangedFileConnection2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitChangedFileConnection(ctx context.Context, sel ast.SelectionSet, v *models.GitChangedFileConnection) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._GitChangedFileConnection(ctx, sel, v)
+}
+
+func (ec *executionContext) marshalNGitCommit2ᚕᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitCommitMetaᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.GitCommitMeta) graphql.Marshaler {
+ ret := make(graphql.Array, len(v))
+ var wg sync.WaitGroup
+ isLen1 := len(v) == 1
+ if !isLen1 {
+ wg.Add(len(v))
+ }
+ for i := range v {
+ i := i
+ fc := &graphql.FieldContext{
+ Index: &i,
+ Result: &v[i],
+ }
+ ctx := graphql.WithFieldContext(ctx, fc)
+ f := func(i int) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = nil
+ }
+ }()
+ if !isLen1 {
+ defer wg.Done()
+ }
+ ret[i] = ec.marshalNGitCommit2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitCommitMeta(ctx, sel, v[i])
+ }
+ if isLen1 {
+ f(i)
+ } else {
+ go f(i)
+ }
+
+ }
+ wg.Wait()
+
+ for _, e := range ret {
+ if e == graphql.Null {
+ return graphql.Null
+ }
+ }
+
+ return ret
+}
+
+func (ec *executionContext) marshalNGitCommit2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitCommitMeta(ctx context.Context, sel ast.SelectionSet, v *models.GitCommitMeta) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._GitCommit(ctx, sel, v)
+}
+
+func (ec *executionContext) marshalNGitCommitConnection2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitCommitConnection(ctx context.Context, sel ast.SelectionSet, v models.GitCommitConnection) graphql.Marshaler {
+ return ec._GitCommitConnection(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNGitCommitConnection2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitCommitConnection(ctx context.Context, sel ast.SelectionSet, v *models.GitCommitConnection) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._GitCommitConnection(ctx, sel, v)
+}
+
+func (ec *executionContext) marshalNGitDiffHunk2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐDiffHunk(ctx context.Context, sel ast.SelectionSet, v repository.DiffHunk) graphql.Marshaler {
+ return ec._GitDiffHunk(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNGitDiffHunk2ᚕgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐDiffHunkᚄ(ctx context.Context, sel ast.SelectionSet, v []repository.DiffHunk) graphql.Marshaler {
+ ret := make(graphql.Array, len(v))
+ var wg sync.WaitGroup
+ isLen1 := len(v) == 1
+ if !isLen1 {
+ wg.Add(len(v))
+ }
+ for i := range v {
+ i := i
+ fc := &graphql.FieldContext{
+ Index: &i,
+ Result: &v[i],
+ }
+ ctx := graphql.WithFieldContext(ctx, fc)
+ f := func(i int) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = nil
+ }
+ }()
+ if !isLen1 {
+ defer wg.Done()
+ }
+ ret[i] = ec.marshalNGitDiffHunk2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐDiffHunk(ctx, sel, v[i])
+ }
+ if isLen1 {
+ f(i)
+ } else {
+ go f(i)
+ }
+
+ }
+ wg.Wait()
+
+ for _, e := range ret {
+ if e == graphql.Null {
+ return graphql.Null
+ }
+ }
+
+ return ret
+}
+
+func (ec *executionContext) marshalNGitDiffLine2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐDiffLine(ctx context.Context, sel ast.SelectionSet, v repository.DiffLine) graphql.Marshaler {
+ return ec._GitDiffLine(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNGitDiffLine2ᚕgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐDiffLineᚄ(ctx context.Context, sel ast.SelectionSet, v []repository.DiffLine) graphql.Marshaler {
+ ret := make(graphql.Array, len(v))
+ var wg sync.WaitGroup
+ isLen1 := len(v) == 1
+ if !isLen1 {
+ wg.Add(len(v))
+ }
+ for i := range v {
+ i := i
+ fc := &graphql.FieldContext{
+ Index: &i,
+ Result: &v[i],
+ }
+ ctx := graphql.WithFieldContext(ctx, fc)
+ f := func(i int) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = nil
+ }
+ }()
+ if !isLen1 {
+ defer wg.Done()
+ }
+ ret[i] = ec.marshalNGitDiffLine2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐDiffLine(ctx, sel, v[i])
+ }
+ if isLen1 {
+ f(i)
+ } else {
+ go f(i)
+ }
+
+ }
+ wg.Wait()
+
+ for _, e := range ret {
+ if e == graphql.Null {
+ return graphql.Null
+ }
+ }
+
+ return ret
+}
+
+func (ec *executionContext) unmarshalNGitDiffLineType2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐDiffLineType(ctx context.Context, v any) (repository.DiffLineType, error) {
+ var res repository.DiffLineType
+ err := res.UnmarshalGQL(v)
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNGitDiffLineType2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐDiffLineType(ctx context.Context, sel ast.SelectionSet, v repository.DiffLineType) graphql.Marshaler {
+ return v
+}
+
+func (ec *executionContext) marshalNGitLastCommit2ᚕᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitLastCommitᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.GitLastCommit) graphql.Marshaler {
+ ret := make(graphql.Array, len(v))
+ var wg sync.WaitGroup
+ isLen1 := len(v) == 1
+ if !isLen1 {
+ wg.Add(len(v))
+ }
+ for i := range v {
+ i := i
+ fc := &graphql.FieldContext{
+ Index: &i,
+ Result: &v[i],
+ }
+ ctx := graphql.WithFieldContext(ctx, fc)
+ f := func(i int) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = nil
+ }
+ }()
+ if !isLen1 {
+ defer wg.Done()
+ }
+ ret[i] = ec.marshalNGitLastCommit2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitLastCommit(ctx, sel, v[i])
+ }
+ if isLen1 {
+ f(i)
+ } else {
+ go f(i)
+ }
+
+ }
+ wg.Wait()
+
+ for _, e := range ret {
+ if e == graphql.Null {
+ return graphql.Null
+ }
+ }
+
+ return ret
+}
+
+func (ec *executionContext) marshalNGitLastCommit2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitLastCommit(ctx context.Context, sel ast.SelectionSet, v *models.GitLastCommit) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._GitLastCommit(ctx, sel, v)
+}
+
+func (ec *executionContext) unmarshalNGitObjectType2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐObjectType(ctx context.Context, v any) (repository.ObjectType, error) {
+ var res repository.ObjectType
+ err := res.UnmarshalGQL(v)
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNGitObjectType2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐObjectType(ctx context.Context, sel ast.SelectionSet, v repository.ObjectType) graphql.Marshaler {
+ return v
+}
+
+func (ec *executionContext) marshalNGitRef2ᚕᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.GitRef) graphql.Marshaler {
+ ret := make(graphql.Array, len(v))
+ var wg sync.WaitGroup
+ isLen1 := len(v) == 1
+ if !isLen1 {
+ wg.Add(len(v))
+ }
+ for i := range v {
+ i := i
+ fc := &graphql.FieldContext{
+ Index: &i,
+ Result: &v[i],
+ }
+ ctx := graphql.WithFieldContext(ctx, fc)
+ f := func(i int) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = nil
+ }
+ }()
+ if !isLen1 {
+ defer wg.Done()
+ }
+ ret[i] = ec.marshalNGitRef2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRef(ctx, sel, v[i])
+ }
+ if isLen1 {
+ f(i)
+ } else {
+ go f(i)
+ }
+
+ }
+ wg.Wait()
+
+ for _, e := range ret {
+ if e == graphql.Null {
+ return graphql.Null
+ }
+ }
+
+ return ret
+}
+
+func (ec *executionContext) marshalNGitRef2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRef(ctx context.Context, sel ast.SelectionSet, v *models.GitRef) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._GitRef(ctx, sel, v)
+}
+
+func (ec *executionContext) marshalNGitRefConnection2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefConnection(ctx context.Context, sel ast.SelectionSet, v models.GitRefConnection) graphql.Marshaler {
+ return ec._GitRefConnection(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNGitRefConnection2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefConnection(ctx context.Context, sel ast.SelectionSet, v *models.GitRefConnection) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._GitRefConnection(ctx, sel, v)
+}
+
+func (ec *executionContext) unmarshalNGitRefType2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefType(ctx context.Context, v any) (models.GitRefType, error) {
+ tmp, err := graphql.UnmarshalString(v)
+ res := unmarshalNGitRefType2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefType[tmp]
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNGitRefType2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefType(ctx context.Context, sel ast.SelectionSet, v models.GitRefType) graphql.Marshaler {
+ _ = sel
+ res := graphql.MarshalString(marshalNGitRefType2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefType[v])
+ if res == graphql.Null {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ }
+ return res
+}
+
+var (
+ unmarshalNGitRefType2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefType = map[string]models.GitRefType{
+ "BRANCH": models.GitRefTypeBranch,
+ "TAG": models.GitRefTypeTag,
+ }
+ marshalNGitRefType2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefType = map[models.GitRefType]string{
+ models.GitRefTypeBranch: "BRANCH",
+ models.GitRefTypeTag: "TAG",
+ }
+)
+
+func (ec *executionContext) marshalNGitTreeEntry2ᚕᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐTreeEntryᚄ(ctx context.Context, sel ast.SelectionSet, v []*repository.TreeEntry) graphql.Marshaler {
+ ret := make(graphql.Array, len(v))
+ var wg sync.WaitGroup
+ isLen1 := len(v) == 1
+ if !isLen1 {
+ wg.Add(len(v))
+ }
+ for i := range v {
+ i := i
+ fc := &graphql.FieldContext{
+ Index: &i,
+ Result: &v[i],
+ }
+ ctx := graphql.WithFieldContext(ctx, fc)
+ f := func(i int) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = nil
+ }
+ }()
+ if !isLen1 {
+ defer wg.Done()
+ }
+ ret[i] = ec.marshalNGitTreeEntry2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐTreeEntry(ctx, sel, v[i])
+ }
+ if isLen1 {
+ f(i)
+ } else {
+ go f(i)
+ }
+
+ }
+ wg.Wait()
+
+ for _, e := range ret {
+ if e == graphql.Null {
+ return graphql.Null
+ }
+ }
+
+ return ret
+}
+
+func (ec *executionContext) marshalNGitTreeEntry2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐTreeEntry(ctx context.Context, sel ast.SelectionSet, v *repository.TreeEntry) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._GitTreeEntry(ctx, sel, v)
+}
+
+func (ec *executionContext) marshalOGitBlob2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitBlob(ctx context.Context, sel ast.SelectionSet, v *models.GitBlob) graphql.Marshaler {
+ if v == nil {
+ return graphql.Null
+ }
+ return ec._GitBlob(ctx, sel, v)
+}
+
+func (ec *executionContext) marshalOGitCommit2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitCommitMeta(ctx context.Context, sel ast.SelectionSet, v *models.GitCommitMeta) graphql.Marshaler {
+ if v == nil {
+ return graphql.Null
+ }
+ return ec._GitCommit(ctx, sel, v)
+}
+
+func (ec *executionContext) marshalOGitFileDiff2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐFileDiff(ctx context.Context, sel ast.SelectionSet, v *repository.FileDiff) graphql.Marshaler {
+ if v == nil {
+ return graphql.Null
+ }
+ return ec._GitFileDiff(ctx, sel, v)
+}
+
+func (ec *executionContext) unmarshalOGitRefType2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefType(ctx context.Context, v any) (*models.GitRefType, error) {
+ if v == nil {
+ return nil, nil
+ }
+ tmp, err := graphql.UnmarshalString(v)
+ res := unmarshalOGitRefType2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefType[tmp]
+ return &res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalOGitRefType2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefType(ctx context.Context, sel ast.SelectionSet, v *models.GitRefType) graphql.Marshaler {
+ if v == nil {
+ return graphql.Null
+ }
+ _ = sel
+ _ = ctx
+ res := graphql.MarshalString(marshalOGitRefType2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefType[*v])
+ return res
+}
+
+var (
+ unmarshalOGitRefType2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefType = map[string]models.GitRefType{
+ "BRANCH": models.GitRefTypeBranch,
+ "TAG": models.GitRefTypeTag,
+ }
+ marshalOGitRefType2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefType = map[models.GitRefType]string{
+ models.GitRefTypeBranch: "BRANCH",
+ models.GitRefTypeTag: "TAG",
+ }
+)
+
+// endregion ***************************** type.gotpl *****************************
@@ -13,6 +13,7 @@ import (
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/introspection"
"github.com/git-bug/git-bug/entity"
+ "github.com/git-bug/git-bug/repository"
"github.com/vektah/gqlparser/v2/ast"
)
@@ -2539,6 +2540,16 @@ func (ec *executionContext) marshalNString2githubᚗcomᚋgitᚑbugᚋgitᚑbug
return v
}
+func (ec *executionContext) unmarshalNString2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐHash(ctx context.Context, v any) (repository.Hash, error) {
+ var res repository.Hash
+ err := res.UnmarshalGQL(v)
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNString2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋrepositoryᚐHash(ctx context.Context, sel ast.SelectionSet, v repository.Hash) graphql.Marshaler {
+ return v
+}
+
func (ec *executionContext) unmarshalNString2string(ctx context.Context, v any) (string, error) {
res, err := graphql.UnmarshalString(v)
return res, graphql.ErrorOnPath(ctx, err)
@@ -9,9 +9,11 @@ import (
"strconv"
"sync"
"sync/atomic"
+ "time"
"github.com/99designs/gqlgen/graphql"
"github.com/git-bug/git-bug/api/graphql/models"
+ "github.com/git-bug/git-bug/repository"
"github.com/vektah/gqlparser/v2/ast"
)
@@ -24,6 +26,12 @@ type RepositoryResolver interface {
AllIdentities(ctx context.Context, obj *models.Repository, after *string, before *string, first *int, last *int) (*models.IdentityConnection, error)
Identity(ctx context.Context, obj *models.Repository, prefix string) (models.IdentityWrapper, error)
UserIdentity(ctx context.Context, obj *models.Repository) (models.IdentityWrapper, error)
+ Refs(ctx context.Context, obj *models.Repository, after *string, before *string, first *int, last *int, typeArg *models.GitRefType) (*models.GitRefConnection, error)
+ Tree(ctx context.Context, obj *models.Repository, ref string, path *string) ([]*repository.TreeEntry, error)
+ Blob(ctx context.Context, obj *models.Repository, ref string, path string) (*models.GitBlob, error)
+ Commits(ctx context.Context, obj *models.Repository, after *string, first *int, ref string, path *string, since *time.Time, until *time.Time) (*models.GitCommitConnection, error)
+ Commit(ctx context.Context, obj *models.Repository, hash string) (*models.GitCommitMeta, error)
+ LastCommits(ctx context.Context, obj *models.Repository, ref string, path *string, names []string) ([]*models.GitLastCommit, error)
ValidLabels(ctx context.Context, obj *models.Repository, after *string, before *string, first *int, last *int) (*models.LabelConnection, error)
}
@@ -248,6 +256,57 @@ func (ec *executionContext) field_Repository_allIdentities_argsLast(
return zeroVal, nil
}
+func (ec *executionContext) field_Repository_blob_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Repository_blob_argsRef(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["ref"] = arg0
+ arg1, err := ec.field_Repository_blob_argsPath(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["path"] = arg1
+ return args, nil
+}
+func (ec *executionContext) field_Repository_blob_argsRef(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (string, error) {
+ if _, ok := rawArgs["ref"]; !ok {
+ var zeroVal string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("ref"))
+ if tmp, ok := rawArgs["ref"]; ok {
+ return ec.unmarshalNString2string(ctx, tmp)
+ }
+
+ var zeroVal string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_blob_argsPath(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (string, error) {
+ if _, ok := rawArgs["path"]; !ok {
+ var zeroVal string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("path"))
+ if tmp, ok := rawArgs["path"]; ok {
+ return ec.unmarshalNString2string(ctx, tmp)
+ }
+
+ var zeroVal string
+ return zeroVal, nil
+}
+
func (ec *executionContext) field_Repository_bug_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
@@ -276,6 +335,177 @@ func (ec *executionContext) field_Repository_bug_argsPrefix(
return zeroVal, nil
}
+func (ec *executionContext) field_Repository_commit_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Repository_commit_argsHash(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["hash"] = arg0
+ return args, nil
+}
+func (ec *executionContext) field_Repository_commit_argsHash(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (string, error) {
+ if _, ok := rawArgs["hash"]; !ok {
+ var zeroVal string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("hash"))
+ if tmp, ok := rawArgs["hash"]; ok {
+ return ec.unmarshalNString2string(ctx, tmp)
+ }
+
+ var zeroVal string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_commits_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Repository_commits_argsAfter(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["after"] = arg0
+ arg1, err := ec.field_Repository_commits_argsFirst(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["first"] = arg1
+ arg2, err := ec.field_Repository_commits_argsRef(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["ref"] = arg2
+ arg3, err := ec.field_Repository_commits_argsPath(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["path"] = arg3
+ arg4, err := ec.field_Repository_commits_argsSince(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["since"] = arg4
+ arg5, err := ec.field_Repository_commits_argsUntil(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["until"] = arg5
+ return args, nil
+}
+func (ec *executionContext) field_Repository_commits_argsAfter(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*string, error) {
+ if _, ok := rawArgs["after"]; !ok {
+ var zeroVal *string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after"))
+ if tmp, ok := rawArgs["after"]; ok {
+ return ec.unmarshalOString2ᚖstring(ctx, tmp)
+ }
+
+ var zeroVal *string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_commits_argsFirst(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*int, error) {
+ if _, ok := rawArgs["first"]; !ok {
+ var zeroVal *int
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("first"))
+ if tmp, ok := rawArgs["first"]; ok {
+ return ec.unmarshalOInt2ᚖint(ctx, tmp)
+ }
+
+ var zeroVal *int
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_commits_argsRef(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (string, error) {
+ if _, ok := rawArgs["ref"]; !ok {
+ var zeroVal string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("ref"))
+ if tmp, ok := rawArgs["ref"]; ok {
+ return ec.unmarshalNString2string(ctx, tmp)
+ }
+
+ var zeroVal string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_commits_argsPath(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*string, error) {
+ if _, ok := rawArgs["path"]; !ok {
+ var zeroVal *string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("path"))
+ if tmp, ok := rawArgs["path"]; ok {
+ return ec.unmarshalOString2ᚖstring(ctx, tmp)
+ }
+
+ var zeroVal *string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_commits_argsSince(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*time.Time, error) {
+ if _, ok := rawArgs["since"]; !ok {
+ var zeroVal *time.Time
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("since"))
+ if tmp, ok := rawArgs["since"]; ok {
+ return ec.unmarshalOTime2ᚖtimeᚐTime(ctx, tmp)
+ }
+
+ var zeroVal *time.Time
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_commits_argsUntil(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*time.Time, error) {
+ if _, ok := rawArgs["until"]; !ok {
+ var zeroVal *time.Time
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("until"))
+ if tmp, ok := rawArgs["until"]; ok {
+ return ec.unmarshalOTime2ᚖtimeᚐTime(ctx, tmp)
+ }
+
+ var zeroVal *time.Time
+ return zeroVal, nil
+}
+
func (ec *executionContext) field_Repository_identity_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
@@ -304,32 +534,111 @@ func (ec *executionContext) field_Repository_identity_argsPrefix(
return zeroVal, nil
}
-func (ec *executionContext) field_Repository_validLabels_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+func (ec *executionContext) field_Repository_lastCommits_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
- arg0, err := ec.field_Repository_validLabels_argsAfter(ctx, rawArgs)
+ arg0, err := ec.field_Repository_lastCommits_argsRef(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["ref"] = arg0
+ arg1, err := ec.field_Repository_lastCommits_argsPath(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["path"] = arg1
+ arg2, err := ec.field_Repository_lastCommits_argsNames(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["names"] = arg2
+ return args, nil
+}
+func (ec *executionContext) field_Repository_lastCommits_argsRef(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (string, error) {
+ if _, ok := rawArgs["ref"]; !ok {
+ var zeroVal string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("ref"))
+ if tmp, ok := rawArgs["ref"]; ok {
+ return ec.unmarshalNString2string(ctx, tmp)
+ }
+
+ var zeroVal string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_lastCommits_argsPath(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*string, error) {
+ if _, ok := rawArgs["path"]; !ok {
+ var zeroVal *string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("path"))
+ if tmp, ok := rawArgs["path"]; ok {
+ return ec.unmarshalOString2ᚖstring(ctx, tmp)
+ }
+
+ var zeroVal *string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_lastCommits_argsNames(
+ ctx context.Context,
+ rawArgs map[string]any,
+) ([]string, error) {
+ if _, ok := rawArgs["names"]; !ok {
+ var zeroVal []string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("names"))
+ if tmp, ok := rawArgs["names"]; ok {
+ return ec.unmarshalNString2ᚕstringᚄ(ctx, tmp)
+ }
+
+ var zeroVal []string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_refs_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Repository_refs_argsAfter(ctx, rawArgs)
if err != nil {
return nil, err
}
args["after"] = arg0
- arg1, err := ec.field_Repository_validLabels_argsBefore(ctx, rawArgs)
+ arg1, err := ec.field_Repository_refs_argsBefore(ctx, rawArgs)
if err != nil {
return nil, err
}
args["before"] = arg1
- arg2, err := ec.field_Repository_validLabels_argsFirst(ctx, rawArgs)
+ arg2, err := ec.field_Repository_refs_argsFirst(ctx, rawArgs)
if err != nil {
return nil, err
}
args["first"] = arg2
- arg3, err := ec.field_Repository_validLabels_argsLast(ctx, rawArgs)
+ arg3, err := ec.field_Repository_refs_argsLast(ctx, rawArgs)
if err != nil {
return nil, err
}
args["last"] = arg3
+ arg4, err := ec.field_Repository_refs_argsType(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["type"] = arg4
return args, nil
}
-func (ec *executionContext) field_Repository_validLabels_argsAfter(
+func (ec *executionContext) field_Repository_refs_argsAfter(
ctx context.Context,
rawArgs map[string]any,
) (*string, error) {
@@ -337,80 +646,626 @@ func (ec *executionContext) field_Repository_validLabels_argsAfter(
var zeroVal *string
return zeroVal, nil
}
-
- ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after"))
- if tmp, ok := rawArgs["after"]; ok {
- return ec.unmarshalOString2ᚖstring(ctx, tmp)
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after"))
+ if tmp, ok := rawArgs["after"]; ok {
+ return ec.unmarshalOString2ᚖstring(ctx, tmp)
+ }
+
+ var zeroVal *string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_refs_argsBefore(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*string, error) {
+ if _, ok := rawArgs["before"]; !ok {
+ var zeroVal *string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before"))
+ if tmp, ok := rawArgs["before"]; ok {
+ return ec.unmarshalOString2ᚖstring(ctx, tmp)
+ }
+
+ var zeroVal *string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_refs_argsFirst(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*int, error) {
+ if _, ok := rawArgs["first"]; !ok {
+ var zeroVal *int
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("first"))
+ if tmp, ok := rawArgs["first"]; ok {
+ return ec.unmarshalOInt2ᚖint(ctx, tmp)
+ }
+
+ var zeroVal *int
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_refs_argsLast(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*int, error) {
+ if _, ok := rawArgs["last"]; !ok {
+ var zeroVal *int
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("last"))
+ if tmp, ok := rawArgs["last"]; ok {
+ return ec.unmarshalOInt2ᚖint(ctx, tmp)
+ }
+
+ var zeroVal *int
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_refs_argsType(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*models.GitRefType, error) {
+ if _, ok := rawArgs["type"]; !ok {
+ var zeroVal *models.GitRefType
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("type"))
+ if tmp, ok := rawArgs["type"]; ok {
+ return ec.unmarshalOGitRefType2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitRefType(ctx, tmp)
+ }
+
+ var zeroVal *models.GitRefType
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_tree_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Repository_tree_argsRef(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["ref"] = arg0
+ arg1, err := ec.field_Repository_tree_argsPath(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["path"] = arg1
+ return args, nil
+}
+func (ec *executionContext) field_Repository_tree_argsRef(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (string, error) {
+ if _, ok := rawArgs["ref"]; !ok {
+ var zeroVal string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("ref"))
+ if tmp, ok := rawArgs["ref"]; ok {
+ return ec.unmarshalNString2string(ctx, tmp)
+ }
+
+ var zeroVal string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_tree_argsPath(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*string, error) {
+ if _, ok := rawArgs["path"]; !ok {
+ var zeroVal *string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("path"))
+ if tmp, ok := rawArgs["path"]; ok {
+ return ec.unmarshalOString2ᚖstring(ctx, tmp)
+ }
+
+ var zeroVal *string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_validLabels_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Repository_validLabels_argsAfter(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["after"] = arg0
+ arg1, err := ec.field_Repository_validLabels_argsBefore(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["before"] = arg1
+ arg2, err := ec.field_Repository_validLabels_argsFirst(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["first"] = arg2
+ arg3, err := ec.field_Repository_validLabels_argsLast(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["last"] = arg3
+ return args, nil
+}
+func (ec *executionContext) field_Repository_validLabels_argsAfter(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*string, error) {
+ if _, ok := rawArgs["after"]; !ok {
+ var zeroVal *string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after"))
+ if tmp, ok := rawArgs["after"]; ok {
+ return ec.unmarshalOString2ᚖstring(ctx, tmp)
+ }
+
+ var zeroVal *string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_validLabels_argsBefore(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*string, error) {
+ if _, ok := rawArgs["before"]; !ok {
+ var zeroVal *string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before"))
+ if tmp, ok := rawArgs["before"]; ok {
+ return ec.unmarshalOString2ᚖstring(ctx, tmp)
+ }
+
+ var zeroVal *string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_validLabels_argsFirst(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*int, error) {
+ if _, ok := rawArgs["first"]; !ok {
+ var zeroVal *int
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("first"))
+ if tmp, ok := rawArgs["first"]; ok {
+ return ec.unmarshalOInt2ᚖint(ctx, tmp)
+ }
+
+ var zeroVal *int
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Repository_validLabels_argsLast(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*int, error) {
+ if _, ok := rawArgs["last"]; !ok {
+ var zeroVal *int
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("last"))
+ if tmp, ok := rawArgs["last"]; ok {
+ return ec.unmarshalOInt2ᚖint(ctx, tmp)
+ }
+
+ var zeroVal *int
+ return zeroVal, nil
+}
+
+// endregion ***************************** args.gotpl *****************************
+
+// region ************************** directives.gotpl **************************
+
+// endregion ************************** directives.gotpl **************************
+
+// region **************************** field.gotpl *****************************
+
+func (ec *executionContext) _Repository_name(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Repository_name(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Repository().Name(rctx, obj)
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ return graphql.Null
+ }
+ res := resTmp.(*string)
+ fc.Result = res
+ return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Repository_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Repository",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _Repository_allBugs(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Repository_allBugs(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Repository().AllBugs(rctx, obj, fc.Args["after"].(*string), fc.Args["before"].(*string), fc.Args["first"].(*int), fc.Args["last"].(*int), fc.Args["query"].(*string))
+ })
+ 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.(*models.BugConnection)
+ fc.Result = res
+ return ec.marshalNBugConnection2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐBugConnection(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Repository_allBugs(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Repository",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "edges":
+ return ec.fieldContext_BugConnection_edges(ctx, field)
+ case "nodes":
+ return ec.fieldContext_BugConnection_nodes(ctx, field)
+ case "pageInfo":
+ return ec.fieldContext_BugConnection_pageInfo(ctx, field)
+ case "totalCount":
+ return ec.fieldContext_BugConnection_totalCount(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type BugConnection", field.Name)
+ },
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_Repository_allBugs_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _Repository_bug(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Repository_bug(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Repository().Bug(rctx, obj, fc.Args["prefix"].(string))
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ return graphql.Null
+ }
+ res := resTmp.(models.BugWrapper)
+ fc.Result = res
+ return ec.marshalOBug2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐBugWrapper(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Repository_bug(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Repository",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "id":
+ return ec.fieldContext_Bug_id(ctx, field)
+ case "humanId":
+ return ec.fieldContext_Bug_humanId(ctx, field)
+ case "status":
+ return ec.fieldContext_Bug_status(ctx, field)
+ case "title":
+ return ec.fieldContext_Bug_title(ctx, field)
+ case "labels":
+ return ec.fieldContext_Bug_labels(ctx, field)
+ case "author":
+ return ec.fieldContext_Bug_author(ctx, field)
+ case "createdAt":
+ return ec.fieldContext_Bug_createdAt(ctx, field)
+ case "lastEdit":
+ return ec.fieldContext_Bug_lastEdit(ctx, field)
+ case "actors":
+ return ec.fieldContext_Bug_actors(ctx, field)
+ case "participants":
+ return ec.fieldContext_Bug_participants(ctx, field)
+ case "comments":
+ return ec.fieldContext_Bug_comments(ctx, field)
+ case "timeline":
+ return ec.fieldContext_Bug_timeline(ctx, field)
+ case "operations":
+ return ec.fieldContext_Bug_operations(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type Bug", field.Name)
+ },
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_Repository_bug_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _Repository_allIdentities(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Repository_allIdentities(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Repository().AllIdentities(rctx, obj, fc.Args["after"].(*string), fc.Args["before"].(*string), fc.Args["first"].(*int), fc.Args["last"].(*int))
+ })
+ 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.(*models.IdentityConnection)
+ fc.Result = res
+ return ec.marshalNIdentityConnection2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐIdentityConnection(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Repository_allIdentities(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Repository",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "edges":
+ return ec.fieldContext_IdentityConnection_edges(ctx, field)
+ case "nodes":
+ return ec.fieldContext_IdentityConnection_nodes(ctx, field)
+ case "pageInfo":
+ return ec.fieldContext_IdentityConnection_pageInfo(ctx, field)
+ case "totalCount":
+ return ec.fieldContext_IdentityConnection_totalCount(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type IdentityConnection", field.Name)
+ },
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_Repository_allIdentities_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _Repository_identity(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Repository_identity(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Repository().Identity(rctx, obj, fc.Args["prefix"].(string))
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ return graphql.Null
}
-
- var zeroVal *string
- return zeroVal, nil
+ res := resTmp.(models.IdentityWrapper)
+ fc.Result = res
+ return ec.marshalOIdentity2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐIdentityWrapper(ctx, field.Selections, res)
}
-func (ec *executionContext) field_Repository_validLabels_argsBefore(
- ctx context.Context,
- rawArgs map[string]any,
-) (*string, error) {
- if _, ok := rawArgs["before"]; !ok {
- var zeroVal *string
- return zeroVal, nil
+func (ec *executionContext) fieldContext_Repository_identity(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Repository",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "id":
+ return ec.fieldContext_Identity_id(ctx, field)
+ case "humanId":
+ return ec.fieldContext_Identity_humanId(ctx, field)
+ case "name":
+ return ec.fieldContext_Identity_name(ctx, field)
+ case "email":
+ return ec.fieldContext_Identity_email(ctx, field)
+ case "login":
+ return ec.fieldContext_Identity_login(ctx, field)
+ case "displayName":
+ return ec.fieldContext_Identity_displayName(ctx, field)
+ case "avatarUrl":
+ return ec.fieldContext_Identity_avatarUrl(ctx, field)
+ case "isProtected":
+ return ec.fieldContext_Identity_isProtected(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type Identity", field.Name)
+ },
}
-
- ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before"))
- if tmp, ok := rawArgs["before"]; ok {
- return ec.unmarshalOString2ᚖstring(ctx, tmp)
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_Repository_identity_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
}
-
- var zeroVal *string
- return zeroVal, nil
+ return fc, nil
}
-func (ec *executionContext) field_Repository_validLabels_argsFirst(
- ctx context.Context,
- rawArgs map[string]any,
-) (*int, error) {
- if _, ok := rawArgs["first"]; !ok {
- var zeroVal *int
- return zeroVal, nil
+func (ec *executionContext) _Repository_userIdentity(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Repository_userIdentity(ctx, field)
+ if err != nil {
+ return graphql.Null
}
-
- ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("first"))
- if tmp, ok := rawArgs["first"]; ok {
- return ec.unmarshalOInt2ᚖint(ctx, tmp)
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Repository().UserIdentity(rctx, obj)
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
}
-
- var zeroVal *int
- return zeroVal, nil
-}
-
-func (ec *executionContext) field_Repository_validLabels_argsLast(
- ctx context.Context,
- rawArgs map[string]any,
-) (*int, error) {
- if _, ok := rawArgs["last"]; !ok {
- var zeroVal *int
- return zeroVal, nil
+ if resTmp == nil {
+ return graphql.Null
}
+ res := resTmp.(models.IdentityWrapper)
+ fc.Result = res
+ return ec.marshalOIdentity2githubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐIdentityWrapper(ctx, field.Selections, res)
+}
- ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("last"))
- if tmp, ok := rawArgs["last"]; ok {
- return ec.unmarshalOInt2ᚖint(ctx, tmp)
+func (ec *executionContext) fieldContext_Repository_userIdentity(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Repository",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "id":
+ return ec.fieldContext_Identity_id(ctx, field)
+ case "humanId":
+ return ec.fieldContext_Identity_humanId(ctx, field)
+ case "name":
+ return ec.fieldContext_Identity_name(ctx, field)
+ case "email":
+ return ec.fieldContext_Identity_email(ctx, field)
+ case "login":
+ return ec.fieldContext_Identity_login(ctx, field)
+ case "displayName":
+ return ec.fieldContext_Identity_displayName(ctx, field)
+ case "avatarUrl":
+ return ec.fieldContext_Identity_avatarUrl(ctx, field)
+ case "isProtected":
+ return ec.fieldContext_Identity_isProtected(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type Identity", field.Name)
+ },
}
-
- var zeroVal *int
- return zeroVal, nil
+ return fc, nil
}
-// endregion ***************************** args.gotpl *****************************
-
-// region ************************** directives.gotpl **************************
-
-// endregion ************************** directives.gotpl **************************
-
-// region **************************** field.gotpl *****************************
-
-func (ec *executionContext) _Repository_name(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) {
- fc, err := ec.fieldContext_Repository_name(ctx, field)
+func (ec *executionContext) _Repository_refs(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Repository_refs(ctx, field)
if err != nil {
return graphql.Null
}
@@ -1122,6 +1122,18 @@ func (ec *executionContext) fieldContext_Query_repository(ctx context.Context, f
return ec.fieldContext_Repository_identity(ctx, field)
case "userIdentity":
return ec.fieldContext_Repository_userIdentity(ctx, field)
+ case "refs":
+ return ec.fieldContext_Repository_refs(ctx, field)
+ case "tree":
+ return ec.fieldContext_Repository_tree(ctx, field)
+ case "blob":
+ return ec.fieldContext_Repository_blob(ctx, field)
+ case "commits":
+ return ec.fieldContext_Repository_commits(ctx, field)
+ case "commit":
+ return ec.fieldContext_Repository_commit(ctx, field)
+ case "lastCommits":
+ return ec.fieldContext_Repository_lastCommits(ctx, field)
case "validLabels":
return ec.fieldContext_Repository_validLabels(ctx, field)
}
@@ -7,6 +7,7 @@ import (
"context"
"errors"
"sync/atomic"
+ "time"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/introspection"
@@ -48,6 +49,7 @@ type ResolverRoot interface {
BugSetTitleOperation() BugSetTitleOperationResolver
BugSetTitleTimelineItem() BugSetTitleTimelineItemResolver
Color() ColorResolver
+ GitCommit() GitCommitResolver
Identity() IdentityResolver
Label() LabelResolver
Mutation() MutationResolver
@@ -293,6 +295,95 @@ type ComplexityRoot struct {
Type func(childComplexity int) int
}
+ GitBlob struct {
+ Hash func(childComplexity int) int
+ IsBinary func(childComplexity int) int
+ IsTruncated func(childComplexity int) int
+ Path func(childComplexity int) int
+ Size func(childComplexity int) int
+ Text func(childComplexity int) int
+ }
+
+ GitChangedFile struct {
+ OldPath func(childComplexity int) int
+ Path func(childComplexity int) int
+ Status func(childComplexity int) int
+ }
+
+ GitChangedFileConnection struct {
+ Nodes func(childComplexity int) int
+ PageInfo func(childComplexity int) int
+ TotalCount func(childComplexity int) int
+ }
+
+ GitCommit struct {
+ AuthorEmail func(childComplexity int) int
+ AuthorName func(childComplexity int) int
+ Date func(childComplexity int) int
+ Diff func(childComplexity int, path string) int
+ Files func(childComplexity int, after *string, before *string, first *int, last *int) int
+ FullMessage func(childComplexity int) int
+ Hash func(childComplexity int) int
+ Message func(childComplexity int) int
+ Parents func(childComplexity int) int
+ ShortHash func(childComplexity int) int
+ }
+
+ GitCommitConnection struct {
+ Nodes func(childComplexity int) int
+ PageInfo func(childComplexity int) int
+ TotalCount func(childComplexity int) int
+ }
+
+ GitDiffHunk struct {
+ Lines func(childComplexity int) int
+ NewLines func(childComplexity int) int
+ NewStart func(childComplexity int) int
+ OldLines func(childComplexity int) int
+ OldStart func(childComplexity int) int
+ }
+
+ GitDiffLine struct {
+ Content func(childComplexity int) int
+ NewLine func(childComplexity int) int
+ OldLine func(childComplexity int) int
+ Type func(childComplexity int) int
+ }
+
+ GitFileDiff struct {
+ Hunks func(childComplexity int) int
+ IsBinary func(childComplexity int) int
+ IsDelete func(childComplexity int) int
+ IsNew func(childComplexity int) int
+ OldPath func(childComplexity int) int
+ Path func(childComplexity int) int
+ }
+
+ GitLastCommit struct {
+ Commit func(childComplexity int) int
+ Name func(childComplexity int) int
+ }
+
+ GitRef struct {
+ Hash func(childComplexity int) int
+ IsDefault func(childComplexity int) int
+ Name func(childComplexity int) int
+ ShortName func(childComplexity int) int
+ Type func(childComplexity int) int
+ }
+
+ GitRefConnection struct {
+ Nodes func(childComplexity int) int
+ PageInfo func(childComplexity int) int
+ TotalCount func(childComplexity int) int
+ }
+
+ GitTreeEntry struct {
+ Hash func(childComplexity int) int
+ Name func(childComplexity int) int
+ ObjectType func(childComplexity int) int
+ }
+
Identity struct {
AvatarUrl func(childComplexity int) int
DisplayName func(childComplexity int) int
@@ -383,9 +474,15 @@ type ComplexityRoot struct {
Repository struct {
AllBugs func(childComplexity int, after *string, before *string, first *int, last *int, query *string) int
AllIdentities func(childComplexity int, after *string, before *string, first *int, last *int) int
+ Blob func(childComplexity int, ref string, path string) int
Bug func(childComplexity int, prefix string) int
+ Commit func(childComplexity int, hash string) int
+ Commits func(childComplexity int, after *string, first *int, ref string, path *string, since *time.Time, until *time.Time) int
Identity func(childComplexity int, prefix string) int
+ LastCommits func(childComplexity int, ref string, path *string, names []string) int
Name func(childComplexity int) int
+ Refs func(childComplexity int, after *string, before *string, first *int, last *int, typeArg *models.GitRefType) int
+ Tree func(childComplexity int, ref string, path *string) int
UserIdentity func(childComplexity int) int
ValidLabels func(childComplexity int, after *string, before *string, first *int, last *int) int
}
@@ -1417,6 +1514,387 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.EntityEvent.Type(childComplexity), true
+ case "GitBlob.hash":
+ if e.complexity.GitBlob.Hash == nil {
+ break
+ }
+
+ return e.complexity.GitBlob.Hash(childComplexity), true
+
+ case "GitBlob.isBinary":
+ if e.complexity.GitBlob.IsBinary == nil {
+ break
+ }
+
+ return e.complexity.GitBlob.IsBinary(childComplexity), true
+
+ case "GitBlob.isTruncated":
+ if e.complexity.GitBlob.IsTruncated == nil {
+ break
+ }
+
+ return e.complexity.GitBlob.IsTruncated(childComplexity), true
+
+ case "GitBlob.path":
+ if e.complexity.GitBlob.Path == nil {
+ break
+ }
+
+ return e.complexity.GitBlob.Path(childComplexity), true
+
+ case "GitBlob.size":
+ if e.complexity.GitBlob.Size == nil {
+ break
+ }
+
+ return e.complexity.GitBlob.Size(childComplexity), true
+
+ case "GitBlob.text":
+ if e.complexity.GitBlob.Text == nil {
+ break
+ }
+
+ return e.complexity.GitBlob.Text(childComplexity), true
+
+ case "GitChangedFile.oldPath":
+ if e.complexity.GitChangedFile.OldPath == nil {
+ break
+ }
+
+ return e.complexity.GitChangedFile.OldPath(childComplexity), true
+
+ case "GitChangedFile.path":
+ if e.complexity.GitChangedFile.Path == nil {
+ break
+ }
+
+ return e.complexity.GitChangedFile.Path(childComplexity), true
+
+ case "GitChangedFile.status":
+ if e.complexity.GitChangedFile.Status == nil {
+ break
+ }
+
+ return e.complexity.GitChangedFile.Status(childComplexity), true
+
+ case "GitChangedFileConnection.nodes":
+ if e.complexity.GitChangedFileConnection.Nodes == nil {
+ break
+ }
+
+ return e.complexity.GitChangedFileConnection.Nodes(childComplexity), true
+
+ case "GitChangedFileConnection.pageInfo":
+ if e.complexity.GitChangedFileConnection.PageInfo == nil {
+ break
+ }
+
+ return e.complexity.GitChangedFileConnection.PageInfo(childComplexity), true
+
+ case "GitChangedFileConnection.totalCount":
+ if e.complexity.GitChangedFileConnection.TotalCount == nil {
+ break
+ }
+
+ return e.complexity.GitChangedFileConnection.TotalCount(childComplexity), true
+
+ case "GitCommit.authorEmail":
+ if e.complexity.GitCommit.AuthorEmail == nil {
+ break
+ }
+
+ return e.complexity.GitCommit.AuthorEmail(childComplexity), true
+
+ case "GitCommit.authorName":
+ if e.complexity.GitCommit.AuthorName == nil {
+ break
+ }
+
+ return e.complexity.GitCommit.AuthorName(childComplexity), true
+
+ case "GitCommit.date":
+ if e.complexity.GitCommit.Date == nil {
+ break
+ }
+
+ return e.complexity.GitCommit.Date(childComplexity), true
+
+ case "GitCommit.diff":
+ if e.complexity.GitCommit.Diff == nil {
+ break
+ }
+
+ args, err := ec.field_GitCommit_diff_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.GitCommit.Diff(childComplexity, args["path"].(string)), true
+
+ case "GitCommit.files":
+ if e.complexity.GitCommit.Files == nil {
+ break
+ }
+
+ args, err := ec.field_GitCommit_files_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.GitCommit.Files(childComplexity, args["after"].(*string), args["before"].(*string), args["first"].(*int), args["last"].(*int)), true
+
+ case "GitCommit.fullMessage":
+ if e.complexity.GitCommit.FullMessage == nil {
+ break
+ }
+
+ return e.complexity.GitCommit.FullMessage(childComplexity), true
+
+ case "GitCommit.hash":
+ if e.complexity.GitCommit.Hash == nil {
+ break
+ }
+
+ return e.complexity.GitCommit.Hash(childComplexity), true
+
+ case "GitCommit.message":
+ if e.complexity.GitCommit.Message == nil {
+ break
+ }
+
+ return e.complexity.GitCommit.Message(childComplexity), true
+
+ case "GitCommit.parents":
+ if e.complexity.GitCommit.Parents == nil {
+ break
+ }
+
+ return e.complexity.GitCommit.Parents(childComplexity), true
+
+ case "GitCommit.shortHash":
+ if e.complexity.GitCommit.ShortHash == nil {
+ break
+ }
+
+ return e.complexity.GitCommit.ShortHash(childComplexity), true
+
+ case "GitCommitConnection.nodes":
+ if e.complexity.GitCommitConnection.Nodes == nil {
+ break
+ }
+
+ return e.complexity.GitCommitConnection.Nodes(childComplexity), true
+
+ case "GitCommitConnection.pageInfo":
+ if e.complexity.GitCommitConnection.PageInfo == nil {
+ break
+ }
+
+ return e.complexity.GitCommitConnection.PageInfo(childComplexity), true
+
+ case "GitCommitConnection.totalCount":
+ if e.complexity.GitCommitConnection.TotalCount == nil {
+ break
+ }
+
+ return e.complexity.GitCommitConnection.TotalCount(childComplexity), true
+
+ case "GitDiffHunk.lines":
+ if e.complexity.GitDiffHunk.Lines == nil {
+ break
+ }
+
+ return e.complexity.GitDiffHunk.Lines(childComplexity), true
+
+ case "GitDiffHunk.newLines":
+ if e.complexity.GitDiffHunk.NewLines == nil {
+ break
+ }
+
+ return e.complexity.GitDiffHunk.NewLines(childComplexity), true
+
+ case "GitDiffHunk.newStart":
+ if e.complexity.GitDiffHunk.NewStart == nil {
+ break
+ }
+
+ return e.complexity.GitDiffHunk.NewStart(childComplexity), true
+
+ case "GitDiffHunk.oldLines":
+ if e.complexity.GitDiffHunk.OldLines == nil {
+ break
+ }
+
+ return e.complexity.GitDiffHunk.OldLines(childComplexity), true
+
+ case "GitDiffHunk.oldStart":
+ if e.complexity.GitDiffHunk.OldStart == nil {
+ break
+ }
+
+ return e.complexity.GitDiffHunk.OldStart(childComplexity), true
+
+ case "GitDiffLine.content":
+ if e.complexity.GitDiffLine.Content == nil {
+ break
+ }
+
+ return e.complexity.GitDiffLine.Content(childComplexity), true
+
+ case "GitDiffLine.newLine":
+ if e.complexity.GitDiffLine.NewLine == nil {
+ break
+ }
+
+ return e.complexity.GitDiffLine.NewLine(childComplexity), true
+
+ case "GitDiffLine.oldLine":
+ if e.complexity.GitDiffLine.OldLine == nil {
+ break
+ }
+
+ return e.complexity.GitDiffLine.OldLine(childComplexity), true
+
+ case "GitDiffLine.type":
+ if e.complexity.GitDiffLine.Type == nil {
+ break
+ }
+
+ return e.complexity.GitDiffLine.Type(childComplexity), true
+
+ case "GitFileDiff.hunks":
+ if e.complexity.GitFileDiff.Hunks == nil {
+ break
+ }
+
+ return e.complexity.GitFileDiff.Hunks(childComplexity), true
+
+ case "GitFileDiff.isBinary":
+ if e.complexity.GitFileDiff.IsBinary == nil {
+ break
+ }
+
+ return e.complexity.GitFileDiff.IsBinary(childComplexity), true
+
+ case "GitFileDiff.isDelete":
+ if e.complexity.GitFileDiff.IsDelete == nil {
+ break
+ }
+
+ return e.complexity.GitFileDiff.IsDelete(childComplexity), true
+
+ case "GitFileDiff.isNew":
+ if e.complexity.GitFileDiff.IsNew == nil {
+ break
+ }
+
+ return e.complexity.GitFileDiff.IsNew(childComplexity), true
+
+ case "GitFileDiff.oldPath":
+ if e.complexity.GitFileDiff.OldPath == nil {
+ break
+ }
+
+ return e.complexity.GitFileDiff.OldPath(childComplexity), true
+
+ case "GitFileDiff.path":
+ if e.complexity.GitFileDiff.Path == nil {
+ break
+ }
+
+ return e.complexity.GitFileDiff.Path(childComplexity), true
+
+ case "GitLastCommit.commit":
+ if e.complexity.GitLastCommit.Commit == nil {
+ break
+ }
+
+ return e.complexity.GitLastCommit.Commit(childComplexity), true
+
+ case "GitLastCommit.name":
+ if e.complexity.GitLastCommit.Name == nil {
+ break
+ }
+
+ return e.complexity.GitLastCommit.Name(childComplexity), true
+
+ case "GitRef.hash":
+ if e.complexity.GitRef.Hash == nil {
+ break
+ }
+
+ return e.complexity.GitRef.Hash(childComplexity), true
+
+ case "GitRef.isDefault":
+ if e.complexity.GitRef.IsDefault == nil {
+ break
+ }
+
+ return e.complexity.GitRef.IsDefault(childComplexity), true
+
+ case "GitRef.name":
+ if e.complexity.GitRef.Name == nil {
+ break
+ }
+
+ return e.complexity.GitRef.Name(childComplexity), true
+
+ case "GitRef.shortName":
+ if e.complexity.GitRef.ShortName == nil {
+ break
+ }
+
+ return e.complexity.GitRef.ShortName(childComplexity), true
+
+ case "GitRef.type":
+ if e.complexity.GitRef.Type == nil {
+ break
+ }
+
+ return e.complexity.GitRef.Type(childComplexity), true
+
+ case "GitRefConnection.nodes":
+ if e.complexity.GitRefConnection.Nodes == nil {
+ break
+ }
+
+ return e.complexity.GitRefConnection.Nodes(childComplexity), true
+
+ case "GitRefConnection.pageInfo":
+ if e.complexity.GitRefConnection.PageInfo == nil {
+ break
+ }
+
+ return e.complexity.GitRefConnection.PageInfo(childComplexity), true
+
+ case "GitRefConnection.totalCount":
+ if e.complexity.GitRefConnection.TotalCount == nil {
+ break
+ }
+
+ return e.complexity.GitRefConnection.TotalCount(childComplexity), true
+
+ case "GitTreeEntry.hash":
+ if e.complexity.GitTreeEntry.Hash == nil {
+ break
+ }
+
+ return e.complexity.GitTreeEntry.Hash(childComplexity), true
+
+ case "GitTreeEntry.name":
+ if e.complexity.GitTreeEntry.Name == nil {
+ break
+ }
+
+ return e.complexity.GitTreeEntry.Name(childComplexity), true
+
+ case "GitTreeEntry.type":
+ if e.complexity.GitTreeEntry.ObjectType == nil {
+ break
+ }
+
+ return e.complexity.GitTreeEntry.ObjectType(childComplexity), true
+
case "Identity.avatarUrl":
if e.complexity.Identity.AvatarUrl == nil {
break
@@ -1832,6 +2310,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Repository.AllIdentities(childComplexity, args["after"].(*string), args["before"].(*string), args["first"].(*int), args["last"].(*int)), true
+ case "Repository.blob":
+ if e.complexity.Repository.Blob == nil {
+ break
+ }
+
+ args, err := ec.field_Repository_blob_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Repository.Blob(childComplexity, args["ref"].(string), args["path"].(string)), true
+
case "Repository.bug":
if e.complexity.Repository.Bug == nil {
break
@@ -1844,6 +2334,30 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Repository.Bug(childComplexity, args["prefix"].(string)), true
+ case "Repository.commit":
+ if e.complexity.Repository.Commit == nil {
+ break
+ }
+
+ args, err := ec.field_Repository_commit_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Repository.Commit(childComplexity, args["hash"].(string)), true
+
+ case "Repository.commits":
+ if e.complexity.Repository.Commits == nil {
+ break
+ }
+
+ args, err := ec.field_Repository_commits_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Repository.Commits(childComplexity, args["after"].(*string), args["first"].(*int), args["ref"].(string), args["path"].(*string), args["since"].(*time.Time), args["until"].(*time.Time)), true
+
case "Repository.identity":
if e.complexity.Repository.Identity == nil {
break
@@ -1856,6 +2370,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Repository.Identity(childComplexity, args["prefix"].(string)), true
+ case "Repository.lastCommits":
+ if e.complexity.Repository.LastCommits == nil {
+ break
+ }
+
+ args, err := ec.field_Repository_lastCommits_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Repository.LastCommits(childComplexity, args["ref"].(string), args["path"].(*string), args["names"].([]string)), true
+
case "Repository.name":
if e.complexity.Repository.Name == nil {
break
@@ -1863,6 +2389,30 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Repository.Name(childComplexity), true
+ case "Repository.refs":
+ if e.complexity.Repository.Refs == nil {
+ break
+ }
+
+ args, err := ec.field_Repository_refs_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Repository.Refs(childComplexity, args["after"].(*string), args["before"].(*string), args["first"].(*int), args["last"].(*int), args["type"].(*models.GitRefType)), true
+
+ case "Repository.tree":
+ if e.complexity.Repository.Tree == nil {
+ break
+ }
+
+ args, err := ec.field_Repository_tree_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Repository.Tree(childComplexity, args["ref"].(string), args["path"].(*string)), true
+
case "Repository.userIdentity":
if e.complexity.Repository.UserIdentity == nil {
break
@@ -2628,6 +3178,225 @@ directive @goTag(
key: String!
value: String
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
+
+directive @goEnum(
+ value: String
+) on ENUM_VALUE
+`, BuiltIn: false},
+ {Name: "../schema/git.graphql", Input: `"""A git branch or tag reference."""
+type GitRef {
+ """Full reference name, e.g. refs/heads/main or refs/tags/v1.0."""
+ name: String!
+ """Short name, e.g. main or v1.0."""
+ shortName: String!
+ """Whether this reference is a branch or a tag."""
+ type: GitRefType!
+ """Commit hash the reference points to."""
+ hash: String!
+ """True for the branch HEAD currently points to."""
+ isDefault: Boolean!
+}
+
+"""An entry in a git tree (directory listing)."""
+type GitTreeEntry
+@goModel(model: "github.com/git-bug/git-bug/repository.TreeEntry") {
+ """File or directory name within the parent tree."""
+ name: String!
+ """Whether this entry is a file, directory, symlink, or submodule."""
+ type: GitObjectType! @goField(name: "ObjectType")
+ """Git object hash."""
+ hash: String!
+}
+
+"""The content of a git blob (file)."""
+type GitBlob {
+ """Path of the file relative to the repository root."""
+ path: String!
+ """Git object hash. Can be used as a stable cache key or to construct a
+ raw download URL."""
+ hash: String!
+ """UTF-8 text content of the file. Null when isBinary is true or when
+ the file is too large to be returned inline (see isTruncated)."""
+ text: String
+ """Size in bytes."""
+ size: Int!
+ """True when the file contains null bytes and is treated as binary.
+ text will be null."""
+ isBinary: Boolean!
+ """True when the file exceeds the maximum inline size and text has been
+ omitted. Use the raw download endpoint to retrieve the full content."""
+ isTruncated: Boolean!
+}
+
+"""Metadata for a single git commit."""
+type GitCommit
+@goModel(model: "github.com/git-bug/git-bug/api/graphql/models.GitCommitMeta") {
+ """Full SHA-1 commit hash."""
+ hash: String!
+ """Abbreviated commit hash, typically 8 characters."""
+ shortHash: String!
+ """First line of the commit message."""
+ message: String!
+ """Full commit message."""
+ fullMessage: String!
+ """Name of the commit author."""
+ authorName: String!
+ """Email address of the commit author."""
+ authorEmail: String!
+ """Timestamp from the author field (when the change was originally made)."""
+ date: Time!
+ """Hashes of parent commits. Empty for the initial commit."""
+ parents: [String!]!
+ """Files changed relative to the first parent (or the empty tree for the
+ initial commit)."""
+ files(
+ """Returns the elements in the list that come after the specified cursor."""
+ after: String
+ """Returns the elements in the list that come before the specified cursor."""
+ before: String
+ """Returns the first _n_ elements from the list."""
+ first: Int
+ """Returns the last _n_ elements from the list."""
+ last: Int
+ ): GitChangedFileConnection!
+ """Unified diff for a single file in this commit."""
+ diff(path: String!): GitFileDiff
+}
+
+"""The last commit that touched each requested entry in a directory."""
+type GitLastCommit {
+ """Entry name within the directory."""
+ name: String!
+ """Most recent commit that modified this entry."""
+ commit: GitCommit!
+}
+
+# ── connection types ──────────────────────────────────────────────────────────
+
+type GitRefConnection {
+ nodes: [GitRef!]!
+ pageInfo: PageInfo!
+ totalCount: Int!
+}
+
+"""Paginated list of commits."""
+type GitCommitConnection {
+ nodes: [GitCommit!]!
+ pageInfo: PageInfo!
+ totalCount: Int!
+}
+
+type GitChangedFileConnection {
+ nodes: [GitChangedFile!]!
+ pageInfo: PageInfo!
+ totalCount: Int!
+}
+
+# ── commit sub-types ──────────────────────────────────────────────────────────
+
+"""A file that was changed in a commit."""
+type GitChangedFile
+@goModel(model: "github.com/git-bug/git-bug/repository.ChangedFile") {
+ """Path of the file in the new version of the commit."""
+ path: String!
+ """Previous path, non-null only for renames."""
+ oldPath: String
+ """How the file was affected by the commit."""
+ status: GitChangeStatus!
+}
+
+"""The diff for a single file in a commit."""
+type GitFileDiff
+@goModel(model: "github.com/git-bug/git-bug/repository.FileDiff") {
+ """Path of the file in the new version."""
+ path: String!
+ """Previous path, non-null only for renames."""
+ oldPath: String
+ """True when the file is binary and no textual diff is available."""
+ isBinary: Boolean!
+ """True when the file was created in this commit."""
+ isNew: Boolean!
+ """True when the file was deleted in this commit."""
+ isDelete: Boolean!
+ """Contiguous blocks of changes. Empty for binary files."""
+ hunks: [GitDiffHunk!]!
+}
+
+"""A contiguous block of changes in a unified diff."""
+type GitDiffHunk
+@goModel(model: "github.com/git-bug/git-bug/repository.DiffHunk") {
+ """Starting line number in the old file."""
+ oldStart: Int!
+ """Number of lines from the old file included in this hunk."""
+ oldLines: Int!
+ """Starting line number in the new file."""
+ newStart: Int!
+ """Number of lines from the new file included in this hunk."""
+ newLines: Int!
+ """Lines in this hunk, including context, additions, and deletions."""
+ lines: [GitDiffLine!]!
+}
+
+"""A single line in a unified diff hunk."""
+type GitDiffLine
+@goModel(model: "github.com/git-bug/git-bug/repository.DiffLine") {
+ """Whether this line is context, an addition, or a deletion."""
+ type: GitDiffLineType!
+ """Raw line content, without the leading +/- prefix."""
+ content: String!
+ """Line number in the old file. 0 for added lines."""
+ oldLine: Int!
+ """Line number in the new file. 0 for deleted lines."""
+ newLine: Int!
+}
+
+# ── enums ─────────────────────────────────────────────────────────────────────
+
+"""The kind of git reference: a branch or a tag."""
+enum GitRefType
+@goModel(model: "github.com/git-bug/git-bug/api/graphql/models.GitRefType") {
+ """A local branch (refs/heads/*)."""
+ BRANCH @goEnum(value: "github.com/git-bug/git-bug/api/graphql/models.GitRefTypeBranch")
+ """An annotated or lightweight tag (refs/tags/*)."""
+ TAG @goEnum(value: "github.com/git-bug/git-bug/api/graphql/models.GitRefTypeTag")
+}
+
+"""The type of object a git tree entry points to."""
+enum GitObjectType
+@goModel(model: "github.com/git-bug/git-bug/repository.ObjectType") {
+ """A directory."""
+ TREE
+ """A regular or executable file."""
+ BLOB
+ """A symbolic link."""
+ SYMLINK
+ """A git submodule."""
+ SUBMODULE
+}
+
+"""How a file was affected by a commit."""
+enum GitChangeStatus
+@goModel(model: "github.com/git-bug/git-bug/repository.ChangeStatus") {
+ """File was created in this commit."""
+ ADDED
+ """File content changed in this commit."""
+ MODIFIED
+ """File was removed in this commit."""
+ DELETED
+ """File was moved or renamed in this commit."""
+ RENAMED
+}
+
+"""The role of a line within a unified diff hunk."""
+enum GitDiffLineType
+@goModel(model: "github.com/git-bug/git-bug/repository.DiffLineType") {
+ """An unchanged line present in both old and new versions."""
+ CONTEXT
+ """A line added in the new version."""
+ ADDED
+ """A line removed from the old version."""
+ DELETED
+}
`, BuiltIn: false},
{Name: "../schema/identity.graphql", Input: `"""Represents an identity"""
type Identity implements Entity {
@@ -2725,7 +3494,7 @@ type OperationEdge {
}
`, BuiltIn: false},
{Name: "../schema/repository.graphql", Input: `type Repository {
- """The name of the repository. Null for the default (unnamed) repository."""
+ """The name of the repository. Null for the default (unnamed) repository in a single-repo setup."""
name: String
"""All the bugs"""
@@ -2742,6 +3511,7 @@ type OperationEdge {
query: String
): BugConnection!
+ """Look up a bug by id prefix. Returns null if no bug matches the prefix."""
bug(prefix: String!): Bug
"""All the identities"""
@@ -2756,11 +3526,59 @@ type OperationEdge {
last: Int
): IdentityConnection!
+ """Look up an identity by id prefix. Returns null if no identity matches the prefix."""
identity(prefix: String!): Identity
"""The identity created or selected by the user as its own"""
userIdentity: Identity
+ """All branches and tags, optionally filtered by type."""
+ refs(
+ """Returns the elements in the list that come after the specified cursor."""
+ after: String
+ """Returns the elements in the list that come before the specified cursor."""
+ before: String
+ """Returns the first _n_ elements from the list."""
+ first: Int
+ """Returns the last _n_ elements from the list."""
+ last: Int
+ """Restrict to references of this type."""
+ type: GitRefType
+ ): GitRefConnection!
+
+ """Directory listing at path under ref. An empty path returns the root tree."""
+ tree(ref: String!, path: String): [GitTreeEntry!]!
+
+ """Content of the file at path under ref. Null if the path does not exist
+ or resolves to a tree rather than a blob."""
+ blob(ref: String!, path: String!): GitBlob
+
+ """Paginated commit log reachable from ref, optionally filtered to commits
+ touching path."""
+ commits(
+ """Returns the elements in the list that come after the specified cursor."""
+ after: String
+ """Returns the first _n_ elements from the list (max 100, default 20)."""
+ first: Int
+ """Branch name, tag name, full ref (e.g. refs/heads/main), or commit hash
+ to start the log from."""
+ ref: String!
+ """Restrict to commits that touched this path."""
+ path: String
+ """Restrict to commits authored on or after this timestamp."""
+ since: Time
+ """Restrict to commits authored before or on this timestamp."""
+ until: Time
+ ): GitCommitConnection!
+
+ """A single commit by hash. Returns null if the hash does not exist in the repository."""
+ commit(hash: String!): GitCommit
+
+ """The most recent commit that touched each of the named entries in the
+ directory at path under ref. Use this to populate last-commit info on a
+ tree listing without blocking the initial tree fetch."""
+ lastCommits(ref: String!, path: String, names: [String!]!): [GitLastCommit!]!
+
"""List of valid labels."""
validLabels(
"""Returns the elements in the list that come after the specified cursor."""
@@ -2801,7 +3619,8 @@ type Query {
"""Server configuration and authentication mode."""
serverConfig: ServerConfig!
- """Access a repository by reference/name. If no ref is given, the default repository is returned if any."""
+ """Access a repository by reference/name. If no ref is given, the default repository is returned if any.
+ Returns null if the referenced repository does not exist."""
repository(ref: String): Repository
"""List all registered repositories."""
@@ -809,4 +809,22 @@ func (ec *executionContext) marshalOHash2ᚕgithubᚗcomᚋgitᚑbugᚋgitᚑbug
return ret
}
+func (ec *executionContext) unmarshalOTime2ᚖtimeᚐTime(ctx context.Context, v any) (*time.Time, error) {
+ if v == nil {
+ return nil, nil
+ }
+ res, err := graphql.UnmarshalTime(v)
+ return &res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalOTime2ᚖtimeᚐTime(ctx context.Context, sel ast.SelectionSet, v *time.Time) graphql.Marshaler {
+ if v == nil {
+ return graphql.Null
+ }
+ _ = sel
+ _ = ctx
+ res := graphql.MarshalTime(*v)
+ return res
+}
+
// endregion ***************************** type.gotpl *****************************
@@ -24,7 +24,7 @@ func TestQueries(t *testing.T) {
require.NoError(t, event.Err)
}
- handler := NewHandler(mrc, nil)
+ handler := NewHandler(mrc, ServerConfig{AuthMode: "local"}, nil)
c := client.New(handler)
@@ -220,6 +220,232 @@ func TestQueries(t *testing.T) {
assert.NoError(t, err)
}
+// TestGitBrowseQueries exercises the git-browsing GraphQL fields (commit, blob,
+// tree, commits, lastCommits) against a synthetic fixture repo with the same
+// commit graph used by RepoBrowseTest:
+//
+// c1 ── c2 ── c3 refs/heads/main
+// └──────── refs/heads/feature
+// c1 ←── refs/tags/v1.0
+func TestGitBrowseQueries(t *testing.T) {
+ repo := repository.CreateGoGitTestRepo(t, false)
+ require.NoError(t, repo.LocalConfig().StoreString("init.defaultBranch", "main"))
+
+ // ── build fixture ─────────────────────────────────────────────────────────
+
+ readmeV1 := []byte("# Hello\n")
+ readmeV3 := []byte("# Hello\n\n## Updated\n")
+ mainV1 := []byte("package main\n")
+ mainV2 := []byte("package main\n\n// updated\n")
+ libV1 := []byte("package lib\n")
+ utilV1 := []byte("package util\n")
+
+ hReadmeV1, err := repo.StoreData(readmeV1)
+ require.NoError(t, err)
+ hReadmeV3, err := repo.StoreData(readmeV3)
+ require.NoError(t, err)
+ hMainV1, err := repo.StoreData(mainV1)
+ require.NoError(t, err)
+ hMainV2, err := repo.StoreData(mainV2)
+ require.NoError(t, err)
+ hLibV1, err := repo.StoreData(libV1)
+ require.NoError(t, err)
+ hUtilV1, err := repo.StoreData(utilV1)
+ require.NoError(t, err)
+
+ srcTreeV1, err := repo.StoreTree([]repository.TreeEntry{
+ {ObjectType: repository.Blob, Hash: hLibV1, Name: "lib.go"},
+ })
+ require.NoError(t, err)
+ rootTreeV1, err := repo.StoreTree([]repository.TreeEntry{
+ {ObjectType: repository.Blob, Hash: hReadmeV1, Name: "README.md"},
+ {ObjectType: repository.Blob, Hash: hMainV1, Name: "main.go"},
+ {ObjectType: repository.Tree, Hash: srcTreeV1, Name: "src"},
+ })
+ require.NoError(t, err)
+
+ srcTreeV2, err := repo.StoreTree([]repository.TreeEntry{
+ {ObjectType: repository.Blob, Hash: hLibV1, Name: "lib.go"},
+ {ObjectType: repository.Blob, Hash: hUtilV1, Name: "util.go"},
+ })
+ require.NoError(t, err)
+ rootTreeV2, err := repo.StoreTree([]repository.TreeEntry{
+ {ObjectType: repository.Blob, Hash: hReadmeV1, Name: "README.md"},
+ {ObjectType: repository.Blob, Hash: hMainV2, Name: "main.go"},
+ {ObjectType: repository.Tree, Hash: srcTreeV2, Name: "src"},
+ })
+ require.NoError(t, err)
+
+ rootTreeV3, err := repo.StoreTree([]repository.TreeEntry{
+ {ObjectType: repository.Blob, Hash: hReadmeV3, Name: "README.md"},
+ {ObjectType: repository.Blob, Hash: hMainV2, Name: "main.go"},
+ {ObjectType: repository.Tree, Hash: srcTreeV2, Name: "src"},
+ })
+ require.NoError(t, err)
+
+ c1, err := repo.StoreCommit(rootTreeV1)
+ require.NoError(t, err)
+ c2, err := repo.StoreCommit(rootTreeV2, c1)
+ require.NoError(t, err)
+ c3, err := repo.StoreCommit(rootTreeV3, c2)
+ require.NoError(t, err)
+
+ require.NoError(t, repo.UpdateRef("refs/heads/main", c3))
+ require.NoError(t, repo.UpdateRef("refs/heads/feature", c2))
+ require.NoError(t, repo.UpdateRef("refs/tags/v1.0", c1))
+
+ // ── set up GraphQL handler ─────────────────────────────────────────────────
+
+ mrc := cache.NewMultiRepoCache()
+ _, events := mrc.RegisterDefaultRepository(repo)
+ for event := range events {
+ require.NoError(t, event.Err)
+ }
+ c := client.New(NewHandler(mrc, ServerConfig{AuthMode: "local"}, nil))
+
+ // ── commit ────────────────────────────────────────────────────────────────
+
+ t.Run("commit", func(t *testing.T) {
+ var resp struct {
+ Repository struct {
+ Commit struct {
+ Hash string
+ Parents []string
+ }
+ }
+ }
+ require.NoError(t, c.Post(`query($hash: String!) {
+ repository { commit(hash: $hash) { hash parents } }
+ }`, &resp, client.Var("hash", string(c3))))
+ got := resp.Repository.Commit
+ require.Equal(t, string(c3), got.Hash)
+ require.Equal(t, []string{string(c2)}, got.Parents)
+ })
+
+ t.Run("commit_not_found", func(t *testing.T) {
+ var resp struct {
+ Repository struct {
+ Commit *struct{ Hash string }
+ }
+ }
+ require.NoError(t, c.Post(`query {
+ repository { commit(hash: "0000000000000000000000000000000000000000") { hash } }
+ }`, &resp))
+ require.Nil(t, resp.Repository.Commit)
+ })
+
+ // ── blob ──────────────────────────────────────────────────────────────────
+
+ t.Run("blob", func(t *testing.T) {
+ var resp struct {
+ Repository struct {
+ Blob struct {
+ Hash string
+ IsBinary bool
+ Size int
+ Text *string
+ }
+ }
+ }
+ require.NoError(t, c.Post(`query {
+ repository { blob(ref: "main", path: "README.md") { hash isBinary size text } }
+ }`, &resp))
+ got := resp.Repository.Blob
+ require.Equal(t, string(hReadmeV3), got.Hash)
+ require.False(t, got.IsBinary)
+ require.Equal(t, len(readmeV3), got.Size)
+ require.NotNil(t, got.Text)
+ require.Equal(t, string(readmeV3), *got.Text)
+ })
+
+ t.Run("blob_not_found", func(t *testing.T) {
+ var resp struct {
+ Repository struct {
+ Blob *struct{ Hash string }
+ }
+ }
+ require.NoError(t, c.Post(`query {
+ repository { blob(ref: "main", path: "nonexistent.go") { hash } }
+ }`, &resp))
+ require.Nil(t, resp.Repository.Blob)
+ })
+
+ // ── tree ──────────────────────────────────────────────────────────────────
+
+ t.Run("tree", func(t *testing.T) {
+ var resp struct {
+ Repository struct {
+ Tree []struct {
+ Name string
+ Type string `json:"type"`
+ }
+ }
+ }
+ require.NoError(t, c.Post(`query {
+ repository { tree(ref: "main", path: "") { name type } }
+ }`, &resp))
+ byName := make(map[string]string)
+ for _, e := range resp.Repository.Tree {
+ byName[e.Name] = e.Type
+ }
+ require.Equal(t, "BLOB", byName["README.md"])
+ require.Equal(t, "BLOB", byName["main.go"])
+ require.Equal(t, "TREE", byName["src"])
+ })
+
+ // ── commits ───────────────────────────────────────────────────────────────
+
+ t.Run("commits", func(t *testing.T) {
+ var resp struct {
+ Repository struct {
+ Commits struct {
+ TotalCount int
+ PageInfo struct{ HasNextPage bool }
+ Nodes []struct{ Hash string }
+ }
+ }
+ }
+ require.NoError(t, c.Post(`query {
+ repository {
+ commits(ref: "main", first: 2) {
+ totalCount pageInfo { hasNextPage } nodes { hash }
+ }
+ }
+ }`, &resp))
+ got := resp.Repository.Commits
+ require.Equal(t, 2, got.TotalCount)
+ require.True(t, got.PageInfo.HasNextPage)
+ require.Equal(t, string(c3), got.Nodes[0].Hash)
+ require.Equal(t, string(c2), got.Nodes[1].Hash)
+ })
+
+ // ── lastCommits ───────────────────────────────────────────────────────────
+
+ t.Run("lastCommits", func(t *testing.T) {
+ var resp struct {
+ Repository struct {
+ LastCommits []struct {
+ Name string
+ Commit struct{ Hash string }
+ }
+ }
+ }
+ require.NoError(t, c.Post(`query {
+ repository {
+ lastCommits(ref: "main", names: ["README.md", "main.go"]) {
+ name commit { hash }
+ }
+ }
+ }`, &resp))
+ byName := make(map[string]string)
+ for _, lc := range resp.Repository.LastCommits {
+ byName[lc.Name] = lc.Commit.Hash
+ }
+ require.Equal(t, string(c3), byName["README.md"]) // changed in c3
+ require.Equal(t, string(c2), byName["main.go"]) // changed in c2
+ })
+}
+
func TestBugEventsSubscription(t *testing.T) {
repo := repository.CreateGoGitTestRepo(t, false)
@@ -229,7 +455,7 @@ func TestBugEventsSubscription(t *testing.T) {
require.NoError(t, event.Err)
}
- h := NewHandler(mrc, nil)
+ h := NewHandler(mrc, ServerConfig{AuthMode: "local"}, nil)
c := client.New(h)
sub := c.Websocket(`subscription { bugEvents { type bug { id } } }`)
@@ -0,0 +1,58 @@
+package models
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "strconv"
+)
+
+// GitRefType is the kind of git reference: a branch or a tag.
+type GitRefType string
+
+const (
+ // GitRefTypeBranch refers to a local branch (refs/heads/*).
+ GitRefTypeBranch GitRefType = "BRANCH"
+ // GitRefTypeTag refers to an annotated or lightweight tag (refs/tags/*).
+ GitRefTypeTag GitRefType = "TAG"
+)
+
+func (e GitRefType) IsValid() bool {
+ switch e {
+ case GitRefTypeBranch, GitRefTypeTag:
+ return true
+ }
+ return false
+}
+
+func (e GitRefType) String() string { return string(e) }
+
+func (e *GitRefType) UnmarshalGQL(v any) error {
+ str, ok := v.(string)
+ if !ok {
+ return fmt.Errorf("enums must be strings")
+ }
+ *e = GitRefType(str)
+ if !e.IsValid() {
+ return fmt.Errorf("%s is not a valid GitRefType", str)
+ }
+ return nil
+}
+
+func (e GitRefType) MarshalGQL(w io.Writer) {
+ fmt.Fprint(w, strconv.Quote(e.String()))
+}
+
+func (e *GitRefType) UnmarshalJSON(b []byte) error {
+ s, err := strconv.Unquote(string(b))
+ if err != nil {
+ return err
+ }
+ return e.UnmarshalGQL(s)
+}
+
+func (e GitRefType) MarshalJSON() ([]byte, error) {
+ var buf bytes.Buffer
+ e.MarshalGQL(&buf)
+ return buf.Bytes(), nil
+}
@@ -269,6 +269,67 @@ type EntityEvent struct {
Entity Entity `json:"entity,omitempty"`
}
+// The content of a git blob (file).
+type GitBlob struct {
+ // Path of the file relative to the repository root.
+ Path string `json:"path"`
+ // Git object hash. Can be used as a stable cache key or to construct a
+ // raw download URL.
+ Hash string `json:"hash"`
+ // UTF-8 text content of the file. Null when isBinary is true or when
+ // the file is too large to be returned inline (see isTruncated).
+ Text *string `json:"text,omitempty"`
+ // Size in bytes.
+ Size int `json:"size"`
+ // True when the file contains null bytes and is treated as binary.
+ // text will be null.
+ IsBinary bool `json:"isBinary"`
+ // True when the file exceeds the maximum inline size and text has been
+ // omitted. Use the raw download endpoint to retrieve the full content.
+ IsTruncated bool `json:"isTruncated"`
+}
+
+type GitChangedFileConnection struct {
+ Nodes []*repository.ChangedFile `json:"nodes"`
+ PageInfo *PageInfo `json:"pageInfo"`
+ TotalCount int `json:"totalCount"`
+}
+
+// Paginated list of commits.
+type GitCommitConnection struct {
+ Nodes []*GitCommitMeta `json:"nodes"`
+ PageInfo *PageInfo `json:"pageInfo"`
+ TotalCount int `json:"totalCount"`
+}
+
+// The last commit that touched each requested entry in a directory.
+type GitLastCommit struct {
+ // Entry name within the directory.
+ Name string `json:"name"`
+ // Most recent commit that modified this entry.
+ Commit *GitCommitMeta `json:"commit"`
+}
+
+// A git branch or tag reference.
+type GitRef struct {
+ // Full reference name, e.g. refs/heads/main or refs/tags/v1.0.
+ Name string `json:"name"`
+ // Short name, e.g. main or v1.0.
+ ShortName string `json:"shortName"`
+ // Whether this reference is a branch or a tag.
+ Type GitRefType `json:"type"`
+ // Commit hash the reference points to.
+ Hash string `json:"hash"`
+ // True for the branch HEAD currently points to.
+ IsDefault bool `json:"isDefault"`
+}
+
+type GitRefConnection struct {
+ Nodes []*GitRef `json:"nodes"`
+ PageInfo *PageInfo `json:"pageInfo"`
+ TotalCount int `json:"totalCount"`
+}
+
type IdentityConnection struct {
Edges []*IdentityEdge `json:"edges"`
Nodes []IdentityWrapper `json:"nodes"`
@@ -3,6 +3,7 @@ package models
import (
"github.com/git-bug/git-bug/cache"
+ "github.com/git-bug/git-bug/repository"
)
type ConnectionInput struct {
@@ -13,6 +14,12 @@ type ConnectionInput struct {
}
type Repository struct {
- Cache *cache.MultiRepoCache
- Repo *cache.RepoCache
+ Repo *cache.RepoCache
+}
+
+// GitCommitMeta is a wrapper around a CommitMeta that includes the Repo,
+// to keep the repo context in sub-resolvers.
+type GitCommitMeta struct {
+ Repo *cache.RepoCache
+ repository.CommitMeta
}
@@ -0,0 +1,74 @@
+package resolvers
+
+import (
+ "context"
+
+ "github.com/git-bug/git-bug/api/graphql/connections"
+ "github.com/git-bug/git-bug/api/graphql/graph"
+ "github.com/git-bug/git-bug/api/graphql/models"
+ "github.com/git-bug/git-bug/cache"
+ "github.com/git-bug/git-bug/repository"
+)
+
+const blobTruncateSize = 1 << 20 // 1 MiB
+
+var _ graph.GitCommitResolver = &gitCommitResolver{}
+
+type gitCommitResolver struct {
+ cache *cache.MultiRepoCache
+}
+
+func (r gitCommitResolver) ShortHash(_ context.Context, obj *models.GitCommitMeta) (string, error) {
+ s := string(obj.Hash)
+ if len(s) > 8 {
+ s = s[:8]
+ }
+ return s, nil
+}
+
+func (r gitCommitResolver) FullMessage(_ context.Context, obj *models.GitCommitMeta) (string, error) {
+ repo := obj.Repo.BrowseRepo()
+ detail, err := repo.CommitDetail(obj.Hash)
+ if err != nil {
+ return "", err
+ }
+ return detail.FullMessage, nil
+}
+
+func (r gitCommitResolver) Parents(_ context.Context, obj *models.GitCommitMeta) ([]string, error) {
+ out := make([]string, len(obj.Parents))
+ for i, h := range obj.Parents {
+ out[i] = string(h)
+ }
+ return out, nil
+}
+
+func (r gitCommitResolver) Files(_ context.Context, obj *models.GitCommitMeta, after *string, before *string, first *int, last *int) (*models.GitChangedFileConnection, error) {
+ repo := obj.Repo.BrowseRepo()
+ detail, err := repo.CommitDetail(obj.Hash)
+ if err != nil {
+ return nil, err
+ }
+
+ input := models.ConnectionInput{After: after, Before: before, First: first, Last: last}
+ edger := func(f repository.ChangedFile, offset int) connections.Edge {
+ return connections.CursorEdge{Cursor: connections.OffsetToCursor(offset)}
+ }
+ conMaker := func(_ []*connections.CursorEdge, nodes []repository.ChangedFile, info *models.PageInfo, total int) (*models.GitChangedFileConnection, error) {
+ ptrs := make([]*repository.ChangedFile, len(nodes))
+ for i := range nodes {
+ ptrs[i] = &nodes[i]
+ }
+ return &models.GitChangedFileConnection{Nodes: ptrs, PageInfo: info, TotalCount: total}, nil
+ }
+ return connections.Connection(detail.Files, edger, conMaker, input)
+}
+
+func (r gitCommitResolver) Diff(_ context.Context, obj *models.GitCommitMeta, path string) (*repository.FileDiff, error) {
+ repo := obj.Repo.BrowseRepo()
+ fd, err := repo.CommitFileDiff(obj.Hash, path)
+ if err != nil {
+ return nil, err
+ }
+ return &fd, nil
+}
@@ -42,12 +42,11 @@ func (r rootQueryResolver) Repository(_ context.Context, ref *string) (*models.R
}
if err != nil {
- return nil, err
+ return nil, nil
}
return &models.Repository{
- Cache: r.cache,
- Repo: repo,
+ Repo: repo,
}, nil
}
@@ -65,7 +64,7 @@ func (r rootQueryResolver) Repositories(_ context.Context, after *string, before
edger := func(repo *cache.RepoCache, offset int) connections.Edge {
return models.RepositoryEdge{
- Node: &models.Repository{Cache: r.cache, Repo: repo},
+ Node: &models.Repository{Repo: repo},
Cursor: connections.OffsetToCursor(offset),
}
}
@@ -1,7 +1,13 @@
package resolvers
import (
+ "bytes"
"context"
+ "errors"
+ "io"
+ "math"
+ "sort"
+ "time"
"github.com/git-bug/git-bug/api/auth"
"github.com/git-bug/git-bug/api/graphql/connections"
@@ -10,6 +16,7 @@ import (
"github.com/git-bug/git-bug/entities/common"
"github.com/git-bug/git-bug/entity"
"github.com/git-bug/git-bug/query"
+ "github.com/git-bug/git-bug/repository"
)
var _ graph.RepositoryResolver = &repoResolver{}
@@ -17,6 +24,9 @@ var _ graph.RepositoryResolver = &repoResolver{}
type repoResolver struct{}
func (repoResolver) Name(_ context.Context, obj *models.Repository) (*string, error) {
+ if obj.Repo.IsDefaultRepo() {
+ return nil, nil
+ }
name := obj.Repo.Name()
if name == "" {
return nil, nil
@@ -90,6 +100,9 @@ func (repoResolver) AllBugs(_ context.Context, obj *models.Repository, after *st
func (repoResolver) Bug(_ context.Context, obj *models.Repository, prefix string) (models.BugWrapper, error) {
excerpt, err := obj.Repo.Bugs().ResolveExcerptPrefix(prefix)
+ if entity.IsErrNotFound(err) {
+ return nil, nil
+ }
if err != nil {
return nil, err
}
@@ -149,6 +162,9 @@ func (repoResolver) AllIdentities(_ context.Context, obj *models.Repository, aft
func (repoResolver) Identity(_ context.Context, obj *models.Repository, prefix string) (models.IdentityWrapper, error) {
excerpt, err := obj.Repo.Identities().ResolveExcerptPrefix(prefix)
+ if entity.IsErrNotFound(err) {
+ return nil, nil
+ }
if err != nil {
return nil, err
}
@@ -192,3 +208,216 @@ func (repoResolver) ValidLabels(_ context.Context, obj *models.Repository, after
return connections.Connection(obj.Repo.Bugs().ValidLabels(), edger, conMaker, input)
}
+
+func (repoResolver) Refs(_ context.Context, obj *models.Repository, after *string, before *string, first *int, last *int, typeArg *models.GitRefType) (*models.GitRefConnection, error) {
+ repo := obj.Repo.BrowseRepo()
+
+ var refs []*models.GitRef
+
+ if typeArg == nil || *typeArg == models.GitRefTypeBranch {
+ branches, err := repo.Branches()
+ if err != nil {
+ return nil, err
+ }
+ for _, b := range branches {
+ refs = append(refs, &models.GitRef{
+ Name: "refs/heads/" + b.Name,
+ ShortName: b.Name,
+ Type: models.GitRefTypeBranch,
+ Hash: string(b.Hash),
+ IsDefault: b.IsDefault,
+ })
+ }
+ }
+
+ if typeArg == nil || *typeArg == models.GitRefTypeTag {
+ tags, err := repo.Tags()
+ if err != nil {
+ return nil, err
+ }
+ for _, t := range tags {
+ refs = append(refs, &models.GitRef{
+ Name: "refs/tags/" + t.Name,
+ ShortName: t.Name,
+ Type: models.GitRefTypeTag,
+ Hash: string(t.Hash),
+ })
+ }
+ }
+
+ // Sort by type (branches before tags) then by short name for stable cursors.
+ sort.Slice(refs, func(i, j int) bool {
+ if refs[i].Type != refs[j].Type {
+ return refs[i].Type < refs[j].Type
+ }
+ return refs[i].ShortName < refs[j].ShortName
+ })
+
+ input := models.ConnectionInput{After: after, Before: before, First: first, Last: last}
+ edger := func(r *models.GitRef, offset int) connections.Edge {
+ return connections.CursorEdge{Cursor: connections.OffsetToCursor(offset)}
+ }
+ conMaker := func(edges []*connections.CursorEdge, nodes []*models.GitRef, info *models.PageInfo, total int) (*models.GitRefConnection, error) {
+ return &models.GitRefConnection{Nodes: nodes, PageInfo: info, TotalCount: total}, nil
+ }
+ return connections.Connection(refs, edger, conMaker, input)
+}
+
+func (repoResolver) Tree(_ context.Context, obj *models.Repository, ref string, path *string) ([]*repository.TreeEntry, error) {
+ repo := obj.Repo.BrowseRepo()
+ p := ""
+ if path != nil {
+ p = *path
+ }
+ entries, err := repo.TreeAtPath(ref, p)
+ if err != nil {
+ return nil, err
+ }
+ ptrs := make([]*repository.TreeEntry, len(entries))
+ for i := range entries {
+ ptrs[i] = &entries[i]
+ }
+ return ptrs, nil
+}
+
+func (repoResolver) Blob(_ context.Context, obj *models.Repository, ref string, path string) (*models.GitBlob, error) {
+ repo := obj.Repo.BrowseRepo()
+ rc, size, hash, err := repo.BlobAtPath(ref, path)
+ if errors.Is(err, repository.ErrNotFound) {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ defer rc.Close()
+
+ limited := io.LimitReader(rc, blobTruncateSize+1)
+ data, err := io.ReadAll(limited)
+ if err != nil {
+ return nil, err
+ }
+
+ // Binary detection: same heuristic as git — a null byte anywhere in the
+ // content means binary. Git caps its probe at 8000 bytes; we probe all
+ // bytes read (up to blobTruncateSize+1) before slicing, so a NUL in the
+ // extra byte also triggers the flag. Files whose first blobTruncateSize
+ // bytes are all non-NUL will be reported as text even if the remainder is
+ // binary; this is a documented prefix-based heuristic.
+ isBinary := bytes.IndexByte(data, 0) >= 0
+
+ isTruncated := int64(len(data)) > blobTruncateSize
+ if isTruncated {
+ data = data[:blobTruncateSize]
+ }
+
+ blob := &models.GitBlob{
+ Path: path,
+ Hash: string(hash),
+ // GraphQL Int is 32-bit; clamp to avoid overflow on 32-bit platforms or for
+ // exceptionally large files (which will be truncated anyway).
+ Size: int(min(size, int64(math.MaxInt32))),
+ IsBinary: isBinary,
+ IsTruncated: isTruncated,
+ }
+ if !isBinary {
+ text := string(data)
+ blob.Text = &text
+ }
+ return blob, nil
+}
+
+func (repoResolver) Commits(_ context.Context, obj *models.Repository, after *string, first *int, ref string, path *string, since *time.Time, until *time.Time) (*models.GitCommitConnection, error) {
+ // This is not using the normal relay pagination (connection.Connection()), because that requires having the
+ // full list in memory. Here, go-git does a partial walk only, which is better.
+
+ repo := obj.Repo.BrowseRepo()
+
+ p := ""
+ if path != nil {
+ p = *path
+ }
+
+ const defaultFirst = 20
+ const maxFirst = 100
+
+ n := defaultFirst
+ if first != nil {
+ n = *first
+ if n > maxFirst {
+ n = maxFirst
+ }
+ }
+ limit := n + 1 // fetch one extra to detect hasNextPage
+
+ var afterHash repository.Hash
+ if after != nil {
+ afterHash = repository.Hash(*after)
+ }
+
+ commits, err := repo.CommitLog(ref, p, limit, afterHash, since, until)
+ if err != nil {
+ return nil, err
+ }
+
+ hasNextPage := false
+ if len(commits) > n {
+ hasNextPage = true
+ commits = commits[:n]
+ }
+
+ nodes := make([]*models.GitCommitMeta, len(commits))
+ for i := range commits {
+ nodes[i] = &models.GitCommitMeta{Repo: obj.Repo, CommitMeta: commits[i]}
+ }
+
+ startCursor := ""
+ endCursor := ""
+ if len(nodes) > 0 {
+ startCursor = string(nodes[0].Hash)
+ endCursor = string(nodes[len(nodes)-1].Hash)
+ }
+
+ return &models.GitCommitConnection{
+ Nodes: nodes,
+ PageInfo: &models.PageInfo{
+ HasNextPage: hasNextPage,
+ HasPreviousPage: after != nil,
+ StartCursor: startCursor,
+ EndCursor: endCursor,
+ },
+ TotalCount: len(nodes), // lower bound; exact total unknown without full walk
+ }, nil
+}
+
+func (repoResolver) Commit(_ context.Context, obj *models.Repository, hash string) (*models.GitCommitMeta, error) {
+ repo := obj.Repo.BrowseRepo()
+ detail, err := repo.CommitDetail(repository.Hash(hash))
+ if errors.Is(err, repository.ErrNotFound) {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ return &models.GitCommitMeta{Repo: obj.Repo, CommitMeta: detail.CommitMeta}, nil
+}
+
+func (repoResolver) LastCommits(_ context.Context, obj *models.Repository, ref string, path *string, names []string) ([]*models.GitLastCommit, error) {
+ repo := obj.Repo.BrowseRepo()
+ p := ""
+ if path != nil {
+ p = *path
+ }
+ byName, err := repo.LastCommitForEntries(ref, p, names)
+ if err != nil {
+ return nil, err
+ }
+ // Iterate over the input names to preserve caller-specified order.
+ result := make([]*models.GitLastCommit, 0, len(names))
+ for _, name := range names {
+ if meta, ok := byName[name]; ok {
+ m := meta
+ result = append(result, &models.GitLastCommit{Name: name, Commit: &models.GitCommitMeta{Repo: obj.Repo, CommitMeta: m}})
+ }
+ }
+ return result, nil
+}
@@ -63,3 +63,9 @@ func (RootResolver) Repository() graph.RepositoryResolver {
func (RootResolver) Bug() graph.BugResolver {
return &bugResolver{}
}
+
+func (r RootResolver) GitCommit() graph.GitCommitResolver {
+ return &gitCommitResolver{
+ cache: r.MultiRepoCache,
+ }
+}
@@ -16,3 +16,7 @@ directive @goTag(
key: String!
value: String
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
+
+directive @goEnum(
+ value: String
+) on ENUM_VALUE
@@ -0,0 +1,214 @@
+"""A git branch or tag reference."""
+type GitRef {
+ """Full reference name, e.g. refs/heads/main or refs/tags/v1.0."""
+ name: String!
+ """Short name, e.g. main or v1.0."""
+ shortName: String!
+ """Whether this reference is a branch or a tag."""
+ type: GitRefType!
+ """Commit hash the reference points to."""
+ hash: String!
+ """True for the branch HEAD currently points to."""
+ isDefault: Boolean!
+}
+
+"""An entry in a git tree (directory listing)."""
+type GitTreeEntry
+@goModel(model: "github.com/git-bug/git-bug/repository.TreeEntry") {
+ """File or directory name within the parent tree."""
+ name: String!
+ """Whether this entry is a file, directory, symlink, or submodule."""
+ type: GitObjectType! @goField(name: "ObjectType")
+ """Git object hash."""
+ hash: String!
+}
+
+"""The content of a git blob (file)."""
+type GitBlob {
+ """Path of the file relative to the repository root."""
+ path: String!
+ """Git object hash. Can be used as a stable cache key or to construct a
+ raw download URL."""
+ hash: String!
+ """UTF-8 text content of the file. Null when isBinary is true or when
+ the file is too large to be returned inline (see isTruncated)."""
+ text: String
+ """Size in bytes."""
+ size: Int!
+ """True when the file contains null bytes and is treated as binary.
+ text will be null."""
+ isBinary: Boolean!
+ """True when the file exceeds the maximum inline size and text has been
+ omitted. Use the raw download endpoint to retrieve the full content."""
+ isTruncated: Boolean!
+}
+
+"""Metadata for a single git commit."""
+type GitCommit
+@goModel(model: "github.com/git-bug/git-bug/api/graphql/models.GitCommitMeta") {
+ """Full SHA-1 commit hash."""
+ hash: String!
+ """Abbreviated commit hash, typically 8 characters."""
+ shortHash: String!
+ """First line of the commit message."""
+ message: String!
+ """Full commit message."""
+ fullMessage: String!
+ """Name of the commit author."""
+ authorName: String!
+ """Email address of the commit author."""
+ authorEmail: String!
+ """Timestamp from the author field (when the change was originally made)."""
+ date: Time!
+ """Hashes of parent commits. Empty for the initial commit."""
+ parents: [String!]!
+ """Files changed relative to the first parent (or the empty tree for the
+ initial commit)."""
+ files(
+ """Returns the elements in the list that come after the specified cursor."""
+ after: String
+ """Returns the elements in the list that come before the specified cursor."""
+ before: String
+ """Returns the first _n_ elements from the list."""
+ first: Int
+ """Returns the last _n_ elements from the list."""
+ last: Int
+ ): GitChangedFileConnection!
+ """Unified diff for a single file in this commit."""
+ diff(path: String!): GitFileDiff
+}
+
+"""The last commit that touched each requested entry in a directory."""
+type GitLastCommit {
+ """Entry name within the directory."""
+ name: String!
+ """Most recent commit that modified this entry."""
+ commit: GitCommit!
+}
+
+# ── connection types ──────────────────────────────────────────────────────────
+
+type GitRefConnection {
+ nodes: [GitRef!]!
+ pageInfo: PageInfo!
+ totalCount: Int!
+}
+
+"""Paginated list of commits."""
+type GitCommitConnection {
+ nodes: [GitCommit!]!
+ pageInfo: PageInfo!
+ totalCount: Int!
+}
+
+type GitChangedFileConnection {
+ nodes: [GitChangedFile!]!
+ pageInfo: PageInfo!
+ totalCount: Int!
+}
+
+# ── commit sub-types ──────────────────────────────────────────────────────────
+
+"""A file that was changed in a commit."""
+type GitChangedFile
+@goModel(model: "github.com/git-bug/git-bug/repository.ChangedFile") {
+ """Path of the file in the new version of the commit."""
+ path: String!
+ """Previous path, non-null only for renames."""
+ oldPath: String
+ """How the file was affected by the commit."""
+ status: GitChangeStatus!
+}
+
+"""The diff for a single file in a commit."""
+type GitFileDiff
+@goModel(model: "github.com/git-bug/git-bug/repository.FileDiff") {
+ """Path of the file in the new version."""
+ path: String!
+ """Previous path, non-null only for renames."""
+ oldPath: String
+ """True when the file is binary and no textual diff is available."""
+ isBinary: Boolean!
+ """True when the file was created in this commit."""
+ isNew: Boolean!
+ """True when the file was deleted in this commit."""
+ isDelete: Boolean!
+ """Contiguous blocks of changes. Empty for binary files."""
+ hunks: [GitDiffHunk!]!
+}
+
+"""A contiguous block of changes in a unified diff."""
+type GitDiffHunk
+@goModel(model: "github.com/git-bug/git-bug/repository.DiffHunk") {
+ """Starting line number in the old file."""
+ oldStart: Int!
+ """Number of lines from the old file included in this hunk."""
+ oldLines: Int!
+ """Starting line number in the new file."""
+ newStart: Int!
+ """Number of lines from the new file included in this hunk."""
+ newLines: Int!
+ """Lines in this hunk, including context, additions, and deletions."""
+ lines: [GitDiffLine!]!
+}
+
+"""A single line in a unified diff hunk."""
+type GitDiffLine
+@goModel(model: "github.com/git-bug/git-bug/repository.DiffLine") {
+ """Whether this line is context, an addition, or a deletion."""
+ type: GitDiffLineType!
+ """Raw line content, without the leading +/- prefix."""
+ content: String!
+ """Line number in the old file. 0 for added lines."""
+ oldLine: Int!
+ """Line number in the new file. 0 for deleted lines."""
+ newLine: Int!
+}
+
+# ── enums ─────────────────────────────────────────────────────────────────────
+
+"""The kind of git reference: a branch or a tag."""
+enum GitRefType
+@goModel(model: "github.com/git-bug/git-bug/api/graphql/models.GitRefType") {
+ """A local branch (refs/heads/*)."""
+ BRANCH @goEnum(value: "github.com/git-bug/git-bug/api/graphql/models.GitRefTypeBranch")
+ """An annotated or lightweight tag (refs/tags/*)."""
+ TAG @goEnum(value: "github.com/git-bug/git-bug/api/graphql/models.GitRefTypeTag")
+}
+
+"""The type of object a git tree entry points to."""
+enum GitObjectType
+@goModel(model: "github.com/git-bug/git-bug/repository.ObjectType") {
+ """A directory."""
+ TREE
+ """A regular or executable file."""
+ BLOB
+ """A symbolic link."""
+ SYMLINK
+ """A git submodule."""
+ SUBMODULE
+}
+
+"""How a file was affected by a commit."""
+enum GitChangeStatus
+@goModel(model: "github.com/git-bug/git-bug/repository.ChangeStatus") {
+ """File was created in this commit."""
+ ADDED
+ """File content changed in this commit."""
+ MODIFIED
+ """File was removed in this commit."""
+ DELETED
+ """File was moved or renamed in this commit."""
+ RENAMED
+}
+
+"""The role of a line within a unified diff hunk."""
+enum GitDiffLineType
+@goModel(model: "github.com/git-bug/git-bug/repository.DiffLineType") {
+ """An unchanged line present in both old and new versions."""
+ CONTEXT
+ """A line added in the new version."""
+ ADDED
+ """A line removed from the old version."""
+ DELETED
+}
@@ -1,5 +1,5 @@
type Repository {
- """The name of the repository. Null for the default (unnamed) repository."""
+ """The name of the repository. Null for the default (unnamed) repository in a single-repo setup."""
name: String
"""All the bugs"""
@@ -16,6 +16,7 @@ type Repository {
query: String
): BugConnection!
+ """Look up a bug by id prefix. Returns null if no bug matches the prefix."""
bug(prefix: String!): Bug
"""All the identities"""
@@ -30,11 +31,59 @@ type Repository {
last: Int
): IdentityConnection!
+ """Look up an identity by id prefix. Returns null if no identity matches the prefix."""
identity(prefix: String!): Identity
"""The identity created or selected by the user as its own"""
userIdentity: Identity
+ """All branches and tags, optionally filtered by type."""
+ refs(
+ """Returns the elements in the list that come after the specified cursor."""
+ after: String
+ """Returns the elements in the list that come before the specified cursor."""
+ before: String
+ """Returns the first _n_ elements from the list."""
+ first: Int
+ """Returns the last _n_ elements from the list."""
+ last: Int
+ """Restrict to references of this type."""
+ type: GitRefType
+ ): GitRefConnection!
+
+ """Directory listing at path under ref. An empty path returns the root tree."""
+ tree(ref: String!, path: String): [GitTreeEntry!]!
+
+ """Content of the file at path under ref. Null if the path does not exist
+ or resolves to a tree rather than a blob."""
+ blob(ref: String!, path: String!): GitBlob
+
+ """Paginated commit log reachable from ref, optionally filtered to commits
+ touching path."""
+ commits(
+ """Returns the elements in the list that come after the specified cursor."""
+ after: String
+ """Returns the first _n_ elements from the list (max 100, default 20)."""
+ first: Int
+ """Branch name, tag name, full ref (e.g. refs/heads/main), or commit hash
+ to start the log from."""
+ ref: String!
+ """Restrict to commits that touched this path."""
+ path: String
+ """Restrict to commits authored on or after this timestamp."""
+ since: Time
+ """Restrict to commits authored before or on this timestamp."""
+ until: Time
+ ): GitCommitConnection!
+
+ """A single commit by hash. Returns null if the hash does not exist in the repository."""
+ commit(hash: String!): GitCommit
+
+ """The most recent commit that touched each of the named entries in the
+ directory at path under ref. Use this to populate last-commit info on a
+ tree listing without blocking the initial tree fetch."""
+ lastCommits(ref: String!, path: String, names: [String!]!): [GitLastCommit!]!
+
"""List of valid labels."""
validLabels(
"""Returns the elements in the list that come after the specified cursor."""
@@ -13,7 +13,8 @@ type Query {
"""Server configuration and authentication mode."""
serverConfig: ServerConfig!
- """Access a repository by reference/name. If no ref is given, the default repository is returned if any."""
+ """Access a repository by reference/name. If no ref is given, the default repository is returned if any.
+ Returns null if the referenced repository does not exist."""
repository(ref: String): Repository
"""List all registered repositories."""
@@ -1,566 +0,0 @@
-package http
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
- "strconv"
- "strings"
-
- "github.com/gorilla/mux"
-
- "github.com/git-bug/git-bug/cache"
- "github.com/git-bug/git-bug/repository"
-)
-
-// ── shared helpers ────────────────────────────────────────────────────────────
-
-func writeJSON(w http.ResponseWriter, v any) {
- w.Header().Set("Content-Type", "application/json")
- if err := json.NewEncoder(w).Encode(v); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
-}
-
-// repoFromPath resolves the repository from the {owner} and {repo} mux path
-// variables. "_" is the wildcard value: owner is always ignored (single-owner
-// for now), and repo "_" resolves to the default repository.
-func repoFromPath(mrc *cache.MultiRepoCache, r *http.Request) (*cache.RepoCache, error) {
- repoVar := mux.Vars(r)["repo"]
- if repoVar == "_" {
- return mrc.DefaultRepo()
- }
- return mrc.ResolveRepo(repoVar)
-}
-
-// browseRepo resolves the repository and asserts it implements RepoBrowse.
-func browseRepo(mrc *cache.MultiRepoCache, r *http.Request) (repository.ClockedRepo, repository.RepoBrowse, error) {
- rc, err := repoFromPath(mrc, r)
- if err != nil {
- return nil, nil, err
- }
- underlying := rc.GetRepo()
- br, ok := underlying.(repository.RepoBrowse)
- if !ok {
- return nil, nil, fmt.Errorf("repository does not support code browsing")
- }
- return underlying, br, nil
-}
-
-// resolveRef tries refs/heads/<ref>, refs/tags/<ref>, then a raw hash.
-func resolveRef(repo repository.ClockedRepo, ref string) (repository.Hash, error) {
- for _, prefix := range []string{"refs/heads/", "refs/tags/", ""} {
- h, err := repo.ResolveRef(prefix + ref)
- if err == nil {
- return h, nil
- }
- }
- return "", repository.ErrNotFound
-}
-
-// resolveTreeAtPath walks the git tree of a commit down to the given path.
-func resolveTreeAtPath(repo repository.ClockedRepo, commitHash repository.Hash, path string) ([]repository.TreeEntry, error) {
- commit, err := repo.ReadCommit(commitHash)
- if err != nil {
- return nil, err
- }
-
- entries, err := repo.ReadTree(commit.TreeHash)
- if err != nil {
- return nil, err
- }
-
- if path == "" {
- return entries, nil
- }
-
- for _, segment := range strings.Split(path, "/") {
- if segment == "" {
- continue
- }
- entry, ok := repository.SearchTreeEntry(entries, segment)
- if !ok {
- return nil, repository.ErrNotFound
- }
- if entry.ObjectType != repository.Tree {
- return nil, repository.ErrNotFound
- }
- entries, err = repo.ReadTree(entry.Hash)
- if err != nil {
- return nil, err
- }
- }
- return entries, nil
-}
-
-// resolveBlobAtPath walks the tree to the given file path and returns its hash.
-func resolveBlobAtPath(repo repository.ClockedRepo, commitHash repository.Hash, path string) (repository.Hash, error) {
- parts := strings.Split(path, "/")
- dirPath := strings.Join(parts[:len(parts)-1], "/")
- fileName := parts[len(parts)-1]
-
- entries, err := resolveTreeAtPath(repo, commitHash, dirPath)
- if err != nil {
- return "", err
- }
-
- entry, ok := repository.SearchTreeEntry(entries, fileName)
- if !ok {
- return "", repository.ErrNotFound
- }
- if entry.ObjectType != repository.Blob {
- return "", repository.ErrNotFound
- }
- return entry.Hash, nil
-}
-
-// isBinaryContent returns true if data contains a null byte (simple heuristic).
-func isBinaryContent(data []byte) bool {
- for _, b := range data {
- if b == 0 {
- return true
- }
- }
- return false
-}
-
-// ── GET /api/repos/{owner}/{repo}/git/refs ────────────────────────────────────
-
-type gitRefsHandler struct{ mrc *cache.MultiRepoCache }
-
-func NewGitRefsHandler(mrc *cache.MultiRepoCache) http.Handler {
- return &gitRefsHandler{mrc: mrc}
-}
-
-type refResponse struct {
- Name string `json:"name"`
- ShortName string `json:"shortName"`
- Type string `json:"type"` // "branch" | "tag"
- Hash string `json:"hash"`
- IsDefault bool `json:"isDefault"`
-}
-
-func (h *gitRefsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- repo, br, err := browseRepo(h.mrc, r)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defaultBranch, _ := br.GetDefaultBranch()
-
- var refs []refResponse
- for _, prefix := range []string{"refs/heads/", "refs/tags/"} {
- names, err := repo.ListRefs(prefix)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- for _, name := range names {
- hash, err := repo.ResolveRef(name)
- if err != nil {
- continue
- }
- refType := "branch"
- if prefix == "refs/tags/" {
- refType = "tag"
- }
- short := strings.TrimPrefix(name, prefix)
- refs = append(refs, refResponse{
- Name: name,
- ShortName: short,
- Type: refType,
- Hash: hash.String(),
- IsDefault: short == defaultBranch,
- })
- }
- }
-
- writeJSON(w, refs)
-}
-
-// ── GET /api/repos/{owner}/{repo}/git/trees/{ref}?path= ──────────────────────
-
-type gitTreeHandler struct{ mrc *cache.MultiRepoCache }
-
-func NewGitTreeHandler(mrc *cache.MultiRepoCache) http.Handler {
- return &gitTreeHandler{mrc: mrc}
-}
-
-type treeEntryResponse struct {
- Name string `json:"name"`
- Type string `json:"type"` // "tree" | "blob"
- Hash string `json:"hash"`
- Mode string `json:"mode"`
- LastCommit *commitMetaResponse `json:"lastCommit,omitempty"`
-}
-
-func (h *gitTreeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- ref := mux.Vars(r)["ref"]
- path := r.URL.Query().Get("path")
-
- repo, br, err := browseRepo(h.mrc, r)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- commitHash, err := resolveRef(repo, ref)
- if err != nil {
- http.Error(w, "ref not found", http.StatusNotFound)
- return
- }
-
- entries, err := resolveTreeAtPath(repo, commitHash, path)
- if err == repository.ErrNotFound {
- http.Error(w, "path not found", http.StatusNotFound)
- return
- }
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- names := make([]string, len(entries))
- for i, e := range entries {
- names[i] = e.Name
- }
- lastCommits, _ := br.LastCommitForEntries(ref, path, names)
-
- resp := make([]treeEntryResponse, 0, len(entries))
- for _, e := range entries {
- objType := "blob"
- mode := "100644"
- if e.ObjectType == repository.Tree {
- objType = "tree"
- mode = "040000"
- }
- item := treeEntryResponse{
- Name: e.Name,
- Type: objType,
- Hash: e.Hash.String(),
- Mode: mode,
- }
- if cm, ok := lastCommits[e.Name]; ok {
- item.LastCommit = toCommitMetaResponse(cm)
- }
- resp = append(resp, item)
- }
-
- writeJSON(w, resp)
-}
-
-// ── GET /api/repos/{owner}/{repo}/git/blobs/{ref}?path= ──────────────────────
-
-type gitBlobHandler struct{ mrc *cache.MultiRepoCache }
-
-func NewGitBlobHandler(mrc *cache.MultiRepoCache) http.Handler {
- return &gitBlobHandler{mrc: mrc}
-}
-
-type blobResponse struct {
- Path string `json:"path"`
- Content string `json:"content"`
- Size int `json:"size"`
- IsBinary bool `json:"isBinary"`
-}
-
-func (h *gitBlobHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- ref := mux.Vars(r)["ref"]
- path := r.URL.Query().Get("path")
-
- if path == "" {
- http.Error(w, "missing path", http.StatusBadRequest)
- return
- }
-
- repo, _, err := browseRepo(h.mrc, r)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- commitHash, err := resolveRef(repo, ref)
- if err != nil {
- http.Error(w, "ref not found", http.StatusNotFound)
- return
- }
-
- blobHash, err := resolveBlobAtPath(repo, commitHash, path)
- if err == repository.ErrNotFound {
- http.Error(w, "path not found", http.StatusNotFound)
- return
- }
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- data, err := repo.ReadData(blobHash)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- isBinary := isBinaryContent(data)
- content := ""
- if !isBinary {
- content = string(data)
- }
-
- writeJSON(w, blobResponse{
- Path: path,
- Content: content,
- Size: len(data),
- IsBinary: isBinary,
- })
-}
-
-// ── GET /api/repos/{owner}/{repo}/git/raw/{ref}/{path} ───────────────────────
-// Serves the raw file content for download. ref and path are both in the URL
-// path, producing human-readable download URLs like:
-//
-// /api/repos/_/_/git/raw/main/src/foo/bar.go
-
-type gitRawHandler struct{ mrc *cache.MultiRepoCache }
-
-func NewGitRawHandler(mrc *cache.MultiRepoCache) http.Handler {
- return &gitRawHandler{mrc: mrc}
-}
-
-func (h *gitRawHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- ref := mux.Vars(r)["ref"]
- path := mux.Vars(r)["path"]
-
- if path == "" {
- http.Error(w, "missing path", http.StatusBadRequest)
- return
- }
-
- repo, _, err := browseRepo(h.mrc, r)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- commitHash, err := resolveRef(repo, ref)
- if err != nil {
- http.Error(w, "ref not found", http.StatusNotFound)
- return
- }
-
- blobHash, err := resolveBlobAtPath(repo, commitHash, path)
- if err == repository.ErrNotFound {
- http.Error(w, "path not found", http.StatusNotFound)
- return
- }
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- data, err := repo.ReadData(blobHash)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- fileName := path[strings.LastIndex(path, "/")+1:]
- w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename=%q`, fileName))
- w.Header().Set("Content-Type", "application/octet-stream")
- w.Write(data)
-}
-
-// ── GET /api/repos/{owner}/{repo}/git/commits?ref=&path=&limit=&after= ───────
-
-type gitCommitsHandler struct{ mrc *cache.MultiRepoCache }
-
-func NewGitCommitsHandler(mrc *cache.MultiRepoCache) http.Handler {
- return &gitCommitsHandler{mrc: mrc}
-}
-
-type commitMetaResponse struct {
- Hash string `json:"hash"`
- ShortHash string `json:"shortHash"`
- Message string `json:"message"`
- AuthorName string `json:"authorName"`
- AuthorEmail string `json:"authorEmail"`
- Date string `json:"date"` // RFC3339
- Parents []string `json:"parents"`
-}
-
-func toCommitMetaResponse(m repository.CommitMeta) *commitMetaResponse {
- parents := make([]string, len(m.Parents))
- for i, p := range m.Parents {
- parents[i] = p.String()
- }
- return &commitMetaResponse{
- Hash: m.Hash.String(),
- ShortHash: m.ShortHash,
- Message: m.Message,
- AuthorName: m.AuthorName,
- AuthorEmail: m.AuthorEmail,
- Date: m.Date.UTC().Format("2006-01-02T15:04:05Z"),
- Parents: parents,
- }
-}
-
-func (h *gitCommitsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- ref := r.URL.Query().Get("ref")
- path := r.URL.Query().Get("path")
- after := repository.Hash(r.URL.Query().Get("after"))
-
- limit := 20
- if l := r.URL.Query().Get("limit"); l != "" {
- if n, err := strconv.Atoi(l); err == nil && n > 0 && n <= 100 {
- limit = n
- }
- }
-
- if ref == "" {
- http.Error(w, "missing ref", http.StatusBadRequest)
- return
- }
-
- _, br, err := browseRepo(h.mrc, r)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- commits, err := br.CommitLog(ref, path, limit, after)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- resp := make([]*commitMetaResponse, len(commits))
- for i, c := range commits {
- resp[i] = toCommitMetaResponse(c)
- }
- writeJSON(w, resp)
-}
-
-// ── GET /api/repos/{owner}/{repo}/git/commits/{sha} ──────────────────────────
-
-type gitCommitHandler struct{ mrc *cache.MultiRepoCache }
-
-func NewGitCommitHandler(mrc *cache.MultiRepoCache) http.Handler {
- return &gitCommitHandler{mrc: mrc}
-}
-
-// ── GET /api/repos/{owner}/{repo}/git/commits/{sha}/diff?path= ───────────────
-
-type gitCommitDiffHandler struct{ mrc *cache.MultiRepoCache }
-
-func NewGitCommitDiffHandler(mrc *cache.MultiRepoCache) http.Handler {
- return &gitCommitDiffHandler{mrc: mrc}
-}
-
-func (h *gitCommitDiffHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- sha := mux.Vars(r)["sha"]
- filePath := r.URL.Query().Get("path")
- if filePath == "" {
- http.Error(w, "missing path", http.StatusBadRequest)
- return
- }
-
- _, br, err := browseRepo(h.mrc, r)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- fd, err := br.CommitFileDiff(repository.Hash(sha), filePath)
- if err == repository.ErrNotFound {
- http.Error(w, "not found", http.StatusNotFound)
- return
- }
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- type diffLineResp struct {
- Type string `json:"type"`
- Content string `json:"content"`
- OldLine int `json:"oldLine,omitempty"`
- NewLine int `json:"newLine,omitempty"`
- }
- type diffHunkResp struct {
- OldStart int `json:"oldStart"`
- OldLines int `json:"oldLines"`
- NewStart int `json:"newStart"`
- NewLines int `json:"newLines"`
- Lines []diffLineResp `json:"lines"`
- }
- type fileDiffResp struct {
- Path string `json:"path"`
- OldPath string `json:"oldPath,omitempty"`
- IsBinary bool `json:"isBinary"`
- IsNew bool `json:"isNew"`
- IsDelete bool `json:"isDelete"`
- Hunks []diffHunkResp `json:"hunks"`
- }
-
- hunks := make([]diffHunkResp, len(fd.Hunks))
- for i, h := range fd.Hunks {
- lines := make([]diffLineResp, len(h.Lines))
- for j, l := range h.Lines {
- lines[j] = diffLineResp{Type: l.Type, Content: l.Content, OldLine: l.OldLine, NewLine: l.NewLine}
- }
- hunks[i] = diffHunkResp{OldStart: h.OldStart, OldLines: h.OldLines, NewStart: h.NewStart, NewLines: h.NewLines, Lines: lines}
- }
-
- writeJSON(w, fileDiffResp{
- Path: fd.Path,
- OldPath: fd.OldPath,
- IsBinary: fd.IsBinary,
- IsNew: fd.IsNew,
- IsDelete: fd.IsDelete,
- Hunks: hunks,
- })
-}
-
-type changedFileResponse struct {
- Path string `json:"path"`
- OldPath string `json:"oldPath,omitempty"`
- Status string `json:"status"`
-}
-
-type commitDetailResponse struct {
- *commitMetaResponse
- FullMessage string `json:"fullMessage"`
- Files []changedFileResponse `json:"files"`
-}
-
-func (h *gitCommitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- sha := mux.Vars(r)["sha"]
-
- _, br, err := browseRepo(h.mrc, r)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- detail, err := br.CommitDetail(repository.Hash(sha))
- if err == repository.ErrNotFound {
- http.Error(w, "commit not found", http.StatusNotFound)
- return
- }
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- files := make([]changedFileResponse, len(detail.Files))
- for i, f := range detail.Files {
- files[i] = changedFileResponse{Path: f.Path, OldPath: f.OldPath, Status: f.Status}
- }
-
- writeJSON(w, commitDetailResponse{
- commitMetaResponse: toCommitMetaResponse(detail.CommitMeta),
- FullMessage: detail.FullMessage,
- Files: files,
- })
-}
@@ -1,331 +0,0 @@
-// Git smart HTTP handler — serves git clone and push using native git
-// subprocesses (git-upload-pack / git-receive-pack --stateless-rpc).
-//
-// Security notes:
-// - No shell is used; exec.Command receives explicit argument slices.
-// - The subprocess environment is sanitised: variables that could redirect
-// git's operations (GIT_DIR, GIT_EXEC_PATH, GIT_SSH, …) are stripped.
-// - The repository path is resolved from our internal config, never from
-// URL parameters or request body content.
-// - Client stderr is captured and discarded; it is never forwarded to the
-// HTTP response.
-//
-// Routes (registered on the /api/repos/{owner}/{repo} subrouter):
-//
-// GET /info/refs?service=git-{upload,receive}-pack → capability advertisement
-// POST /git-upload-pack → fetch / clone
-// POST /git-receive-pack → push (blocked in read-only mode)
-
-package http
-
-import (
- "bytes"
- "compress/gzip"
- "fmt"
- "io"
- "net/http"
- "os"
- "os/exec"
- "strings"
-
- pktline "github.com/go-git/go-git/v5/plumbing/format/pktline"
-
- "github.com/git-bug/git-bug/cache"
-)
-
-// GitServeHandler exposes the repository over git's smart HTTP protocol.
-type GitServeHandler struct {
- mrc *cache.MultiRepoCache
- readOnly bool
-}
-
-func NewGitServeHandler(mrc *cache.MultiRepoCache, readOnly bool) *GitServeHandler {
- return &GitServeHandler{mrc: mrc, readOnly: readOnly}
-}
-
-// ServeInfoRefs handles GET /info/refs — the capability advertisement step.
-// Runs `git {upload,receive}-pack --stateless-rpc --advertise-refs` and
-// prepends the required PKT-LINE service header.
-// For upload-pack the advertised refs are filtered to heads and tags only so
-// that cloners do not inadvertently fetch git-bug internal objects.
-func (h *GitServeHandler) ServeInfoRefs(w http.ResponseWriter, r *http.Request) {
- service := r.URL.Query().Get("service")
- if service != "git-upload-pack" && service != "git-receive-pack" {
- http.Error(w, "unknown service", http.StatusForbidden)
- return
- }
- if service == "git-receive-pack" && h.readOnly {
- http.Error(w, "repository is read-only", http.StatusForbidden)
- return
- }
-
- repoPath, err := h.repoPathFor(r)
- if err != nil {
- http.Error(w, err.Error(), http.StatusNotFound)
- return
- }
-
- // "git-upload-pack" → "upload-pack", "git-receive-pack" → "receive-pack"
- subCmd := strings.TrimPrefix(service, "git-")
-
- cmd := exec.CommandContext(r.Context(),
- "git", subCmd, "--stateless-rpc", "--advertise-refs", repoPath)
- cmd.Env = safeGitEnv()
-
- out, err := cmd.Output()
- if err != nil {
- http.Error(w, "git advertisement failed", http.StatusInternalServerError)
- return
- }
-
- w.Header().Set("Cache-Control", "no-cache")
- w.Header().Set("Content-Type", fmt.Sprintf("application/x-%s-advertisement", service))
-
- // PKT-LINE service header required by the smart HTTP protocol.
- enc := pktline.NewEncoder(w)
- if err := enc.EncodeString(fmt.Sprintf("# service=%s\n", service)); err != nil {
- return
- }
- if err := enc.Flush(); err != nil {
- return
- }
-
- // For upload-pack, filter out internal git-bug refs (refs/bugs/,
- // refs/identities/, …) so cloners only receive source code objects.
- if service == "git-upload-pack" {
- _ = writeFilteredInfoRefs(w, out)
- } else {
- _, _ = w.Write(out)
- }
-}
-
-// ServeUploadPack handles POST /git-upload-pack — serves a fetch or clone.
-// The request body is piped directly to `git upload-pack --stateless-rpc`.
-func (h *GitServeHandler) ServeUploadPack(w http.ResponseWriter, r *http.Request) {
- repoPath, err := h.repoPathFor(r)
- if err != nil {
- http.Error(w, err.Error(), http.StatusNotFound)
- return
- }
-
- body, err := requestBody(r)
- if err != nil {
- http.Error(w, "decompressing request: "+err.Error(), http.StatusBadRequest)
- return
- }
- defer body.Close()
-
- cmd := exec.CommandContext(r.Context(),
- "git", "upload-pack", "--stateless-rpc", repoPath)
- cmd.Env = safeGitEnv()
- cmd.Stdin = body
-
- w.Header().Set("Content-Type", "application/x-git-upload-pack-result")
- w.Header().Set("Cache-Control", "no-cache")
- cmd.Stdout = w
-
- var stderr bytes.Buffer
- cmd.Stderr = &stderr
- // Errors after this point can't change the HTTP status (headers already
- // committed on first write), so we just return silently.
- _ = cmd.Run()
-}
-
-// ServeReceivePack handles POST /git-receive-pack — accepts a push.
-// Before running git, the PKT-LINE ref-update commands are parsed so that the
-// git-bug cache can be synchronised for any git-bug namespaces that were
-// updated.
-func (h *GitServeHandler) ServeReceivePack(w http.ResponseWriter, r *http.Request) {
- if h.readOnly {
- http.Error(w, "repository is read-only", http.StatusForbidden)
- return
- }
-
- repoPath, err := h.repoPathFor(r)
- if err != nil {
- http.Error(w, err.Error(), http.StatusNotFound)
- return
- }
-
- body, err := requestBody(r)
- if err != nil {
- http.Error(w, "decompressing request: "+err.Error(), http.StatusBadRequest)
- return
- }
- defer body.Close()
-
- // Parse the PKT-LINE ref-update commands so we know which git-bug entities
- // to resync after the push completes. The full request body is
- // reconstructed (commands + flush + packfile) for git's stdin.
- updatedRefs, fullBody, err := parseReceivePackCommands(body)
- if err != nil {
- http.Error(w, "parsing receive-pack request: "+err.Error(), http.StatusBadRequest)
- return
- }
-
- cmd := exec.CommandContext(r.Context(),
- "git", "receive-pack", "--stateless-rpc", repoPath)
- cmd.Env = safeGitEnv()
- cmd.Stdin = fullBody
-
- w.Header().Set("Content-Type", "application/x-git-receive-pack-result")
- w.Header().Set("Cache-Control", "no-cache")
- cmd.Stdout = w
-
- var stderr bytes.Buffer
- cmd.Stderr = &stderr
-
- if err := cmd.Run(); err != nil {
- // Headers may already be committed; best-effort return.
- return
- }
-
- h.syncAfterPush(r, updatedRefs)
-}
-
-// ── helpers ───────────────────────────────────────────────────────────────────
-
-// repoPathFor returns the filesystem path of the repository referenced in the
-// request URL variables. The path is always resolved from our internal
-// MultiRepoCache configuration — it is never derived from request content.
-func (h *GitServeHandler) repoPathFor(r *http.Request) (string, error) {
- rc, err := repoFromPath(h.mrc, r)
- if err != nil {
- return "", err
- }
- return rc.GetPath(), nil
-}
-
-// syncAfterPush updates the git-bug in-memory cache for any refs that were
-// updated by the push.
-func (h *GitServeHandler) syncAfterPush(r *http.Request, refs []string) {
- if len(refs) == 0 {
- return
- }
- rc, err := repoFromPath(h.mrc, r)
- if err != nil {
- return
- }
- _ = rc.SyncLocalRefs(refs)
-}
-
-// writeFilteredInfoRefs re-encodes the raw PKT-LINE advertisement output from
-// git, keeping only HEAD and refs/heads/* and refs/tags/*. The first line is
-// always forwarded unchanged because it carries the server capability list
-// (appended after a NUL byte).
-func writeFilteredInfoRefs(w io.Writer, raw []byte) error {
- scanner := pktline.NewScanner(bytes.NewReader(raw))
- enc := pktline.NewEncoder(w)
- first := true
- for scanner.Scan() {
- b := scanner.Bytes()
- if len(b) == 0 { // flush packet
- return enc.Flush()
- }
- if first {
- // First line always passes — it carries server capabilities.
- first = false
- if err := enc.Encode(b); err != nil {
- return err
- }
- continue
- }
- // Lines are "<sha> <refname>\n"; strip the newline to get the ref name.
- line := strings.TrimSuffix(string(b), "\n")
- parts := strings.SplitN(line, " ", 2)
- if len(parts) == 2 {
- ref := parts[1]
- if strings.HasPrefix(ref, "refs/heads/") || strings.HasPrefix(ref, "refs/tags/") {
- if err := enc.Encode(b); err != nil {
- return err
- }
- }
- }
- }
- return scanner.Err()
-}
-
-// parseReceivePackCommands reads the PKT-LINE ref-update command lines from the
-// receive-pack request body (up to and including the flush packet), extracts
-// the ref names, and returns an io.Reader that replays the full original body
-// (commands + flush + packfile) for the git subprocess.
-func parseReceivePackCommands(r io.Reader) (refs []string, full io.Reader, err error) {
- // TeeReader mirrors everything consumed by the scanner into cmds, so we
- // can replay it verbatim later.
- var cmds bytes.Buffer
- scanner := pktline.NewScanner(io.TeeReader(r, &cmds))
- for scanner.Scan() {
- b := scanner.Bytes()
- if len(b) == 0 { // flush — end of command list
- break
- }
- // Command format: "<old-sha> <new-sha> <refname>\0<caps>" (first line)
- // or "<old-sha> <new-sha> <refname>" (subsequent)
- line := strings.TrimSuffix(string(b), "\n")
- if i := strings.IndexByte(line, 0); i >= 0 {
- line = line[:i] // strip NUL + capability list
- }
- parts := strings.SplitN(line, " ", 3)
- if len(parts) == 3 {
- refs = append(refs, parts[2])
- }
- }
- if err = scanner.Err(); err != nil {
- return nil, nil, err
- }
- // cmds holds [commands + flush]; r holds the remaining packfile data.
- return refs, io.MultiReader(&cmds, r), nil
-}
-
-// requestBody returns the request body, transparently decompressing it when
-// the client sent Content-Encoding: gzip (git does this by default).
-func requestBody(r *http.Request) (io.ReadCloser, error) {
- if r.Header.Get("Content-Encoding") == "gzip" {
- gr, err := gzip.NewReader(r.Body)
- if err != nil {
- return nil, err
- }
- return gr, nil
- }
- return r.Body, nil
-}
-
-// safeGitEnv returns a sanitised copy of the process environment for use with
-// git subprocesses. Variables that could redirect git's operations to
-// unintended paths or trigger credential prompts are removed.
-func safeGitEnv() []string {
- // These variables could redirect git internals to attacker-controlled
- // paths or commands when the git-bug server process itself inherits a
- // tainted environment.
- blocked := map[string]bool{
- "GIT_DIR": true,
- "GIT_WORK_TREE": true,
- "GIT_INDEX_FILE": true,
- "GIT_OBJECT_DIRECTORY": true,
- "GIT_ALTERNATE_OBJECT_DIRECTORIES": true,
- "GIT_EXEC_PATH": true,
- "GIT_SSH": true,
- "GIT_SSH_COMMAND": true,
- "GIT_PROXY_COMMAND": true,
- "GIT_ASKPASS": true,
- "SSH_ASKPASS": true,
- "GIT_TRACE": true,
- "GIT_TRACE_PACKET": true,
- "GIT_TRACE_PERFORMANCE": true,
- }
- parent := os.Environ()
- safe := make([]string, 0, len(parent)+1)
- for _, kv := range parent {
- key := kv
- if i := strings.IndexByte(kv, '='); i >= 0 {
- key = kv[:i]
- }
- if !blocked[key] {
- safe = append(safe, kv)
- }
- }
- // Prevent git from blocking on a credential/passphrase prompt, which
- // would hang the HTTP handler goroutine.
- safe = append(safe, "GIT_TERMINAL_PROMPT=0")
- return safe
-}
@@ -21,9 +21,14 @@ func (c *RepoCache) Name() string {
return c.name
}
-// GetPath returns the root directory path of the underlying git repository.
-func (c *RepoCache) GetPath() string {
- return c.repo.GetPath()
+// IsDefaultRepo reports whether this is an unnamed (single-repo) repository.
+func (c *RepoCache) IsDefaultRepo() bool {
+ return c.name == defaultRepoName
+}
+
+// BrowseRepo returns the underlying RepoBrowse implementation.
+func (c *RepoCache) BrowseRepo() repository.RepoBrowse {
+ return c.repo
}
// LocalConfig give access to the repository scoped configuration
@@ -76,12 +81,6 @@ func (c *RepoCache) ReadData(hash repository.Hash) ([]byte, error) {
return c.repo.ReadData(hash)
}
-// GetRepo returns the underlying repository for operations not covered by
-// RepoCache (e.g. git object browsing). Callers may type-assert to
-// repository.RepoBrowse for extended read-only access.
-func (c *RepoCache) GetRepo() repository.ClockedRepo {
- return c.repo
-}
// StoreData will store arbitrary data and return the corresponding hash
func (c *RepoCache) StoreData(data []byte) (repository.Hash, error) {
@@ -1,6 +1,7 @@
package execenv
import (
+ "context"
"encoding/json"
"fmt"
"io"
@@ -19,6 +20,7 @@ const gitBugNamespace = "git-bug"
// Env is the environment of a command
type Env struct {
+ Ctx context.Context
Repo repository.ClockedRepo
Backend *cache.RepoCache
In In
@@ -26,8 +28,9 @@ type Env struct {
Err Out
}
-func NewEnv() *Env {
+func NewEnv(ctx context.Context) *Env {
return &Env{
+ Ctx: ctx,
Repo: nil,
In: in{Reader: os.Stdin},
Out: out{Writer: os.Stdout},
@@ -93,6 +93,7 @@ func newTestEnv(t *testing.T, isTerminal bool) *Env {
})
return &Env{
+ Ctx: t.Context(),
Repo: repo,
Backend: backend,
In: &TestIn{Buffer: &bytes.Buffer{}, forceIsTerminal: isTerminal},
@@ -1,6 +1,7 @@
package commands
import (
+ "context"
"os"
"github.com/spf13/cobra"
@@ -11,7 +12,7 @@ import (
"github.com/git-bug/git-bug/commands/user"
)
-func NewRootCommand(version string) *cobra.Command {
+func NewRootCommand(ctx context.Context, version string) *cobra.Command {
cmd := &cobra.Command{
Use: execenv.RootCommandName,
Short: "A bug tracker embedded in Git",
@@ -54,7 +55,7 @@ the same git remote you are already using to collaborate with other people.
child.GroupID = groupID
}
- env := execenv.NewEnv()
+ env := execenv.NewEnv(ctx)
addCmdWithGroup(bugcmd.NewBugCommand(env), entityGroup)
addCmdWithGroup(usercmd.NewUserCommand(env), entityGroup)
@@ -5,14 +5,10 @@ import (
"errors"
"fmt"
"io"
- "log"
"net"
"net/http"
"net/url"
- "os"
- "os/signal"
"strconv"
- "syscall"
"time"
"github.com/99designs/gqlgen/graphql/playground"
@@ -35,7 +31,7 @@ import (
const webUIOpenConfigKey = "git-bug.webui.open"
type webUIOptions struct {
- host string
+ bind string
port int
open bool
noOpen bool
@@ -70,10 +66,10 @@ Available git config:
flags := cmd.Flags()
flags.SortFlags = false
- flags.StringVar(&options.host, "host", "127.0.0.1", "Network address or hostname to listen to (default to 127.0.0.1)")
+ flags.StringVar(&options.bind, "bind", "127.0.0.1", "Network address to bind to (default to 127.0.0.1)")
+ flags.IntVarP(&options.port, "port", "p", 0, "Port to listen on (default to random available port)")
flags.BoolVar(&options.open, "open", false, "Automatically open the web UI in the default browser")
flags.BoolVar(&options.noOpen, "no-open", false, "Prevent the automatic opening of the web UI in the default browser")
- flags.IntVarP(&options.port, "port", "p", 0, "Port to listen to (default to random available port)")
flags.BoolVar(&options.readOnly, "read-only", false, "Whether to run the web UI in read-only mode")
flags.BoolVar(&options.logErrors, "log-errors", false, "Whether to log errors")
flags.StringVarP(&options.query, "query", "q", "", "The query to open in the web UI bug list")
@@ -86,25 +82,8 @@ Available git config:
return cmd
}
-func runWebUI(env *execenv.Env, opts webUIOptions) error {
- if opts.port == 0 {
- var err error
- opts.port, err = freeport.GetFreePort()
- if err != nil {
- return err
- }
- }
-
- addr := net.JoinHostPort(opts.host, strconv.Itoa(opts.port))
- baseURL := fmt.Sprintf("http://%s", addr)
- webUiAddr := baseURL
- toOpen := webUiAddr
-
- if len(opts.query) > 0 {
- // Explicitly set the query parameter instead of going with a default one.
- toOpen = fmt.Sprintf("%s/?q=%s", webUiAddr, url.QueryEscape(opts.query))
- }
-
+// setupRoutes builds the router and registers all API and UI routes.
+func setupRoutes(env *execenv.Env, opts webUIOptions, baseURL string) (*mux.Router, func() error, error) {
// Collect enabled login providers.
var providers []provider.Provider
if opts.githubClientId != "" {
@@ -131,18 +110,15 @@ func runWebUI(env *execenv.Env, opts webUIOptions) error {
// Single-user mode: inject the identity from git config for every request.
author, err := identity.GetUserIdentity(env.Repo)
if err != nil {
- return err
+ return nil, nil, err
}
router.Use(auth.Middleware(author.Id()))
}
mrc := cache.NewMultiRepoCache()
-
_, events := mrc.RegisterDefaultRepository(env.Repo)
-
- err := execenv.CacheBuildProgressBar(env, events)
- if err != nil {
- return err
+ if err := execenv.CacheBuildProgressBar(env, events); err != nil {
+ return nil, nil, err
}
var errOut io.Writer
@@ -172,80 +148,55 @@ func runWebUI(env *execenv.Env, opts webUIOptions) error {
router.Path("/auth/adopt").Methods("POST").HandlerFunc(ah.HandleAdopt)
}
- // Top-level API routes
router.Path("/playground").Handler(playground.Handler("git-bug", "/graphql"))
router.Path("/graphql").Handler(graphqlHandler)
- // /api/repos/{owner}/{repo}/ subrouter.
- // owner is reserved for future use; "_" means "local".
- // repo "_" resolves to the default repository.
- //
- // In oauth mode all API endpoints require a valid session, making the
- // server safe to deploy publicly. In local and readonly modes the
- // middleware only injects identity without blocking.
- apiRepos := router.PathPrefix("/api/repos/{owner}/{repo}").Subrouter()
- if authMode == "external" {
- apiRepos.Use(auth.RequireAuth)
- }
- apiRepos.Path("/git/refs").Methods("GET").Handler(httpapi.NewGitRefsHandler(mrc))
- apiRepos.Path("/git/trees/{ref}").Methods("GET").Handler(httpapi.NewGitTreeHandler(mrc))
- apiRepos.Path("/git/blobs/{ref}").Methods("GET").Handler(httpapi.NewGitBlobHandler(mrc))
- apiRepos.Path("/git/raw/{ref}/{path:.*}").Methods("GET").Handler(httpapi.NewGitRawHandler(mrc))
- apiRepos.Path("/git/commits").Methods("GET").Handler(httpapi.NewGitCommitsHandler(mrc))
- apiRepos.Path("/git/commits/{sha}").Methods("GET").Handler(httpapi.NewGitCommitHandler(mrc))
- apiRepos.Path("/git/commits/{sha}/diff").Methods("GET").Handler(httpapi.NewGitCommitDiffHandler(mrc))
- apiRepos.Path("/file/{hash}").Methods("GET").Handler(httpapi.NewGitFileHandler(mrc))
- apiRepos.Path("/upload").Methods("POST").Handler(httpapi.NewGitUploadFileHandler(mrc))
-
- // Git smart HTTP — clone, fetch, push.
- gitSrv := httpapi.NewGitServeHandler(mrc, opts.readOnly)
- apiRepos.Path("/info/refs").Methods("GET").HandlerFunc(gitSrv.ServeInfoRefs)
- apiRepos.Path("/git-upload-pack").Methods("POST").HandlerFunc(gitSrv.ServeUploadPack)
- apiRepos.Path("/git-receive-pack").Methods("POST").HandlerFunc(gitSrv.ServeReceivePack)
+ // File and upload routes for bug attachments.
+ router.Path("/gitfile/{repo}/{hash}").Handler(httpapi.NewGitFileHandler(mrc))
+ router.Path("/upload/{repo}").Methods("POST").Handler(httpapi.NewGitUploadFileHandler(mrc))
router.PathPrefix("/").Handler(webui2.NewHandler())
- srv := &http.Server{
- Addr: addr,
- Handler: router,
- }
-
- done := make(chan bool)
- quit := make(chan os.Signal, 1)
-
- // register as handler of the interrupt signal to trigger the teardown
- signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
-
- go func() {
- <-quit
- env.Out.Println("WebUI is shutting down...")
-
- ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
- defer cancel()
-
- srv.SetKeepAlivesEnabled(false)
- if err := srv.Shutdown(ctx); err != nil {
- log.Fatalf("Could not gracefully shutdown the WebUI: %v\n", err)
- }
+ return router, mrc.Close, nil
+}
- // Teardown
- err = mrc.Close()
+func runWebUI(env *execenv.Env, opts webUIOptions) error {
+ if opts.port == 0 {
+ var err error
+ opts.port, err = freeport.GetFreePort()
if err != nil {
- env.Out.Println(err)
+ return err
}
+ }
+
+ addr := net.JoinHostPort(opts.bind, strconv.Itoa(opts.port))
+ baseURL := "http://" + addr
- close(done)
+ router, closeRoutes, err := setupRoutes(env, opts, baseURL)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err := closeRoutes(); err != nil {
+ env.Err.Println(err)
+ }
}()
- env.Out.Printf("Web UI: %s\n", webUiAddr)
- env.Out.Printf("Graphql API: http://%s/graphql\n", addr)
- env.Out.Printf("Graphql Playground: http://%s/playground\n", addr)
- if authMode == "external" {
+ server := &http.Server{Addr: addr, Handler: router}
+
+ env.Out.Printf("Web UI: %s\n", baseURL)
+ env.Out.Printf("Graphql API: %s/graphql\n", baseURL)
+ env.Out.Printf("Graphql Playground: %s/playground\n", baseURL)
+ if opts.githubClientId != "" {
env.Out.Printf("Login callback URL: %s/auth/callback\n", baseURL)
env.Out.Println(" ↳ Register this URL in your OAuth/OIDC application settings")
}
- env.Out.Println("Press Ctrl+c to quit")
+ env.Out.Printf("\n[ Press Ctrl+c to quit ]\n\n")
+ toOpen := baseURL
+ if len(opts.query) > 0 {
+ toOpen = fmt.Sprintf("%s/?q=%s", baseURL, url.QueryEscape(opts.query))
+ }
configOpen, err := env.Repo.AnyConfig().ReadBool(webUIOpenConfigKey)
if errors.Is(err, repository.ErrNoConfigEntry) {
// default to true
@@ -253,23 +204,65 @@ func runWebUI(env *execenv.Env, opts webUIOptions) error {
} else if err != nil {
return err
}
+ if (configOpen && !opts.noOpen) || opts.open {
+ go openWhenUp(env, toOpen)
+ }
+
+ go func() {
+ <-env.Ctx.Done()
+ env.Out.Println("shutting down...")
+ shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ server.SetKeepAlivesEnabled(false)
+ if err := server.Shutdown(shutdownCtx); err != nil {
+ env.Err.Printf("Could not gracefully shutdown the HTTP server: %v\n", err)
+ }
+ }()
- shouldOpen := (configOpen && !opts.noOpen) || opts.open
+ if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
+ return err
+ }
+ return nil
+}
- if shouldOpen {
- err = open.Run(toOpen)
- if err != nil {
- env.Out.Println(err)
+func openWhenUp(env *execenv.Env, toOpen string) {
+ const maxAttempts = 3
+ if isUp(toOpen, maxAttempts, 3*time.Second) {
+ if err := open.Run(toOpen); err != nil {
+ env.Err.Println(err)
+ return
}
+ env.Out.Printf("opened your default browser to url: %s\n", toOpen)
+ return
}
+ env.Err.Printf(
+ "uh oh! it appears that the http server hasn't started.\n"+
+ "we failed to reach %s after %d attempts.\n",
+ toOpen, maxAttempts,
+ )
+}
- err = srv.ListenAndServe()
- if err != nil && err != http.ErrServerClosed {
- return err
+func isUp(url string, maxRetries int, initialDelay time.Duration) bool {
+ client := &http.Client{
+ Timeout: 5 * time.Second,
}
- <-done
+ delay := initialDelay
- env.Out.Println("WebUI stopped")
- return nil
+ for attempt := 1; attempt <= maxRetries; attempt++ {
+ resp, err := client.Head(url)
+ if err == nil {
+ _ = resp.Body.Close()
+ if resp.StatusCode >= 200 && resp.StatusCode < 400 {
+ return true
+ }
+ }
+
+ if attempt < maxRetries {
+ time.Sleep(delay)
+ delay *= 2
+ }
+ }
+
+ return false
}
@@ -1,6 +1,7 @@
package main
import (
+ "context"
"fmt"
"os"
"path/filepath"
@@ -34,7 +35,7 @@ func main() {
wg.Add(1)
go func(name string, f func(*cobra.Command) error) {
defer wg.Done()
- root := commands.NewRootCommand("")
+ root := commands.NewRootCommand(context.Background(), "")
err := f(root)
if err != nil {
fmt.Printf(" - %s: FATAL\n", name)
@@ -1,8 +1,6 @@
module github.com/git-bug/git-bug
-go 1.24.0
-
-toolchain go1.24.2
+go 1.25.0
require (
github.com/99designs/gqlgen v0.17.73
@@ -32,17 +30,19 @@ require (
github.com/vbauerster/mpb/v8 v8.8.2
github.com/vektah/gqlparser/v2 v2.5.26
gitlab.com/gitlab-org/api/client-go v0.116.0
- golang.org/x/crypto v0.45.0
- golang.org/x/mod v0.29.0
- golang.org/x/net v0.47.0
- golang.org/x/oauth2 v0.27.0
- golang.org/x/sync v0.18.0
- golang.org/x/sys v0.38.0
- golang.org/x/term v0.37.0
- golang.org/x/text v0.31.0
+ golang.org/x/crypto v0.49.0
+ golang.org/x/mod v0.34.0
+ golang.org/x/net v0.52.0
+ golang.org/x/oauth2 v0.36.0
+ golang.org/x/sync v0.20.0
+ golang.org/x/sys v0.42.0
+ golang.org/x/term v0.41.0
+ golang.org/x/text v0.35.0
golang.org/x/vuln v1.1.3
)
+tool github.com/99designs/gqlgen
+
require (
dario.cat/mergo v1.0.1 // indirect
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
@@ -113,14 +113,12 @@ require (
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
- golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect
+ golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 // indirect
golang.org/x/time v0.3.0 // indirect
- golang.org/x/tools v0.38.0 // indirect
+ golang.org/x/tools v0.42.0 // indirect
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect
golang.org/x/tools/godoc v0.1.0-deprecated // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
-
-tool github.com/99designs/gqlgen
@@ -257,28 +257,28 @@ go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
-golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
+golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
+golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
-golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
+golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
+golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
-golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
-golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
-golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
+golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
+golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
+golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
+golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -291,33 +291,33 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU=
-golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
+golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
+golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0=
+golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
-golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
+golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
+golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
+golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
-golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
+golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
+golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY=
golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
@@ -4,14 +4,20 @@
package main
import (
+ "context"
"os"
+ "os/signal"
+ "syscall"
"github.com/git-bug/git-bug/commands"
)
func main() {
+ ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
+ defer cancel()
+
v, _ := getVersion()
- root := commands.NewRootCommand(v)
+ root := commands.NewRootCommand(ctx, v)
if err := root.Execute(); err != nil {
os.Exit(1)
}
@@ -1,6 +1,7 @@
package main
import (
+ "context"
"fmt"
"os"
"path/filepath"
@@ -26,7 +27,7 @@ func main() {
wg.Add(1)
go func(name string, f func(*cobra.Command) error) {
defer wg.Done()
- root := commands.NewRootCommand("")
+ root := commands.NewRootCommand(context.Background(), "")
err := f(root)
if err != nil {
fmt.Printf(" - %s: %v\n", name, err)
@@ -0,0 +1,160 @@
+package repository
+
+import (
+ "fmt"
+ "io"
+ "strconv"
+ "time"
+)
+
+// ChangeStatus describes how a file was affected by a commit.
+type ChangeStatus string
+
+const (
+ ChangeStatusAdded ChangeStatus = "added"
+ ChangeStatusModified ChangeStatus = "modified"
+ ChangeStatusDeleted ChangeStatus = "deleted"
+ ChangeStatusRenamed ChangeStatus = "renamed"
+)
+
+func (s ChangeStatus) MarshalGQL(w io.Writer) {
+ switch s {
+ case ChangeStatusAdded:
+ fmt.Fprint(w, strconv.Quote("ADDED"))
+ case ChangeStatusModified:
+ fmt.Fprint(w, strconv.Quote("MODIFIED"))
+ case ChangeStatusDeleted:
+ fmt.Fprint(w, strconv.Quote("DELETED"))
+ case ChangeStatusRenamed:
+ fmt.Fprint(w, strconv.Quote("RENAMED"))
+ default:
+ panic(fmt.Sprintf("unknown ChangeStatus value %q", string(s)))
+ }
+}
+
+func (s *ChangeStatus) UnmarshalGQL(v any) error {
+ str, ok := v.(string)
+ if !ok {
+ return fmt.Errorf("enums must be strings")
+ }
+ switch str {
+ case "ADDED":
+ *s = ChangeStatusAdded
+ case "MODIFIED":
+ *s = ChangeStatusModified
+ case "DELETED":
+ *s = ChangeStatusDeleted
+ case "RENAMED":
+ *s = ChangeStatusRenamed
+ default:
+ return fmt.Errorf("%q is not a valid ChangeStatus", str)
+ }
+ return nil
+}
+
+// DiffLineType is the role of a line within a unified diff hunk.
+type DiffLineType string
+
+const (
+ DiffLineContext DiffLineType = "context"
+ DiffLineAdded DiffLineType = "added"
+ DiffLineDeleted DiffLineType = "deleted"
+)
+
+func (t DiffLineType) MarshalGQL(w io.Writer) {
+ switch t {
+ case DiffLineContext:
+ fmt.Fprint(w, strconv.Quote("CONTEXT"))
+ case DiffLineAdded:
+ fmt.Fprint(w, strconv.Quote("ADDED"))
+ case DiffLineDeleted:
+ fmt.Fprint(w, strconv.Quote("DELETED"))
+ default:
+ panic(fmt.Sprintf("unknown DiffLineType value %q", string(t)))
+ }
+}
+
+func (t *DiffLineType) UnmarshalGQL(v any) error {
+ str, ok := v.(string)
+ if !ok {
+ return fmt.Errorf("enums must be strings")
+ }
+ switch str {
+ case "CONTEXT":
+ *t = DiffLineContext
+ case "ADDED":
+ *t = DiffLineAdded
+ case "DELETED":
+ *t = DiffLineDeleted
+ default:
+ return fmt.Errorf("%q is not a valid DiffLineType", str)
+ }
+ return nil
+}
+
+// CommitMeta holds the metadata for a single commit, suitable for listing.
+type CommitMeta struct {
+ Hash Hash
+ Message string
+ AuthorName string
+ AuthorEmail string
+ Date time.Time
+ Parents []Hash
+}
+
+// ChangedFile describes a file that was modified in a commit.
+type ChangedFile struct {
+ Path string
+ OldPath *string // non-nil for renames
+ Status ChangeStatus
+}
+
+// CommitDetail extends CommitMeta with the full message and the list of
+// changed files (relative to the first parent).
+type CommitDetail struct {
+ CommitMeta
+ FullMessage string
+ Files []ChangedFile
+}
+
+// DiffLine represents one line in a unified diff hunk.
+type DiffLine struct {
+ Type DiffLineType
+ Content string
+ OldLine int
+ NewLine int
+}
+
+// DiffHunk is a contiguous block of changes in a unified diff.
+type DiffHunk struct {
+ OldStart int
+ OldLines int
+ NewStart int
+ NewLines int
+ Lines []DiffLine
+}
+
+// FileDiff is the diff for a single file in a commit.
+type FileDiff struct {
+ Path string
+ OldPath *string // non-nil for renames
+ IsBinary bool
+ IsNew bool
+ IsDelete bool
+ Hunks []DiffHunk
+}
+
+// BranchInfo describes a local branch returned by RepoBrowse.Branches.
+type BranchInfo struct {
+ Name string
+ Hash Hash // commit hash
+ IsDefault bool // true for the branch HEAD points to
+}
+
+// TagInfo describes a tag returned by RepoBrowse.Tags.
+type TagInfo struct {
+ Name string
+ // Hash is always the target commit hash. For annotated tags the tag
+ // object is dereferenced; for lightweight tags this is the ref hash.
+ Hash Hash
+}
@@ -65,3 +65,10 @@ func deArmorSignature(armoredSig io.Reader) (io.Reader, error) {
}
return block.Body, nil
}
+
+func must[T any](v T, err error) T {
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
@@ -19,8 +19,9 @@ import (
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode"
- "github.com/go-git/go-git/v5/plumbing/format/diff"
+ fdiff "github.com/go-git/go-git/v5/plumbing/format/diff"
"github.com/go-git/go-git/v5/plumbing/object"
+ lru "github.com/hashicorp/golang-lru/v2"
"golang.org/x/sync/errgroup"
"golang.org/x/sys/execabs"
@@ -30,6 +31,17 @@ import (
const clockPath = "clocks"
const indexPath = "indexes"
+// lastCommitDepthLimit is the maximum number of commits walked by
+// LastCommitForEntries. Entries not found within this horizon are omitted from
+// the result rather than stalling the caller indefinitely.
+const lastCommitDepthLimit = 1000
+
+// lastCommitCacheSize is the number of (resolvedHash, dirPath) pairs kept in
+// the LRU cache for LastCommitForEntries. Each entry holds one CommitMeta per
+// directory entry (≈ a few KB for a typical directory), so 256 slots ≈ a few
+// MB of memory at most.
+const lastCommitCacheSize = 256
+
var _ ClockedRepo = &GoGitRepo{}
var _ TestedRepo = &GoGitRepo{}
@@ -48,6 +60,13 @@ type GoGitRepo struct {
indexesMutex sync.Mutex
indexes map[string]Index
+ // lastCommitCache caches LastCommitForEntries results keyed by
+ // "<treeHash>\x00<path>". Git trees are content-addressed and
+ // immutable, so entries never need invalidation and can be shared
+ // across refs that point to the same directory tree. The LRU bounds
+ // memory to lastCommitCacheSize unique (treeHash, directory) pairs.
+ lastCommitCache *lru.Cache[string, map[string]CommitMeta]
+
keyring Keyring
localStorage LocalStorage
}
@@ -73,12 +92,13 @@ func OpenGoGitRepo(path, namespace string, clockLoaders []ClockLoader) (*GoGitRe
}
repo := &GoGitRepo{
- r: r,
- path: path,
- clocks: make(map[string]lamport.Clock),
- indexes: make(map[string]Index),
- keyring: k,
- localStorage: billyLocalStorage{Filesystem: osfs.New(filepath.Join(path, namespace))},
+ r: r,
+ path: path,
+ clocks: make(map[string]lamport.Clock),
+ indexes: make(map[string]Index),
+ lastCommitCache: must(lru.New[string, map[string]CommitMeta](lastCommitCacheSize)),
+ keyring: k,
+ localStorage: billyLocalStorage{Filesystem: osfs.New(filepath.Join(path, namespace))},
}
loaderToRun := make([]ClockLoader, 0, len(clockLoaders))
@@ -127,12 +147,13 @@ func InitGoGitRepo(path, namespace string) (*GoGitRepo, error) {
}
return &GoGitRepo{
- r: r,
- path: filepath.Join(path, ".git"),
- clocks: make(map[string]lamport.Clock),
- indexes: make(map[string]Index),
- keyring: k,
- localStorage: billyLocalStorage{Filesystem: osfs.New(filepath.Join(path, ".git", namespace))},
+ r: r,
+ path: filepath.Join(path, ".git"),
+ clocks: make(map[string]lamport.Clock),
+ indexes: make(map[string]Index),
+ lastCommitCache: must(lru.New[string, map[string]CommitMeta](lastCommitCacheSize)),
+ keyring: k,
+ localStorage: billyLocalStorage{Filesystem: osfs.New(filepath.Join(path, ".git", namespace))},
}, nil
}
@@ -152,12 +173,13 @@ func InitBareGoGitRepo(path, namespace string) (*GoGitRepo, error) {
}
return &GoGitRepo{
- r: r,
- path: path,
- clocks: make(map[string]lamport.Clock),
- indexes: make(map[string]Index),
- keyring: k,
- localStorage: billyLocalStorage{Filesystem: osfs.New(filepath.Join(path, namespace))},
+ r: r,
+ path: path,
+ clocks: make(map[string]lamport.Clock),
+ indexes: make(map[string]Index),
+ lastCommitCache: must(lru.New[string, map[string]CommitMeta](lastCommitCacheSize)),
+ keyring: k,
+ localStorage: billyLocalStorage{Filesystem: osfs.New(filepath.Join(path, namespace))},
}, nil
}
@@ -830,48 +852,376 @@ func (repo *GoGitRepo) ReadCommit(hash Hash) (Commit, error) {
var _ RepoBrowse = &GoGitRepo{}
-func (repo *GoGitRepo) GetDefaultBranch() (string, error) {
+func (repo *GoGitRepo) AllClocks() (map[string]lamport.Clock, error) {
+ repo.clocksMutex.Lock()
+ defer repo.clocksMutex.Unlock()
+
+ result := make(map[string]lamport.Clock)
+
+ files, err := os.ReadDir(filepath.Join(repo.localStorage.Root(), clockPath))
+ if os.IsNotExist(err) {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ for _, file := range files {
+ name := file.Name()
+ if c, ok := repo.clocks[name]; ok {
+ result[name] = c
+ } else {
+ c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
+ if err != nil {
+ return nil, err
+ }
+ repo.clocks[name] = c
+ result[name] = c
+ }
+ }
+
+ return result, nil
+}
+
+// GetOrCreateClock return a Lamport clock stored in the Repo.
+// If the clock doesn't exist, it's created.
+func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
+ repo.clocksMutex.Lock()
+ defer repo.clocksMutex.Unlock()
+
+ c, err := repo.getClock(name)
+ if err == nil {
+ return c, nil
+ }
+ if err != ErrClockNotExist {
+ return nil, err
+ }
+
+ c, err = lamport.NewPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
+ if err != nil {
+ return nil, err
+ }
+
+ repo.clocks[name] = c
+ return c, nil
+}
+
+func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) {
+ if c, ok := repo.clocks[name]; ok {
+ return c, nil
+ }
+
+ c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
+ if err == nil {
+ repo.clocks[name] = c
+ return c, nil
+ }
+ if err == lamport.ErrClockNotExist {
+ return nil, ErrClockNotExist
+ }
+ return nil, err
+}
+
+// Increment is equivalent to c = GetOrCreateClock(name) + c.Increment()
+func (repo *GoGitRepo) Increment(name string) (lamport.Time, error) {
+ c, err := repo.GetOrCreateClock(name)
+ if err != nil {
+ return lamport.Time(0), err
+ }
+ return c.Increment()
+}
+
+// Witness is equivalent to c = GetOrCreateClock(name) + c.Witness(time)
+func (repo *GoGitRepo) Witness(name string, time lamport.Time) error {
+ c, err := repo.GetOrCreateClock(name)
+ if err != nil {
+ return err
+ }
+ return c.Witness(time)
+}
+
+// commitToMeta converts a go-git Commit to a CommitMeta.
+func commitToMeta(c *object.Commit) CommitMeta {
+ h := Hash(c.Hash.String())
+ parents := make([]Hash, len(c.ParentHashes))
+ for i, p := range c.ParentHashes {
+ parents[i] = Hash(p.String())
+ }
+ // Use first line of message as the short message.
+ msg := strings.TrimSpace(c.Message)
+ if idx := strings.Index(msg, "\n"); idx >= 0 {
+ msg = msg[:idx]
+ }
+ return CommitMeta{
+ Hash: h,
+ Message: msg,
+ AuthorName: c.Author.Name,
+ AuthorEmail: c.Author.Email,
+ Date: c.Author.When,
+ Parents: parents,
+ }
+}
+
+// peelToCommit follows tag objects until it reaches a commit hash.
+// This is necessary for annotated tags, whose ref hash points to a tag object
+// rather than directly to a commit.
+func (repo *GoGitRepo) peelToCommit(h plumbing.Hash) (plumbing.Hash, error) {
+ for {
+ if _, err := repo.r.CommitObject(h); err == nil {
+ return h, nil
+ }
+ tagObj, err := repo.r.TagObject(h)
+ if err != nil {
+ return plumbing.ZeroHash, ErrNotFound
+ }
+ h = tagObj.Target
+ }
+}
+
+// resolveRefToHash resolves a branch/tag name or raw hash to a commit hash.
+// Resolution order: refs/heads/<ref>, refs/tags/<ref>, full ref name, raw commit hash.
+// Annotated tags are peeled to their target commit.
+func (repo *GoGitRepo) resolveRefToHash(ref string) (plumbing.Hash, error) {
+ for _, prefix := range []string{"refs/heads/", "refs/tags/"} {
+ r, err := repo.r.Reference(plumbing.ReferenceName(prefix+ref), true)
+ if err == nil {
+ return repo.peelToCommit(r.Hash())
+ }
+ }
+ // try as a full ref name
+ r, err := repo.r.Reference(plumbing.ReferenceName(ref), true)
+ if err == nil {
+ return repo.peelToCommit(r.Hash())
+ }
+ // try as a raw commit hash
+ h := plumbing.NewHash(ref)
+ if h != plumbing.ZeroHash {
+ if _, err := repo.r.CommitObject(h); err == nil {
+ return h, nil
+ }
+ }
+ return plumbing.ZeroHash, ErrNotFound
+}
+
+// defaultBranchName returns the short name of the default branch.
+func (repo *GoGitRepo) defaultBranchName() string {
+ repo.rMutex.Lock()
+ defer repo.rMutex.Unlock()
+
+ // refs/remotes/origin/HEAD is a symbolic ref set by git clone that points
+ // to the remote's default branch (e.g. refs/remotes/origin/main). It is
+ // the most reliable signal for "what does the upstream consider default".
+ ref, err := repo.r.Reference("refs/remotes/origin/HEAD", false)
+ if err == nil && ref.Type() == plumbing.SymbolicReference {
+ const prefix = "refs/remotes/origin/"
+ if target := ref.Target().String(); strings.HasPrefix(target, prefix) {
+ return strings.TrimPrefix(target, prefix)
+ }
+ }
+ // Fall back to well-known names for repos without a configured remote.
+ for _, name := range []string{"main", "master", "trunk", "develop"} {
+ _, err := repo.r.Reference(plumbing.NewBranchReferenceName(name), false)
+ if err == nil {
+ return name
+ }
+ }
+ return ""
+}
+
+// Branches returns all local branches. IsDefault marks the upstream's default
+// branch, determined in order:
+// 1. refs/remotes/origin/HEAD (set by git clone, reflects the server default)
+// 2. First match among: main, master, trunk, develop
+// 3. No branch marked if none of the above resolve
+func (repo *GoGitRepo) Branches() ([]BranchInfo, error) {
+ defaultBranch := repo.defaultBranchName()
+
repo.rMutex.Lock()
defer repo.rMutex.Unlock()
- head, err := repo.r.Head()
+ refs, err := repo.r.References()
+ if err != nil {
+ return nil, err
+ }
+
+ var branches []BranchInfo
+ err = refs.ForEach(func(r *plumbing.Reference) error {
+ if !r.Name().IsBranch() {
+ return nil
+ }
+ branches = append(branches, BranchInfo{
+ Name: r.Name().Short(),
+ Hash: Hash(r.Hash().String()),
+ IsDefault: r.Name().Short() == defaultBranch,
+ })
+ return nil
+ })
if err != nil {
- return "main", nil // sensible fallback for detached HEAD
+ return nil, err
}
- return head.Name().Short(), nil
+ if branches == nil {
+ branches = []BranchInfo{}
+ }
+ return branches, nil
}
-func (repo *GoGitRepo) ReadCommitMeta(hash Hash) (CommitMeta, error) {
+// Tags returns all tags. For annotated tags the hash is dereferenced to the
+// target commit; for lightweight tags it is the commit hash directly.
+func (repo *GoGitRepo) Tags() ([]TagInfo, error) {
repo.rMutex.Lock()
defer repo.rMutex.Unlock()
- commit, err := repo.r.CommitObject(plumbing.NewHash(hash.String()))
- if err == plumbing.ErrObjectNotFound {
- return CommitMeta{}, ErrNotFound
+ refs, err := repo.r.References()
+ if err != nil {
+ return nil, err
+ }
+
+ var tags []TagInfo
+ err = refs.ForEach(func(r *plumbing.Reference) error {
+ if !r.Name().IsTag() {
+ return nil
+ }
+ // Peel to the target commit hash, handling arbitrarily nested tag objects.
+ commit, err := repo.peelToCommit(r.Hash())
+ if err != nil {
+ // Skip refs that don't resolve to a commit (shouldn't happen for tags).
+ return nil
+ }
+ tags = append(tags, TagInfo{
+ Name: r.Name().Short(),
+ Hash: Hash(commit.String()),
+ })
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ if tags == nil {
+ tags = []TagInfo{}
+ }
+ return tags, nil
+}
+
+// TreeAtPath returns the entries of the directory at path under ref.
+func (repo *GoGitRepo) TreeAtPath(ref, path string) ([]TreeEntry, error) {
+ path = strings.Trim(path, "/")
+
+ repo.rMutex.Lock()
+ defer repo.rMutex.Unlock()
+
+ startHash, err := repo.resolveRefToHash(ref)
+ if err != nil {
+ return nil, ErrNotFound
+ }
+ commit, err := repo.r.CommitObject(startHash)
+ if err != nil {
+ return nil, err
+ }
+ tree, err := commit.Tree()
+ if err != nil {
+ return nil, err
+ }
+ if path != "" {
+ subtree, err := tree.Tree(path)
+ if err != nil {
+ return nil, ErrNotFound
+ }
+ tree = subtree
+ }
+
+ entries := make([]TreeEntry, len(tree.Entries))
+ for i, e := range tree.Entries {
+ entries[i] = TreeEntry{
+ Name: e.Name,
+ Hash: Hash(e.Hash.String()),
+ ObjectType: objectTypeFromFileMode(e.Mode),
+ }
+ }
+ return entries, nil
+}
+
+// objectTypeFromFileMode maps a go-git filemode to the repository ObjectType.
+func objectTypeFromFileMode(m filemode.FileMode) ObjectType {
+ switch m {
+ case filemode.Dir:
+ return Tree
+ case filemode.Regular:
+ return Blob
+ case filemode.Executable:
+ return Executable
+ case filemode.Symlink:
+ return Symlink
+ case filemode.Submodule:
+ return Submodule
+ default:
+ return Unknown
+ }
+}
+
+// BlobAtPath returns the content, size, and git object hash of the file at
+// path under ref. rMutex is held for the entire function, covering all
+// shared-Scanner access (CommitObject, Tree, File). The returned reader is
+// safe to use without the mutex: small blobs are already materialized into a
+// MemoryObject (bytes.Reader) by the time File() returns; large blobs come
+// back as an FSObject whose Reader() opens its own independent file handle and
+// Scanner and then reads via ReadAt — no shared state is touched after this
+// function returns. Callers must Close the reader.
+func (repo *GoGitRepo) BlobAtPath(ref, path string) (io.ReadCloser, int64, Hash, error) {
+ path = strings.Trim(path, "/")
+ if path == "" {
+ return nil, 0, "", ErrNotFound
+ }
+
+ repo.rMutex.Lock()
+ defer repo.rMutex.Unlock()
+
+ startHash, err := repo.resolveRefToHash(ref)
+ if err != nil {
+ return nil, 0, "", ErrNotFound
}
+ commit, err := repo.r.CommitObject(startHash)
+ if err != nil {
+ return nil, 0, "", err
+ }
+ tree, err := commit.Tree()
if err != nil {
- return CommitMeta{}, err
+ return nil, 0, "", err
+ }
+ f, err := tree.File(path)
+ if err != nil {
+ return nil, 0, "", ErrNotFound
+ }
+ r, err := f.Reader()
+ if err != nil {
+ return nil, 0, "", err
}
- return commitToMeta(commit), nil
+ return r, f.Blob.Size, Hash(f.Blob.Hash.String()), nil
}
-func (repo *GoGitRepo) CommitLog(ref string, path string, limit int, after Hash) ([]CommitMeta, error) {
+// CommitLog returns at most limit commits reachable from ref, optionally
+// filtered to those that touched path, starting after the given cursor hash,
+// and bounded by the since/until author-date range.
+func (repo *GoGitRepo) CommitLog(ref, path string, limit int, after Hash, since, until *time.Time) ([]CommitMeta, error) {
repo.rMutex.Lock()
defer repo.rMutex.Unlock()
- h, err := repo.resolveShortRef(ref)
+ startHash, err := repo.resolveRefToHash(ref)
if err != nil {
return nil, err
}
- opts := &gogit.LogOptions{From: h}
+ // Normalize path: strip leading/trailing slashes so prefix matching works.
+ path = strings.Trim(path, "/")
+
+ opts := &gogit.LogOptions{
+ From: startHash,
+ Order: gogit.LogOrderCommitterTime,
+ }
if path != "" {
opts.PathFilter = func(p string) bool {
return p == path || strings.HasPrefix(p, path+"/")
}
- // PathFilter requires OrderCommitterTime for correct results
- opts.Order = gogit.LogOrderCommitterTime
}
iter, err := repo.r.Log(opts)
@@ -880,185 +1230,207 @@ func (repo *GoGitRepo) CommitLog(ref string, path string, limit int, after Hash)
}
defer iter.Close()
- var commits []CommitMeta
+ var result []CommitMeta
skipping := after != ""
-
for {
- if len(commits) >= limit {
- break
- }
- commit, err := iter.Next()
+ c, err := iter.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
+ h := Hash(c.Hash.String())
if skipping {
- if Hash(commit.Hash.String()) == after {
+ if h == after {
skipping = false
}
continue
}
- commits = append(commits, commitToMeta(commit))
- }
-
- return commits, nil
-}
-
-// resolveShortRef resolves a short branch/tag name or full ref to a commit hash.
-// Must be called with rMutex held.
-func (repo *GoGitRepo) resolveShortRef(ref string) (plumbing.Hash, error) {
- // Try as full ref first, then refs/heads/, refs/tags/, then raw hash.
- for _, prefix := range []string{"", "refs/heads/", "refs/tags/"} {
- r, err := repo.r.Reference(plumbing.ReferenceName(prefix+ref), true)
- if err == nil {
- return r.Hash(), nil
+ if since != nil && c.Author.When.Before(*since) {
+ continue
+ }
+ if until != nil && c.Author.When.After(*until) {
+ continue
+ }
+ result = append(result, commitToMeta(c))
+ if limit > 0 && len(result) >= limit {
+ break
}
}
- // Fall back to treating it as a commit hash directly.
- h := plumbing.NewHash(ref)
- if !h.IsZero() {
- return h, nil
- }
- return plumbing.ZeroHash, fmt.Errorf("cannot resolve ref %q", ref)
+ return result, nil
}
-func commitToMeta(c *object.Commit) CommitMeta {
- msg := strings.TrimSpace(c.Message)
- if i := strings.IndexByte(msg, '\n'); i >= 0 {
- msg = msg[:i]
+// treeEntriesAtPath returns the tree hash and a name→entry-hash map for the
+// directory at dirPath inside the given commit. An empty dirPath means the
+// root tree. The tree hash is content-addressed and can be used as a stable
+// cache key regardless of which branch or ref was resolved.
+func treeEntriesAtPath(c *object.Commit, dirPath string) (plumbing.Hash, map[string]plumbing.Hash, error) {
+ tree, err := c.Tree()
+ if err != nil {
+ return plumbing.ZeroHash, nil, err
}
- parents := make([]Hash, len(c.ParentHashes))
- for i, p := range c.ParentHashes {
- parents[i] = Hash(p.String())
+ if dirPath != "" {
+ subtree, err := tree.Tree(dirPath)
+ if err != nil {
+ return plumbing.ZeroHash, nil, err
+ }
+ tree = subtree
}
- h := Hash(c.Hash.String())
- return CommitMeta{
- Hash: h,
- ShortHash: h.String()[:7],
- Message: msg,
- AuthorName: c.Author.Name,
- AuthorEmail: c.Author.Email,
- Date: c.Author.When,
- Parents: parents,
+ result := make(map[string]plumbing.Hash, len(tree.Entries))
+ for _, e := range tree.Entries {
+ result[e.Name] = e.Hash
}
+ return tree.Hash, result, nil
}
-// LastCommitForEntries walks the commit history once (newest-first) and returns
-// the most recent commit that modified each named entry in dirPath.
+// LastCommitForEntries performs a single history walk to find, for each name,
+// the most recent commit that changed that entry in the directory at path.
//
-// Instead of computing recursive tree diffs, it reads only the shallow tree at
-// dirPath for consecutive commits and compares entry hashes directly. This is
-// O(commits × entries) with cheap hash comparisons rather than O(commits × all
-// changed files in repo).
-func (repo *GoGitRepo) LastCommitForEntries(ref string, dirPath string, names []string) (map[string]CommitMeta, error) {
+// Results are cached by (dirTreeHash, path). Because git trees are
+// content-addressed, two refs that point to the same directory tree share one
+// cache entry, and the cache never needs invalidation: a changed directory
+// produces a new tree hash, which becomes a new key.
+func (repo *GoGitRepo) LastCommitForEntries(ref, path string, names []string) (map[string]CommitMeta, error) {
+ // Normalize path up front so the cache key is canonical.
+ path = strings.Trim(path, "/")
+
+ // Resolve ref and load the current directory tree in one brief lock.
+ // We need the tree hash for the cache key and we keep the entries to
+ // seed the parent-reuse optimisation in the walk below.
repo.rMutex.Lock()
- defer repo.rMutex.Unlock()
-
- h, err := repo.resolveShortRef(ref)
+ startHash, err := repo.resolveRefToHash(ref)
+ if err != nil {
+ repo.rMutex.Unlock()
+ return nil, err
+ }
+ startCommit, err := repo.r.CommitObject(startHash)
if err != nil {
+ repo.rMutex.Unlock()
return nil, err
}
+ treeHash, startEntries, err := treeEntriesAtPath(startCommit, path)
+ repo.rMutex.Unlock()
+ if err != nil {
+ // path doesn't exist at HEAD — nothing to return.
+ return map[string]CommitMeta{}, nil
+ }
+
+ // The cache is keyed by the directory's tree hash (content-addressed)
+ // plus the path so two directories with identical content but different
+ // locations don't collide.
+ cacheKey := treeHash.String() + "\x00" + path
- result := make(map[string]CommitMeta, len(names))
- if len(names) == 0 {
+ // Cache hit: filter the stored result down to the requested names.
+ if cached, ok := repo.lastCommitCache.Get(cacheKey); ok {
+ result := make(map[string]CommitMeta, len(names))
+ for _, n := range names {
+ if m, found := cached[n]; found {
+ result[n] = m
+ }
+ }
return result, nil
}
- // Build lookup set for fast membership test.
- want := make(map[string]bool, len(names))
- for _, n := range names {
- want[n] = true
+ // Cache miss: walk history for ALL entries in this directory so the
+ // cached result is complete and valid for any future name subset.
+ remaining := make(map[string]bool, len(startEntries))
+ for name := range startEntries {
+ remaining[name] = true
}
+ result := make(map[string]CommitMeta, len(remaining))
+
+ repo.rMutex.Lock()
- iter, err := repo.r.Log(&gogit.LogOptions{From: h, Order: gogit.LogOrderCommitterTime})
+ iter, err := repo.r.Log(&gogit.LogOptions{
+ From: startHash,
+ Order: gogit.LogOrderCommitterTime,
+ })
if err != nil {
+ repo.rMutex.Unlock()
return nil, err
}
- defer iter.Close()
-
- // dirHashes reads the entry hashes at dirPath for the given commit tree.
- // Returns a map of entry name → blob/tree hash (shallow, no recursion).
- dirHashes := func(tree *object.Tree) map[string]plumbing.Hash {
- t := tree
- if dirPath != "" {
- sub, err := tree.Tree(dirPath)
- if err != nil {
- return nil
- }
- t = sub
- }
- m := make(map[string]plumbing.Hash, len(t.Entries))
- for _, e := range t.Entries {
- if want[e.Name] {
- m[e.Name] = e.Hash
- }
- }
- return m
- }
- // Walk newest→oldest, comparing each commit's directory snapshot with the
- // previous (newer) commit's snapshot. When a hash differs, the newer commit
- // is the one that last changed that entry.
- var prevHashes map[string]plumbing.Hash
- var prevMeta CommitMeta
+ // Seed the parent-reuse cache with the entries we already fetched above
+ // so the first iteration's current-tree read is skipped for free.
+ // In a linear history this halves tree reads for every subsequent step:
+ // the parent fetched at depth D is the current commit at depth D+1.
+ cachedParentHash := startHash
+ cachedParentEntries := startEntries
- for len(result) < len(names) {
- commit, err := iter.Next()
+ for depth := 0; len(remaining) > 0 && depth < lastCommitDepthLimit; depth++ {
+ c, err := iter.Next()
if err == io.EOF {
break
}
if err != nil {
- return result, nil
+ iter.Close()
+ repo.rMutex.Unlock()
+ return nil, err
}
- tree, err := commit.Tree()
- if err != nil {
- continue
+ var currentEntries map[string]plumbing.Hash
+ if c.Hash == cachedParentHash && cachedParentEntries != nil {
+ currentEntries = cachedParentEntries
+ } else {
+ _, currentEntries, err = treeEntriesAtPath(c, path)
+ if err != nil {
+ // path may not exist in this commit; treat as empty
+ currentEntries = map[string]plumbing.Hash{}
+ }
}
- currHashes := dirHashes(tree)
- meta := commitToMeta(commit)
- if prevHashes != nil {
- for name := range want {
- if _, done := result[name]; done {
- continue
- }
- prev, inPrev := prevHashes[name]
- curr, inCurr := currHashes[name]
- // If the entry existed in prevHashes but differs (or is gone now),
- // the previous (newer) commit is when it was last changed.
- if inPrev && (!inCurr || prev != curr) {
- result[name] = prevMeta
- }
+ var parentEntries map[string]plumbing.Hash
+ cachedParentHash = plumbing.ZeroHash
+ cachedParentEntries = nil
+ if len(c.ParentHashes) > 0 {
+ if parent, err := c.Parents().Next(); err == nil {
+ _, parentEntries, _ = treeEntriesAtPath(parent, path)
+ cachedParentHash = c.ParentHashes[0]
+ cachedParentEntries = parentEntries
+ }
+ }
+
+ meta := commitToMeta(c)
+ for name := range remaining {
+ curHash, inCurrent := currentEntries[name]
+ parentHash, inParent := parentEntries[name]
+ if inCurrent != inParent || (inCurrent && curHash != parentHash) {
+ result[name] = meta
+ delete(remaining, name)
}
}
+ }
- prevHashes = currHashes
- prevMeta = meta
+ iter.Close()
+ repo.rMutex.Unlock()
+
+ // Store a defensive copy so that callers cannot mutate cached entries.
+ // The cached map contains all directory entries, not just the requested
+ // names, so future calls for the same directory are fully served from
+ // cache regardless of which names they request.
+ cached := make(map[string]CommitMeta, len(result))
+ for k, v := range result {
+ cached[k] = v
}
+ repo.lastCommitCache.Add(cacheKey, cached)
- // Any names still present in prevHashes were last changed at the oldest
- // commit we reached (the entry existed there and we never saw it change).
- for name := range want {
- if _, done := result[name]; done {
- continue
- }
- if _, exists := prevHashes[name]; exists {
- result[name] = prevMeta
+ // Return only the entries that were requested.
+ filtered := make(map[string]CommitMeta, len(names))
+ for _, n := range names {
+ if m, ok := result[n]; ok {
+ filtered[n] = m
}
}
-
- return result, nil
+ return filtered, nil
}
-// CommitDetail returns full metadata for a commit plus its changed files.
+// CommitDetail returns the full commit metadata and list of changed files.
func (repo *GoGitRepo) CommitDetail(hash Hash) (CommitDetail, error) {
repo.rMutex.Lock()
defer repo.rMutex.Unlock()
- commit, err := repo.r.CommitObject(plumbing.NewHash(hash.String()))
+ c, err := repo.r.CommitObject(plumbing.NewHash(hash.String()))
if err == plumbing.ErrObjectNotFound {
return CommitDetail{}, ErrNotFound
}
@@ -1066,56 +1438,63 @@ func (repo *GoGitRepo) CommitDetail(hash Hash) (CommitDetail, error) {
return CommitDetail{}, err
}
- detail := CommitDetail{
- CommitMeta: commitToMeta(commit),
- FullMessage: strings.TrimSpace(commit.Message),
- }
-
- tree, err := commit.Tree()
+ toTree, err := c.Tree()
if err != nil {
- return detail, nil
+ return CommitDetail{}, err
}
- var parentTree *object.Tree
- if len(commit.ParentHashes) > 0 {
- if parent, err := commit.Parent(0); err == nil {
- parentTree, _ = parent.Tree()
+ var fromTree *object.Tree
+ if len(c.ParentHashes) > 0 {
+ parent, err := repo.r.CommitObject(c.ParentHashes[0])
+ if err != nil {
+ return CommitDetail{}, fmt.Errorf("loading parent commit: %w", err)
+ }
+ fromTree, err = parent.Tree()
+ if err != nil {
+ return CommitDetail{}, fmt.Errorf("loading parent tree: %w", err)
}
- }
- if parentTree == nil {
- parentTree = &object.Tree{}
}
- changes, err := object.DiffTree(parentTree, tree)
+ changes, err := object.DiffTree(fromTree, toTree)
if err != nil {
- return detail, nil
+ return CommitDetail{}, err
}
- for _, change := range changes {
- from, to := change.From.Name, change.To.Name
- var f ChangedFile
- switch {
- case from == "":
- f = ChangedFile{Path: to, Status: "added"}
- case to == "":
- f = ChangedFile{Path: from, Status: "deleted"}
- case from != to:
- f = ChangedFile{Path: to, OldPath: from, Status: "renamed"}
- default:
- f = ChangedFile{Path: to, Status: "modified"}
- }
- detail.Files = append(detail.Files, f)
+ // Use ch.From.Name / ch.To.Name directly — these come from the tree
+ // metadata and do not require reading any blob content.
+ files := make([]ChangedFile, 0, len(changes))
+ for _, ch := range changes {
+ files = append(files, changedFileFromChange(ch.From.Name, ch.To.Name))
}
- return detail, nil
+ return CommitDetail{
+ CommitMeta: commitToMeta(c),
+ FullMessage: c.Message,
+ Files: files,
+ }, nil
}
-// CommitFileDiff returns the structured diff for a single file in a commit.
+func changedFileFromChange(fromName, toName string) ChangedFile {
+ switch {
+ case fromName == "":
+ return ChangedFile{Path: toName, Status: ChangeStatusAdded}
+ case toName == "":
+ return ChangedFile{Path: fromName, Status: ChangeStatusDeleted}
+ case fromName != toName:
+ op := fromName
+ return ChangedFile{Path: toName, OldPath: &op, Status: ChangeStatusRenamed}
+ default:
+ return ChangedFile{Path: toName, Status: ChangeStatusModified}
+ }
+}
+
+// CommitFileDiff returns the unified diff for a single file in a commit,
+// relative to the first parent.
func (repo *GoGitRepo) CommitFileDiff(hash Hash, filePath string) (FileDiff, error) {
repo.rMutex.Lock()
defer repo.rMutex.Unlock()
- commit, err := repo.r.CommitObject(plumbing.NewHash(hash.String()))
+ c, err := repo.r.CommitObject(plumbing.NewHash(hash.String()))
if err == plumbing.ErrObjectNotFound {
return FileDiff{}, ErrNotFound
}
@@ -1123,250 +1502,175 @@ func (repo *GoGitRepo) CommitFileDiff(hash Hash, filePath string) (FileDiff, err
return FileDiff{}, err
}
- tree, err := commit.Tree()
+ toTree, err := c.Tree()
if err != nil {
return FileDiff{}, err
}
- var parentTree *object.Tree
- if len(commit.ParentHashes) > 0 {
- if parent, err := commit.Parent(0); err == nil {
- parentTree, _ = parent.Tree()
+ var fromTree *object.Tree
+ if len(c.ParentHashes) > 0 {
+ parent, err := repo.r.CommitObject(c.ParentHashes[0])
+ if err != nil {
+ return FileDiff{}, fmt.Errorf("loading parent commit: %w", err)
+ }
+ fromTree, err = parent.Tree()
+ if err != nil {
+ return FileDiff{}, fmt.Errorf("loading parent tree: %w", err)
}
- }
- if parentTree == nil {
- parentTree = &object.Tree{}
}
- changes, err := object.DiffTree(parentTree, tree)
+ changes, err := object.DiffTree(fromTree, toTree)
if err != nil {
return FileDiff{}, err
}
- for _, change := range changes {
- from, to := change.From.Name, change.To.Name
- if to != filePath && from != filePath {
+ for _, ch := range changes {
+ name := ch.To.Name
+ if name == "" {
+ name = ch.From.Name
+ }
+ // match on either new or old path
+ if name != filePath && ch.From.Name != filePath {
continue
}
- patch, err := change.Patch()
+ from, to, err := ch.Files()
if err != nil {
return FileDiff{}, err
}
- fps := patch.FilePatches()
- if len(fps) == 0 {
- return FileDiff{}, ErrNotFound
+ patch, err := ch.Patch()
+ if err != nil {
+ return FileDiff{}, err
}
- fp := fps[0]
- fromFile, toFile := fp.Files()
fd := FileDiff{
- IsBinary: fp.IsBinary(),
- IsNew: fromFile == nil,
- IsDelete: toFile == nil,
+ IsNew: from == nil,
+ IsDelete: to == nil,
}
- if toFile != nil {
- fd.Path = toFile.Path()
- } else if fromFile != nil {
- fd.Path = fromFile.Path()
+ if to != nil {
+ fd.Path = to.Name
}
- if fromFile != nil && toFile != nil && fromFile.Path() != toFile.Path() {
- fd.OldPath = fromFile.Path()
+ if from != nil {
+ if fd.Path == "" {
+ fd.Path = from.Name
+ } else if from.Name != fd.Path {
+ op := from.Name
+ fd.OldPath = &op
+ }
}
- if !fd.IsBinary {
- fd.Hunks = buildDiffHunks(fp.Chunks())
+ fps := patch.FilePatches()
+ if len(fps) > 0 {
+ fp := fps[0]
+ fd.IsBinary = fp.IsBinary()
+ if !fd.IsBinary {
+ fd.Hunks = buildDiffHunks(fp)
+ }
}
return fd, nil
}
-
return FileDiff{}, ErrNotFound
}
-// buildDiffHunks converts go-git diff chunks into DiffHunks with context lines.
-func buildDiffHunks(chunks []diff.Chunk) []DiffHunk {
- const ctx = 3
-
- type line struct {
- op diff.Operation
+// buildDiffHunks converts a go-git FilePatch into DiffHunks with line numbers
+// and context grouping.
+func buildDiffHunks(fp fdiff.FilePatch) []DiffHunk {
+ type pendingLine struct {
+ typ DiffLineType
content string
oldLine int
newLine int
}
- // Expand chunks into individual lines.
- var lines []line
- oldN, newN := 1, 1
- for _, chunk := range chunks {
- parts := strings.Split(chunk.Content(), "\n")
- // Split always produces a trailing empty element if content ends with \n.
- if len(parts) > 0 && parts[len(parts)-1] == "" {
- parts = parts[:len(parts)-1]
- }
- for _, p := range parts {
- l := line{op: chunk.Type(), content: p}
- switch chunk.Type() {
- case diff.Equal:
- l.oldLine, l.newLine = oldN, newN
- oldN++
- newN++
- case diff.Add:
- l.newLine = newN
- newN++
- case diff.Delete:
- l.oldLine = oldN
- oldN++
- }
- lines = append(lines, l)
- }
- }
-
- // Collect indices of changed lines.
- var changed []int
- for i, l := range lines {
- if l.op != diff.Equal {
- changed = append(changed, i)
+ var allLines []pendingLine
+ oldLine, newLine := 1, 1
+ for _, chunk := range fp.Chunks() {
+ lines := strings.Split(chunk.Content(), "\n")
+ // strip trailing empty element produced by a trailing newline
+ if len(lines) > 0 && lines[len(lines)-1] == "" {
+ lines = lines[:len(lines)-1]
}
- }
- if len(changed) == 0 {
- return nil
- }
-
- // Merge overlapping/adjacent change windows into hunk ranges.
- type hunkRange struct{ start, end int }
- var ranges []hunkRange
- i := 0
- for i < len(changed) {
- start := max(0, changed[i]-ctx)
- end := changed[i]
- j := i
- for j < len(changed) && changed[j] <= end+ctx {
- end = changed[j]
- j++
- }
- end = min(len(lines)-1, end+ctx)
- ranges = append(ranges, hunkRange{start, end})
- i = j
- }
-
- // Build DiffHunks from ranges.
- hunks := make([]DiffHunk, 0, len(ranges))
- for _, r := range ranges {
- hunk := DiffHunk{}
- for _, l := range lines[r.start : r.end+1] {
- if hunk.OldStart == 0 && l.oldLine > 0 {
- hunk.OldStart = l.oldLine
+ switch chunk.Type() {
+ case fdiff.Equal:
+ for _, l := range lines {
+ allLines = append(allLines, pendingLine{DiffLineContext, l, oldLine, newLine})
+ oldLine++
+ newLine++
}
- if hunk.NewStart == 0 && l.newLine > 0 {
- hunk.NewStart = l.newLine
+ case fdiff.Add:
+ for _, l := range lines {
+ allLines = append(allLines, pendingLine{DiffLineAdded, l, 0, newLine})
+ newLine++
}
- dl := DiffLine{Content: l.content, OldLine: l.oldLine, NewLine: l.newLine}
- switch l.op {
- case diff.Equal:
- dl.Type = "context"
- hunk.OldLines++
- hunk.NewLines++
- case diff.Add:
- dl.Type = "added"
- hunk.NewLines++
- case diff.Delete:
- dl.Type = "deleted"
- hunk.OldLines++
+ case fdiff.Delete:
+ for _, l := range lines {
+ allLines = append(allLines, pendingLine{DiffLineDeleted, l, oldLine, 0})
+ oldLine++
}
- hunk.Lines = append(hunk.Lines, dl)
}
- hunks = append(hunks, hunk)
- }
- return hunks
-}
-
-func (repo *GoGitRepo) AllClocks() (map[string]lamport.Clock, error) {
- repo.clocksMutex.Lock()
- defer repo.clocksMutex.Unlock()
-
- result := make(map[string]lamport.Clock)
-
- files, err := os.ReadDir(filepath.Join(repo.localStorage.Root(), clockPath))
- if os.IsNotExist(err) {
- return nil, nil
}
- if err != nil {
- return nil, err
+ if len(allLines) == 0 {
+ return nil
}
- for _, file := range files {
- name := file.Name()
- if c, ok := repo.clocks[name]; ok {
- result[name] = c
+ const ctx = 3 // context lines around each changed block
+
+ // find spans of changed lines
+ type span struct{ start, end int }
+ var spans []span
+ for i, l := range allLines {
+ if l.typ == DiffLineContext {
+ continue
+ }
+ if len(spans) == 0 || i > spans[len(spans)-1].end+1 {
+ spans = append(spans, span{i, i})
} else {
- c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
- if err != nil {
- return nil, err
- }
- repo.clocks[name] = c
- result[name] = c
+ spans[len(spans)-1].end = i
}
}
- return result, nil
-}
-
-// GetOrCreateClock return a Lamport clock stored in the Repo.
-// If the clock doesn't exist, it's created.
-func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
- repo.clocksMutex.Lock()
- defer repo.clocksMutex.Unlock()
-
- c, err := repo.getClock(name)
- if err == nil {
- return c, nil
- }
- if err != ErrClockNotExist {
- return nil, err
- }
-
- c, err = lamport.NewPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
- if err != nil {
- return nil, err
- }
-
- repo.clocks[name] = c
- return c, nil
-}
-
-func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) {
- if c, ok := repo.clocks[name]; ok {
- return c, nil
- }
-
- c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
- if err == nil {
- repo.clocks[name] = c
- return c, nil
- }
- if err == lamport.ErrClockNotExist {
- return nil, ErrClockNotExist
- }
- return nil, err
-}
-
-// Increment is equivalent to c = GetOrCreateClock(name) + c.Increment()
-func (repo *GoGitRepo) Increment(name string) (lamport.Time, error) {
- c, err := repo.GetOrCreateClock(name)
- if err != nil {
- return lamport.Time(0), err
+ // expand each span by ctx lines and merge overlapping ones
+ var merged []span
+ for _, s := range spans {
+ s.start = max(0, s.start-ctx)
+ s.end = min(len(allLines)-1, s.end+ctx)
+ if len(merged) > 0 && s.start <= merged[len(merged)-1].end+1 {
+ merged[len(merged)-1].end = s.end
+ } else {
+ merged = append(merged, s)
+ }
}
- return c.Increment()
-}
-// Witness is equivalent to c = GetOrCreateClock(name) + c.Witness(time)
-func (repo *GoGitRepo) Witness(name string, time lamport.Time) error {
- c, err := repo.GetOrCreateClock(name)
- if err != nil {
- return err
+ hunks := make([]DiffHunk, 0, len(merged))
+ for _, s := range merged {
+ segment := allLines[s.start : s.end+1]
+ dl := make([]DiffLine, len(segment))
+ var oldStart, newStart, oldCount, newCount int
+ for i, l := range segment {
+ dl[i] = DiffLine{Type: l.typ, Content: l.content, OldLine: l.oldLine, NewLine: l.newLine}
+ if l.oldLine > 0 {
+ if oldStart == 0 {
+ oldStart = l.oldLine
+ }
+ oldCount++
+ }
+ if l.newLine > 0 {
+ if newStart == 0 {
+ newStart = l.newLine
+ }
+ newCount++
+ }
+ }
+ hunks = append(hunks, DiffHunk{
+ OldStart: oldStart,
+ OldLines: oldCount,
+ NewStart: newStart,
+ NewLines: newCount,
+ Lines: dl,
+ })
}
- return c.Witness(time)
+ return hunks
}
// AddRemote add a new remote to the repository
@@ -4,8 +4,10 @@ import (
"bytes"
"crypto/sha1"
"fmt"
+ "io"
"strings"
"sync"
+ "time"
"github.com/99designs/keyring"
"github.com/ProtonMail/go-crypto/openpgp"
@@ -24,7 +26,7 @@ type mockRepo struct {
*mockRepoCommon
*mockRepoStorage
*mockRepoIndex
- *mockRepoData
+ *mockRepoDataBrowse
*mockRepoClock
*mockRepoTest
}
@@ -33,14 +35,14 @@ func (m *mockRepo) Close() error { return nil }
func NewMockRepo() *mockRepo {
return &mockRepo{
- mockRepoConfig: NewMockRepoConfig(),
- mockRepoKeyring: NewMockRepoKeyring(),
- mockRepoCommon: NewMockRepoCommon(),
- mockRepoStorage: NewMockRepoStorage(),
- mockRepoIndex: newMockRepoIndex(),
- mockRepoData: NewMockRepoData(),
- mockRepoClock: NewMockRepoClock(),
- mockRepoTest: NewMockRepoTest(),
+ mockRepoConfig: NewMockRepoConfig(),
+ mockRepoKeyring: NewMockRepoKeyring(),
+ mockRepoCommon: NewMockRepoCommon(),
+ mockRepoStorage: NewMockRepoStorage(),
+ mockRepoIndex: newMockRepoIndex(),
+ mockRepoDataBrowse: newMockRepoDataBrowse(),
+ mockRepoClock: NewMockRepoClock(),
+ mockRepoTest: NewMockRepoTest(),
}
}
@@ -119,10 +121,6 @@ func (r *mockRepoCommon) GetRemotes() (map[string]string, error) {
}, nil
}
-// GetPath returns an empty string for in-memory mock repos.
-func (r *mockRepoCommon) GetPath() string {
- return ""
-}
var _ RepoStorage = &mockRepoStorage{}
@@ -224,47 +222,51 @@ func (m *mockIndex) Close() error {
return nil
}
-var _ RepoData = &mockRepoData{}
+var _ RepoData = &mockRepoDataBrowse{}
type commit struct {
treeHash Hash
parents []Hash
sig string
+ date time.Time
+ message string
}
-type mockRepoData struct {
- blobs map[Hash][]byte
- trees map[Hash]string
- commits map[Hash]commit
- refs map[string]Hash
+type mockRepoDataBrowse struct {
+ blobs map[Hash][]byte
+ trees map[Hash]string
+ commits map[Hash]commit
+ refs map[string]Hash
+ defaultBranch string
}
-func NewMockRepoData() *mockRepoData {
- return &mockRepoData{
- blobs: make(map[Hash][]byte),
- trees: make(map[Hash]string),
- commits: make(map[Hash]commit),
- refs: make(map[string]Hash),
+func newMockRepoDataBrowse() *mockRepoDataBrowse {
+ return &mockRepoDataBrowse{
+ blobs: make(map[Hash][]byte),
+ trees: make(map[Hash]string),
+ commits: make(map[Hash]commit),
+ refs: make(map[string]Hash),
+ defaultBranch: "main",
}
}
-func (r *mockRepoData) FetchRefs(remote string, prefixes ...string) (string, error) {
+func (r *mockRepoDataBrowse) FetchRefs(remote string, prefixes ...string) (string, error) {
panic("implement me")
}
// PushRefs push git refs to a remote
-func (r *mockRepoData) PushRefs(remote string, prefixes ...string) (string, error) {
+func (r *mockRepoDataBrowse) PushRefs(remote string, prefixes ...string) (string, error) {
panic("implement me")
}
-func (r *mockRepoData) StoreData(data []byte) (Hash, error) {
+func (r *mockRepoDataBrowse) StoreData(data []byte) (Hash, error) {
rawHash := sha1.Sum(data)
hash := Hash(fmt.Sprintf("%x", rawHash))
r.blobs[hash] = data
return hash, nil
}
-func (r *mockRepoData) ReadData(hash Hash) ([]byte, error) {
+func (r *mockRepoDataBrowse) ReadData(hash Hash) ([]byte, error) {
data, ok := r.blobs[hash]
if !ok {
return nil, ErrNotFound
@@ -273,7 +275,7 @@ func (r *mockRepoData) ReadData(hash Hash) ([]byte, error) {
return data, nil
}
-func (r *mockRepoData) StoreTree(entries []TreeEntry) (Hash, error) {
+func (r *mockRepoDataBrowse) StoreTree(entries []TreeEntry) (Hash, error) {
buffer := prepareTreeEntries(entries)
rawHash := sha1.Sum(buffer.Bytes())
hash := Hash(fmt.Sprintf("%x", rawHash))
@@ -282,7 +284,7 @@ func (r *mockRepoData) StoreTree(entries []TreeEntry) (Hash, error) {
return hash, nil
}
-func (r *mockRepoData) ReadTree(hash Hash) ([]TreeEntry, error) {
+func (r *mockRepoDataBrowse) ReadTree(hash Hash) ([]TreeEntry, error) {
var data string
data, ok := r.trees[hash]
@@ -305,11 +307,11 @@ func (r *mockRepoData) ReadTree(hash Hash) ([]TreeEntry, error) {
return readTreeEntries(data)
}
-func (r *mockRepoData) StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) {
+func (r *mockRepoDataBrowse) StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) {
return r.StoreSignedCommit(treeHash, nil, parents...)
}
-func (r *mockRepoData) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) {
+func (r *mockRepoDataBrowse) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) {
hasher := sha1.New()
hasher.Write([]byte(treeHash))
for _, parent := range parents {
@@ -320,6 +322,7 @@ func (r *mockRepoData) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity,
c := commit{
treeHash: treeHash,
parents: parents,
+ date: time.Now(),
}
if signKey != nil {
// unlike go-git, we only sign the tree hash for simplicity instead of all the fields (parents ...)
@@ -333,7 +336,7 @@ func (r *mockRepoData) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity,
return hash, nil
}
-func (r *mockRepoData) ReadCommit(hash Hash) (Commit, error) {
+func (r *mockRepoDataBrowse) ReadCommit(hash Hash) (Commit, error) {
c, ok := r.commits[hash]
if !ok {
return Commit{}, ErrNotFound
@@ -355,7 +358,7 @@ func (r *mockRepoData) ReadCommit(hash Hash) (Commit, error) {
return result, nil
}
-func (r *mockRepoData) ResolveRef(ref string) (Hash, error) {
+func (r *mockRepoDataBrowse) ResolveRef(ref string) (Hash, error) {
h, ok := r.refs[ref]
if !ok {
return "", ErrNotFound
@@ -363,17 +366,17 @@ func (r *mockRepoData) ResolveRef(ref string) (Hash, error) {
return h, nil
}
-func (r *mockRepoData) UpdateRef(ref string, hash Hash) error {
+func (r *mockRepoDataBrowse) UpdateRef(ref string, hash Hash) error {
r.refs[ref] = hash
return nil
}
-func (r *mockRepoData) RemoveRef(ref string) error {
+func (r *mockRepoDataBrowse) RemoveRef(ref string) error {
delete(r.refs, ref)
return nil
}
-func (r *mockRepoData) ListRefs(refPrefix string) ([]string, error) {
+func (r *mockRepoDataBrowse) ListRefs(refPrefix string) ([]string, error) {
var keys []string
for k := range r.refs {
@@ -385,12 +388,12 @@ func (r *mockRepoData) ListRefs(refPrefix string) ([]string, error) {
return keys, nil
}
-func (r *mockRepoData) RefExist(ref string) (bool, error) {
+func (r *mockRepoDataBrowse) RefExist(ref string) (bool, error) {
_, exist := r.refs[ref]
return exist, nil
}
-func (r *mockRepoData) CopyRef(source string, dest string) error {
+func (r *mockRepoDataBrowse) CopyRef(source string, dest string) error {
hash, exist := r.refs[source]
if !exist {
@@ -401,10 +404,446 @@ func (r *mockRepoData) CopyRef(source string, dest string) error {
return nil
}
-func (r *mockRepoData) ListCommits(ref string) ([]Hash, error) {
+func (r *mockRepoDataBrowse) ListCommits(ref string) ([]Hash, error) {
return nonNativeListCommits(r, ref)
}
+// resolveRef resolves a ref matching the RepoBrowse contract:
+// refs/heads/<ref>, refs/tags/<ref>, full ref name, raw commit hash.
+func (r *mockRepoDataBrowse) resolveRef(ref string) (Hash, error) {
+ for _, candidate := range []string{"refs/heads/" + ref, "refs/tags/" + ref, ref} {
+ if h, ok := r.refs[candidate]; ok {
+ return h, nil
+ }
+ }
+ if _, ok := r.commits[Hash(ref)]; ok {
+ return Hash(ref), nil
+ }
+ return "", ErrNotFound
+}
+
+// treeEntriesAtHash parses the entries of the tree stored under hash.
+func (r *mockRepoDataBrowse) treeEntriesAtHash(hash Hash) ([]TreeEntry, error) {
+ data, ok := r.trees[hash]
+ if !ok {
+ return nil, ErrNotFound
+ }
+ return readTreeEntries(data)
+}
+
+// treeEntriesAt returns the directory entries at path inside the tree rooted at
+// treeHash. path="" returns root entries. Returns ErrNotFound if path doesn't
+// exist or resolves to a blob rather than a tree.
+func (r *mockRepoDataBrowse) treeEntriesAt(treeHash Hash, path string) ([]TreeEntry, error) {
+ path = strings.Trim(path, "/")
+ if path == "" {
+ return r.treeEntriesAtHash(treeHash)
+ }
+ seg, rest, _ := strings.Cut(path, "/")
+ entries, err := r.treeEntriesAtHash(treeHash)
+ if err != nil {
+ return nil, err
+ }
+ for _, e := range entries {
+ if e.Name != seg || e.ObjectType != Tree {
+ continue
+ }
+ if rest == "" {
+ return r.treeEntriesAtHash(e.Hash)
+ }
+ return r.treeEntriesAt(e.Hash, rest)
+ }
+ return nil, ErrNotFound
+}
+
+// blobHashAt walks the tree to find the blob hash for the file at path.
+func (r *mockRepoDataBrowse) blobHashAt(treeHash Hash, path string) (Hash, error) {
+ path = strings.Trim(path, "/")
+ seg, rest, hasRest := strings.Cut(path, "/")
+ entries, err := r.treeEntriesAtHash(treeHash)
+ if err != nil {
+ return "", err
+ }
+ for _, e := range entries {
+ if e.Name != seg {
+ continue
+ }
+ if !hasRest {
+ return e.Hash, nil
+ }
+ if e.ObjectType != Tree {
+ return "", ErrNotFound
+ }
+ return r.blobHashAt(e.Hash, rest)
+ }
+ return "", ErrNotFound
+}
+
+// diffTrees returns the changed files between two trees, recursing into
+// sub-trees. fromHash=="" means an empty (non-existent) tree.
+func (r *mockRepoDataBrowse) diffTrees(fromHash, toHash Hash, prefix string) []ChangedFile {
+ var fromEntries, toEntries []TreeEntry
+ if fromHash != "" {
+ fromEntries, _ = r.treeEntriesAtHash(fromHash)
+ }
+ if toHash != "" {
+ toEntries, _ = r.treeEntriesAtHash(toHash)
+ }
+
+ fromMap := make(map[string]TreeEntry, len(fromEntries))
+ for _, e := range fromEntries {
+ fromMap[e.Name] = e
+ }
+ toMap := make(map[string]TreeEntry, len(toEntries))
+ for _, e := range toEntries {
+ toMap[e.Name] = e
+ }
+
+ var result []ChangedFile
+ for _, e := range toEntries {
+ path := prefix + e.Name
+ f, existed := fromMap[e.Name]
+ if e.ObjectType == Tree {
+ var sub Hash
+ if existed {
+ sub = f.Hash
+ }
+ result = append(result, r.diffTrees(sub, e.Hash, path+"/")...)
+ } else if !existed {
+ result = append(result, ChangedFile{Path: path, Status: ChangeStatusAdded})
+ } else if f.Hash != e.Hash {
+ result = append(result, ChangedFile{Path: path, Status: ChangeStatusModified})
+ }
+ }
+ for _, f := range fromEntries {
+ if _, exists := toMap[f.Name]; exists {
+ continue
+ }
+ path := prefix + f.Name
+ if f.ObjectType == Tree {
+ result = append(result, r.diffTrees(f.Hash, "", path+"/")...)
+ } else {
+ result = append(result, ChangedFile{Path: path, Status: ChangeStatusDeleted})
+ }
+ }
+ return result
+}
+
+func mockCommitMeta(hash Hash, c commit) CommitMeta {
+ return CommitMeta{
+ Hash: hash,
+ Parents: c.parents,
+ Date: c.date,
+ Message: c.message,
+ }
+}
+
+func (r *mockRepoDataBrowse) Branches() ([]BranchInfo, error) {
+ var branches []BranchInfo
+ for ref, hash := range r.refs {
+ name, ok := strings.CutPrefix(ref, "refs/heads/")
+ if !ok {
+ continue
+ }
+ branches = append(branches, BranchInfo{
+ Name: name,
+ Hash: hash,
+ IsDefault: name == r.defaultBranch,
+ })
+ }
+ return branches, nil
+}
+
+func (r *mockRepoDataBrowse) Tags() ([]TagInfo, error) {
+ var tags []TagInfo
+ for ref, hash := range r.refs {
+ name, ok := strings.CutPrefix(ref, "refs/tags/")
+ if !ok {
+ continue
+ }
+ tags = append(tags, TagInfo{Name: name, Hash: hash})
+ }
+ return tags, nil
+}
+
+func (r *mockRepoDataBrowse) TreeAtPath(ref, path string) ([]TreeEntry, error) {
+ startHash, err := r.resolveRef(ref)
+ if err != nil {
+ return nil, ErrNotFound
+ }
+ c, ok := r.commits[startHash]
+ if !ok {
+ return nil, ErrNotFound
+ }
+ return r.treeEntriesAt(c.treeHash, path)
+}
+
+func (r *mockRepoDataBrowse) BlobAtPath(ref, path string) (io.ReadCloser, int64, Hash, error) {
+ startHash, err := r.resolveRef(ref)
+ if err != nil {
+ return nil, 0, "", ErrNotFound
+ }
+ c, ok := r.commits[startHash]
+ if !ok {
+ return nil, 0, "", ErrNotFound
+ }
+ blobHash, err := r.blobHashAt(c.treeHash, path)
+ if err != nil {
+ return nil, 0, "", ErrNotFound
+ }
+ data, ok := r.blobs[blobHash]
+ if !ok {
+ return nil, 0, "", ErrNotFound
+ }
+ return io.NopCloser(bytes.NewReader(data)), int64(len(data)), blobHash, nil
+}
+
+func (r *mockRepoDataBrowse) CommitLog(ref, path string, limit int, after Hash, since, until *time.Time) ([]CommitMeta, error) {
+ startHash, err := r.resolveRef(ref)
+ if err != nil {
+ return nil, ErrNotFound
+ }
+ path = strings.Trim(path, "/")
+ var result []CommitMeta
+ skipping := after != ""
+ current := startHash
+ seen := make(map[Hash]bool)
+ for {
+ if seen[current] {
+ break
+ }
+ seen[current] = true
+ c, ok := r.commits[current]
+ if !ok {
+ break
+ }
+ if skipping {
+ if current == after {
+ skipping = false
+ }
+ if len(c.parents) == 0 {
+ break
+ }
+ current = c.parents[0]
+ continue
+ }
+ meta := mockCommitMeta(current, c)
+ if since != nil && meta.Date.Before(*since) {
+ if len(c.parents) == 0 {
+ break
+ }
+ current = c.parents[0]
+ continue
+ }
+ if until != nil && meta.Date.After(*until) {
+ if len(c.parents) == 0 {
+ break
+ }
+ current = c.parents[0]
+ continue
+ }
+ if path != "" {
+ var fromTreeHash Hash
+ if len(c.parents) > 0 {
+ if parent, ok := r.commits[c.parents[0]]; ok {
+ fromTreeHash = parent.treeHash
+ }
+ }
+ touched := false
+ for _, f := range r.diffTrees(fromTreeHash, c.treeHash, "") {
+ if f.Path == path || strings.HasPrefix(f.Path, path+"/") {
+ touched = true
+ break
+ }
+ }
+ if !touched {
+ if len(c.parents) == 0 {
+ break
+ }
+ current = c.parents[0]
+ continue
+ }
+ }
+ result = append(result, meta)
+ if limit > 0 && len(result) >= limit {
+ break
+ }
+ if len(c.parents) == 0 {
+ break
+ }
+ current = c.parents[0]
+ }
+ return result, nil
+}
+
+func (r *mockRepoDataBrowse) LastCommitForEntries(ref, path string, names []string) (map[string]CommitMeta, error) {
+ startHash, err := r.resolveRef(ref)
+ if err != nil {
+ return nil, ErrNotFound
+ }
+ path = strings.Trim(path, "/")
+ remaining := make(map[string]bool, len(names))
+ for _, n := range names {
+ remaining[n] = true
+ }
+ result := make(map[string]CommitMeta)
+ current := startHash
+ seen := make(map[Hash]bool)
+ for len(remaining) > 0 {
+ if seen[current] {
+ break
+ }
+ seen[current] = true
+ c, ok := r.commits[current]
+ if !ok {
+ break
+ }
+ curEntries, err := r.treeEntriesAt(c.treeHash, path)
+ if err != nil {
+ if len(c.parents) == 0 {
+ break
+ }
+ current = c.parents[0]
+ continue
+ }
+ curMap := make(map[string]Hash, len(curEntries))
+ for _, e := range curEntries {
+ curMap[e.Name] = e.Hash
+ }
+ if len(c.parents) == 0 {
+ for name := range remaining {
+ if _, ok := curMap[name]; ok {
+ result[name] = mockCommitMeta(current, c)
+ delete(remaining, name)
+ }
+ }
+ break
+ }
+ pc, ok := r.commits[c.parents[0]]
+ if !ok {
+ break
+ }
+ parentEntries, _ := r.treeEntriesAt(pc.treeHash, path)
+ parentMap := make(map[string]Hash, len(parentEntries))
+ for _, e := range parentEntries {
+ parentMap[e.Name] = e.Hash
+ }
+ for name := range remaining {
+ cur, curExists := curMap[name]
+ par, parExists := parentMap[name]
+ if curExists && (!parExists || cur != par) {
+ result[name] = mockCommitMeta(current, c)
+ delete(remaining, name)
+ }
+ }
+ current = c.parents[0]
+ }
+ return result, nil
+}
+
+func (r *mockRepoDataBrowse) CommitDetail(hash Hash) (CommitDetail, error) {
+ c, ok := r.commits[hash]
+ if !ok {
+ return CommitDetail{}, ErrNotFound
+ }
+ var fromTreeHash Hash
+ if len(c.parents) > 0 {
+ if parent, ok := r.commits[c.parents[0]]; ok {
+ fromTreeHash = parent.treeHash
+ }
+ }
+ return CommitDetail{
+ CommitMeta: mockCommitMeta(hash, c),
+ Files: r.diffTrees(fromTreeHash, c.treeHash, ""),
+ }, nil
+}
+
+func (r *mockRepoDataBrowse) CommitFileDiff(hash Hash, filePath string) (FileDiff, error) {
+ c, ok := r.commits[hash]
+ if !ok {
+ return FileDiff{}, ErrNotFound
+ }
+ var fromTreeHash Hash
+ if len(c.parents) > 0 {
+ if parent, ok := r.commits[c.parents[0]]; ok {
+ fromTreeHash = parent.treeHash
+ }
+ }
+ files := r.diffTrees(fromTreeHash, c.treeHash, "")
+ var matched *ChangedFile
+ for i := range files {
+ if files[i].Path == filePath {
+ matched = &files[i]
+ break
+ }
+ }
+ if matched == nil {
+ return FileDiff{}, ErrNotFound
+ }
+ fd := FileDiff{
+ Path: filePath,
+ IsNew: matched.Status == ChangeStatusAdded,
+ IsDelete: matched.Status == ChangeStatusDeleted,
+ }
+ var oldContent, newContent []byte
+ if fromTreeHash != "" {
+ if bh, err := r.blobHashAt(fromTreeHash, filePath); err == nil {
+ oldContent = r.blobs[bh]
+ }
+ }
+ if bh, err := r.blobHashAt(c.treeHash, filePath); err == nil {
+ newContent = r.blobs[bh]
+ }
+ fd.Hunks = mockDiffHunks(oldContent, newContent)
+ return fd, nil
+}
+
+// mockDiffHunks produces a single DiffHunk using a prefix/suffix scan.
+func mockDiffHunks(old, new []byte) []DiffHunk {
+ oldLines := splitBlobLines(old)
+ newLines := splitBlobLines(new)
+ i := 0
+ for i < len(oldLines) && i < len(newLines) && oldLines[i] == newLines[i] {
+ i++
+ }
+ j, k := len(oldLines), len(newLines)
+ for j > i && k > i && oldLines[j-1] == newLines[k-1] {
+ j--
+ k--
+ }
+ if j == i && k == i {
+ return nil // no changed region
+ }
+ oldLine, newLine := 1, 1
+ var lines []DiffLine
+ for _, l := range oldLines[:i] {
+ lines = append(lines, DiffLine{Type: DiffLineContext, Content: l, OldLine: oldLine, NewLine: newLine})
+ oldLine++
+ newLine++
+ }
+ for _, l := range oldLines[i:j] {
+ lines = append(lines, DiffLine{Type: DiffLineDeleted, Content: l, OldLine: oldLine})
+ oldLine++
+ }
+ for _, l := range newLines[i:k] {
+ lines = append(lines, DiffLine{Type: DiffLineAdded, Content: l, NewLine: newLine})
+ newLine++
+ }
+ for _, l := range oldLines[j:] {
+ lines = append(lines, DiffLine{Type: DiffLineContext, Content: l, OldLine: oldLine, NewLine: newLine})
+ oldLine++
+ newLine++
+ }
+ return []DiffHunk{{OldStart: 1, OldLines: len(oldLines), NewStart: 1, NewLines: len(newLines), Lines: lines}}
+}
+
+func splitBlobLines(data []byte) []string {
+ if len(data) == 0 {
+ return nil
+ }
+ return strings.Split(strings.TrimRight(string(data), "\n"), "\n")
+}
+
var _ RepoClock = &mockRepoClock{}
type mockRepoClock struct {
@@ -29,6 +29,7 @@ type Repo interface {
RepoStorage
RepoIndex
RepoData
+ RepoBrowse
Close() error
}
@@ -76,10 +77,6 @@ type RepoCommon interface {
// GetRemotes returns the configured remotes repositories.
GetRemotes() (map[string]string, error)
- // GetPath returns the root directory path of the repository (the directory
- // that contains the .git folder for bare repos, or the .git folder itself
- // for bare clones). Returns an empty string for in-memory/mock repos.
- GetPath() string
}
type LocalStorage interface {
@@ -188,7 +185,7 @@ type RepoData interface {
// ListRefs will return a list of Git ref matching the given refspec
ListRefs(refPrefix string) ([]string, error)
- // RefExist will check if a reference exist in Git
+ // RefExist will check if a reference exists in Git
RefExist(ref string) (bool, error)
// CopyRef will create a new reference with the same value as another one
@@ -199,90 +196,6 @@ type RepoData interface {
ListCommits(ref string) ([]Hash, error)
}
-// CommitMeta holds the display-relevant metadata of a git commit.
-type CommitMeta struct {
- Hash Hash
- ShortHash string
- Message string // first line only
- AuthorName string
- AuthorEmail string
- Date time.Time
- Parents []Hash
-}
-
-// RepoBrowse extends a repo with read-only methods needed for code browsing.
-// Implemented by GoGitRepo; not part of the core Repo interface because not
-// all repository implementations (e.g. in-memory test repos) need it.
-type RepoBrowse interface {
- // GetDefaultBranch returns the short name of the branch HEAD points to.
- GetDefaultBranch() (string, error)
-
- // ReadCommitMeta reads full commit metadata including author and message.
- ReadCommitMeta(hash Hash) (CommitMeta, error)
-
- // CommitLog returns up to limit commits reachable from ref (short name or
- // full ref), optionally filtered to commits that touch path.
- // If after is non-empty, results start after that commit hash (pagination).
- CommitLog(ref string, path string, limit int, after Hash) ([]CommitMeta, error)
-
- // LastCommitForEntries walks the commit history once and returns the most
- // recent commit that touched each named entry inside dirPath.
- // dirPath is the directory being browsed (empty = repo root).
- // names are the immediate child names (files/dirs) inside that directory.
- // The returned map is keyed by entry name; missing entries were not found.
- LastCommitForEntries(ref string, dirPath string, names []string) (map[string]CommitMeta, error)
-
- // CommitDetail returns the full metadata for a single commit plus the list
- // of files it changed relative to its first parent.
- CommitDetail(hash Hash) (CommitDetail, error)
-
- // CommitFileDiff returns the structured diff for a single file in a commit
- // relative to its first parent. path is the current file path (or old path
- // for deletions).
- CommitFileDiff(hash Hash, path string) (FileDiff, error)
-}
-
-// DiffLine is a single line in a diff hunk.
-type DiffLine struct {
- Type string // "context" | "added" | "deleted"
- Content string
- OldLine int // 0 for added lines
- NewLine int // 0 for deleted lines
-}
-
-// DiffHunk is a contiguous block of changes with surrounding context.
-type DiffHunk struct {
- OldStart int
- OldLines int
- NewStart int
- NewLines int
- Lines []DiffLine
-}
-
-// FileDiff holds the diff for a single file in a commit.
-type FileDiff struct {
- Path string
- OldPath string // non-empty for renames
- IsBinary bool
- IsNew bool
- IsDelete bool
- Hunks []DiffHunk
-}
-
-// ChangedFile describes a single file changed by a commit.
-type ChangedFile struct {
- Path string // current path (or old path for deletions)
- OldPath string // only set for renames
- Status string // "added" | "modified" | "deleted" | "renamed"
-}
-
-// CommitDetail extends CommitMeta with the full message body and changed files.
-type CommitDetail struct {
- CommitMeta
- FullMessage string
- Files []ChangedFile
-}
-
// RepoClock give access to Lamport clocks
type RepoClock interface {
// AllClocks return all the known clocks
@@ -299,11 +212,63 @@ type RepoClock interface {
Witness(name string, time lamport.Time) error
}
+// RepoBrowse is implemented by all Repo implementations and provides
+// code-browsing endpoints (file tree, history, diffs).
+//
+// All methods accepting a ref parameter resolve it in order:
+// refs/heads/<ref>, refs/tags/<ref>, full ref name, raw commit hash.
+type RepoBrowse interface {
+ // Branches returns all local branches (refs/heads/*).
+ // IsDefault marks the branch HEAD points to.
+ // All other ref namespaces — including git-bug's internal refs
+ // (refs/bugs/, refs/identities/, …) — are excluded.
+ Branches() ([]BranchInfo, error)
+
+ // Tags returns all tags (refs/tags/*).
+ // All other ref namespaces are excluded.
+ Tags() ([]TagInfo, error)
+
+ // TreeAtPath returns the entries of the directory at path under ref.
+ // An empty path returns the root tree.
+ // Returns ErrNotFound if ref or path does not exist, or if path
+ // resolves to a blob rather than a tree.
+ // Symlinks appear as entries with ObjectType Symlink; they are not followed.
+ TreeAtPath(ref, path string) ([]TreeEntry, error)
+
+ // BlobAtPath returns the raw content, byte size, and git object hash of
+ // the file at path under ref. Returns ErrNotFound if ref or path does
+ // not exist, or if path resolves to a tree. Symlinks are not followed.
+ // The caller must close the reader.
+ BlobAtPath(ref, path string) (io.ReadCloser, int64, Hash, error)
+
+ // CommitLog returns at most limit commits reachable from ref, filtered
+ // to those touching path (empty = unrestricted). after is an exclusive
+ // cursor; pass Hash("") for no cursor. since and until bound the author
+ // date (inclusive); pass nil for no bound. Merge commits appear once,
+ // compared against the first parent only.
+ CommitLog(ref, path string, limit int, after Hash, since, until *time.Time) ([]CommitMeta, error)
+
+ // LastCommitForEntries returns the most recent commit that touched each
+ // name in the directory at path under ref. Entries not resolved within
+ // the implementation's depth limit are silently absent from the result.
+ LastCommitForEntries(ref, path string, names []string) (map[string]CommitMeta, error)
+
+ // CommitDetail returns the full metadata and changed-file list for a
+ // single commit identified by its hash. Diffs against the first parent
+ // only; the initial commit is diffed against the empty tree.
+ CommitDetail(hash Hash) (CommitDetail, error)
+
+ // CommitFileDiff returns the unified diff for a single file in a commit
+ // identified by its hash. Diffs against the first parent only; the
+ // initial commit is diffed against the empty tree.
+ CommitFileDiff(hash Hash, filePath string) (FileDiff, error)
+}
+
// ClockLoader hold which logical clock need to exist for an entity and
// how to create them if they don't.
type ClockLoader struct {
- // Clocks hold the name of all the clocks this loader deal with.
- // Those clocks will be checked when the repo load. If not present or broken,
+ // Clocks hold the name of all the clocks this loader deals with.
+ // Those clocks will be checked when the repo loads. If not present or broken,
// Witnesser will be used to create them.
Clocks []string
// Witnesser is a function that will initialize the clocks of a repo
@@ -311,13 +276,13 @@ type ClockLoader struct {
Witnesser func(repo ClockedRepo) error
}
-// TestedRepo is an extended ClockedRepo with function for testing only
+// TestedRepo is an extended ClockedRepo with functions for testing only
type TestedRepo interface {
ClockedRepo
repoTest
}
-// repoTest give access to test only functions
+// repoTest give access to test-only functions
type repoTest interface {
// AddRemote add a new remote to the repository
AddRemote(name string, url string) error
@@ -1,9 +1,11 @@
package repository
import (
+ "io"
"math/rand"
"os"
"testing"
+ "time"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/stretchr/testify/require"
@@ -27,6 +29,10 @@ func RepoTest(t *testing.T, creator RepoCreator) {
RepoDataSignatureTest(t, repo)
})
+ t.Run("Browse", func(t *testing.T) {
+ RepoBrowseTest(t, repo)
+ })
+
t.Run("Config", func(t *testing.T) {
RepoConfigTest(t, repo)
})
@@ -360,3 +366,377 @@ func randomData() []byte {
}
return b
}
+
+// browsable is the interface required by RepoBrowseTest.
+type browsable interface {
+ RepoConfig
+ RepoData
+ RepoBrowse
+}
+
+// RepoBrowseTest exercises the RepoBrowse interface against any implementation.
+//
+// Commit graph (oldest → newest):
+//
+// c1 ── c2 ── c3 refs/heads/main (default)
+// └──────── refs/heads/feature
+// c1 ←── refs/tags/v1.0
+func RepoBrowseTest(t *testing.T, repo browsable) {
+ t.Helper()
+
+ require.NoError(t, repo.LocalConfig().StoreString("init.defaultBranch", "main"))
+
+ // ── build fixture ─────────────────────────────────────────────────────────
+
+ readmeV1 := []byte("# Hello\n")
+ readmeV3 := []byte("# Hello\n\n## Updated\n")
+ mainV1 := []byte("package main\n")
+ mainV2 := []byte("package main\n\n// updated\n")
+ libV1 := []byte("package lib\n")
+ utilV1 := []byte("package util\n")
+
+ hReadmeV1, err := repo.StoreData(readmeV1)
+ require.NoError(t, err)
+ hReadmeV3, err := repo.StoreData(readmeV3)
+ require.NoError(t, err)
+ hMainV1, err := repo.StoreData(mainV1)
+ require.NoError(t, err)
+ hMainV2, err := repo.StoreData(mainV2)
+ require.NoError(t, err)
+ hLibV1, err := repo.StoreData(libV1)
+ require.NoError(t, err)
+ hUtilV1, err := repo.StoreData(utilV1)
+ require.NoError(t, err)
+
+ srcTreeV1, err := repo.StoreTree([]TreeEntry{
+ {ObjectType: Blob, Hash: hLibV1, Name: "lib.go"},
+ })
+ require.NoError(t, err)
+ rootTreeV1, err := repo.StoreTree([]TreeEntry{
+ {ObjectType: Blob, Hash: hReadmeV1, Name: "README.md"},
+ {ObjectType: Blob, Hash: hMainV1, Name: "main.go"},
+ {ObjectType: Tree, Hash: srcTreeV1, Name: "src"},
+ })
+ require.NoError(t, err)
+
+ srcTreeV2, err := repo.StoreTree([]TreeEntry{
+ {ObjectType: Blob, Hash: hLibV1, Name: "lib.go"},
+ {ObjectType: Blob, Hash: hUtilV1, Name: "util.go"},
+ })
+ require.NoError(t, err)
+ rootTreeV2, err := repo.StoreTree([]TreeEntry{
+ {ObjectType: Blob, Hash: hReadmeV1, Name: "README.md"},
+ {ObjectType: Blob, Hash: hMainV2, Name: "main.go"},
+ {ObjectType: Tree, Hash: srcTreeV2, Name: "src"},
+ })
+ require.NoError(t, err)
+
+ rootTreeV3, err := repo.StoreTree([]TreeEntry{
+ {ObjectType: Blob, Hash: hReadmeV3, Name: "README.md"},
+ {ObjectType: Blob, Hash: hMainV2, Name: "main.go"},
+ {ObjectType: Tree, Hash: srcTreeV2, Name: "src"},
+ })
+ require.NoError(t, err)
+
+ c1, err := repo.StoreCommit(rootTreeV1)
+ require.NoError(t, err)
+ c2, err := repo.StoreCommit(rootTreeV2, c1)
+ require.NoError(t, err)
+ c3, err := repo.StoreCommit(rootTreeV3, c2)
+ require.NoError(t, err)
+
+ require.NoError(t, repo.UpdateRef("refs/heads/main", c3))
+ require.NoError(t, repo.UpdateRef("refs/heads/feature", c2))
+ require.NoError(t, repo.UpdateRef("refs/tags/v1.0", c1))
+
+ // ── Branches ──────────────────────────────────────────────────────────────
+
+ t.Run("Branches", func(t *testing.T) {
+ branches, err := repo.Branches()
+ require.NoError(t, err)
+ require.Len(t, branches, 2)
+
+ byName := make(map[string]BranchInfo)
+ for _, b := range branches {
+ byName[b.Name] = b
+ }
+
+ require.Equal(t, c3, byName["main"].Hash)
+ require.True(t, byName["main"].IsDefault)
+
+ require.Equal(t, c2, byName["feature"].Hash)
+ require.False(t, byName["feature"].IsDefault)
+ })
+
+ // ── Tags ──────────────────────────────────────────────────────────────────
+
+ t.Run("Tags", func(t *testing.T) {
+ tags, err := repo.Tags()
+ require.NoError(t, err)
+ require.Len(t, tags, 1)
+ require.Equal(t, "v1.0", tags[0].Name)
+ require.Equal(t, c1, tags[0].Hash)
+ })
+
+ // ── TreeAtPath ────────────────────────────────────────────────────────────
+
+ t.Run("TreeAtPath", func(t *testing.T) {
+ entries, err := repo.TreeAtPath("main", "")
+ require.NoError(t, err)
+ byName := make(map[string]TreeEntry)
+ for _, e := range entries {
+ byName[e.Name] = e
+ }
+ require.Equal(t, Blob, byName["README.md"].ObjectType)
+ require.Equal(t, Blob, byName["main.go"].ObjectType)
+ require.Equal(t, Tree, byName["src"].ObjectType)
+
+ // subdirectory
+ srcEntries, err := repo.TreeAtPath("main", "src")
+ require.NoError(t, err)
+ srcByName := make(map[string]TreeEntry)
+ for _, e := range srcEntries {
+ srcByName[e.Name] = e
+ }
+ require.Equal(t, Blob, srcByName["lib.go"].ObjectType)
+ require.Equal(t, Blob, srcByName["util.go"].ObjectType)
+
+ // v1.0 tag (at c1) predates util.go — src only has lib.go
+ v1Src, err := repo.TreeAtPath("v1.0", "src")
+ require.NoError(t, err)
+ require.Len(t, v1Src, 1)
+ require.Equal(t, "lib.go", v1Src[0].Name)
+
+ // unknown ref
+ _, err = repo.TreeAtPath("nonexistent-ref", "")
+ require.ErrorIs(t, err, ErrNotFound)
+
+ // path resolves to a blob, not a tree
+ _, err = repo.TreeAtPath("main", "README.md")
+ require.Error(t, err)
+ })
+
+ // ── BlobAtPath ────────────────────────────────────────────────────────────
+
+ t.Run("BlobAtPath", func(t *testing.T) {
+ rc, size, hash, err := repo.BlobAtPath("main", "README.md")
+ require.NoError(t, err)
+ defer rc.Close()
+ data, err := io.ReadAll(rc)
+ require.NoError(t, err)
+ require.Equal(t, readmeV3, data)
+ require.Equal(t, int64(len(readmeV3)), size)
+ require.NotEmpty(t, hash)
+
+ // feature branch still has readmeV1
+ rc2, _, _, err := repo.BlobAtPath("feature", "README.md")
+ require.NoError(t, err)
+ data2, err := io.ReadAll(rc2)
+ rc2.Close()
+ require.NoError(t, err)
+ require.Equal(t, readmeV1, data2)
+
+ // file in subdirectory
+ rc3, _, _, err := repo.BlobAtPath("main", "src/lib.go")
+ require.NoError(t, err)
+ data3, err := io.ReadAll(rc3)
+ rc3.Close()
+ require.NoError(t, err)
+ require.Equal(t, libV1, data3)
+
+ // path not found
+ _, _, _, err = repo.BlobAtPath("main", "nonexistent.go")
+ require.ErrorIs(t, err, ErrNotFound)
+
+ // hash is stable across calls for the same content
+ rc4, _, hash2, err := repo.BlobAtPath("main", "README.md")
+ require.NoError(t, err)
+ rc4.Close()
+ require.Equal(t, hash, hash2, "blob hash should be stable across calls")
+
+ // different content → different hash
+ rc5, _, hashLib, err := repo.BlobAtPath("main", "src/lib.go")
+ require.NoError(t, err)
+ rc5.Close()
+ require.NotEqual(t, hash, hashLib, "different files should have different hashes")
+ })
+
+ // ── CommitLog ─────────────────────────────────────────────────────────────
+
+ t.Run("CommitLog", func(t *testing.T) {
+ // all commits, newest first
+ commits, err := repo.CommitLog("main", "", 10, "", nil, nil)
+ require.NoError(t, err)
+ require.Len(t, commits, 3)
+ require.Equal(t, c3, commits[0].Hash)
+ require.Equal(t, c2, commits[1].Hash)
+ require.Equal(t, c1, commits[2].Hash)
+
+ // limit
+ limited, err := repo.CommitLog("main", "", 2, "", nil, nil)
+ require.NoError(t, err)
+ require.Len(t, limited, 2)
+ require.Equal(t, c3, limited[0].Hash)
+ require.Equal(t, c2, limited[1].Hash)
+
+ // after cursor (exclusive): start after c3 → get c2, c1
+ after, err := repo.CommitLog("main", "", 10, c3, nil, nil)
+ require.NoError(t, err)
+ require.Len(t, after, 2)
+ require.Equal(t, c2, after[0].Hash)
+ require.Equal(t, c1, after[1].Hash)
+
+ // feature branch only has c1, c2
+ featureLog, err := repo.CommitLog("feature", "", 10, "", nil, nil)
+ require.NoError(t, err)
+ require.Len(t, featureLog, 2)
+ require.Equal(t, c2, featureLog[0].Hash)
+
+ // path filtering: only commits that touched the given path
+ // README.md was created in c1 and updated in c3
+ readmeLog, err := repo.CommitLog("main", "README.md", 10, "", nil, nil)
+ require.NoError(t, err)
+ require.Len(t, readmeLog, 2)
+ require.Equal(t, c3, readmeLog[0].Hash)
+ require.Equal(t, c1, readmeLog[1].Hash)
+ })
+
+ t.Run("CommitLog/since-until", func(t *testing.T) {
+ // since = far future → no commits
+ future := time.Now().Add(24 * time.Hour)
+ none, err := repo.CommitLog("main", "", 10, "", &future, nil)
+ require.NoError(t, err)
+ require.Empty(t, none, "since=future should return no commits")
+
+ // until = zero time (long before any real commit) → no commits
+ zero := time.Time{}
+ none2, err := repo.CommitLog("main", "", 10, "", nil, &zero)
+ require.NoError(t, err)
+ require.Empty(t, none2, "until=zero should return no commits")
+
+ // Both bounds open → all commits returned (filtering is a no-op)
+ all, err := repo.CommitLog("main", "", 10, "", nil, nil)
+ require.NoError(t, err)
+ require.Len(t, all, 3, "nil since/until should return all commits")
+
+ // since = far past and until = far future → all commits still returned
+ past := time.Unix(0, 0)
+ all2, err := repo.CommitLog("main", "", 10, "", &past, &future)
+ require.NoError(t, err)
+ require.Len(t, all2, 3, "wide since/until bounds should return all commits")
+ })
+
+ // ── LastCommitForEntries ──────────────────────────────────────────────────
+
+ t.Run("LastCommitForEntries", func(t *testing.T) {
+ result, err := repo.LastCommitForEntries("main", "", []string{"README.md", "main.go", "src"})
+ require.NoError(t, err)
+
+ // README.md was last changed in c3
+ require.Equal(t, c3, result["README.md"].Hash)
+ // main.go was last changed in c2
+ require.Equal(t, c2, result["main.go"].Hash)
+ // src tree changed in c2 (util.go added)
+ require.Equal(t, c2, result["src"].Hash)
+
+ // subdirectory: last commits for entries in src/
+ srcResult, err := repo.LastCommitForEntries("main", "src", []string{"lib.go", "util.go"})
+ require.NoError(t, err)
+ // lib.go was added in c1 and never changed
+ require.Equal(t, c1, srcResult["lib.go"].Hash)
+ // util.go was added in c2
+ require.Equal(t, c2, srcResult["util.go"].Hash)
+
+ // requesting a name that doesn't exist returns no entry for it
+ partial, err := repo.LastCommitForEntries("main", "", []string{"README.md", "ghost.txt"})
+ require.NoError(t, err)
+ require.Contains(t, partial, "README.md")
+ require.NotContains(t, partial, "ghost.txt")
+ })
+
+ t.Run("LastCommitForEntries/cache-subset", func(t *testing.T) {
+ // First call with one name — seeds (or hits) the cache for this directory.
+ r1, err := repo.LastCommitForEntries("main", "", []string{"README.md"})
+ require.NoError(t, err)
+ require.Contains(t, r1, "README.md")
+ require.Equal(t, c3, r1["README.md"].Hash)
+
+ // Second call for the same directory but a different name.
+ // A buggy implementation that caches only the requested subset would
+ // return an empty map here (cache hit, but "main.go" was never stored).
+ r2, err := repo.LastCommitForEntries("main", "", []string{"main.go"})
+ require.NoError(t, err)
+ require.Contains(t, r2, "main.go", "second call with different name should hit correct result, not empty cache")
+ require.Equal(t, c2, r2["main.go"].Hash)
+
+ // Third call requesting both names should also work.
+ r3, err := repo.LastCommitForEntries("main", "", []string{"README.md", "main.go"})
+ require.NoError(t, err)
+ require.Equal(t, c3, r3["README.md"].Hash)
+ require.Equal(t, c2, r3["main.go"].Hash)
+ })
+
+ // ── CommitDetail ──────────────────────────────────────────────────────────
+
+ t.Run("CommitDetail", func(t *testing.T) {
+ detail, err := repo.CommitDetail(c2)
+ require.NoError(t, err)
+ require.Equal(t, c2, detail.Hash)
+ require.Equal(t, []Hash{c1}, detail.Parents)
+
+ filesByPath := make(map[string]ChangedFile)
+ for _, f := range detail.Files {
+ filesByPath[f.Path] = f
+ }
+ require.Equal(t, ChangeStatusModified, filesByPath["main.go"].Status)
+ require.Equal(t, ChangeStatusAdded, filesByPath["src/util.go"].Status)
+
+ // initial commit: diffs against empty tree, everything is "added"
+ initDetail, err := repo.CommitDetail(c1)
+ require.NoError(t, err)
+ for _, f := range initDetail.Files {
+ require.Equal(t, ChangeStatusAdded, f.Status, "file %s", f.Path)
+ }
+
+ // unknown hash
+ _, err = repo.CommitDetail(randomHash())
+ require.ErrorIs(t, err, ErrNotFound)
+ })
+
+ // ── CommitFileDiff ────────────────────────────────────────────────────────
+
+ t.Run("CommitFileDiff", func(t *testing.T) {
+ fd, err := repo.CommitFileDiff(c2, "main.go")
+ require.NoError(t, err)
+ require.Equal(t, "main.go", fd.Path)
+ require.False(t, fd.IsBinary)
+ require.False(t, fd.IsNew)
+ require.False(t, fd.IsDelete)
+ require.NotEmpty(t, fd.Hunks)
+
+ // find the added lines
+ var addedContent []string
+ for _, h := range fd.Hunks {
+ for _, l := range h.Lines {
+ if l.Type == DiffLineAdded {
+ addedContent = append(addedContent, l.Content)
+ }
+ }
+ }
+ require.Contains(t, addedContent, "// updated")
+
+ // new file in initial commit
+ initFD, err := repo.CommitFileDiff(c1, "main.go")
+ require.NoError(t, err)
+ require.True(t, initFD.IsNew)
+ require.Equal(t, "main.go", initFD.Path)
+
+ // file not in this commit's diff
+ _, err = repo.CommitFileDiff(c3, "main.go")
+ require.ErrorIs(t, err, ErrNotFound)
+
+ // unknown hash
+ _, err = repo.CommitFileDiff(randomHash(), "main.go")
+ require.ErrorIs(t, err, ErrNotFound)
+ })
+}
@@ -3,6 +3,8 @@ package repository
import (
"bytes"
"fmt"
+ "io"
+ "strconv"
"strings"
)
@@ -15,9 +17,12 @@ type TreeEntry struct {
type ObjectType int
const (
- Unknown ObjectType = iota
- Blob
- Tree
+ Unknown ObjectType = iota
+ Blob // regular file (100644)
+ Tree // directory (040000)
+ Executable // executable file (100755)
+ Symlink // symbolic link (120000)
+ Submodule // git submodule (160000)
)
func ParseTreeEntry(line string) (TreeEntry, error) {
@@ -54,17 +59,64 @@ func (ot ObjectType) Format() string {
return "100644 blob"
case Tree:
return "040000 tree"
+ case Executable:
+ return "100755 blob"
+ case Symlink:
+ return "120000 blob"
+ case Submodule:
+ return "160000 commit"
default:
panic("Unknown git object type")
}
}
+func (ot ObjectType) MarshalGQL(w io.Writer) {
+ switch ot {
+ case Tree:
+ fmt.Fprint(w, strconv.Quote("TREE"))
+ case Blob, Executable:
+ fmt.Fprint(w, strconv.Quote("BLOB"))
+ case Symlink:
+ fmt.Fprint(w, strconv.Quote("SYMLINK"))
+ case Submodule:
+ fmt.Fprint(w, strconv.Quote("SUBMODULE"))
+ default:
+ panic(fmt.Sprintf("unknown ObjectType value %d", int(ot)))
+ }
+}
+
+func (ot *ObjectType) UnmarshalGQL(v any) error {
+ str, ok := v.(string)
+ if !ok {
+ return fmt.Errorf("enums must be strings")
+ }
+ switch str {
+ case "TREE":
+ *ot = Tree
+ case "BLOB":
+ *ot = Blob
+ case "SYMLINK":
+ *ot = Symlink
+ case "SUBMODULE":
+ *ot = Submodule
+ default:
+ return fmt.Errorf("%q is not a valid ObjectType", str)
+ }
+ return nil
+}
+
func ParseObjectType(mode, objType string) (ObjectType, error) {
switch {
case mode == "100644" && objType == "blob":
return Blob, nil
case mode == "040000" && objType == "tree":
return Tree, nil
+ case mode == "100755" && objType == "blob":
+ return Executable, nil
+ case mode == "120000" && objType == "blob":
+ return Symlink, nil
+ case mode == "160000" && objType == "commit":
+ return Submodule, nil
default:
return Unknown, fmt.Errorf("Unknown git object type %s %s", mode, objType)
}
@@ -541,6 +541,233 @@ export enum EntityEventType {
Updated = 'UPDATED'
}
+/** The content of a git blob (file). */
+export type GitBlob = {
+ __typename?: 'GitBlob';
+ /**
+ * Git object hash. Can be used as a stable cache key or to construct a
+ * raw download URL.
+ */
+ hash: Scalars['String']['output'];
+ /**
+ * True when the file contains null bytes and is treated as binary.
+ * text will be null.
+ */
+ isBinary: Scalars['Boolean']['output'];
+ /**
+ * True when the file exceeds the maximum inline size and text has been
+ * omitted. Use the raw download endpoint to retrieve the full content.
+ */
+ isTruncated: Scalars['Boolean']['output'];
+ /** Path of the file relative to the repository root. */
+ path: Scalars['String']['output'];
+ /** Size in bytes. */
+ size: Scalars['Int']['output'];
+ /**
+ * UTF-8 text content of the file. Null when isBinary is true or when
+ * the file is too large to be returned inline (see isTruncated).
+ */
+ text?: Maybe<Scalars['String']['output']>;
+};
+
+/** How a file was affected by a commit. */
+export enum GitChangeStatus {
+ /** File was created in this commit. */
+ Added = 'ADDED',
+ /** File was removed in this commit. */
+ Deleted = 'DELETED',
+ /** File content changed in this commit. */
+ Modified = 'MODIFIED',
+ /** File was moved or renamed in this commit. */
+ Renamed = 'RENAMED'
+}
+
+/** A file that was changed in a commit. */
+export type GitChangedFile = {
+ __typename?: 'GitChangedFile';
+ /** Previous path, non-null only for renames. */
+ oldPath?: Maybe<Scalars['String']['output']>;
+ /** Path of the file in the new version of the commit. */
+ path: Scalars['String']['output'];
+ /** How the file was affected by the commit. */
+ status: GitChangeStatus;
+};
+
+export type GitChangedFileConnection = {
+ __typename?: 'GitChangedFileConnection';
+ nodes: Array<GitChangedFile>;
+ pageInfo: PageInfo;
+ totalCount: Scalars['Int']['output'];
+};
+
+/** Metadata for a single git commit. */
+export type GitCommit = {
+ __typename?: 'GitCommit';
+ /** Email address of the commit author. */
+ authorEmail: Scalars['String']['output'];
+ /** Name of the commit author. */
+ authorName: Scalars['String']['output'];
+ /** Timestamp from the author field (when the change was originally made). */
+ date: Scalars['Time']['output'];
+ /** Unified diff for a single file in this commit. */
+ diff?: Maybe<GitFileDiff>;
+ /**
+ * Files changed relative to the first parent (or the empty tree for the
+ * initial commit).
+ */
+ files: GitChangedFileConnection;
+ /** Full commit message. */
+ fullMessage: Scalars['String']['output'];
+ /** Full SHA-1 commit hash. */
+ hash: Scalars['String']['output'];
+ /** First line of the commit message. */
+ message: Scalars['String']['output'];
+ /** Hashes of parent commits. Empty for the initial commit. */
+ parents: Array<Scalars['String']['output']>;
+ /** Abbreviated commit hash, typically 8 characters. */
+ shortHash: Scalars['String']['output'];
+};
+
+
+/** Metadata for a single git commit. */
+export type GitCommitDiffArgs = {
+ path: Scalars['String']['input'];
+};
+
+
+/** Metadata for a single git commit. */
+export type GitCommitFilesArgs = {
+ after?: InputMaybe<Scalars['String']['input']>;
+ before?: InputMaybe<Scalars['String']['input']>;
+ first?: InputMaybe<Scalars['Int']['input']>;
+ last?: InputMaybe<Scalars['Int']['input']>;
+};
+
+/** Paginated list of commits. */
+export type GitCommitConnection = {
+ __typename?: 'GitCommitConnection';
+ nodes: Array<GitCommit>;
+ pageInfo: PageInfo;
+ totalCount: Scalars['Int']['output'];
+};
+
+/** A contiguous block of changes in a unified diff. */
+export type GitDiffHunk = {
+ __typename?: 'GitDiffHunk';
+ /** Lines in this hunk, including context, additions, and deletions. */
+ lines: Array<GitDiffLine>;
+ /** Number of lines from the new file included in this hunk. */
+ newLines: Scalars['Int']['output'];
+ /** Starting line number in the new file. */
+ newStart: Scalars['Int']['output'];
+ /** Number of lines from the old file included in this hunk. */
+ oldLines: Scalars['Int']['output'];
+ /** Starting line number in the old file. */
+ oldStart: Scalars['Int']['output'];
+};
+
+/** A single line in a unified diff hunk. */
+export type GitDiffLine = {
+ __typename?: 'GitDiffLine';
+ /** Raw line content, without the leading +/- prefix. */
+ content: Scalars['String']['output'];
+ /** Line number in the new file. 0 for deleted lines. */
+ newLine: Scalars['Int']['output'];
+ /** Line number in the old file. 0 for added lines. */
+ oldLine: Scalars['Int']['output'];
+ /** Whether this line is context, an addition, or a deletion. */
+ type: GitDiffLineType;
+};
+
+/** The role of a line within a unified diff hunk. */
+export enum GitDiffLineType {
+ /** A line added in the new version. */
+ Added = 'ADDED',
+ /** An unchanged line present in both old and new versions. */
+ Context = 'CONTEXT',
+ /** A line removed from the old version. */
+ Deleted = 'DELETED'
+}
+
+/** The diff for a single file in a commit. */
+export type GitFileDiff = {
+ __typename?: 'GitFileDiff';
+ /** Contiguous blocks of changes. Empty for binary files. */
+ hunks: Array<GitDiffHunk>;
+ /** True when the file is binary and no textual diff is available. */
+ isBinary: Scalars['Boolean']['output'];
+ /** True when the file was deleted in this commit. */
+ isDelete: Scalars['Boolean']['output'];
+ /** True when the file was created in this commit. */
+ isNew: Scalars['Boolean']['output'];
+ /** Previous path, non-null only for renames. */
+ oldPath?: Maybe<Scalars['String']['output']>;
+ /** Path of the file in the new version. */
+ path: Scalars['String']['output'];
+};
+
+/** The last commit that touched each requested entry in a directory. */
+export type GitLastCommit = {
+ __typename?: 'GitLastCommit';
+ /** Most recent commit that modified this entry. */
+ commit: GitCommit;
+ /** Entry name within the directory. */
+ name: Scalars['String']['output'];
+};
+
+/** The type of object a git tree entry points to. */
+export enum GitObjectType {
+ /** A regular or executable file. */
+ Blob = 'BLOB',
+ /** A git submodule. */
+ Submodule = 'SUBMODULE',
+ /** A symbolic link. */
+ Symlink = 'SYMLINK',
+ /** A directory. */
+ Tree = 'TREE'
+}
+
+/** A git branch or tag reference. */
+export type GitRef = {
+ __typename?: 'GitRef';
+ /** Commit hash the reference points to. */
+ hash: Scalars['String']['output'];
+ /** True for the branch HEAD currently points to. */
+ isDefault: Scalars['Boolean']['output'];
+ /** Full reference name, e.g. refs/heads/main or refs/tags/v1.0. */
+ name: Scalars['String']['output'];
+ /** Short name, e.g. main or v1.0. */
+ shortName: Scalars['String']['output'];
+ /** Whether this reference is a branch or a tag. */
+ type: GitRefType;
+};
+
+export type GitRefConnection = {
+ __typename?: 'GitRefConnection';
+ nodes: Array<GitRef>;
+ pageInfo: PageInfo;
+ totalCount: Scalars['Int']['output'];
+};
+
+/** The kind of git reference: a branch or a tag. */
+export enum GitRefType {
+ /** A local branch (refs/heads/*). */
+ Branch = 'BRANCH',
+ /** An annotated or lightweight tag (refs/tags/*). */
+ Tag = 'TAG'
+}
+
+/** An entry in a git tree (directory listing). */
+export type GitTreeEntry = {
+ __typename?: 'GitTreeEntry';
+ /** Git object hash. */
+ hash: Scalars['String']['output'];
+ /** File or directory name within the parent tree. */
+ name: Scalars['String']['output'];
+ /** Whether this entry is a file, directory, symlink, or submodule. */
+ type: GitObjectType;
+};
+
/** Represents an identity */
export type Identity = Entity & {
__typename?: 'Identity';
@@ -734,7 +961,10 @@ export type Query = {
__typename?: 'Query';
/** List all registered repositories. */
repositories: RepositoryConnection;
- /** Access a repository by reference/name. If no ref is given, the default repository is returned if any. */
+ /**
+ * Access a repository by reference/name. If no ref is given, the default repository is returned if any.
+ * Returns null if the referenced repository does not exist.
+ */
repository?: Maybe<Repository>;
/** Server configuration and authentication mode. */
serverConfig: ServerConfig;
@@ -759,10 +989,34 @@ export type Repository = {
allBugs: BugConnection;
/** All the identities */
allIdentities: IdentityConnection;
+ /**
+ * Content of the file at path under ref. Null if the path does not exist
+ * or resolves to a tree rather than a blob.
+ */
+ blob?: Maybe<GitBlob>;
+ /** Look up a bug by id prefix. Returns null if no bug matches the prefix. */
bug?: Maybe<Bug>;
+ /** A single commit by hash. Returns null if the hash does not exist in the repository. */
+ commit?: Maybe<GitCommit>;
+ /**
+ * Paginated commit log reachable from ref, optionally filtered to commits
+ * touching path.
+ */
+ commits: GitCommitConnection;
+ /** Look up an identity by id prefix. Returns null if no identity matches the prefix. */
identity?: Maybe<Identity>;
- /** The name of the repository. Null for the default (unnamed) repository. */
+ /**
+ * The most recent commit that touched each of the named entries in the
+ * directory at path under ref. Use this to populate last-commit info on a
+ * tree listing without blocking the initial tree fetch.
+ */
+ lastCommits: Array<GitLastCommit>;
+ /** The name of the repository. Null for the default (unnamed) repository in a single-repo setup. */
name?: Maybe<Scalars['String']['output']>;
+ /** All branches and tags, optionally filtered by type. */
+ refs: GitRefConnection;
+ /** Directory listing at path under ref. An empty path returns the root tree. */
+ tree: Array<GitTreeEntry>;
/** The identity created or selected by the user as its own */
userIdentity?: Maybe<Identity>;
/** List of valid labels. */
@@ -787,16 +1041,59 @@ export type RepositoryAllIdentitiesArgs = {
};
+export type RepositoryBlobArgs = {
+ path: Scalars['String']['input'];
+ ref: Scalars['String']['input'];
+};
+
+
export type RepositoryBugArgs = {
prefix: Scalars['String']['input'];
};
+export type RepositoryCommitArgs = {
+ hash: Scalars['String']['input'];
+};
+
+
+export type RepositoryCommitsArgs = {
+ after?: InputMaybe<Scalars['String']['input']>;
+ first?: InputMaybe<Scalars['Int']['input']>;
+ path?: InputMaybe<Scalars['String']['input']>;
+ ref: Scalars['String']['input'];
+ since?: InputMaybe<Scalars['Time']['input']>;
+ until?: InputMaybe<Scalars['Time']['input']>;
+};
+
+
export type RepositoryIdentityArgs = {
prefix: Scalars['String']['input'];
};
+export type RepositoryLastCommitsArgs = {
+ names: Array<Scalars['String']['input']>;
+ path?: InputMaybe<Scalars['String']['input']>;
+ ref: Scalars['String']['input'];
+};
+
+
+export type RepositoryRefsArgs = {
+ after?: InputMaybe<Scalars['String']['input']>;
+ before?: InputMaybe<Scalars['String']['input']>;
+ first?: InputMaybe<Scalars['Int']['input']>;
+ last?: InputMaybe<Scalars['Int']['input']>;
+ type?: InputMaybe<GitRefType>;
+};
+
+
+export type RepositoryTreeArgs = {
+ path?: InputMaybe<Scalars['String']['input']>;
+ ref: Scalars['String']['input'];
+};
+
+
export type RepositoryValidLabelsArgs = {
after?: InputMaybe<Scalars['String']['input']>;
before?: InputMaybe<Scalars['String']['input']>;
@@ -1,55 +1,78 @@
-import { useState, useEffect } from 'react'
+// Paginated commit history grouped by calendar date. Each row links to the
+// commit detail page. Used in CodePage's "History" view.
+
+import { useState } from 'react'
import { Link } from 'react-router-dom'
import { formatDistanceToNow } from 'date-fns'
import { GitCommit } from 'lucide-react'
+import { gql, useQuery } from '@apollo/client'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
-import { getCommits } from '@/lib/gitApi'
-import type { GitCommit as GitCommitType } from '@/lib/gitApi'
import { useRepo } from '@/lib/repo'
+const COMMITS_QUERY = gql`
+ query CommitList($repo: String, $ref: String!, $path: String, $after: String, $first: Int) {
+ repository(ref: $repo) {
+ commits(ref: $ref, path: $path, after: $after, first: $first) {
+ nodes {
+ hash
+ shortHash
+ message
+ authorName
+ date
+ }
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ }
+ }
+ }
+`
+
+const PAGE_SIZE = 30
+
interface CommitListProps {
ref_: string
path?: string
}
-const PAGE_SIZE = 30
+type CommitNode = {
+ hash: string
+ shortHash: string
+ message: string
+ authorName: string
+ date: string
+}
-// Paginated commit history grouped by calendar date. Each row links to the
-// commit detail page. Used in CodePage's "History" view.
export function CommitList({ ref_, path }: CommitListProps) {
const repo = useRepo()
- const [commits, setCommits] = useState<GitCommitType[]>([])
- const [loading, setLoading] = useState(true)
+ const [cursor, setCursor] = useState<string | null>(null)
+ const [allCommits, setAllCommits] = useState<CommitNode[]>([])
+
+ const { loading, error, fetchMore } = useQuery(COMMITS_QUERY, {
+ variables: { repo, ref: ref_, path: path ?? null, after: null, first: PAGE_SIZE },
+ skip: !ref_,
+ onCompleted(data) {
+ const nodes = data?.repository?.commits?.nodes ?? []
+ setAllCommits(nodes)
+ setCursor(data?.repository?.commits?.pageInfo?.endCursor ?? null)
+ },
+ })
+
+ const hasMore = !!cursor && allCommits.length > 0 && allCommits.length % PAGE_SIZE === 0
const [loadingMore, setLoadingMore] = useState(false)
- const [error, setError] = useState<string | null>(null)
- const [hasMore, setHasMore] = useState(true)
-
- useEffect(() => {
- setCommits([])
- setHasMore(true)
- setError(null)
- setLoading(true)
- getCommits(ref_, { path, limit: PAGE_SIZE })
- .then((data) => {
- setCommits(data)
- setHasMore(data.length === PAGE_SIZE)
- })
- .catch((e: Error) => setError(e.message))
- .finally(() => setLoading(false))
- }, [ref_, path])
function loadMore() {
- const last = commits[commits.length - 1]
- if (!last) return
+ if (!cursor) return
setLoadingMore(true)
- getCommits(ref_, { path, limit: PAGE_SIZE, after: last.hash })
- .then((data) => {
- setCommits((prev) => [...prev, ...data])
- setHasMore(data.length === PAGE_SIZE)
- })
- .catch((e: Error) => setError(e.message))
- .finally(() => setLoadingMore(false))
+ fetchMore({
+ variables: { after: cursor },
+ }).then((result) => {
+ const newNodes = result.data?.repository?.commits?.nodes ?? []
+ setAllCommits((prev) => [...prev, ...newNodes])
+ setCursor(result.data?.repository?.commits?.pageInfo?.endCursor ?? null)
+ }).finally(() => setLoadingMore(false))
}
if (loading) return <CommitListSkeleton />
@@ -57,13 +80,12 @@ export function CommitList({ ref_, path }: CommitListProps) {
if (error) {
return (
<div className="rounded-md border border-border px-4 py-8 text-center text-sm text-destructive">
- {error}
+ {error.message}
</div>
)
}
- // Group commits by date (YYYY-MM-DD)
- const groups = groupByDate(commits)
+ const groups = groupByDate(allCommits)
return (
<div className="space-y-6">
@@ -91,12 +113,11 @@ export function CommitList({ ref_, path }: CommitListProps) {
)
}
-function CommitRow({ commit, repo }: { commit: GitCommitType; repo: string | null }) {
+function CommitRow({ commit, repo }: { commit: CommitNode; repo: string | null }) {
const commitPath = repo ? `/${repo}/commit/${commit.hash}` : `/commit/${commit.hash}`
return (
<div className="flex items-center gap-3 bg-background px-4 py-3 hover:bg-muted/30">
<GitCommit className="size-4 shrink-0 text-muted-foreground" />
-
<div className="min-w-0 flex-1">
<Link
to={commitPath}
@@ -109,7 +130,6 @@ function CommitRow({ commit, repo }: { commit: GitCommitType; repo: string | nul
{formatDistanceToNow(new Date(commit.date), { addSuffix: true })}
</p>
</div>
-
<Link
to={commitPath}
className="shrink-0 font-mono text-xs text-muted-foreground hover:text-foreground hover:underline"
@@ -121,8 +141,8 @@ function CommitRow({ commit, repo }: { commit: GitCommitType; repo: string | nul
)
}
-function groupByDate(commits: GitCommitType[]): [string, GitCommitType[]][] {
- const map = new Map<string, GitCommitType[]>()
+function groupByDate(commits: CommitNode[]): [string, CommitNode[]][] {
+ const map = new Map<string, CommitNode[]>()
for (const c of commits) {
const date = new Date(c.date).toLocaleDateString('en-US', {
year: 'numeric',
@@ -1,48 +1,71 @@
// Collapsible diff view for a single file in a commit.
-// Diff is fetched lazily on first expand to avoid loading large diffs upfront.
+// Diff is fetched lazily on first expand via GraphQL.
import { useState } from 'react'
import { ChevronRight, FilePlus, FileMinus, FileEdit } from 'lucide-react'
+import { gql, useLazyQuery } from '@apollo/client'
import { cn } from '@/lib/utils'
-import { getCommitDiff } from '@/lib/gitApi'
-import type { FileDiff, DiffHunk } from '@/lib/gitApi'
+import { useRepo } from '@/lib/repo'
+
+const DIFF_QUERY = gql`
+ query FileDiff($repo: String, $hash: String!, $path: String!) {
+ repository(ref: $repo) {
+ commit(hash: $hash) {
+ diff(path: $path) {
+ path
+ oldPath
+ isBinary
+ isNew
+ isDelete
+ hunks {
+ oldStart
+ oldLines
+ newStart
+ newLines
+ lines {
+ type
+ content
+ oldLine
+ newLine
+ }
+ }
+ }
+ }
+ }
+ }
+`
interface FileDiffViewProps {
- sha: string
+ hash: string
path: string
oldPath?: string
- status: 'added' | 'modified' | 'deleted' | 'renamed'
+ status: string
}
-const statusIcon = {
- added: <FilePlus className="size-3.5 text-green-600 dark:text-green-400" />,
- deleted: <FileMinus className="size-3.5 text-red-500 dark:text-red-400" />,
- modified: <FileEdit className="size-3.5 text-yellow-500 dark:text-yellow-400" />,
- renamed: <FileEdit className="size-3.5 text-blue-500 dark:text-blue-400" />,
+const statusIcon: Record<string, React.ReactNode> = {
+ ADDED: <FilePlus className="size-3.5 text-green-600 dark:text-green-400" />,
+ DELETED: <FileMinus className="size-3.5 text-red-500 dark:text-red-400" />,
+ MODIFIED: <FileEdit className="size-3.5 text-yellow-500 dark:text-yellow-400" />,
+ RENAMED: <FileEdit className="size-3.5 text-blue-500 dark:text-blue-400" />,
}
+const statusBadge: Record<string, string> = { ADDED: 'A', DELETED: 'D', MODIFIED: 'M', RENAMED: 'R' }
-const statusBadge = { added: 'A', deleted: 'D', modified: 'M', renamed: 'R' }
-
-export function FileDiffView({ sha, path, oldPath, status }: FileDiffViewProps) {
+export function FileDiffView({ hash, path, oldPath, status }: FileDiffViewProps) {
+ const repo = useRepo()
const [open, setOpen] = useState(false)
- const [diff, setDiff] = useState<FileDiff | null>(null)
- const [loading, setLoading] = useState(false)
- const [error, setError] = useState<string | null>(null)
+ const [fetchDiff, { data, loading, error }] = useLazyQuery(DIFF_QUERY)
function toggle() {
- if (!open && diff === null && !loading) {
- setLoading(true)
- getCommitDiff(sha, path)
- .then(setDiff)
- .catch((e: Error) => setError(e.message))
- .finally(() => setLoading(false))
+ if (!open && !data && !loading) {
+ fetchDiff({ variables: { repo, hash, path } })
}
setOpen((v) => !v)
}
+ const diff = data?.repository?.commit?.diff
+
return (
<div className="divide-y divide-border">
- {/* File header row — always visible, click to toggle */}
<button
onClick={toggle}
className="flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-muted/50 transition-colors"
@@ -53,9 +76,9 @@ export function FileDiffView({ sha, path, oldPath, status }: FileDiffViewProps)
open && 'rotate-90',
)}
/>
- {statusIcon[status]}
+ {statusIcon[status] ?? <FileEdit className="size-3.5 text-muted-foreground" />}
<span className="min-w-0 flex-1 font-mono text-sm">
- {status === 'renamed' ? (
+ {status === 'RENAMED' ? (
<>
<span className="text-muted-foreground line-through">{oldPath}</span>
{' → '}
@@ -64,18 +87,17 @@ export function FileDiffView({ sha, path, oldPath, status }: FileDiffViewProps)
) : path}
</span>
<span className="shrink-0 rounded border border-border px-1.5 py-0.5 font-mono text-xs text-muted-foreground">
- {statusBadge[status]}
+ {statusBadge[status] ?? '?'}
</span>
</button>
- {/* Diff body */}
{open && (
<div className="overflow-x-auto">
{loading && (
<div className="px-4 py-3 text-xs text-muted-foreground">Loading diff…</div>
)}
{error && (
- <div className="px-4 py-3 text-xs text-destructive">Failed to load diff: {error}</div>
+ <div className="px-4 py-3 text-xs text-destructive">Failed to load diff: {error.message}</div>
)}
{diff && (
diff.isBinary ? (
@@ -83,7 +105,7 @@ export function FileDiffView({ sha, path, oldPath, status }: FileDiffViewProps)
) : diff.hunks.length === 0 ? (
<div className="px-4 py-3 text-xs text-muted-foreground">No changes</div>
) : (
- diff.hunks.map((hunk, i) => <Hunk key={i} hunk={hunk} />)
+ diff.hunks.map((hunk: HunkType, i: number) => <Hunk key={i} hunk={hunk} />)
)
)}
</div>
@@ -92,10 +114,12 @@ export function FileDiffView({ sha, path, oldPath, status }: FileDiffViewProps)
)
}
-function Hunk({ hunk }: { hunk: DiffHunk }) {
+type LineType = { type: string; content: string; oldLine: number; newLine: number }
+type HunkType = { oldStart: number; oldLines: number; newStart: number; newLines: number; lines: LineType[] }
+
+function Hunk({ hunk }: { hunk: HunkType }) {
return (
<div className="font-mono text-xs leading-5">
- {/* Hunk header */}
<div className="bg-blue-50 px-4 py-0.5 text-blue-600 dark:bg-blue-950/40 dark:text-blue-400 select-none">
@@ -{hunk.oldStart},{hunk.oldLines} +{hunk.newStart},{hunk.newLines} @@
</div>
@@ -104,32 +128,28 @@ function Hunk({ hunk }: { hunk: DiffHunk }) {
key={i}
className={cn(
'flex',
- line.type === 'added' && 'bg-green-50 dark:bg-green-950/30',
- line.type === 'deleted' && 'bg-red-50 dark:bg-red-950/30',
+ line.type === 'ADDED' && 'bg-green-50 dark:bg-green-950/30',
+ line.type === 'DELETED' && 'bg-red-50 dark:bg-red-950/30',
)}
>
- {/* Old line number */}
<span className="w-10 shrink-0 select-none border-r border-border/50 px-2 text-right text-muted-foreground/50">
{line.oldLine || ''}
</span>
- {/* New line number */}
<span className="w-10 shrink-0 select-none border-r border-border/50 px-2 text-right text-muted-foreground/50">
{line.newLine || ''}
</span>
- {/* Sign */}
<span className={cn(
'w-5 shrink-0 select-none text-center',
- line.type === 'added' && 'text-green-600 dark:text-green-400',
- line.type === 'deleted' && 'text-red-500 dark:text-red-400',
- line.type === 'context' && 'text-muted-foreground/40',
+ line.type === 'ADDED' && 'text-green-600 dark:text-green-400',
+ line.type === 'DELETED' && 'text-red-500 dark:text-red-400',
+ line.type === 'CONTEXT' && 'text-muted-foreground/40',
)}>
- {line.type === 'added' ? '+' : line.type === 'deleted' ? '-' : ' '}
+ {line.type === 'ADDED' ? '+' : line.type === 'DELETED' ? '-' : ' '}
</span>
- {/* Content */}
<pre className={cn(
'flex-1 overflow-visible whitespace-pre px-2',
- line.type === 'added' && 'text-green-900 dark:text-green-200',
- line.type === 'deleted' && 'text-red-900 dark:text-red-200',
+ line.type === 'ADDED' && 'text-green-900 dark:text-green-200',
+ line.type === 'DELETED' && 'text-red-900 dark:text-red-200',
)}>
{line.content}
</pre>
@@ -3,13 +3,22 @@ import { Link } from 'react-router-dom'
import { formatDistanceToNow } from 'date-fns'
import { Skeleton } from '@/components/ui/skeleton'
import { useRepo } from '@/lib/repo'
-import type { GitTreeEntry } from '@/lib/gitApi'
+import type { GitTreeEntry } from '@/__generated__/graphql'
+
+export interface TreeEntryWithCommit extends GitTreeEntry {
+ lastCommit?: {
+ hash: string
+ shortHash: string
+ message: string
+ date: string
+ }
+}
interface FileTreeProps {
- entries: GitTreeEntry[]
+ entries: TreeEntryWithCommit[]
path: string
loading?: boolean
- onNavigate: (entry: GitTreeEntry) => void
+ onNavigate: (entry: TreeEntryWithCommit) => void
onNavigateUp: () => void
}
@@ -18,7 +27,7 @@ interface FileTreeProps {
export function FileTree({ entries, path, loading, onNavigate, onNavigateUp }: FileTreeProps) {
// Directories first, then files — each group alphabetical
const sorted = [...entries].sort((a, b) => {
- if (a.type !== b.type) return a.type === 'tree' ? -1 : 1
+ if (a.type !== b.type) return a.type === 'TREE' ? -1 : 1
return a.name.localeCompare(b.name)
})
@@ -54,10 +63,10 @@ function FileTreeRow({
entry,
onNavigate,
}: {
- entry: GitTreeEntry
- onNavigate: (entry: GitTreeEntry) => void
+ entry: TreeEntryWithCommit
+ onNavigate: (entry: TreeEntryWithCommit) => void
}) {
- const isDir = entry.type === 'tree'
+ const isDir = entry.type === 'TREE'
const repo = useRepo()
return (
@@ -1,23 +1,22 @@
+// Syntax-highlighted file viewer with line numbers and copy button.
+// highlight.js is loaded lazily so it doesn't bloat the initial bundle.
+
import { useState, useEffect } from 'react'
-import { Copy, Download } from 'lucide-react'
+import { Copy } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
-import { getRawUrl } from '@/lib/gitApi'
-import type { GitBlob } from '@/lib/gitApi'
+import type { GitBlob } from '@/__generated__/graphql'
interface FileViewerProps {
blob: GitBlob
- ref: string
loading?: boolean
}
-// Syntax-highlighted file viewer with line numbers, copy, and download buttons.
-// highlight.js is loaded lazily (dynamic import) so it doesn't bloat the initial bundle.
-export function FileViewer({ blob, ref, loading }: FileViewerProps) {
+export function FileViewer({ blob, loading }: FileViewerProps) {
const [highlighted, setHighlighted] = useState<{ html: string; lineCount: number } | null>(null)
useEffect(() => {
- if (blob.isBinary || !blob.content) {
+ if (blob.isBinary || !blob.text) {
setHighlighted({ html: '', lineCount: 0 })
return
}
@@ -27,11 +26,11 @@ export function FileViewer({ blob, ref, loading }: FileViewerProps) {
if (cancelled) return
const ext = blob.path.split('.').pop() ?? ''
const result = hljs.getLanguage(ext)
- ? hljs.highlight(blob.content, { language: ext })
- : hljs.highlightAuto(blob.content)
+ ? hljs.highlight(blob.text!, { language: ext })
+ : hljs.highlightAuto(blob.text!)
setHighlighted({
html: result.value,
- lineCount: blob.content.split('\n').length,
+ lineCount: blob.text!.split('\n').length,
})
})
return () => { cancelled = true }
@@ -41,32 +40,19 @@ export function FileViewer({ blob, ref, loading }: FileViewerProps) {
const { html, lineCount } = highlighted
function copyToClipboard() {
- navigator.clipboard.writeText(blob.content)
+ if (blob.text) navigator.clipboard.writeText(blob.text)
}
return (
<div className="overflow-hidden rounded-md border border-border">
- {/* Metadata bar */}
<div className="flex items-center justify-between border-b border-border bg-muted/40 px-4 py-2 text-xs text-muted-foreground">
<span>
{lineCount.toLocaleString()} lines · {formatBytes(blob.size)}
+ {blob.isTruncated && ' · truncated'}
</span>
- <div className="flex items-center gap-1">
- <Button
- variant="ghost"
- size="icon"
- className="size-7"
- onClick={copyToClipboard}
- title="Copy"
- >
- <Copy className="size-3.5" />
- </Button>
- <Button variant="ghost" size="icon" className="size-7" asChild title="Download">
- <a href={getRawUrl(ref, blob.path)} download>
- <Download className="size-3.5" />
- </a>
- </Button>
- </div>
+ <Button variant="ghost" size="icon" className="size-7" onClick={copyToClipboard} title="Copy">
+ <Copy className="size-3.5" />
+ </Button>
</div>
{blob.isBinary ? (
@@ -74,8 +60,6 @@ export function FileViewer({ blob, ref, loading }: FileViewerProps) {
Binary file — {formatBytes(blob.size)}
</div>
) : (
- // Line numbers are a fixed column; code scrolls horizontally independently.
- // Keeping them in separate divs avoids having to split highlighted HTML by line.
<div className="flex overflow-x-auto font-mono text-xs leading-5">
<div
className="select-none border-r border-border bg-muted/20 px-4 py-4 text-right text-muted-foreground/50"
@@ -86,10 +70,7 @@ export function FileViewer({ blob, ref, loading }: FileViewerProps) {
))}
</div>
<pre className="flex-1 overflow-visible px-4 py-4">
- <code
- className="hljs"
- dangerouslySetInnerHTML={{ __html: html }}
- />
+ <code className="hljs" dangerouslySetInnerHTML={{ __html: html }} />
</pre>
</div>
)}
@@ -3,7 +3,7 @@ import { GitBranch, Tag, Check, ChevronsUpDown } from 'lucide-react'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
-import type { GitRef } from '@/lib/gitApi'
+import type { GitRef } from '@/__generated__/graphql'
import { cn } from '@/lib/utils'
interface RefSelectorProps {
@@ -21,8 +21,8 @@ export function RefSelector({ refs, currentRef, onSelect }: RefSelectorProps) {
const filtered = refs.filter((r) =>
r.shortName.toLowerCase().includes(filter.toLowerCase()),
)
- const branches = filtered.filter((r) => r.type === 'branch')
- const tags = filtered.filter((r) => r.type === 'tag')
+ const branches = filtered.filter((r) => r.type === 'BRANCH')
+ const tags = filtered.filter((r) => r.type === 'TAG')
return (
<Popover open={open} onOpenChange={setOpen}>
@@ -95,7 +95,7 @@ function RefItem({
active && 'font-medium',
)}
>
- {ref_.type === 'branch' ? (
+ {ref_.type === 'BRANCH' ? (
<GitBranch className="size-3 shrink-0 text-muted-foreground" />
) : (
<Tag className="size-3 shrink-0 text-muted-foreground" />
@@ -1,127 +0,0 @@
-// REST API client for git repository browsing.
-// Endpoints are served by the Go backend under /api/repos/{owner}/{repo}/git/*.
-// "_" is the wildcard value for both owner and repo (resolves to local / default).
-
-const BASE = '/api/repos/_/_'
-
-export interface GitRef {
- name: string // full ref: "refs/heads/main"
- shortName: string // "main"
- type: 'branch' | 'tag'
- hash: string
- isDefault: boolean
-}
-
-export interface GitTreeEntry {
- name: string
- type: 'tree' | 'blob'
- hash: string
- mode: string
- // Last commit touching this entry (may be absent if expensive to compute)
- lastCommit?: {
- hash: string
- shortHash: string
- message: string
- authorName: string
- date: string
- }
-}
-
-export interface GitBlob {
- path: string
- content: string // UTF-8 text; empty string when isBinary is true
- size: number
- isBinary: boolean
-}
-
-export interface GitCommit {
- hash: string
- shortHash: string
- message: string
- authorName: string
- authorEmail: string
- date: string
- parents: string[]
-}
-
-export interface GitCommitDetail extends GitCommit {
- fullMessage: string
- files: Array<{
- path: string
- oldPath?: string
- status: 'added' | 'modified' | 'deleted' | 'renamed'
- }>
-}
-
-export interface DiffLine {
- type: 'context' | 'added' | 'deleted'
- content: string
- oldLine: number
- newLine: number
-}
-
-export interface DiffHunk {
- oldStart: number
- oldLines: number
- newStart: number
- newLines: number
- lines: DiffLine[]
-}
-
-export interface FileDiff {
- path: string
- oldPath?: string
- isBinary: boolean
- isNew: boolean
- isDelete: boolean
- hunks: DiffHunk[]
-}
-
-// ── Fetch helpers ─────────────────────────────────────────────────────────────
-
-async function get<T>(path: string, params: Record<string, string> = {}): Promise<T> {
- const search = new URLSearchParams(params).toString()
- const url = `${BASE}${path}${search ? `?${search}` : ''}`
- const res = await fetch(url, { credentials: 'include' })
- if (!res.ok) {
- const text = await res.text().catch(() => res.statusText)
- throw new Error(text || res.statusText)
- }
- return res.json()
-}
-
-// ── API calls ─────────────────────────────────────────────────────────────────
-
-export function getRefs(): Promise<GitRef[]> {
- return get('/git/refs')
-}
-
-export function getTree(ref: string, path: string): Promise<GitTreeEntry[]> {
- return get(`/git/trees/${encodeURIComponent(ref)}`, path ? { path } : {})
-}
-
-export function getBlob(ref: string, path: string): Promise<GitBlob> {
- return get(`/git/blobs/${encodeURIComponent(ref)}`, { path })
-}
-
-export function getRawUrl(ref: string, path: string): string {
- return `${BASE}/git/raw/${encodeURIComponent(ref)}/${path}`
-}
-
-export function getCommits(
- ref: string,
- opts: { path?: string; limit?: number; after?: string } = {},
-): Promise<GitCommit[]> {
- const params: Record<string, string> = { ref, limit: String(opts.limit ?? 20) }
- if (opts.path) params.path = opts.path
- if (opts.after) params.after = opts.after
- return get('/git/commits', params)
-}
-
-export function getCommit(sha: string): Promise<GitCommitDetail> {
- return get(`/git/commits/${sha}`)
-}
-
-export function getCommitDiff(sha: string, path: string): Promise<FileDiff> {
- return get(`/git/commits/${sha}/diff`, { path })
-}
@@ -1,7 +1,10 @@
-import { useState, useEffect } from 'react'
+// Code browser page. Switches between tree view, file viewer, and commit
+// history via ?type= search param. Ref is selected via ?ref=.
+
+import { useEffect } from 'react'
import { useSearchParams } from 'react-router-dom'
import { gql, useQuery } from '@apollo/client'
-import { AlertCircle, Check, Copy, GitCommit } from 'lucide-react'
+import { AlertCircle, GitCommit } from 'lucide-react'
import { CodeBreadcrumb } from '@/components/code/CodeBreadcrumb'
import { RefSelector } from '@/components/code/RefSelector'
import { FileTree } from '@/components/code/FileTree'
@@ -9,103 +12,147 @@ import { FileViewer } from '@/components/code/FileViewer'
import { CommitList } from '@/components/code/CommitList'
import { Skeleton } from '@/components/ui/skeleton'
import { Button } from '@/components/ui/button'
-import { getRefs, getTree, getBlob } from '@/lib/gitApi'
-import type { GitRef, GitTreeEntry, GitBlob } from '@/lib/gitApi'
import { useRepo } from '@/lib/repo'
import { Markdown } from '@/components/content/Markdown'
+import type { GitRef, GitTreeEntry, GitBlob, GitLastCommit } from '@/__generated__/graphql'
+import type { TreeEntryWithCommit } from '@/components/code/FileTree'
-const REPO_NAME_QUERY = gql`
- query RepoName($ref: String) {
- repository(ref: $ref) {
+const REFS_QUERY = gql`
+ query CodePageRefs($repo: String) {
+ repository(ref: $repo) {
name
+ refs {
+ nodes {
+ name
+ shortName
+ type
+ hash
+ isDefault
+ }
+ }
+ }
+ }
+`
+
+const TREE_QUERY = gql`
+ query CodePageTree($repo: String, $ref: String!, $path: String) {
+ repository(ref: $repo) {
+ tree(ref: $ref, path: $path) {
+ name
+ type
+ hash
+ }
+ }
+ }
+`
+
+const LAST_COMMITS_QUERY = gql`
+ query CodePageLastCommits($repo: String, $ref: String!, $path: String, $names: [String!]!) {
+ repository(ref: $repo) {
+ lastCommits(ref: $ref, path: $path, names: $names) {
+ name
+ commit {
+ hash
+ shortHash
+ message
+ date
+ }
+ }
+ }
+ }
+`
+
+const BLOB_QUERY = gql`
+ query CodePageBlob($repo: String, $ref: String!, $path: String!) {
+ repository(ref: $repo) {
+ blob(ref: $ref, path: $path) {
+ path
+ hash
+ text
+ size
+ isBinary
+ isTruncated
+ }
}
}
`
type ViewMode = 'tree' | 'blob' | 'commits'
-// Code browser page (/:repo). Switches between tree view, file viewer, and
-// commit history via the ?type= search param. Ref is selected via ?ref=.
export function CodePage() {
const repo = useRepo()
const [searchParams, setSearchParams] = useSearchParams()
- const [refs, setRefs] = useState<GitRef[]>([])
- const [refsLoading, setRefsLoading] = useState(true)
- const [error, setError] = useState<string | null>(null)
-
- const [entries, setEntries] = useState<GitTreeEntry[]>([])
- const [blob, setBlob] = useState<GitBlob | null>(null)
- const [readme, setReadme] = useState<string | null>(null)
- const [contentLoading, setContentLoading] = useState(false)
-
const currentRef = searchParams.get('ref') ?? ''
const currentPath = searchParams.get('path') ?? ''
const viewMode: ViewMode = (searchParams.get('type') as ViewMode) ?? 'tree'
- // Load refs once on mount
- useEffect(() => {
- getRefs()
- .then((data) => {
- setRefs(data)
- // If no ref in URL yet, use the default branch
- if (!searchParams.get('ref')) {
- const defaultRef = data.find((r) => r.isDefault) ?? data[0]
- if (defaultRef) {
- setSearchParams(
- (prev) => { prev.set('ref', defaultRef.shortName); return prev },
- { replace: true },
- )
- }
- }
- })
- .catch((e: Error) => setError(e.message))
- .finally(() => setRefsLoading(false))
- }, []) // eslint-disable-line react-hooks/exhaustive-deps
+ const { data: refsData, loading: refsLoading, error: refsError } = useQuery(REFS_QUERY, {
+ variables: { repo },
+ })
+ const refs: GitRef[] = refsData?.repository?.refs?.nodes ?? []
- // Load tree or blob when ref/path/mode changes
+ // Set default ref from query result once loaded
useEffect(() => {
- if (!currentRef) return
- setContentLoading(true)
- setEntries([])
- setBlob(null)
- setReadme(null)
-
- const load =
- viewMode === 'blob'
- ? getBlob(currentRef, currentPath).then((b) => setBlob(b))
- : getTree(currentRef, currentPath).then((e) => {
- setEntries(e)
- const readmeEntry = e.find((entry) =>
- entry.type === 'blob' &&
- /^readme(\.md|\.txt|\.rst)?$/i.test(entry.name),
- )
- if (readmeEntry) {
- const readmePath = currentPath
- ? `${currentPath}/${readmeEntry.name}`
- : readmeEntry.name
- getBlob(currentRef, readmePath)
- .then((b) => !b.isBinary && setReadme(b.content))
- .catch(() => {/* best-effort */})
- }
- })
-
- load
- .catch((e: Error) => setError(e.message))
- .finally(() => setContentLoading(false))
- }, [currentRef, currentPath, viewMode])
+ if (refsLoading || refs.length === 0 || searchParams.get('ref')) return
+ const defaultRef = refs.find((r: GitRef) => r.isDefault) ?? refs[0]
+ if (defaultRef) {
+ setSearchParams(
+ (prev) => { prev.set('ref', defaultRef.shortName); return prev },
+ { replace: true },
+ )
+ }
+ }, [refsLoading, refs.length]) // eslint-disable-line react-hooks/exhaustive-deps
+
+ const inTreeMode = viewMode === 'tree' && !!currentRef
+ const inBlobMode = viewMode === 'blob' && !!currentRef && !!currentPath
+
+ const { data: treeData, loading: treeLoading } = useQuery(TREE_QUERY, {
+ variables: { repo, ref: currentRef, path: currentPath || null },
+ skip: !inTreeMode,
+ })
+ const entries: GitTreeEntry[] = treeData?.repository?.tree ?? []
+
+ const entryNames = entries.map((e: GitTreeEntry) => e.name)
+ const { data: lastCommitsData } = useQuery(LAST_COMMITS_QUERY, {
+ variables: { repo, ref: currentRef, path: currentPath || null, names: entryNames },
+ skip: !inTreeMode || entryNames.length === 0,
+ })
+ const lastCommitsByName = new Map<string, GitLastCommit>(
+ (lastCommitsData?.repository?.lastCommits ?? []).map((lc: GitLastCommit) => [lc.name, lc]),
+ )
+ const entriesWithCommits: TreeEntryWithCommit[] = entries.map((e: GitTreeEntry) => ({
+ ...e,
+ lastCommit: lastCommitsByName.get(e.name)?.commit ?? undefined,
+ }))
+
+ const { data: blobData, loading: blobLoading } = useQuery(BLOB_QUERY, {
+ variables: { repo, ref: currentRef, path: currentPath },
+ skip: !inBlobMode,
+ })
+ const blob: GitBlob | null = blobData?.repository?.blob ?? null
+
+ const readmeEntry = entries.find(
+ (e: GitTreeEntry) => e.type === 'BLOB' && /^readme(\.md|\.txt|\.rst)?$/i.test(e.name),
+ )
+ const readmePath = readmeEntry
+ ? (currentPath ? `${currentPath}/${readmeEntry.name}` : readmeEntry.name)
+ : null
+ const { data: readmeBlobData } = useQuery(BLOB_QUERY, {
+ variables: { repo, ref: currentRef, path: readmePath },
+ skip: !inTreeMode || !readmePath,
+ })
+ const readme: string | null = readmeBlobData?.repository?.blob?.text ?? null
+
+ const repoName = refsData?.repository?.name ?? repo ?? 'default-repo'
function navigate(path: string, type: ViewMode = 'tree') {
- setSearchParams((prev) => {
- prev.set('path', path)
- prev.set('type', type)
- return prev
- })
+ setSearchParams((prev) => { prev.set('path', path); prev.set('type', type); return prev })
}
- function handleEntryClick(entry: GitTreeEntry) {
+ function handleEntryClick(entry: TreeEntryWithCommit) {
const newPath = currentPath ? `${currentPath}/${entry.name}` : entry.name
- navigate(newPath, entry.type === 'blob' ? 'blob' : 'tree')
+ navigate(newPath, entry.type === 'BLOB' ? 'blob' : 'tree')
}
function handleNavigateUp() {
@@ -116,42 +163,22 @@ export function CodePage() {
function handleRefSelect(ref: GitRef) {
setSearchParams((prev) => {
- prev.set('ref', ref.shortName)
- prev.set('path', '')
- prev.set('type', 'tree')
- return prev
+ prev.set('ref', ref.shortName); prev.set('path', ''); prev.set('type', 'tree'); return prev
})
}
- const { data: repoData } = useQuery(REPO_NAME_QUERY, { variables: { ref: repo } })
- const repoName = repoData?.repository?.name ?? repo ?? 'default-repo'
-
- const cloneUrl = `${window.location.origin}/api/repos/_/_`
- const cloneCmd = `git clone ${cloneUrl} ${repoName}`
- const [copied, setCopied] = useState(false)
- function handleCopy() {
- navigator.clipboard.writeText(cloneCmd).then(() => {
- setCopied(true)
- setTimeout(() => setCopied(false), 1500)
- })
- }
-
- if (error) {
+ if (refsError) {
return (
<div className="flex flex-col items-center gap-3 py-16 text-center">
<AlertCircle className="size-8 text-muted-foreground" />
<p className="text-sm font-medium">Code browser unavailable</p>
- <p className="max-w-sm text-xs text-muted-foreground">{error}</p>
- <p className="max-w-sm text-xs text-muted-foreground">
- Make sure the git-bug server is running and the repository supports code browsing.
- </p>
+ <p className="max-w-sm text-xs text-muted-foreground">{refsError.message}</p>
</div>
)
}
return (
<div className="space-y-4">
- {/* Top bar: breadcrumb + ref selector */}
<div className="flex flex-wrap items-center justify-between gap-3">
{refsLoading ? (
<Skeleton className="h-5 w-48" />
@@ -168,12 +195,7 @@ export function CodePage() {
<Button
variant={viewMode === 'commits' ? 'secondary' : 'outline'}
size="sm"
- onClick={() =>
- navigate(
- currentPath,
- viewMode === 'commits' ? 'tree' : 'commits',
- )
- }
+ onClick={() => navigate(currentPath, viewMode === 'commits' ? 'tree' : 'commits')}
>
<GitCommit className="size-3.5" />
History
@@ -187,24 +209,14 @@ export function CodePage() {
</div>
</div>
- {/* Clone command */}
- <div className="flex items-center gap-2 rounded-md border bg-muted/40 px-3 py-1.5">
- <span className="text-xs text-muted-foreground shrink-0">clone</span>
- <code className="flex-1 truncate text-xs">{cloneCmd}</code>
- <Button variant="ghost" size="icon" className="size-6 shrink-0" onClick={handleCopy}>
- {copied ? <Check className="size-3 text-green-600" /> : <Copy className="size-3" />}
- </Button>
- </div>
-
- {/* Content */}
{viewMode === 'commits' ? (
<CommitList ref_={currentRef} path={currentPath || undefined} />
) : viewMode === 'tree' || !blob ? (
<>
<FileTree
- entries={entries}
+ entries={entriesWithCommits}
path={currentPath}
- loading={contentLoading}
+ loading={treeLoading}
onNavigate={handleEntryClick}
onNavigateUp={handleNavigateUp}
/>
@@ -220,7 +232,7 @@ export function CodePage() {
)}
</>
) : (
- <FileViewer blob={blob} ref={currentRef} loading={contentLoading} />
+ <FileViewer blob={blob} loading={blobLoading} />
)}
</div>
)
@@ -1,45 +1,63 @@
-import { useState, useEffect } from 'react'
+// Commit detail page (/:repo/commit/:hash). Shows commit metadata, full
+// message, parent links, and changed files with lazy diffs.
+
import { Link, useParams, useNavigate } from 'react-router-dom'
import { format } from 'date-fns'
import { ArrowLeft, GitCommit } from 'lucide-react'
+import { gql, useQuery } from '@apollo/client'
import { Skeleton } from '@/components/ui/skeleton'
-import { getCommit } from '@/lib/gitApi'
-import type { GitCommitDetail } from '@/lib/gitApi'
import { useRepo } from '@/lib/repo'
import { FileDiffView } from '@/components/code/FileDiffView'
-// Commit detail page (/:repo/commit/:hash). Shows commit metadata, full message,
-// parent links, and the list of files changed with add/modify/delete/rename status.
+const COMMIT_QUERY = gql`
+ query CommitPageDetail($repo: String, $hash: String!) {
+ repository(ref: $repo) {
+ commit(hash: $hash) {
+ hash
+ shortHash
+ message
+ fullMessage
+ authorName
+ authorEmail
+ date
+ parents
+ files {
+ nodes {
+ path
+ oldPath
+ status
+ }
+ }
+ }
+ }
+ }
+`
+
export function CommitPage() {
const { hash } = useParams<{ hash: string }>()
const navigate = useNavigate()
const repo = useRepo()
- const [commit, setCommit] = useState<GitCommitDetail | null>(null)
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState<string | null>(null)
- useEffect(() => {
- setLoading(true)
- setError(null)
- getCommit(hash!)
- .then(setCommit)
- .catch((e: Error) => setError(e.message))
- .finally(() => setLoading(false))
- }, [hash])
+ const { data, loading, error } = useQuery(COMMIT_QUERY, {
+ variables: { repo, hash },
+ skip: !hash,
+ })
if (loading) return <CommitPageSkeleton />
if (error) {
return (
<div className="py-16 text-center text-sm text-destructive">
- Failed to load commit: {error}
+ Failed to load commit: {error.message}
</div>
)
}
+ const commit = data?.repository?.commit
if (!commit) return null
const date = new Date(commit.date)
+ const files = commit.files?.nodes ?? []
return (
<div>
@@ -51,14 +69,12 @@ export function CommitPage() {
Back
</button>
- {/* Header */}
<div className="mb-6 rounded-md border border-border p-5">
<div className="mb-1 flex items-start gap-3">
<GitCommit className="mt-1 size-5 shrink-0 text-muted-foreground" />
<h1 className="text-lg font-semibold leading-snug">{commit.message}</h1>
</div>
- {/* Full message body (if multi-line) */}
{commit.fullMessage.includes('\n') && (
<pre className="mb-4 ml-8 mt-3 whitespace-pre-wrap font-sans text-sm text-muted-foreground">
{commit.fullMessage.split('\n').slice(1).join('\n').trim()}
@@ -68,19 +84,16 @@ export function CommitPage() {
<div className="ml-8 mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground">
<span>
<span className="font-medium text-foreground">{commit.authorName}</span>
- {commit.authorEmail && (
- <span> <{commit.authorEmail}></span>
- )}
+ {commit.authorEmail && <span> <{commit.authorEmail}></span>}
</span>
<span title={date.toISOString()}>{format(date, 'PPP')}</span>
</div>
<div className="ml-8 mt-3 flex flex-wrap gap-3 text-xs">
<span className="text-muted-foreground">
- commit{' '}
- <code className="font-mono text-foreground">{commit.hash}</code>
+ commit <code className="font-mono text-foreground">{commit.hash}</code>
</span>
- {commit.parents.map((p) => (
+ {commit.parents.map((p: string) => (
<span key={p} className="text-muted-foreground">
parent{' '}
<Link
@@ -94,21 +107,20 @@ export function CommitPage() {
</div>
</div>
- {/* Changed files — each row is collapsible and loads its diff lazily */}
<div>
<h2 className="mb-3 text-sm font-semibold text-muted-foreground">
- {commit.files.length} file{commit.files.length !== 1 ? 's' : ''} changed
+ {files.length} file{files.length !== 1 ? 's' : ''} changed
</h2>
<div className="overflow-hidden rounded-md border border-border divide-y divide-border">
- {commit.files.length === 0 && (
+ {files.length === 0 && (
<p className="px-4 py-4 text-sm text-muted-foreground">No file changes.</p>
)}
- {commit.files.map((file) => (
+ {files.map((file: { path: string; oldPath?: string | null; status: string }) => (
<FileDiffView
key={file.path}
- sha={commit.hash}
+ hash={commit.hash}
path={file.path}
- oldPath={file.oldPath}
+ oldPath={file.oldPath ?? undefined}
status={file.status}
/>
))}
@@ -1 +1 @@
@@ -31,8 +31,7 @@ export default defineConfig({
'/graphql': { target: API_URL, changeOrigin: true },
'/gitfile': { target: API_URL, changeOrigin: true },
'/upload': { target: API_URL, changeOrigin: true },
- '/api': { target: API_URL, changeOrigin: true },
- '/auth': { target: API_URL, changeOrigin: true },
+'/auth': { target: API_URL, changeOrigin: true },
},
},
})