Commit e2e53c15 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'fix-code-inteligence' into 'master'

Init CodeIntelligence in the refactored blob viewer

See merge request gitlab-org/gitlab!81800
parents c9d6d99d 66c32b7f
...@@ -7,6 +7,23 @@ export default { ...@@ -7,6 +7,23 @@ export default {
components: { components: {
Popover, Popover,
}, },
props: {
codeNavigationPath: {
type: String,
required: false,
default: null,
},
blobPath: {
type: String,
required: false,
default: null,
},
pathPrefix: {
type: String,
required: false,
default: null,
},
},
computed: { computed: {
...mapState([ ...mapState([
'currentDefinition', 'currentDefinition',
...@@ -16,6 +33,14 @@ export default { ...@@ -16,6 +33,14 @@ export default {
]), ]),
}, },
mounted() { mounted() {
if (this.codeNavigationPath && this.blobPath && this.pathPrefix) {
const initialData = {
blobs: [{ path: this.blobPath, codeNavigationPath: this.codeNavigationPath }],
definitionPathPrefix: this.pathPrefix,
};
this.setInitialData(initialData);
}
this.body = document.body; this.body = document.body;
eventHub.$on('showBlobInteractionZones', this.showBlobInteractionZones); eventHub.$on('showBlobInteractionZones', this.showBlobInteractionZones);
...@@ -28,7 +53,7 @@ export default { ...@@ -28,7 +53,7 @@ export default {
this.removeGlobalEventListeners(); this.removeGlobalEventListeners();
}, },
methods: { methods: {
...mapActions(['fetchData', 'showDefinition', 'showBlobInteractionZones']), ...mapActions(['fetchData', 'showDefinition', 'showBlobInteractionZones', 'setInitialData']),
addGlobalEventListeners() { addGlobalEventListeners() {
if (this.body) { if (this.body) {
this.body.addEventListener('click', this.showDefinition); this.body.addEventListener('click', this.showDefinition);
......
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import TableOfContents from '~/blob/components/table_contents.vue'; import TableOfContents from '~/blob/components/table_contents.vue';
...@@ -11,7 +12,9 @@ import initWebIdeLink from '~/pages/projects/shared/web_ide_link'; ...@@ -11,7 +12,9 @@ import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue'; import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import '~/sourcegraph/load'; import '~/sourcegraph/load';
import createStore from '~/code_navigation/store';
Vue.use(Vuex);
Vue.use(VueApollo); Vue.use(VueApollo);
Vue.use(VueRouter); Vue.use(VueRouter);
...@@ -29,6 +32,7 @@ if (viewBlobEl) { ...@@ -29,6 +32,7 @@ if (viewBlobEl) {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el: viewBlobEl, el: viewBlobEl,
store: createStore(),
router, router,
apolloProvider, apolloProvider,
provide: { provide: {
...@@ -78,7 +82,7 @@ GpgBadges.fetch(); ...@@ -78,7 +82,7 @@ GpgBadges.fetch();
const codeNavEl = document.getElementById('js-code-navigation'); const codeNavEl = document.getElementById('js-code-navigation');
if (codeNavEl) { if (codeNavEl && !viewBlobEl) {
const { codeNavigationPath, blobPath, definitionPathPrefix } = codeNavEl.dataset; const { codeNavigationPath, blobPath, definitionPathPrefix } = codeNavEl.dataset;
// eslint-disable-next-line promise/catch-or-return // eslint-disable-next-line promise/catch-or-return
......
...@@ -11,6 +11,7 @@ import { __ } from '~/locale'; ...@@ -11,6 +11,7 @@ import { __ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue'; import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import CodeIntelligence from '~/code_navigation/components/app.vue';
import getRefMixin from '../mixins/get_ref'; import getRefMixin from '../mixins/get_ref';
import blobInfoQuery from '../queries/blob_info.query.graphql'; import blobInfoQuery from '../queries/blob_info.query.graphql';
import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE } from '../constants'; import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE } from '../constants';
...@@ -30,6 +31,7 @@ export default { ...@@ -30,6 +31,7 @@ export default {
GlButton, GlButton,
ForkSuggestion, ForkSuggestion,
WebIdeLink, WebIdeLink,
CodeIntelligence,
}, },
mixins: [getRefMixin, glFeatureFlagMixin()], mixins: [getRefMixin, glFeatureFlagMixin()],
inject: { inject: {
...@@ -274,6 +276,12 @@ export default { ...@@ -274,6 +276,12 @@ export default {
:loading="isLoadingLegacyViewer" :loading="isLoadingLegacyViewer"
/> />
<component :is="blobViewer" v-else :blob="blobInfo" class="blob-viewer" /> <component :is="blobViewer" v-else :blob="blobInfo" class="blob-viewer" />
<code-intelligence
v-if="blobViewer || legacyViewerLoaded"
:code-navigation-path="blobInfo.codeNavigationPath"
:blob-path="blobInfo.path"
:path-prefix="blobInfo.projectBlobPathRoot"
/>
</div> </div>
</div> </div>
</template> </template>
...@@ -52,6 +52,8 @@ export const DEFAULT_BLOB_INFO = { ...@@ -52,6 +52,8 @@ export const DEFAULT_BLOB_INFO = {
ideEditPath: '', ideEditPath: '',
forkAndEditPath: '', forkAndEditPath: '',
ideForkAndEditPath: '', ideForkAndEditPath: '',
codeNavigationPath: '',
projectBlobPathRoot: '',
forkAndViewPath: '', forkAndViewPath: '',
storedExternally: false, storedExternally: false,
externalStorage: '', externalStorage: '',
......
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import { escapeFileUrl } from '~/lib/utils/url_utility'; import { escapeFileUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale'; import { __ } from '~/locale';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link'; import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import PerformancePlugin from '~/performance/vue_performance_plugin'; import PerformancePlugin from '~/performance/vue_performance_plugin';
import createStore from '~/code_navigation/store';
import App from './components/app.vue'; import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue'; import Breadcrumbs from './components/breadcrumbs.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue'; import DirectoryDownloadLinks from './components/directory_download_links.vue';
...@@ -19,6 +21,7 @@ import createRouter from './router'; ...@@ -19,6 +21,7 @@ import createRouter from './router';
import { updateFormAction } from './utils/dom'; import { updateFormAction } from './utils/dom';
import { setTitle } from './utils/title'; import { setTitle } from './utils/title';
Vue.use(Vuex);
Vue.use(PerformancePlugin, { Vue.use(PerformancePlugin, {
components: ['SimpleViewer', 'BlobContent'], components: ['SimpleViewer', 'BlobContent'],
}); });
...@@ -200,6 +203,7 @@ export default function setupVueRepositoryList() { ...@@ -200,6 +203,7 @@ export default function setupVueRepositoryList() {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el, el,
store: createStore(),
router, router,
apolloProvider, apolloProvider,
render(h) { render(h) {
......
...@@ -31,6 +31,8 @@ query getBlobInfo( ...@@ -31,6 +31,8 @@ query getBlobInfo(
ideEditPath ideEditPath
forkAndEditPath forkAndEditPath
ideForkAndEditPath ideForkAndEditPath
codeNavigationPath
projectBlobPathRoot
forkAndViewPath forkAndViewPath
environmentFormattedExternalUrl environmentFormattedExternalUrl
environmentExternalUrlForRouteMap environmentExternalUrlForRouteMap
......
...@@ -134,6 +134,12 @@ module Types ...@@ -134,6 +134,12 @@ module Types
null: true, null: true,
calls_gitaly: true calls_gitaly: true
field :code_navigation_path, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'Web path for code navigation.'
field :project_blob_path_root, GraphQL::Types::String, null: true,
description: 'Web path for the root of the blob.'
def raw_text_blob def raw_text_blob
object.data unless object.binary? object.data unless object.binary?
end end
......
...@@ -131,6 +131,14 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated ...@@ -131,6 +131,14 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
external_storage_url_or_path(url_helpers.project_raw_url(project, ref_qualified_path), project) external_storage_url_or_path(url_helpers.project_raw_url(project, ref_qualified_path), project)
end end
def code_navigation_path
Gitlab::CodeNavigationPath.new(project, blob.commit_id).full_json_path_for(blob.path)
end
def project_blob_path_root
project_blob_path(project, blob.commit_id)
end
private private
def url_helpers def url_helpers
......
...@@ -15034,6 +15034,7 @@ Returns [`Tree`](#tree). ...@@ -15034,6 +15034,7 @@ Returns [`Tree`](#tree).
| <a id="repositoryblobblamepath"></a>`blamePath` | [`String`](#string) | Web path to blob blame page. | | <a id="repositoryblobblamepath"></a>`blamePath` | [`String`](#string) | Web path to blob blame page. |
| <a id="repositoryblobcancurrentuserpushtobranch"></a>`canCurrentUserPushToBranch` | [`Boolean`](#boolean) | Whether the current user can push to the branch. | | <a id="repositoryblobcancurrentuserpushtobranch"></a>`canCurrentUserPushToBranch` | [`Boolean`](#boolean) | Whether the current user can push to the branch. |
| <a id="repositoryblobcanmodifyblob"></a>`canModifyBlob` | [`Boolean`](#boolean) | Whether the current user can modify the blob. | | <a id="repositoryblobcanmodifyblob"></a>`canModifyBlob` | [`Boolean`](#boolean) | Whether the current user can modify the blob. |
| <a id="repositoryblobcodenavigationpath"></a>`codeNavigationPath` | [`String`](#string) | Web path for code navigation. |
| <a id="repositoryblobcodeowners"></a>`codeOwners` | [`[UserCore!]`](#usercore) | List of code owners for the blob. | | <a id="repositoryblobcodeowners"></a>`codeOwners` | [`[UserCore!]`](#usercore) | List of code owners for the blob. |
| <a id="repositoryblobeditblobpath"></a>`editBlobPath` | [`String`](#string) | Web path to edit the blob in the old-style editor. | | <a id="repositoryblobeditblobpath"></a>`editBlobPath` | [`String`](#string) | Web path to edit the blob in the old-style editor. |
| <a id="repositoryblobenvironmentexternalurlforroutemap"></a>`environmentExternalUrlForRouteMap` | [`String`](#string) | Web path to blob on an environment. | | <a id="repositoryblobenvironmentexternalurlforroutemap"></a>`environmentExternalUrlForRouteMap` | [`String`](#string) | Web path to blob on an environment. |
...@@ -15057,6 +15058,7 @@ Returns [`Tree`](#tree). ...@@ -15057,6 +15058,7 @@ Returns [`Tree`](#tree).
| <a id="repositoryblobpermalinkpath"></a>`permalinkPath` | [`String`](#string) | Web path to blob permalink. | | <a id="repositoryblobpermalinkpath"></a>`permalinkPath` | [`String`](#string) | Web path to blob permalink. |
| <a id="repositoryblobpipelineeditorpath"></a>`pipelineEditorPath` | [`String`](#string) | Web path to edit .gitlab-ci.yml file. | | <a id="repositoryblobpipelineeditorpath"></a>`pipelineEditorPath` | [`String`](#string) | Web path to edit .gitlab-ci.yml file. |
| <a id="repositoryblobplaindata"></a>`plainData` | [`String`](#string) | Blob plain highlighted data. | | <a id="repositoryblobplaindata"></a>`plainData` | [`String`](#string) | Blob plain highlighted data. |
| <a id="repositoryblobprojectblobpathroot"></a>`projectBlobPathRoot` | [`String`](#string) | Web path for the root of the blob. |
| <a id="repositoryblobrawblob"></a>`rawBlob` | [`String`](#string) | Raw content of the blob. | | <a id="repositoryblobrawblob"></a>`rawBlob` | [`String`](#string) | Raw content of the blob. |
| <a id="repositoryblobrawpath"></a>`rawPath` | [`String`](#string) | Web path to download the raw blob. | | <a id="repositoryblobrawpath"></a>`rawPath` | [`String`](#string) | Web path to download the raw blob. |
| <a id="repositoryblobrawsize"></a>`rawSize` | [`Int`](#int) | Size (in bytes) of the blob, or the blob target if stored externally. | | <a id="repositoryblobrawsize"></a>`rawSize` | [`Int`](#int) | Size (in bytes) of the blob, or the blob target if stored externally. |
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import axios from 'axios'; import axios from 'axios';
...@@ -28,6 +29,9 @@ let mockResolver; ...@@ -28,6 +29,9 @@ let mockResolver;
Vue.use(VueApollo); Vue.use(VueApollo);
const createMockStore = () =>
new Vuex.Store({ actions: { fetchData: jest.fn, setInitialData: jest.fn() } });
const createComponent = async (mockData = {}) => { const createComponent = async (mockData = {}) => {
const { const {
blob = simpleViewerMock, blob = simpleViewerMock,
...@@ -58,6 +62,7 @@ const createComponent = async (mockData = {}) => { ...@@ -58,6 +62,7 @@ const createComponent = async (mockData = {}) => {
const fakeApollo = createMockApollo([[blobInfoQuery, mockResolver]]); const fakeApollo = createMockApollo([[blobInfoQuery, mockResolver]]);
wrapper = mountExtended(BlobContentViewer, { wrapper = mountExtended(BlobContentViewer, {
store: createMockStore(),
router, router,
apolloProvider: fakeApollo, apolloProvider: fakeApollo,
propsData: { propsData: {
......
...@@ -5,13 +5,14 @@ import App from '~/code_navigation/components/app.vue'; ...@@ -5,13 +5,14 @@ import App from '~/code_navigation/components/app.vue';
import Popover from '~/code_navigation/components/popover.vue'; import Popover from '~/code_navigation/components/popover.vue';
import createState from '~/code_navigation/store/state'; import createState from '~/code_navigation/store/state';
const setInitialData = jest.fn();
const fetchData = jest.fn(); const fetchData = jest.fn();
const showDefinition = jest.fn(); const showDefinition = jest.fn();
let wrapper; let wrapper;
Vue.use(Vuex); Vue.use(Vuex);
function factory(initialState = {}) { function factory(initialState = {}, props = {}) {
const store = new Vuex.Store({ const store = new Vuex.Store({
state: { state: {
...createState(), ...createState(),
...@@ -19,12 +20,13 @@ function factory(initialState = {}) { ...@@ -19,12 +20,13 @@ function factory(initialState = {}) {
definitionPathPrefix: 'https://test.com/blob/main', definitionPathPrefix: 'https://test.com/blob/main',
}, },
actions: { actions: {
setInitialData,
fetchData, fetchData,
showDefinition, showDefinition,
}, },
}); });
wrapper = shallowMount(App, { store }); wrapper = shallowMount(App, { store, propsData: { ...props } });
} }
describe('Code navigation app component', () => { describe('Code navigation app component', () => {
...@@ -32,6 +34,19 @@ describe('Code navigation app component', () => { ...@@ -32,6 +34,19 @@ describe('Code navigation app component', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it('sets initial data on mount if the correct props are passed', () => {
const codeNavigationPath = 'code/nav/path.js';
const path = 'blob/path.js';
const definitionPathPrefix = 'path/prefix';
factory({}, { codeNavigationPath, blobPath: path, pathPrefix: definitionPathPrefix });
expect(setInitialData).toHaveBeenCalledWith(expect.anything(), {
blobs: [{ codeNavigationPath, path }],
definitionPathPrefix,
});
});
it('fetches data on mount', () => { it('fetches data on mount', () => {
factory(); factory();
......
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import axios from 'axios'; import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
...@@ -17,9 +18,11 @@ import DownloadViewer from '~/repository/components/blob_viewers/download_viewer ...@@ -17,9 +18,11 @@ import DownloadViewer from '~/repository/components/blob_viewers/download_viewer
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue'; import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue'; import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
import blobInfoQuery from '~/repository/queries/blob_info.query.graphql'; import blobInfoQuery from '~/repository/queries/blob_info.query.graphql';
import CodeIntelligence from '~/code_navigation/components/app.vue';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import { isLoggedIn } from '~/lib/utils/common_utils'; import { isLoggedIn } from '~/lib/utils/common_utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import httpStatusCodes from '~/lib/utils/http_status';
import { import {
simpleViewerMock, simpleViewerMock,
richViewerMock, richViewerMock,
...@@ -38,6 +41,9 @@ let mockResolver; ...@@ -38,6 +41,9 @@ let mockResolver;
const mockAxios = new MockAdapter(axios); const mockAxios = new MockAdapter(axios);
const createMockStore = () =>
new Vuex.Store({ actions: { fetchData: jest.fn, setInitialData: jest.fn() } });
const createComponent = async (mockData = {}, mountFn = shallowMount) => { const createComponent = async (mockData = {}, mountFn = shallowMount) => {
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -75,6 +81,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount) => { ...@@ -75,6 +81,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount) => {
wrapper = extendedWrapper( wrapper = extendedWrapper(
mountFn(BlobContentViewer, { mountFn(BlobContentViewer, {
store: createMockStore(),
apolloProvider: fakeApollo, apolloProvider: fakeApollo,
propsData: propsMock, propsData: propsMock,
mixins: [{ data: () => ({ ref: refMock }) }], mixins: [{ data: () => ({ ref: refMock }) }],
...@@ -104,6 +111,7 @@ describe('Blob content viewer component', () => { ...@@ -104,6 +111,7 @@ describe('Blob content viewer component', () => {
const findBlobContent = () => wrapper.findComponent(BlobContent); const findBlobContent = () => wrapper.findComponent(BlobContent);
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup); const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion); const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
const findCodeIntelligence = () => wrapper.findComponent(CodeIntelligence);
beforeEach(() => { beforeEach(() => {
isLoggedIn.mockReturnValue(true); isLoggedIn.mockReturnValue(true);
...@@ -219,6 +227,26 @@ describe('Blob content viewer component', () => { ...@@ -219,6 +227,26 @@ describe('Blob content viewer component', () => {
loadViewer.mockRestore(); loadViewer.mockRestore();
}); });
it('renders a CodeIntelligence component with the correct props', async () => {
loadViewer.mockReturnValue(SourceViewer);
await createComponent();
expect(findCodeIntelligence().props()).toMatchObject({
codeNavigationPath: simpleViewerMock.codeNavigationPath,
blobPath: simpleViewerMock.path,
pathPrefix: simpleViewerMock.projectBlobPathRoot,
});
});
it('does not load a CodeIntelligence component when no viewers are loaded', async () => {
const url = 'some_file.js?format=json&viewer=rich';
mockAxios.onGet(url).replyOnce(httpStatusCodes.INTERNAL_SERVER_ERROR);
await createComponent({ blob: { ...richViewerMock, fileType: 'unknown' } });
expect(findCodeIntelligence().exists()).toBe(false);
});
it('does not render a BlobContent component if a Blob viewer is available', async () => { it('does not render a BlobContent component if a Blob viewer is available', async () => {
loadViewer.mockReturnValue(() => true); loadViewer.mockReturnValue(() => true);
await createComponent({ blob: richViewerMock }); await createComponent({ blob: richViewerMock });
......
...@@ -13,6 +13,8 @@ export const simpleViewerMock = { ...@@ -13,6 +13,8 @@ export const simpleViewerMock = {
forkAndEditPath: 'some_file.js/fork/edit', forkAndEditPath: 'some_file.js/fork/edit',
ideForkAndEditPath: 'some_file.js/fork/ide', ideForkAndEditPath: 'some_file.js/fork/ide',
forkAndViewPath: 'some_file.js/fork/view', forkAndViewPath: 'some_file.js/fork/view',
codeNavigationPath: '',
projectBlobPathRoot: '',
environmentFormattedExternalUrl: '', environmentFormattedExternalUrl: '',
environmentExternalUrlForRouteMap: '', environmentExternalUrlForRouteMap: '',
canModifyBlob: true, canModifyBlob: true,
......
...@@ -31,6 +31,8 @@ RSpec.describe Types::Repository::BlobType do ...@@ -31,6 +31,8 @@ RSpec.describe Types::Repository::BlobType do
:permalink_path, :permalink_path,
:environment_formatted_external_url, :environment_formatted_external_url,
:environment_external_url_for_route_map, :environment_external_url_for_route_map,
:code_navigation_path,
:project_blob_path_root,
:code_owners, :code_owners,
:simple_viewer, :simple_viewer,
:rich_viewer, :rich_viewer,
......
...@@ -154,6 +154,16 @@ RSpec.describe BlobPresenter do ...@@ -154,6 +154,16 @@ RSpec.describe BlobPresenter do
end end
end end
describe '#code_navigation_path' do
let(:code_navigation_path) { Gitlab::CodeNavigationPath.new(project, blob.commit_id).full_json_path_for(blob.path) }
it { expect(presenter.code_navigation_path).to eq(code_navigation_path) }
end
describe '#project_blob_path_root' do
it { expect(presenter.project_blob_path_root).to eq("/#{project.full_path}/-/blob/HEAD") }
end
context 'given a Gitlab::Graphql::Representation::TreeEntry' do context 'given a Gitlab::Graphql::Representation::TreeEntry' do
let(:blob) { Gitlab::Graphql::Representation::TreeEntry.new(super(), repository) } let(:blob) { Gitlab::Graphql::Representation::TreeEntry.new(super(), repository) }
......
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