diff --git a/api/graphql/graph/git.generated.go b/api/graphql/graph/git.generated.go index 1c1e59ce605b3d71003f7f0fe6060ffc270a7a13..4f719b7dd1cdb6c4bb3c6930b0b3bce6210001dd 100644 --- a/api/graphql/graph/git.generated.go +++ b/api/graphql/graph/git.generated.go @@ -2319,50 +2319,6 @@ func (ec *executionContext) fieldContext_GitRef_hash(_ context.Context, field gr 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 { @@ -2410,8 +2366,6 @@ func (ec *executionContext) fieldContext_GitRefConnection_nodes(_ context.Contex 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) }, @@ -3414,11 +3368,6 @@ func (ec *executionContext) _GitRef(ctx context.Context, sel ast.SelectionSet, o 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)) } diff --git a/api/graphql/graph/repository.generated.go b/api/graphql/graph/repository.generated.go index ca746413d1f33f4bc382f3e92fd6d71f8cd7b543..1ed350b21067bf6bca5e80f932c0871337352724 100644 --- a/api/graphql/graph/repository.generated.go +++ b/api/graphql/graph/repository.generated.go @@ -31,6 +31,7 @@ type RepositoryResolver interface { 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) + Head(ctx context.Context, obj *models.Repository) (*models.GitCommitMeta, error) ValidLabels(ctx context.Context, obj *models.Repository, after *string, before *string, first *int, last *int) (*models.LabelConnection, error) } @@ -1655,6 +1656,69 @@ func (ec *executionContext) fieldContext_Repository_lastCommits(ctx context.Cont return fc, nil } +func (ec *executionContext) _Repository_head(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Repository_head(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().Head(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*models.GitCommitMeta) + fc.Result = res + return ec.marshalOGitCommit2ᚖgithubᚗcomᚋgitᚑbugᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐGitCommitMeta(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Repository_head(_ 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 "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) _Repository_validLabels(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Repository_validLabels(ctx, field) if err != nil { @@ -1833,6 +1897,8 @@ func (ec *executionContext) fieldContext_RepositoryConnection_nodes(_ context.Co return ec.fieldContext_Repository_commit(ctx, field) case "lastCommits": return ec.fieldContext_Repository_lastCommits(ctx, field) + case "head": + return ec.fieldContext_Repository_head(ctx, field) case "validLabels": return ec.fieldContext_Repository_validLabels(ctx, field) } @@ -2047,6 +2113,8 @@ func (ec *executionContext) fieldContext_RepositoryEdge_node(_ context.Context, return ec.fieldContext_Repository_commit(ctx, field) case "lastCommits": return ec.fieldContext_Repository_lastCommits(ctx, field) + case "head": + return ec.fieldContext_Repository_head(ctx, field) case "validLabels": return ec.fieldContext_Repository_validLabels(ctx, field) } @@ -2492,6 +2560,39 @@ func (ec *executionContext) _Repository(ctx context.Context, sel ast.SelectionSe continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "head": + 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._Repository_head(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) }) case "validLabels": field := field diff --git a/api/graphql/graph/root.generated.go b/api/graphql/graph/root.generated.go index 3a487d9862408b03e1365a06f962c7d82d49d507..f98f85e51aac8785a0ff75ace48f40a9395fb04b 100644 --- a/api/graphql/graph/root.generated.go +++ b/api/graphql/graph/root.generated.go @@ -1082,6 +1082,8 @@ func (ec *executionContext) fieldContext_Query_repository(ctx context.Context, f return ec.fieldContext_Repository_commit(ctx, field) case "lastCommits": return ec.fieldContext_Repository_lastCommits(ctx, field) + case "head": + return ec.fieldContext_Repository_head(ctx, field) case "validLabels": return ec.fieldContext_Repository_validLabels(ctx, field) } diff --git a/api/graphql/graph/root_.generated.go b/api/graphql/graph/root_.generated.go index 8d2e2aa1a930023ea1cbb89a694cf09af04ce634..dfccd87bf2f78a2ce5af36870588330de8159713 100644 --- a/api/graphql/graph/root_.generated.go +++ b/api/graphql/graph/root_.generated.go @@ -367,7 +367,6 @@ type ComplexityRoot struct { 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 @@ -479,6 +478,7 @@ type ComplexityRoot struct { 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 + Head func(childComplexity int) int Identity func(childComplexity int, prefix string) int LastCommits func(childComplexity int, ref string, path *string, names []string) int Name func(childComplexity int) int @@ -1821,13 +1821,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin 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 @@ -2354,6 +2347,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin 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.head": + if e.complexity.Repository.Head == nil { + break + } + + return e.complexity.Repository.Head(childComplexity), true + case "Repository.identity": if e.complexity.Repository.Identity == nil { break @@ -3175,8 +3175,6 @@ type GitRef { 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).""" @@ -3564,6 +3562,10 @@ type OperationEdge { tree listing without blocking the initial tree fetch.""" lastCommits(ref: String!, path: String, names: [String!]!): [GitLastCommit!]! + """The currently checked-out commit (branch, tag, hash ...) in the git repository. + Null if there is none (bare repo).""" + head: GitCommit + """List of valid labels.""" validLabels( """Returns the elements in the list that come after the specified cursor.""" diff --git a/api/graphql/models/gen_models.go b/api/graphql/models/gen_models.go index 33b9b4417e326399d36d647478421e4465561982..51e6452862411cc4ef8658e683f5768783f56595 100644 --- a/api/graphql/models/gen_models.go +++ b/api/graphql/models/gen_models.go @@ -320,8 +320,6 @@ type GitRef struct { 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 { diff --git a/api/graphql/resolvers/repo.go b/api/graphql/resolvers/repo.go index 63104480f4a9e3dab8a742babaf87e292dbab132..bc19cf3572d0616a4281dbbfa2c8ef0178c9f31b 100644 --- a/api/graphql/resolvers/repo.go +++ b/api/graphql/resolvers/repo.go @@ -222,7 +222,6 @@ func (repoResolver) Refs(_ context.Context, obj *models.Repository, after *strin ShortName: b.Name, Type: models.GitRefTypeBranch, Hash: string(b.Hash), - IsDefault: b.IsDefault, }) } } @@ -422,3 +421,14 @@ func (repoResolver) LastCommits(_ context.Context, obj *models.Repository, ref s } return result, nil } + +func (repoResolver) Head(_ context.Context, obj *models.Repository) (*models.GitCommitMeta, error) { + meta, err := obj.Repo.BrowseRepo().Head() + if errors.Is(err, repository.ErrNotFound) { + return nil, nil + } + if err != nil { + return nil, err + } + return &models.GitCommitMeta{Repo: obj.Repo, CommitMeta: meta}, nil +} diff --git a/api/graphql/schema/git.graphql b/api/graphql/schema/git.graphql index 3e28db7474acfe15d9d11986b8bac1b85dbea3d6..abae2a7d07ad34936cbbd8092976ad46f06b3867 100644 --- a/api/graphql/schema/git.graphql +++ b/api/graphql/schema/git.graphql @@ -8,8 +8,6 @@ type GitRef { 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).""" diff --git a/api/graphql/schema/repository.graphql b/api/graphql/schema/repository.graphql index 3705d7e715fcf2fb6724f99af8edbb129c8d02d5..3e986a560f317e546e8a8f14e60fd36d50b029d1 100644 --- a/api/graphql/schema/repository.graphql +++ b/api/graphql/schema/repository.graphql @@ -84,6 +84,11 @@ type Repository { tree listing without blocking the initial tree fetch.""" lastCommits(ref: String!, path: String, names: [String!]!): [GitLastCommit!]! + """The commit pointed to by HEAD in the git repository. + Null if HEAD cannot be resolved to a commit, for example in an empty or unborn + repository, or if HEAD is missing or invalid.""" + head: GitCommit + """List of valid labels.""" validLabels( """Returns the elements in the list that come after the specified cursor.""" diff --git a/repository/browse.go b/repository/browse.go index 4fa994c386b0eaa31e7dfddb849c14fa5a6be59d..f0159b5f7d1a5e0d7a6971ac4b98cc0bebecaaa3 100644 --- a/repository/browse.go +++ b/repository/browse.go @@ -146,9 +146,8 @@ type FileDiff struct { // 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 + Name string + Hash Hash // commit hash } // TagInfo describes a tag returned by RepoBrowse.Tags. diff --git a/repository/gogit.go b/repository/gogit.go index 797837b1491eb072422cef60e8a2c05c05fe2c28..1403eb8c196fddfe4be7e6aa979481246bb91a45 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -999,39 +999,8 @@ func (repo *GoGitRepo) resolveRefToHash(ref string) (plumbing.Hash, error) { 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 +// Branches returns all local branches (refs/heads/*). func (repo *GoGitRepo) Branches() ([]BranchInfo, error) { - defaultBranch := repo.defaultBranchName() - repo.rMutex.Lock() defer repo.rMutex.Unlock() @@ -1046,9 +1015,8 @@ func (repo *GoGitRepo) Branches() ([]BranchInfo, error) { return nil } branches = append(branches, BranchInfo{ - Name: r.Name().Short(), - Hash: Hash(r.Hash().String()), - IsDefault: r.Name().Short() == defaultBranch, + Name: r.Name().Short(), + Hash: Hash(r.Hash().String()), }) return nil }) @@ -1582,6 +1550,30 @@ func (repo *GoGitRepo) CommitFileDiff(hash Hash, filePath string) (FileDiff, err return FileDiff{}, ErrNotFound } +// Head returns the commit that HEAD currently points to. +func (repo *GoGitRepo) Head() (CommitMeta, error) { + repo.rMutex.Lock() + defer repo.rMutex.Unlock() + + ref, err := repo.r.Head() + if err == plumbing.ErrReferenceNotFound { + return CommitMeta{}, ErrNotFound + } + if err != nil { + return CommitMeta{}, err + } + + c, err := repo.r.CommitObject(ref.Hash()) + if err == plumbing.ErrObjectNotFound { + return CommitMeta{}, ErrNotFound + } + if err != nil { + return CommitMeta{}, err + } + + return commitToMeta(c), nil +} + // buildDiffHunks converts a go-git FilePatch into DiffHunks with line numbers // and context grouping. func buildDiffHunks(fp fdiff.FilePatch) []DiffHunk { diff --git a/repository/mock_repo.go b/repository/mock_repo.go index 01d48f52b2fadceee976d6e8fcf0ce0270ec7ca9..e13e18cfdfc2b314885c4513c60f5900fd3cc8dd 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -232,20 +232,18 @@ type commit struct { } type mockRepoDataBrowse struct { - blobs map[Hash][]byte - trees map[Hash]string - commits map[Hash]commit - refs map[string]Hash - defaultBranch string + blobs map[Hash][]byte + trees map[Hash]string + commits map[Hash]commit + refs 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", + blobs: make(map[Hash][]byte), + trees: make(map[Hash]string), + commits: make(map[Hash]commit), + refs: make(map[string]Hash), } } @@ -545,9 +543,8 @@ func (r *mockRepoDataBrowse) Branches() ([]BranchInfo, error) { continue } branches = append(branches, BranchInfo{ - Name: name, - Hash: hash, - IsDefault: name == r.defaultBranch, + Name: name, + Hash: hash, }) } return branches, nil @@ -797,6 +794,18 @@ func (r *mockRepoDataBrowse) CommitFileDiff(hash Hash, filePath string) (FileDif return fd, nil } +func (r *mockRepoDataBrowse) Head() (CommitMeta, error) { + hash, ok := r.refs["HEAD"] + if !ok { + return CommitMeta{}, ErrNotFound + } + c, ok := r.commits[hash] + if !ok { + return CommitMeta{}, ErrNotFound + } + return mockCommitMeta(hash, c), nil +} + // mockDiffHunks produces a single DiffHunk using a prefix/suffix scan. func mockDiffHunks(old, new []byte) []DiffHunk { oldLines := splitBlobLines(old) diff --git a/repository/repo.go b/repository/repo.go index 72bcfe3fea388dfe2a963deb33a730d359710f3e..e081038a0d285304ba0c095e6c9a201aa6abdf6c 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -219,7 +219,6 @@ type RepoClock interface { // refs/heads/, refs/tags/, 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) @@ -262,6 +261,11 @@ type RepoBrowse interface { // 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) + + // Head returns the commit that HEAD currently points to. + // Returns ErrNotFound if HEAD cannot be resolved to a commit, including + // for an empty (unborn) repository. + Head() (CommitMeta, error) } // ClockLoader hold which logical clock need to exist for an entity and diff --git a/repository/repo_testing.go b/repository/repo_testing.go index 2ba28a1856759f557047d833c95c09fc0cad7706..53780c145e291c3ec0af1b8ecdc72e4cae24b008 100644 --- a/repository/repo_testing.go +++ b/repository/repo_testing.go @@ -466,10 +466,7 @@ func RepoBrowseTest(t *testing.T, repo browsable) { } 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 ────────────────────────────────────────────────────────────────── @@ -771,4 +768,14 @@ func RepoBrowseTest(t *testing.T, repo browsable) { _, err = repo.CommitFileDiff(randomHash(), "main.go") require.ErrorIs(t, err, ErrNotFound) }) + + // ── Head ────────────────────────────────────────────────────────────────── + + t.Run("Head", func(t *testing.T) { + require.NoError(t, repo.UpdateRef("HEAD", c3)) + + meta, err := repo.Head() + require.NoError(t, err) + require.Equal(t, c3, meta.Hash) + }) }