mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 16:31:47 +00:00
* log: remove use of description paramter in Scoped * temporarily point to sglog branch * bazel configure + gazelle * remove additional use of description param * use latest versions of zoekt,log,mountinfo * go.mod
457 lines
14 KiB
Go
457 lines
14 KiB
Go
package graphqlbackend
|
|
|
|
import (
|
|
"context"
|
|
"io/fs"
|
|
"net/url"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/graph-gophers/graphql-go"
|
|
"github.com/graph-gophers/graphql-go/relay"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
|
|
"github.com/sourcegraph/log"
|
|
|
|
"github.com/sourcegraph/sourcegraph/cmd/frontend/backend"
|
|
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/externallink"
|
|
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil"
|
|
"github.com/sourcegraph/sourcegraph/internal/api"
|
|
"github.com/sourcegraph/sourcegraph/internal/database"
|
|
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
|
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
|
"github.com/sourcegraph/sourcegraph/internal/trace"
|
|
"github.com/sourcegraph/sourcegraph/lib/errors"
|
|
)
|
|
|
|
func (r *schemaResolver) gitCommitByID(ctx context.Context, id graphql.ID) (*GitCommitResolver, error) {
|
|
repoID, commitID, err := unmarshalGitCommitID(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
repo, err := r.repositoryByID(ctx, repoID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return repo.Commit(ctx, &RepositoryCommitArgs{Rev: string(commitID)})
|
|
}
|
|
|
|
// GitCommitResolver resolves git commits.
|
|
//
|
|
// Prefer using NewGitCommitResolver to create an instance of the commit resolver.
|
|
type GitCommitResolver struct {
|
|
logger log.Logger
|
|
db database.DB
|
|
gitserverClient gitserver.Client
|
|
repoResolver *RepositoryResolver
|
|
|
|
// inputRev is the Git revspec that the user originally requested that resolved to this Git commit. It is used
|
|
// to avoid redirecting a user browsing a revision "mybranch" to the absolute commit ID as they follow links in the UI.
|
|
inputRev *string
|
|
|
|
// fetch + serve sourcegraph stored user information
|
|
includeUserInfo bool
|
|
|
|
// oid MUST be specified and a 40-character Git SHA.
|
|
oid GitObjectID
|
|
|
|
gitRepo api.RepoName
|
|
|
|
// commit should not be accessed directly since it might not be initialized.
|
|
// Use the resolver methods instead.
|
|
commit *gitdomain.Commit
|
|
commitOnce sync.Once
|
|
commitErr error
|
|
}
|
|
|
|
// NewGitCommitResolver returns a new CommitResolver. When commit is set to nil,
|
|
// commit will be loaded lazily as needed by the resolver. Pass in a commit when
|
|
// you have batch-loaded a bunch of them and already have them at hand.
|
|
func NewGitCommitResolver(db database.DB, gsClient gitserver.Client, repo *RepositoryResolver, id api.CommitID, commit *gitdomain.Commit) *GitCommitResolver {
|
|
repoName := repo.RepoName()
|
|
return &GitCommitResolver{
|
|
logger: log.Scoped("gitCommitResolver").With(
|
|
log.String("repo", string(repoName)),
|
|
log.String("commitID", string(id)),
|
|
),
|
|
db: db,
|
|
gitserverClient: gsClient,
|
|
repoResolver: repo,
|
|
includeUserInfo: true,
|
|
gitRepo: repoName,
|
|
oid: GitObjectID(id),
|
|
commit: commit,
|
|
}
|
|
}
|
|
|
|
func (r *GitCommitResolver) resolveCommit(ctx context.Context) (*gitdomain.Commit, error) {
|
|
r.commitOnce.Do(func() {
|
|
if r.commit != nil {
|
|
return
|
|
}
|
|
|
|
opts := gitserver.ResolveRevisionOptions{}
|
|
r.commit, r.commitErr = r.gitserverClient.GetCommit(ctx, r.gitRepo, api.CommitID(r.oid), opts)
|
|
})
|
|
return r.commit, r.commitErr
|
|
}
|
|
|
|
// gitCommitGQLID is a type used for marshaling and unmarshalling a Git commit's
|
|
// GraphQL ID.
|
|
type gitCommitGQLID struct {
|
|
Repository graphql.ID `json:"r"`
|
|
CommitID GitObjectID `json:"c"`
|
|
}
|
|
|
|
func marshalGitCommitID(repo graphql.ID, commitID GitObjectID) graphql.ID {
|
|
return relay.MarshalID("GitCommit", gitCommitGQLID{Repository: repo, CommitID: commitID})
|
|
}
|
|
|
|
func unmarshalGitCommitID(id graphql.ID) (repoID graphql.ID, commitID GitObjectID, err error) {
|
|
var spec gitCommitGQLID
|
|
err = relay.UnmarshalSpec(id, &spec)
|
|
return spec.Repository, spec.CommitID, err
|
|
}
|
|
|
|
func (r *GitCommitResolver) ID() graphql.ID {
|
|
return marshalGitCommitID(r.repoResolver.ID(), r.oid)
|
|
}
|
|
|
|
func (r *GitCommitResolver) Repository() *RepositoryResolver { return r.repoResolver }
|
|
|
|
func (r *GitCommitResolver) OID() GitObjectID { return r.oid }
|
|
|
|
func (r *GitCommitResolver) InputRev() *string { return r.inputRev }
|
|
|
|
func (r *GitCommitResolver) AbbreviatedOID() string {
|
|
return string(r.oid)[:7]
|
|
}
|
|
|
|
func (r *GitCommitResolver) PerforceChangelist(ctx context.Context) (*PerforceChangelistResolver, error) {
|
|
return toPerforceChangelistResolver(ctx, r)
|
|
}
|
|
|
|
func (r *GitCommitResolver) Author(ctx context.Context) (*signatureResolver, error) {
|
|
commit, err := r.resolveCommit(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return toSignatureResolver(r.db, &commit.Author, r.includeUserInfo), nil
|
|
}
|
|
|
|
func (r *GitCommitResolver) Committer(ctx context.Context) (*signatureResolver, error) {
|
|
commit, err := r.resolveCommit(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return toSignatureResolver(r.db, commit.Committer, r.includeUserInfo), nil
|
|
}
|
|
|
|
func (r *GitCommitResolver) Message(ctx context.Context) (string, error) {
|
|
commit, err := r.resolveCommit(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(commit.Message), err
|
|
}
|
|
|
|
func (r *GitCommitResolver) Subject(ctx context.Context) (string, error) {
|
|
commit, err := r.resolveCommit(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if subject := maybeTransformP4Subject(ctx, r.repoResolver, commit); subject != nil {
|
|
return *subject, nil
|
|
}
|
|
|
|
return commit.Message.Subject(), nil
|
|
}
|
|
|
|
func (r *GitCommitResolver) Body(ctx context.Context) (*string, error) {
|
|
if r.repoResolver.isPerforceDepot(ctx) {
|
|
return nil, nil
|
|
}
|
|
|
|
commit, err := r.resolveCommit(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
body := commit.Message.Body()
|
|
if body == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
return &body, nil
|
|
}
|
|
|
|
func (r *GitCommitResolver) Parents(ctx context.Context) ([]*GitCommitResolver, error) {
|
|
commit, err := r.resolveCommit(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resolvers := make([]*GitCommitResolver, len(commit.Parents))
|
|
// TODO(tsenart): We can get the parent commits in batch from gitserver instead of doing
|
|
// N roundtrips. We already have a git.Commits method. Maybe we can use that.
|
|
for i, parent := range commit.Parents {
|
|
resolvers[i] = NewGitCommitResolver(r.db, r.gitserverClient, r.repoResolver, parent, nil)
|
|
}
|
|
return resolvers, nil
|
|
}
|
|
|
|
func (r *GitCommitResolver) URL() string {
|
|
repoUrl := r.repoResolver.url()
|
|
repoUrl.Path += "/-/commit/" + r.inputRevOrImmutableRev()
|
|
return repoUrl.String()
|
|
}
|
|
|
|
func (r *GitCommitResolver) CanonicalURL() string {
|
|
repoUrl := r.repoResolver.url()
|
|
repoUrl.Path += "/-/commit/" + string(r.oid)
|
|
return repoUrl.String()
|
|
}
|
|
|
|
func (r *GitCommitResolver) ExternalURLs(ctx context.Context) ([]*externallink.Resolver, error) {
|
|
repo, err := r.repoResolver.repo(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return externallink.Commit(ctx, r.db, repo, api.CommitID(r.oid))
|
|
}
|
|
|
|
func (r *GitCommitResolver) Tree(ctx context.Context, args *struct {
|
|
Path string
|
|
Recursive bool
|
|
}) (*GitTreeEntryResolver, error) {
|
|
treeEntry, err := r.path(ctx, args.Path, func(stat fs.FileInfo) error {
|
|
if !stat.Mode().IsDir() {
|
|
return errors.Errorf("not a directory: %q", args.Path)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Note: args.Recursive is deprecated
|
|
if treeEntry != nil {
|
|
treeEntry.isRecursive = args.Recursive
|
|
}
|
|
return treeEntry, nil
|
|
}
|
|
|
|
func (r *GitCommitResolver) Blob(ctx context.Context, args *struct {
|
|
Path string
|
|
}) (*GitTreeEntryResolver, error) {
|
|
return r.path(ctx, args.Path, func(stat fs.FileInfo) error {
|
|
if mode := stat.Mode(); !(mode.IsRegular() || mode.Type()&fs.ModeSymlink != 0) {
|
|
return errors.Errorf("not a blob: %q", args.Path)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (r *GitCommitResolver) File(ctx context.Context, args *struct {
|
|
Path string
|
|
}) (*GitTreeEntryResolver, error) {
|
|
return r.Blob(ctx, args)
|
|
}
|
|
|
|
func (r *GitCommitResolver) Path(ctx context.Context, args *struct {
|
|
Path string
|
|
}) (*GitTreeEntryResolver, error) {
|
|
return r.path(ctx, args.Path, func(_ fs.FileInfo) error { return nil })
|
|
}
|
|
|
|
func (r *GitCommitResolver) path(ctx context.Context, path string, validate func(fs.FileInfo) error) (_ *GitTreeEntryResolver, err error) {
|
|
if path == "" {
|
|
// This is referring to the root tree, will always exist, and will always be a directory,
|
|
// so we can skip the gitserver call to resolve the tree object. This is a common operation,
|
|
// so it's worth optimizing for.
|
|
return NewGitTreeEntryResolver(r.db, r.gitserverClient, GitTreeEntryResolverOpts{
|
|
Commit: r,
|
|
Stat: &rootTreeFileInfo{},
|
|
}), nil
|
|
}
|
|
|
|
tr, ctx := trace.New(ctx, "GitCommitResolver.path", attribute.String("path", path))
|
|
defer tr.EndWithErr(&err)
|
|
|
|
stat, err := r.gitserverClient.Stat(ctx, r.gitRepo, api.CommitID(r.oid), path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
if err := validate(stat); err != nil {
|
|
return nil, err
|
|
}
|
|
opts := GitTreeEntryResolverOpts{
|
|
Commit: r,
|
|
Stat: stat,
|
|
}
|
|
return NewGitTreeEntryResolver(r.db, r.gitserverClient, opts), nil
|
|
}
|
|
|
|
// rootTreeFileInfo implements the FileInfo interface for the
|
|
// root tree of a commit, which is guaranteed to be a directory
|
|
// and is guaranteed to exist.
|
|
type rootTreeFileInfo struct{}
|
|
|
|
var _ os.FileInfo = (*rootTreeFileInfo)(nil)
|
|
|
|
func (*rootTreeFileInfo) IsDir() bool { return true }
|
|
func (*rootTreeFileInfo) ModTime() time.Time { return time.Time{} }
|
|
func (*rootTreeFileInfo) Mode() fs.FileMode { return fs.ModeDir }
|
|
func (*rootTreeFileInfo) Name() string { return "" }
|
|
func (*rootTreeFileInfo) Size() int64 { return 0 }
|
|
func (*rootTreeFileInfo) Sys() any { return nil }
|
|
|
|
func (r *GitCommitResolver) FileNames(ctx context.Context) ([]string, error) {
|
|
return r.gitserverClient.LsFiles(ctx, r.gitRepo, api.CommitID(r.oid))
|
|
}
|
|
|
|
func (r *GitCommitResolver) Languages(ctx context.Context) ([]string, error) {
|
|
repo, err := r.repoResolver.repo(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inventory, err := backend.NewRepos(r.logger, r.db, r.gitserverClient).GetInventory(ctx, repo, api.CommitID(r.oid), false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
names := make([]string, len(inventory.Languages))
|
|
for i, l := range inventory.Languages {
|
|
names[i] = l.Name
|
|
}
|
|
return names, nil
|
|
}
|
|
|
|
func (r *GitCommitResolver) LanguageStatistics(ctx context.Context) ([]*languageStatisticsResolver, error) {
|
|
repo, err := r.repoResolver.repo(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inventory, err := backend.NewRepos(r.logger, r.db, r.gitserverClient).GetInventory(ctx, repo, api.CommitID(r.oid), false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stats := make([]*languageStatisticsResolver, 0, len(inventory.Languages))
|
|
for _, lang := range inventory.Languages {
|
|
stats = append(stats, &languageStatisticsResolver{
|
|
l: lang,
|
|
})
|
|
}
|
|
return stats, nil
|
|
}
|
|
|
|
type AncestorsArgs struct {
|
|
graphqlutil.ConnectionArgs
|
|
Query *string
|
|
Path *string
|
|
Follow bool
|
|
After *string
|
|
AfterCursor *string
|
|
Before *string
|
|
}
|
|
|
|
func (r *GitCommitResolver) Ancestors(ctx context.Context, args *AncestorsArgs) (*gitCommitConnectionResolver, error) {
|
|
return &gitCommitConnectionResolver{
|
|
db: r.db,
|
|
gitserverClient: r.gitserverClient,
|
|
revisionRange: string(r.oid),
|
|
first: args.ConnectionArgs.First,
|
|
query: args.Query,
|
|
path: args.Path,
|
|
follow: args.Follow,
|
|
after: args.After,
|
|
afterCursor: args.AfterCursor,
|
|
before: args.Before,
|
|
repo: r.repoResolver,
|
|
}, nil
|
|
}
|
|
|
|
func (r *GitCommitResolver) Diff(ctx context.Context, args *struct {
|
|
Base *string
|
|
}) (*RepositoryComparisonResolver, error) {
|
|
oidString := string(r.oid)
|
|
base := oidString + "~"
|
|
if args.Base != nil {
|
|
base = *args.Base
|
|
}
|
|
return NewRepositoryComparison(ctx, r.db, r.gitserverClient, r.repoResolver, &RepositoryComparisonInput{
|
|
Base: &base,
|
|
Head: &oidString,
|
|
FetchMissing: false,
|
|
})
|
|
}
|
|
|
|
func (r *GitCommitResolver) BehindAhead(ctx context.Context, args *struct {
|
|
Revspec string
|
|
}) (*behindAheadCountsResolver, error) {
|
|
counts, err := r.gitserverClient.GetBehindAhead(ctx, r.gitRepo, args.Revspec, string(r.oid))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &behindAheadCountsResolver{
|
|
behind: int32(counts.Behind),
|
|
ahead: int32(counts.Ahead),
|
|
}, nil
|
|
}
|
|
|
|
type behindAheadCountsResolver struct{ behind, ahead int32 }
|
|
|
|
func (r *behindAheadCountsResolver) Behind() int32 { return r.behind }
|
|
func (r *behindAheadCountsResolver) Ahead() int32 { return r.ahead }
|
|
|
|
// inputRevOrImmutableRev returns the input revspec, if it is provided and nonempty. Otherwise it returns the
|
|
// canonical OID for the revision.
|
|
func (r *GitCommitResolver) inputRevOrImmutableRev() string {
|
|
if r.inputRev != nil && *r.inputRev != "" {
|
|
return *r.inputRev
|
|
}
|
|
return string(r.oid)
|
|
}
|
|
|
|
// repoRevURL returns the URL path prefix to use when constructing URLs to resources at this
|
|
// revision. Unlike inputRevOrImmutableRev, it does NOT use the OID if no input revspec is
|
|
// given. This is because the convention in the frontend is for repo-rev URLs to omit the "@rev"
|
|
// portion (unlike for commit page URLs, which must include some revspec in
|
|
// "/REPO/-/commit/REVSPEC").
|
|
func (r *GitCommitResolver) repoRevURL() *url.URL {
|
|
// Dereference to copy to avoid mutation
|
|
repoUrl := *r.repoResolver.RepoMatch.URL()
|
|
var rev string
|
|
if r.inputRev != nil {
|
|
rev = *r.inputRev // use the original input rev from the user
|
|
} else {
|
|
rev = string(r.oid)
|
|
}
|
|
if rev != "" {
|
|
repoUrl.Path += "@" + rev
|
|
}
|
|
return &repoUrl
|
|
}
|
|
|
|
func (r *GitCommitResolver) canonicalRepoRevURL() *url.URL {
|
|
// Dereference to copy the URL to avoid mutation
|
|
repoUrl := *r.repoResolver.RepoMatch.URL()
|
|
repoUrl.Path += "@" + string(r.oid)
|
|
return &repoUrl
|
|
}
|
|
|
|
func (r *GitCommitResolver) Ownership(ctx context.Context, args ListOwnershipArgs) (OwnershipConnectionResolver, error) {
|
|
return EnterpriseResolvers.ownResolver.GitCommitOwnership(ctx, r, args)
|
|
}
|