diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c9c402f1e49..f0e3dcf749c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -231,6 +231,7 @@ Dockerfile @sourcegraph/distribution # Precise code intel /cmd/precise-code-intel-bundle-manager/ @sourcegraph/code-intel /cmd/precise-code-intel-worker/ @sourcegraph/code-intel +/cmd/precise-code-intel-indexer/ @sourcegraph/code-intel /internal/cmd/precise-code-intel-test @sourcegraph/code-intel /internal/codeintel @sourcegraph/code-intel /enterprise/internal/codeintel @sourcegraph/code-intel diff --git a/cmd/precise-code-intel-indexer/internal/indexer/processor.go b/cmd/precise-code-intel-indexer/internal/indexer/processor.go index 66529b4ad49..5c66ed5e3bf 100644 --- a/cmd/precise-code-intel-indexer/internal/indexer/processor.go +++ b/cmd/precise-code-intel-indexer/internal/indexer/processor.go @@ -41,9 +41,17 @@ func (p *processor) Process(ctx context.Context, index db.Index) error { } func (p *processor) index(ctx context.Context, repoDir string, index db.Index) error { + tag, exact, err := p.gitserverClient.Tags(ctx, p.db, index.RepositoryID, index.Commit) + if err != nil { + return err + } + if !exact { + tag = fmt.Sprintf("%s-%s", tag, index.Commit[:12]) + } + args := []string{ "--repositoryRoot=.", - "--moduleVersion=NONE", // TODO - find git tags on this commit to support module version + fmt.Sprintf("--moduleVersion=%s", tag), } return command(repoDir, "lsif-go", args...) diff --git a/internal/codeintel/gitserver/client.go b/internal/codeintel/gitserver/client.go index dfad597e64a..fd48d736ac4 100644 --- a/internal/codeintel/gitserver/client.go +++ b/internal/codeintel/gitserver/client.go @@ -27,6 +27,11 @@ type Client interface { // FileExists determines whether a file exists in a particular commit of a repository. FileExists(ctx context.Context, db db.DB, repositoryID int, commit, file string) (bool, error) + + // Tags returns the git tags associated with the given commit along with a boolean indicating whether + // or not the tag was attached directly to the commit. If no tags exist at or before this commit, the + // tag is an empty string. + Tags(ctx context.Context, db db.DB, repositoryID int, commit string) (string, bool, error) } type defaultClient struct{} @@ -52,3 +57,7 @@ func (c *defaultClient) Archive(ctx context.Context, db db.DB, repositoryID int, func (c *defaultClient) FileExists(ctx context.Context, db db.DB, repositoryID int, commit, file string) (bool, error) { return FileExists(ctx, db, repositoryID, commit, file) } + +func (c *defaultClient) Tags(ctx context.Context, db db.DB, repositoryID int, commit string) (string, bool, error) { + return Tags(ctx, db, repositoryID, commit) +} diff --git a/internal/codeintel/gitserver/mocks/mock_client.go b/internal/codeintel/gitserver/mocks/mock_client.go index a6ca2e00a33..961884a0838 100644 --- a/internal/codeintel/gitserver/mocks/mock_client.go +++ b/internal/codeintel/gitserver/mocks/mock_client.go @@ -29,6 +29,9 @@ type MockClient struct { // HeadFunc is an instance of a mock function object controlling the // behavior of the method Head. HeadFunc *ClientHeadFunc + // TagsFunc is an instance of a mock function object controlling the + // behavior of the method Tags. + TagsFunc *ClientTagsFunc } // NewMockClient creates a new mock of the Client interface. All methods @@ -60,6 +63,11 @@ func NewMockClient() *MockClient { return "", nil }, }, + TagsFunc: &ClientTagsFunc{ + defaultHook: func(context.Context, db.DB, int, string) (string, bool, error) { + return "", false, nil + }, + }, } } @@ -82,6 +90,9 @@ func NewMockClientFrom(i gitserver.Client) *MockClient { HeadFunc: &ClientHeadFunc{ defaultHook: i.Head, }, + TagsFunc: &ClientTagsFunc{ + defaultHook: i.Tags, + }, } } @@ -658,3 +669,120 @@ func (c ClientHeadFuncCall) Args() []interface{} { func (c ClientHeadFuncCall) Results() []interface{} { return []interface{}{c.Result0, c.Result1} } + +// ClientTagsFunc describes the behavior when the Tags method of the parent +// MockClient instance is invoked. +type ClientTagsFunc struct { + defaultHook func(context.Context, db.DB, int, string) (string, bool, error) + hooks []func(context.Context, db.DB, int, string) (string, bool, error) + history []ClientTagsFuncCall + mutex sync.Mutex +} + +// Tags delegates to the next hook function in the queue and stores the +// parameter and result values of this invocation. +func (m *MockClient) Tags(v0 context.Context, v1 db.DB, v2 int, v3 string) (string, bool, error) { + r0, r1, r2 := m.TagsFunc.nextHook()(v0, v1, v2, v3) + m.TagsFunc.appendCall(ClientTagsFuncCall{v0, v1, v2, v3, r0, r1, r2}) + return r0, r1, r2 +} + +// SetDefaultHook sets function that is called when the Tags method of the +// parent MockClient instance is invoked and the hook queue is empty. +func (f *ClientTagsFunc) SetDefaultHook(hook func(context.Context, db.DB, int, string) (string, bool, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// Tags method of the parent MockClient instance inovkes the hook at the +// front of the queue and discards it. After the queue is empty, the default +// hook function is invoked for any future action. +func (f *ClientTagsFunc) PushHook(hook func(context.Context, db.DB, int, string) (string, bool, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultDefaultHook with a function that returns +// the given values. +func (f *ClientTagsFunc) SetDefaultReturn(r0 string, r1 bool, r2 error) { + f.SetDefaultHook(func(context.Context, db.DB, int, string) (string, bool, error) { + return r0, r1, r2 + }) +} + +// PushReturn calls PushDefaultHook with a function that returns the given +// values. +func (f *ClientTagsFunc) PushReturn(r0 string, r1 bool, r2 error) { + f.PushHook(func(context.Context, db.DB, int, string) (string, bool, error) { + return r0, r1, r2 + }) +} + +func (f *ClientTagsFunc) nextHook() func(context.Context, db.DB, int, string) (string, bool, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *ClientTagsFunc) appendCall(r0 ClientTagsFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of ClientTagsFuncCall objects describing the +// invocations of this function. +func (f *ClientTagsFunc) History() []ClientTagsFuncCall { + f.mutex.Lock() + history := make([]ClientTagsFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// ClientTagsFuncCall is an object that describes an invocation of method +// Tags on an instance of MockClient. +type ClientTagsFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 db.DB + // Arg2 is the value of the 3rd argument passed to this method + // invocation. + Arg2 int + // Arg3 is the value of the 4th argument passed to this method + // invocation. + Arg3 string + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 string + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 bool + // Result2 is the value of the 3rd result returned from this method + // invocation. + Result2 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c ClientTagsFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1, c.Arg2, c.Arg3} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c ClientTagsFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1, c.Result2} +} diff --git a/internal/codeintel/gitserver/tags.go b/internal/codeintel/gitserver/tags.go new file mode 100644 index 00000000000..7f152201977 --- /dev/null +++ b/internal/codeintel/gitserver/tags.go @@ -0,0 +1,38 @@ +package gitserver + +import ( + "context" + + "github.com/sourcegraph/sourcegraph/internal/codeintel/db" +) + +// Tags returns the git tags associated with the given commit along with a boolean indicating whether +// or not the tag was attached directly to the commit. If no tags exist at or before this commit, the +// tag is an empty string. +func Tags(ctx context.Context, db db.DB, repositoryID int, commit string) (string, bool, error) { + tag, err := execGitCommand(ctx, db, repositoryID, "tag", "-l", "--points-at", commit) + if err != nil { + return "", false, err + } + if tag != "" { + return tag, true, nil + } + + // git describe --tags will exit with status 128 (fatal: No names found, cannot describe anything) + // when there are no tags known to the given repo. In order to prevent a gitserver error from + // occurring, we first check to see if there are any tags and early-exit. + tags, err := execGitCommand(ctx, db, repositoryID, "tag", commit) + if err != nil { + return "", false, err + } + if tags == "" { + return "", false, nil + } + + tag, err = execGitCommand(ctx, db, repositoryID, "describe", "--tags", "--abbrev=0", commit) + if err != nil { + return "", false, err + } + + return tag, false, nil +}