Commit 3c2fb068 authored by Igor Drozdov's avatar Igor Drozdov

Load preprocessed code-nav-data directly from object storage

The LSIF data is going to be a ZIP file with JSON files
And using FE we can fetch a particular file from this ZIP
parent c25b124e
...@@ -44,7 +44,6 @@ const Api = { ...@@ -44,7 +44,6 @@ const Api = {
mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines', mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
adminStatisticsPath: '/api/:version/application/statistics', adminStatisticsPath: '/api/:version/application/statistics',
pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id', pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id',
lsifPath: '/api/:version/projects/:id/commits/:commit_id/lsif/info',
environmentsPath: '/api/:version/projects/:id/environments', environmentsPath: '/api/:version/projects/:id/environments',
group(groupId, callback) { group(groupId, callback) {
...@@ -474,14 +473,6 @@ const Api = { ...@@ -474,14 +473,6 @@ const Api = {
return axios.get(url); return axios.get(url);
}, },
lsifData(projectPath, commitId, paths) {
const url = Api.buildUrl(this.lsifPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':commit_id', commitId);
return axios.get(url, { params: { paths } });
},
environments(id) { environments(id) {
const url = Api.buildUrl(this.environmentsPath).replace(':id', encodeURIComponent(id)); const url = Api.buildUrl(this.environmentsPath).replace(':id', encodeURIComponent(id));
return axios.get(url); return axios.get(url);
......
...@@ -7,7 +7,7 @@ export default { ...@@ -7,7 +7,7 @@ export default {
Popover, Popover,
}, },
computed: { computed: {
...mapState(['currentDefinition', 'currentDefinitionPosition']), ...mapState(['currentDefinition', 'currentDefinitionPosition', 'definitionPathPrefix']),
}, },
mounted() { mounted() {
this.blobViewer = document.querySelector('.blob-viewer'); this.blobViewer = document.querySelector('.blob-viewer');
...@@ -39,5 +39,6 @@ export default { ...@@ -39,5 +39,6 @@ export default {
v-if="currentDefinition" v-if="currentDefinition"
:position="currentDefinitionPosition" :position="currentDefinitionPosition"
:data="currentDefinition" :data="currentDefinition"
:definition-path-prefix="definitionPathPrefix"
/> />
</template> </template>
...@@ -14,6 +14,10 @@ export default { ...@@ -14,6 +14,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
definitionPathPrefix: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -27,6 +31,11 @@ export default { ...@@ -27,6 +31,11 @@ export default {
top: `${this.position.y + this.position.height}px`, top: `${this.position.y + this.position.height}px`,
}; };
}, },
definitionPath() {
return (
this.data.definition_path && `${this.definitionPathPrefix}/${this.data.definition_path}`
);
},
}, },
watch: { watch: {
position: { position: {
...@@ -67,8 +76,8 @@ export default { ...@@ -67,8 +76,8 @@ export default {
{{ hover.value }} {{ hover.value }}
</p> </p>
</div> </div>
<div v-if="data.definition_url" class="popover-body"> <div v-if="definitionPath" class="popover-body">
<gl-button :href="data.definition_url" target="_blank" class="w-100" variant="default"> <gl-button :href="definitionPath" target="_blank" class="w-100" variant="default">
{{ __('Go to definition') }} {{ __('Go to definition') }}
</gl-button> </gl-button>
</div> </div>
......
import api from '~/api'; import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils'; import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils';
...@@ -12,11 +12,10 @@ export default { ...@@ -12,11 +12,10 @@ export default {
fetchData({ commit, dispatch, state }) { fetchData({ commit, dispatch, state }) {
commit(types.REQUEST_DATA); commit(types.REQUEST_DATA);
api axios
.lsifData(state.projectPath, state.commitId, [state.blobPath]) .get(state.codeNavUrl)
.then(({ data }) => { .then(({ data }) => {
const dataForPath = data[state.blobPath]; const normalizedData = data.reduce((acc, d) => {
const normalizedData = dataForPath.reduce((acc, d) => {
if (d.hover) { if (d.hover) {
acc[`${d.start_line}:${d.start_char}`] = d; acc[`${d.start_line}:${d.start_char}`] = d;
addInteractionClass(d); addInteractionClass(d);
......
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.SET_INITIAL_DATA](state, { projectPath, commitId, blobPath }) { [types.SET_INITIAL_DATA](state, { codeNavUrl, definitionPathPrefix }) {
state.projectPath = projectPath; state.codeNavUrl = codeNavUrl;
state.commitId = commitId; state.definitionPathPrefix = definitionPathPrefix;
state.blobPath = blobPath;
}, },
[types.REQUEST_DATA](state) { [types.REQUEST_DATA](state) {
state.loading = true; state.loading = true;
......
...@@ -208,11 +208,24 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -208,11 +208,24 @@ class Projects::BlobController < Projects::ApplicationController
.last_for_path(@repository, @ref, @path).sha .last_for_path(@repository, @ref, @path).sha
end end
def set_code_navigation_build
return if Feature.disabled?(:code_navigation, @project)
artifact =
Ci::JobArtifact
.for_sha(@blob.commit_id, @project.id)
.for_job_name(Ci::Build::CODE_NAVIGATION_JOB_NAME)
.last
@code_navigation_build = artifact&.job
end
def show_html def show_html
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
environment_params[:find_latest] = true environment_params[:find_latest] = true
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@last_commit = @repository.last_commit_for_path(@commit.id, @blob.path) @last_commit = @repository.last_commit_for_path(@commit.id, @blob.path)
set_code_navigation_build
render 'show' render 'show'
end end
......
...@@ -33,6 +33,8 @@ module Ci ...@@ -33,6 +33,8 @@ module Ci
scheduler_failure: 2 scheduler_failure: 2
}.freeze }.freeze
CODE_NAVIGATION_JOB_NAME = 'code_navigation'
has_one :deployment, as: :deployable, class_name: 'Deployment' has_one :deployment, as: :deployable, class_name: 'Deployment'
has_one :resource, class_name: 'Ci::Resource', inverse_of: :build has_one :resource, class_name: 'Ci::Resource', inverse_of: :build
has_many :trace_sections, class_name: 'Ci::BuildTraceSection' has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
......
...@@ -80,6 +80,7 @@ module Ci ...@@ -80,6 +80,7 @@ module Ci
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) } scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) } scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) }
scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) } scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) }
scope :for_job_name, ->(name) { joins(:job).where(ci_builds: { name: name }) }
scope :with_file_types, -> (file_types) do scope :with_file_types, -> (file_types) do
types = self.file_types.select { |file_type| file_types.include?(file_type) }.values types = self.file_types.select { |file_type| file_types.include?(file_type) }.values
......
...@@ -9,8 +9,9 @@ ...@@ -9,8 +9,9 @@
= render "projects/blob/auxiliary_viewer", blob: blob = render "projects/blob/auxiliary_viewer", blob: blob
#blob-content-holder.blob-content-holder #blob-content-holder.blob-content-holder
- if native_code_navigation_enabled?(@project) - if @code_navigation_build
#js-code-navigation{ data: { commit_id: blob.commit_id, blob_path: blob.path, project_path: @project.full_path } } - code_nav_url = raw_project_job_artifacts_url(@project, @code_navigation_build, path: "lsif/#{blob.path}")
#js-code-navigation{ data: { code_nav_url: "#{code_nav_url}.json", definition_path_prefix: project_blob_path(@project, @ref) } }
%article.file-holder %article.file-holder
= render 'projects/blob/header', blob: blob = render 'projects/blob/header', blob: blob
= render 'projects/blob/content', blob: blob = render 'projects/blob/content', blob: blob
...@@ -8,18 +8,17 @@ describe Projects::BlobController do ...@@ -8,18 +8,17 @@ describe Projects::BlobController do
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
describe "GET show" do describe "GET show" do
def request
get(:show, params: { namespace_id: project.namespace, project_id: project, id: id })
end
render_views render_views
context 'with file path' do context 'with file path' do
before do before do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
get(:show, request
params: {
namespace_id: project.namespace,
project_id: project,
id: id
})
end end
context "valid branch, valid file" do context "valid branch, valid file" do
...@@ -119,6 +118,32 @@ describe Projects::BlobController do ...@@ -119,6 +118,32 @@ describe Projects::BlobController do
end end
end end
end end
context 'when there is an artifact with code navigation data' do
let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) }
let!(:job) { create(:ci_build, pipeline: pipeline, name: Ci::Build::CODE_NAVIGATION_JOB_NAME) }
let!(:artifact) { create(:ci_job_artifact, :lsif, job: job) }
let(:id) { 'master/README.md' }
it 'assigns code_navigation_build variable' do
request
expect(assigns[:code_navigation_build]).to eq(job)
end
context 'when code_navigation feature is disabled' do
before do
stub_feature_flags(code_navigation: false)
end
it 'does not assign code_navigation_build variable' do
request
expect(assigns[:code_navigation_build]).to be_nil
end
end
end
end end
describe 'GET diff' do describe 'GET diff' do
......
...@@ -16,6 +16,7 @@ function factory(initialState = {}) { ...@@ -16,6 +16,7 @@ function factory(initialState = {}) {
state: { state: {
...createState(), ...createState(),
...initialState, ...initialState,
definitionPathPrefix: 'https://test.com/blob/master',
}, },
actions: { actions: {
fetchData, fetchData,
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Popover from '~/code_navigation/components/popover.vue'; import Popover from '~/code_navigation/components/popover.vue';
const DEFINITION_PATH_PREFIX = 'http:/';
const MOCK_CODE_DATA = Object.freeze({ const MOCK_CODE_DATA = Object.freeze({
hover: [ hover: [
{ {
...@@ -8,7 +10,7 @@ const MOCK_CODE_DATA = Object.freeze({ ...@@ -8,7 +10,7 @@ const MOCK_CODE_DATA = Object.freeze({
value: 'console.log', value: 'console.log',
}, },
], ],
definition_url: 'http://test.com', definition_path: 'test.com',
}); });
const MOCK_DOCS_DATA = Object.freeze({ const MOCK_DOCS_DATA = Object.freeze({
...@@ -18,13 +20,13 @@ const MOCK_DOCS_DATA = Object.freeze({ ...@@ -18,13 +20,13 @@ const MOCK_DOCS_DATA = Object.freeze({
value: 'console.log', value: 'console.log',
}, },
], ],
definition_url: 'http://test.com', definition_path: 'test.com',
}); });
let wrapper; let wrapper;
function factory(position, data) { function factory(position, data, definitionPathPrefix) {
wrapper = shallowMount(Popover, { propsData: { position, data } }); wrapper = shallowMount(Popover, { propsData: { position, data, definitionPathPrefix } });
} }
describe('Code navigation popover component', () => { describe('Code navigation popover component', () => {
...@@ -33,14 +35,14 @@ describe('Code navigation popover component', () => { ...@@ -33,14 +35,14 @@ describe('Code navigation popover component', () => {
}); });
it('renders popover', () => { it('renders popover', () => {
factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA); factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA, DEFINITION_PATH_PREFIX);
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
describe('code output', () => { describe('code output', () => {
it('renders code output', () => { it('renders code output', () => {
factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA); factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA, DEFINITION_PATH_PREFIX);
expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(true); expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(true);
expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(false); expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(false);
...@@ -49,7 +51,7 @@ describe('Code navigation popover component', () => { ...@@ -49,7 +51,7 @@ describe('Code navigation popover component', () => {
describe('documentation output', () => { describe('documentation output', () => {
it('renders code output', () => { it('renders code output', () => {
factory({ x: 0, y: 0, height: 0 }, MOCK_DOCS_DATA); factory({ x: 0, y: 0, height: 0 }, MOCK_DOCS_DATA, DEFINITION_PATH_PREFIX);
expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(false); expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(false);
expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(true); expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(true);
......
...@@ -27,12 +27,10 @@ describe('Code navigation actions', () => { ...@@ -27,12 +27,10 @@ describe('Code navigation actions', () => {
describe('fetchData', () => { describe('fetchData', () => {
let mock; let mock;
const state = {
projectPath: 'gitlab-org/gitlab', const codeNavUrl =
commitId: '123', 'gitlab-org/gitlab-shell/-/jobs/1114/artifacts/raw/lsif/cmd/check/main.go.json';
blobPath: 'index', const state = { codeNavUrl };
};
const apiUrl = '/api/1/projects/gitlab-org%2Fgitlab/commits/123/lsif/info';
beforeEach(() => { beforeEach(() => {
window.gon = { api_version: '1' }; window.gon = { api_version: '1' };
...@@ -45,20 +43,18 @@ describe('Code navigation actions', () => { ...@@ -45,20 +43,18 @@ describe('Code navigation actions', () => {
describe('success', () => { describe('success', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(apiUrl).replyOnce(200, { mock.onGet(codeNavUrl).replyOnce(200, [
index: [ {
{ start_line: 0,
start_line: 0, start_char: 0,
start_char: 0, hover: { value: '123' },
hover: { value: '123' }, },
}, {
{ start_line: 1,
start_line: 1, start_char: 0,
start_char: 0, hover: null,
hover: null, },
}, ]);
],
});
}); });
it('commits REQUEST_DATA_SUCCESS with normalized data', done => { it('commits REQUEST_DATA_SUCCESS with normalized data', done => {
...@@ -106,7 +102,7 @@ describe('Code navigation actions', () => { ...@@ -106,7 +102,7 @@ describe('Code navigation actions', () => {
describe('error', () => { describe('error', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(apiUrl).replyOnce(500); mock.onGet(codeNavUrl).replyOnce(500);
}); });
it('dispatches requestDataError', done => { it('dispatches requestDataError', done => {
......
...@@ -11,14 +11,12 @@ describe('Code navigation mutations', () => { ...@@ -11,14 +11,12 @@ describe('Code navigation mutations', () => {
describe('SET_INITIAL_DATA', () => { describe('SET_INITIAL_DATA', () => {
it('sets initial data', () => { it('sets initial data', () => {
mutations.SET_INITIAL_DATA(state, { mutations.SET_INITIAL_DATA(state, {
projectPath: 'test', codeNavUrl: 'https://test.com/builds/1005',
commitId: '123', definitionPathPrefix: 'https://test.com/blob/master',
blobPath: 'index.js',
}); });
expect(state.projectPath).toBe('test'); expect(state.codeNavUrl).toBe('https://test.com/builds/1005');
expect(state.commitId).toBe('123'); expect(state.definitionPathPrefix).toBe('https://test.com/blob/master');
expect(state.blobPath).toBe('index.js');
}); });
}); });
......
...@@ -140,6 +140,18 @@ describe Ci::JobArtifact do ...@@ -140,6 +140,18 @@ describe Ci::JobArtifact do
end end
end end
describe '.for_job_name' do
it 'returns job artifacts for a given job name' do
first_job = create(:ci_build, name: 'first')
second_job = create(:ci_build, name: 'second')
first_artifact = create(:ci_job_artifact, job: first_job)
second_artifact = create(:ci_job_artifact, job: second_job)
expect(described_class.for_job_name(first_job.name)).to eq([first_artifact])
expect(described_class.for_job_name(second_job.name)).to eq([second_artifact])
end
end
describe 'callbacks' do describe 'callbacks' do
subject { create(:ci_job_artifact, :archive) } subject { create(:ci_job_artifact, :archive) }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment