Commit 6b89ab11 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'tz-ide-open-mr' into 'master'

Basic Setup for MR Showing

Closes #44840 and #44839

See merge request gitlab-org/gitlab-ce!17952
parents d2ad0bcc 9d958af5
......@@ -10,6 +10,9 @@ const Api = {
projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels',
mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
groupLabelsPath: '/groups/:namespace_path/-/labels',
licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key',
......@@ -22,10 +25,8 @@ const Api = {
createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath)
.replace(':id', groupId);
return axios.get(url)
.then(({ data }) => {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
return axios.get(url).then(({ data }) => {
callback(data);
return data;
......@@ -35,11 +36,15 @@ const Api = {
// Return groups list. Filtered by query
groups(query, options, callback = $.noop) {
const url = Api.buildUrl(Api.groupsPath);
return axios.get(url, {
params: Object.assign({
return axios
.get(url, {
params: Object.assign(
{
search: query,
per_page: 20,
}, options),
},
options,
),
})
.then(({ data }) => {
callback(data);
......@@ -51,7 +56,8 @@ const Api = {
// Return namespaces list. Filtered by query
namespaces(query, callback) {
const url = Api.buildUrl(Api.namespacesPath);
return axios.get(url, {
return axios
.get(url, {
params: {
search: query,
per_page: 20,
......@@ -73,7 +79,8 @@ const Api = {
defaults.membership = true;
}
return axios.get(url, {
return axios
.get(url, {
params: Object.assign(defaults, options),
})
.then(({ data }) => {
......@@ -85,8 +92,32 @@ const Api = {
// Return single project
project(projectPath) {
const url = Api.buildUrl(Api.projectPath)
.replace(':id', encodeURIComponent(projectPath));
const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath));
return axios.get(url);
},
// Return Merge Request for project
mergeRequest(projectPath, mergeRequestId) {
const url = Api.buildUrl(Api.mergeRequestPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
return axios.get(url);
},
mergeRequestChanges(projectPath, mergeRequestId) {
const url = Api.buildUrl(Api.mergeRequestChangesPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
return axios.get(url);
},
mergeRequestVersions(projectPath, mergeRequestId) {
const url = Api.buildUrl(Api.mergeRequestVersionsPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
return axios.get(url);
},
......@@ -102,7 +133,8 @@ const Api = {
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
}
return axios.post(url, {
return axios
.post(url, {
label: data,
})
.then(res => callback(res.data))
......@@ -111,9 +143,9 @@ const Api = {
// Return group projects list. Filtered by query
groupProjects(groupId, query, callback) {
const url = Api.buildUrl(Api.groupProjectsPath)
.replace(':id', groupId);
return axios.get(url, {
const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
return axios
.get(url, {
params: {
search: query,
per_page: 20,
......@@ -124,8 +156,7 @@ const Api = {
commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const url = Api.buildUrl(Api.commitPath)
.replace(':id', encodeURIComponent(id));
const url = Api.buildUrl(Api.commitPath).replace(':id', encodeURIComponent(id));
return axios.post(url, JSON.stringify(data), {
headers: {
'Content-Type': 'application/json; charset=utf-8',
......@@ -136,39 +167,34 @@ const Api = {
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', encodeURIComponent(id))
.replace(':branch', branch);
.replace(':branch', encodeURIComponent(branch));
return axios.get(url);
},
// Return text for a specific license
licenseText(key, data, callback) {
const url = Api.buildUrl(Api.licensePath)
.replace(':key', key);
return axios.get(url, {
const url = Api.buildUrl(Api.licensePath).replace(':key', key);
return axios
.get(url, {
params: data,
})
.then(res => callback(res.data));
},
gitignoreText(key, callback) {
const url = Api.buildUrl(Api.gitignorePath)
.replace(':key', key);
return axios.get(url)
.then(({ data }) => callback(data));
const url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
return axios.get(url).then(({ data }) => callback(data));
},
gitlabCiYml(key, callback) {
const url = Api.buildUrl(Api.gitlabCiYmlPath)
.replace(':key', key);
return axios.get(url)
.then(({ data }) => callback(data));
const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
return axios.get(url).then(({ data }) => callback(data));
},
dockerfileYml(key, callback) {
const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
return axios.get(url)
.then(({ data }) => callback(data));
return axios.get(url).then(({ data }) => callback(data));
},
issueTemplate(namespacePath, projectPath, key, type, callback) {
......@@ -177,7 +203,8 @@ const Api = {
.replace(':type', type)
.replace(':project_path', projectPath)
.replace(':namespace_path', namespacePath);
return axios.get(url)
return axios
.get(url)
.then(({ data }) => callback(null, data))
.catch(callback);
},
......@@ -185,10 +212,13 @@ const Api = {
users(query, options) {
const url = Api.buildUrl(this.usersPath);
return axios.get(url, {
params: Object.assign({
params: Object.assign(
{
search: query,
per_page: 20,
}, options),
},
options,
),
});
},
......
<script>
import icon from '~/vue_shared/components/icon.vue';
import icon from '~/vue_shared/components/icon.vue';
export default {
export default {
components: {
icon,
},
......@@ -19,7 +19,7 @@
return `multi-${this.changedIcon}`;
},
},
};
};
</script>
<template>
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { __, sprintf } from '~/locale';
export default {
export default {
components: {
Icon,
},
......@@ -11,6 +12,11 @@
required: false,
default: false,
},
mergeRequestId: {
type: String,
required: false,
default: '',
},
viewer: {
type: String,
required: true,
......@@ -20,12 +26,19 @@
required: true,
},
},
computed: {
mergeReviewLine() {
return sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
mergeRequestId: this.mergeRequestId,
});
},
},
methods: {
changeMode(mode) {
this.$emit('click', mode);
},
},
};
};
</script>
<template>
......@@ -43,7 +56,10 @@
}"
data-toggle="dropdown"
>
<template v-if="viewer === 'editor'">
<template v-if="viewer === 'mrdiff' && mergeRequestId">
{{ mergeReviewLine }}
</template>
<template v-else-if="viewer === 'editor'">
{{ __('Editing') }}
</template>
<template v-else>
......@@ -57,6 +73,29 @@
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
<template v-if="mergeRequestId">
<li>
<a
href="#"
@click.prevent="changeMode('mrdiff')"
:class="{
'is-active': viewer === 'mrdiff',
}"
>
<strong class="dropdown-menu-inner-title">
{{ mergeReviewLine }}
</strong>
<span class="dropdown-menu-inner-content">
{{ __('Compare changes with the merge request target branch') }}
</span>
</a>
</li>
<li
role="separator"
class="divider"
>
</li>
</template>
<li>
<a
href="#"
......
<script>
import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue';
import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue';
export default {
export default {
components: {
ideSidebar,
ideContextbar,
......@@ -31,7 +31,7 @@
},
},
computed: {
...mapState(['changedFiles', 'openFiles', 'viewer']),
...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
......@@ -45,7 +45,7 @@
return returnValue;
};
},
};
};
</script>
<template>
......@@ -63,6 +63,7 @@
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
:merge-request-id="currentMergeRequestId"
/>
<repo-editor
class="multi-file-edit-pane-content"
......
<script>
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
icon,
},
directives: {
tooltip,
},
};
</script>
<template>
<icon
name="git-merge"
v-tooltip
title="__('Part of merge request changes')"
css-classes="ide-file-changed-icon"
:size="12"
/>
</template>
<script>
/* global monaco */
import { mapState, mapActions } from 'vuex';
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
......@@ -13,12 +13,8 @@ export default {
},
},
computed: {
...mapState([
'leftPanelCollapsed',
'rightPanelCollapsed',
'viewer',
'delayViewerUpdated',
]),
...mapState(['leftPanelCollapsed', 'rightPanelCollapsed', 'viewer', 'delayViewerUpdated']),
...mapGetters(['currentMergeRequest']),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.raw;
},
......@@ -68,9 +64,14 @@ export default {
this.editor.clearEditor();
this.getRawFileData(this.file)
this.getRawFileData({
path: this.file.path,
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
})
.then(() => {
const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
const viewerPromise = this.delayViewerUpdated
? this.updateViewer('editor')
: Promise.resolve();
return viewerPromise;
})
......@@ -78,7 +79,7 @@ export default {
this.updateDelayViewerUpdated(false);
this.createEditorInstance();
})
.catch((err) => {
.catch(err => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err;
});
......@@ -101,9 +102,13 @@ export default {
this.model = this.editor.createModel(this.file);
if (this.viewer === 'mrdiff') {
this.editor.attachMergeRequestModel(this.model);
} else {
this.editor.attachModel(this.model);
}
this.model.onChange((model) => {
this.model.onChange(model => {
const { file } = model;
if (file.active) {
......
......@@ -6,6 +6,7 @@ import router from '../ide_router';
import newDropdown from './new_dropdown/index.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue';
import mrFileIcon from './mr_file_icon.vue';
export default {
name: 'RepoFile',
......@@ -15,6 +16,7 @@ export default {
fileStatusIcon,
fileIcon,
changedFileIcon,
mrFileIcon,
},
props: {
file: {
......@@ -56,10 +58,7 @@ export default {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
clickFile() {
// Manual Action if a tree is selected/opened
if (
this.isTree &&
this.$router.currentRoute.path === `/project${this.file.url}`
) {
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path);
}
......@@ -102,11 +101,15 @@ export default {
:file="file"
/>
</span>
<span class="pull-right">
<mr-file-icon
v-if="file.mrChange"
/>
<changed-file-icon
:file="file"
v-if="file.changed || file.tempFile"
class="prepend-top-5 pull-right"
/>
</span>
<new-dropdown
v-if="isTree"
:project-id="file.projectId"
......
<script>
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import '~/lib/utils/datetime_utility';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import '~/lib/utils/datetime_utility';
export default {
export default {
components: {
icon,
},
......@@ -21,7 +21,7 @@
return `Locked by ${this.file.file_lock.user.name}`;
},
},
};
};
</script>
<template>
......
<script>
import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue';
import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue';
export default {
export default {
components: {
RepoTab,
EditorMode,
......@@ -21,6 +21,11 @@
type: Boolean,
required: true,
},
mergeRequestId: {
type: String,
required: false,
default: '',
},
},
data() {
return {
......@@ -30,13 +35,12 @@
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow =
this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions(['updateViewer']),
},
};
};
</script>
<template>
......@@ -55,6 +59,7 @@
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
:merge-request-id="mergeRequestId"
@click="updateViewer"
/>
</div>
......
......@@ -44,7 +44,7 @@ const router = new VueRouter({
component: EmptyRouterComponent,
},
{
path: 'mr/:mrid',
path: 'merge_requests/:mrid',
component: EmptyRouterComponent,
},
],
......@@ -76,9 +76,7 @@ router.beforeEach((to, from, next) => {
.then(() => {
if (to.params[0]) {
const path =
to.params[0].slice(-1) === '/'
? to.params[0].slice(0, -1)
: to.params[0];
to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
const treeEntry = store.state.entries[path];
if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry);
......@@ -96,6 +94,60 @@ router.beforeEach((to, from, next) => {
);
throw e;
});
} else if (to.params.mrid) {
store.dispatch('updateViewer', 'mrdiff');
store
.dispatch('getMergeRequestData', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
})
.then(mr => {
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: mr.source_branch,
});
return store.dispatch('getFiles', {
projectId: fullProjectId,
branchId: mr.source_branch,
});
})
.then(() =>
store.dispatch('getMergeRequestVersions', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
}),
)
.then(() =>
store.dispatch('getMergeRequestChanges', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
}),
)
.then(mrChanges => {
mrChanges.changes.forEach((change, ind) => {
const changeTreeEntry = store.state.entries[change.new_path];
if (changeTreeEntry) {
store.dispatch('setFileMrChange', {
file: changeTreeEntry,
mrChange: change,
});
if (ind < 10) {
store.dispatch('getFileData', {
path: change.new_path,
makeFileActive: ind === 0,
});
}
}
});
})
.catch(e => {
flash('Error while loading the merge request. Please try again.');
throw e;
});
}
})
.catch(e => {
......
......@@ -21,6 +21,15 @@ export default class Model {
new this.monaco.Uri(null, null, this.file.path),
)),
);
if (this.file.mrChange) {
this.disposable.add(
(this.baseModel = this.monaco.editor.createModel(
this.file.baseRaw,
undefined,
new this.monaco.Uri(null, null, `target/${this.file.path}`),
)),
);
}
this.events = new Map();
......@@ -28,10 +37,7 @@ export default class Model {
this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
eventHub.$on(
`editor.update.model.content.${this.file.path}`,
this.updateContent,
);
eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
get url() {
......@@ -58,6 +64,10 @@ export default class Model {
return this.originalModel;
}
getBaseModel() {
return this.baseModel;
}
setValue(value) {
this.getModel().setValue(value);
}
......@@ -78,13 +88,7 @@ export default class Model {
this.disposable.dispose();
this.events.clear();
eventHub.$off(
`editor.update.model.dispose.${this.file.path}`,
this.dispose,
);
eventHub.$off(
`editor.update.model.content.${this.file.path}`,
this.updateContent,
);
eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose);
eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
}
......@@ -109,11 +109,19 @@ export default class Editor {
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
}
attachMergeRequestModel(model) {
this.instance.setModel({
original: model.getBaseModel(),
modified: model.getModel(),
});
this.monaco.editor.createDiffNavigator(this.instance, {
alwaysRevealFirst: true,
});
}
setupMonacoTheme() {
this.monaco.editor.defineTheme(
gitlabTheme.themeName,
gitlabTheme.monacoTheme,
);
this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
this.monaco.editor.setTheme('gitlab');
}
......@@ -161,8 +169,6 @@ export default class Editor {
onPositionChange(cb) {
if (!this.instance.onDidChangeCursorPosition) return;
this.disposable.add(
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
);
this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
}
}
......@@ -20,12 +20,35 @@ export default {
return Promise.resolve(file.raw);
}
return Vue.http.get(file.rawPath, { params: { format: 'json' } })
return Vue.http.get(file.rawPath, { params: { format: 'json' } }).then(res => res.text());
},
getBaseRawFileData(file, sha) {
if (file.tempFile) {
return Promise.resolve(file.baseRaw);
}
if (file.baseRaw) {
return Promise.resolve(file.baseRaw);
}
return Vue.http
.get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
params: { format: 'json' },
})
.then(res => res.text());
},
getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`);
},
getProjectMergeRequestData(projectId, mergeRequestId) {
return Api.mergeRequest(projectId, mergeRequestId);
},
getProjectMergeRequestChanges(projectId, mergeRequestId) {
return Api.mergeRequestChanges(projectId, mergeRequestId);
},
getProjectMergeRequestVersions(projectId, mergeRequestId) {
return Api.mergeRequestVersions(projectId, mergeRequestId);
},
getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId);
},
......
......@@ -6,8 +6,7 @@ import FilesDecoratorWorker from './workers/files_decorator_worker';
export const redirectToUrl = (_, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) =>
commit(types.SET_INITIAL_DATA, data);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach(file => {
......@@ -43,14 +42,11 @@ export const createTempEntry = (
) =>
new Promise(resolve => {
const worker = new FilesDecoratorWorker();
const fullName =
name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (state.entries[name]) {
flash(
`The name "${name
.split('/')
.pop()}" is already taken in this directory.`,
`The name "${name.split('/').pop()}" is already taken in this directory.`,
'alert',
document,
null,
......@@ -119,3 +115,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
export * from './actions/merge_request';
......@@ -46,53 +46,63 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
commit(types.SET_CURRENT_BRANCH, file.branchId);
};
export const getFileData = ({ state, commit, dispatch }, file) => {
export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
const file = state.entries[path];
commit(types.TOGGLE_LOADING, { entry: file });
return service
.getFileData(file.url)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
return res.json();
})
.then(data => {
commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, file.path);
dispatch('setFileActive', file.path);
commit(types.TOGGLE_FILE_OPEN, path);
if (makeFileActive) dispatch('setFileActive', path);
commit(types.TOGGLE_LOADING, { entry: file });
})
.catch(() => {
commit(types.TOGGLE_LOADING, { entry: file });
flash(
'Error loading file data. Please try again.',
'alert',
document,
null,
false,
true,
);
flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
});
};
export const getRawFileData = ({ commit, dispatch }, file) =>
export const setFileMrChange = ({ state, commit }, { file, mrChange }) => {
commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
};
export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
const file = state.entries[path];
return new Promise((resolve, reject) => {
service
.getRawFileData(file)
.then(raw => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
if (file.mrChange && file.mrChange.new_file === false) {
service
.getBaseRawFileData(file, baseSha)
.then(baseRaw => {
commit(types.SET_FILE_BASE_RAW_DATA, {
file,
baseRaw,
});
resolve(raw);
})
.catch(e => {
reject(e);
});
} else {
resolve(raw);
}
})
.catch(() =>
flash(
'Error loading file content. Please try again.',
'alert',
document,
null,
false,
true,
),
);
.catch(() => {
flash('Error loading file content. Please try again.');
reject();
});
});
};
export const changeFileContent = ({ state, commit }, { path, content }) => {
const file = state.entries[path];
......@@ -119,10 +129,7 @@ export const setFileEOL = ({ getters, commit }, { eol }) => {
}
};
export const setEditorPosition = (
{ getters, commit },
{ editorRow, editorColumn },
) => {
export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn }) => {
if (getters.activeFile) {
commit(types.SET_FILE_POSITION, {
file: getters.activeFile,
......
import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
export const getMergeRequestData = (
{ commit, state, dispatch },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
service
.getProjectMergeRequestData(projectId, mergeRequestId)
.then(res => res.data)
.then(data => {
commit(types.SET_MERGE_REQUEST, {
projectPath: projectId,
mergeRequestId,
mergeRequest: data,
});
if (!state.currentMergeRequestId) {
commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
}
resolve(data);
})
.catch(() => {
flash('Error loading merge request data. Please try again.');
reject(new Error(`Merge Request not loaded ${projectId}`));
});
} else {
resolve(state.projects[projectId].mergeRequests[mergeRequestId]);
}
});
export const getMergeRequestChanges = (
{ commit, state, dispatch },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) {
service
.getProjectMergeRequestChanges(projectId, mergeRequestId)
.then(res => res.data)
.then(data => {
commit(types.SET_MERGE_REQUEST_CHANGES, {
projectPath: projectId,
mergeRequestId,
changes: data,
});
resolve(data);
})
.catch(() => {
flash('Error loading merge request changes. Please try again.');
reject(new Error(`Merge Request Changes not loaded ${projectId}`));
});
} else {
resolve(state.projects[projectId].mergeRequests[mergeRequestId].changes);
}
});
export const getMergeRequestVersions = (
{ commit, state, dispatch },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId].versions.length || force) {
service
.getProjectMergeRequestVersions(projectId, mergeRequestId)
.then(res => res.data)
.then(data => {
commit(types.SET_MERGE_REQUEST_VERSIONS, {
projectPath: projectId,
mergeRequestId,
versions: data,
});
resolve(data);
})
.catch(() => {
flash('Error loading merge request versions. Please try again.');
reject(new Error(`Merge Request Versions not loaded ${projectId}`));
});
} else {
resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions);
}
});
......@@ -2,9 +2,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
import {
findEntry,
} from '../utils';
import { findEntry } from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit, dispatch }, path) => {
......@@ -21,23 +19,24 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
dispatch('setFileActive', row.path);
} else {
dispatch('getFileData', row);
dispatch('getFileData', { path: row.path });
}
};
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
service.getTreeLastCommit(tree.lastCommitPath)
.then((res) => {
service
.getTreeLastCommit(tree.lastCommitPath)
.then(res => {
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
return res.json();
})
.then((data) => {
data.forEach((lastCommit) => {
.then(data => {
data.forEach(lastCommit => {
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
if (entry) {
......@@ -50,10 +49,8 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
};
export const getFiles = (
{ state, commit, dispatch },
{ projectId, branchId } = {},
) => new Promise((resolve, reject) => {
export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
new Promise((resolve, reject) => {
if (!state.trees[`${projectId}/${branchId}`]) {
const selectedProject = state.projects[projectId];
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
......@@ -61,15 +58,21 @@ export const getFiles = (
service
.getFiles(selectedProject.web_url, branchId)
.then(res => res.json())
.then((data) => {
.then(data => {
const worker = new FilesDecoratorWorker();
worker.addEventListener('message', (e) => {
worker.addEventListener('message', e => {
const { entries, treeList } = e.data;
const selectedTree = state.trees[`${projectId}/${branchId}`];
commit(types.SET_ENTRIES, entries);
commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList });
commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
commit(types.SET_DIRECTORY_DATA, {
treePath: `${projectId}/${branchId}`,
data: treeList,
});
commit(types.TOGGLE_LOADING, {
entry: selectedTree,
forceValue: false,
});
worker.terminate();
......@@ -82,12 +85,11 @@ export const getFiles = (
branchId,
});
})
.catch((e) => {
.catch(e => {
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
reject(e);
});
} else {
resolve();
}
});
});
export const activeFile = state =>
state.openFiles.find(file => file.active) || null;
export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
export const modifiedFiles = state =>
state.changedFiles.filter(f => !f.tempFile);
export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
export const projectsWithTrees = state =>
Object.keys(state.projects).map(projectId => {
......@@ -23,8 +21,17 @@ export const projectsWithTrees = state =>
};
});
export const currentMergeRequest = state => {
if (state.projects[state.currentProjectId]) {
return state.projects[state.currentProjectId].mergeRequests[state.currentMergeRequestId];
}
return null;
};
// eslint-disable-next-line no-confusing-arrow
export const currentIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length;
export const hasMergeRequest = state => !!state.currentMergeRequestId;
......@@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
// Merge Request Mutation Types
export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
export const SET_CURRENT_MERGE_REQUEST = 'SET_CURRENT_MERGE_REQUEST';
export const SET_MERGE_REQUEST_CHANGES = 'SET_MERGE_REQUEST_CHANGES';
export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
// Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
......@@ -28,6 +34,7 @@ export const SET_FILE_DATA = 'SET_FILE_DATA';
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
......@@ -39,5 +46,6 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const SET_ENTRIES = 'SET_ENTRIES';
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
import * as types from './mutation_types';
import projectMutations from './mutations/project';
import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
......@@ -11,10 +12,7 @@ export default {
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
if (entry.path) {
Object.assign(state.entries[entry.path], {
loading:
forceValue !== undefined
? forceValue
: !state.entries[entry.path].loading,
loading: forceValue !== undefined ? forceValue : !state.entries[entry.path].loading,
});
} else {
Object.assign(entry, {
......@@ -83,9 +81,7 @@ export default {
if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], {
tree: state.trees[`${projectId}/${branchId}`].tree.concat(
data.treeList,
),
tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
});
}
},
......@@ -100,6 +96,7 @@ export default {
});
},
...projectMutations,
...mergeRequestMutation,
...fileMutations,
...treeMutations,
...branchMutations,
......
......@@ -28,6 +28,8 @@ export default {
rawPath: data.raw_path,
binary: data.binary,
renderError: data.render_error,
raw: null,
baseRaw: null,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
......@@ -35,6 +37,11 @@ export default {
raw,
});
},
[types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
Object.assign(state.entries[file.path], {
baseRaw,
});
},
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw;
......@@ -59,6 +66,11 @@ export default {
editorColumn,
});
},
[types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
Object.assign(state.entries[file.path], {
mrChange,
});
},
[types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], {
content: state.entries[path].raw,
......
import * as types from '../mutation_types';
export default {
[types.SET_CURRENT_MERGE_REQUEST](state, currentMergeRequestId) {
Object.assign(state, {
currentMergeRequestId,
});
},
[types.SET_MERGE_REQUEST](state, { projectPath, mergeRequestId, mergeRequest }) {
Object.assign(state.projects[projectPath], {
mergeRequests: {
[mergeRequestId]: {
...mergeRequest,
active: true,
changes: [],
versions: [],
baseCommitSha: null,
},
},
});
},
[types.SET_MERGE_REQUEST_CHANGES](state, { projectPath, mergeRequestId, changes }) {
Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
changes,
});
},
[types.SET_MERGE_REQUEST_VERSIONS](state, { projectPath, mergeRequestId, versions }) {
Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
versions,
baseCommitSha: versions.length ? versions[0].base_commit_sha : null,
});
},
};
......@@ -11,6 +11,7 @@ export default {
Object.assign(project, {
tree: [],
branches: {},
mergeRequests: {},
active: true,
});
......
export default () => ({
currentProjectId: '',
currentBranchId: '',
currentMergeRequestId: '',
changedFiles: [],
endpoints: {},
lastCommitMsg: '',
......
......@@ -38,7 +38,7 @@ export const dataStructure = () => ({
eol: '',
});
export const decorateData = (entity) => {
export const decorateData = entity => {
const {
id,
projectId,
......@@ -57,7 +57,6 @@ export const decorateData = (entity) => {
base64 = false,
file_lock,
} = entity;
return {
......@@ -80,17 +79,15 @@ export const decorateData = (entity) => {
base64,
file_lock,
};
};
export const findEntry = (tree, type, name, prop = 'name') => tree.find(
f => f.type === type && f[prop] === name,
);
export const findEntry = (tree, type, name, prop = 'name') =>
tree.find(f => f.type === type && f[prop] === name);
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
export const setPageTitle = (title) => {
export const setPageTitle = title => {
document.title = title;
};
......@@ -120,6 +117,11 @@ const sortTreesByTypeAndName = (a, b) => {
return 0;
};
export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, {
export const sortTree = sortedTree =>
sortedTree
.map(entity =>
Object.assign(entity, {
tree: entity.tree.length ? sortTree(entity.tree) : [],
})).sort(sortTreesByTypeAndName);
}),
)
.sort(sortTreesByTypeAndName);
......@@ -51,7 +51,7 @@ export function removeParams(params) {
const url = document.createElement('a');
url.href = window.location.href;
params.forEach((param) => {
params.forEach(param => {
url.search = removeParamQueryString(url.search, param);
});
......@@ -83,3 +83,11 @@ export function refreshCurrentPage() {
export function redirectTo(url) {
return window.location.assign(url);
}
export function webIDEUrl(route = undefined) {
let returnUrl = `${gon.relative_url_root}/-/ide/`;
if (route) {
returnUrl += `project${route}`;
}
return returnUrl;
}
<script>
import tooltip from '~/vue_shared/directives/tooltip';
import { n__ } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { n__ } from '~/locale';
import { webIDEUrl } from '~/lib/utils/url_utility';
import icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
export default {
name: 'MRWidgetHeader',
directives: {
tooltip,
......@@ -41,13 +42,16 @@
isTargetBranchLong() {
return this.isBranchTitleLong(this.mr.targetBranch);
},
webIdePath() {
return webIDEUrl(this.mr.statusPath.replace('.json', ''));
},
},
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
},
};
};
</script>
<template>
<div class="mr-source-target">
......@@ -96,6 +100,13 @@
</div>
<div v-if="mr.isOpen">
<a
v-if="!mr.sourceBranchRemoved"
:href="webIdePath"
class="btn btn-sm btn-default inline js-web-ide"
>
{{ s__("mrWidget|Web IDE") }}
</a>
<button
data-target="#modal_merge_info"
data-toggle="modal"
......
......@@ -53,6 +53,7 @@
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
max-width: inherit;
svg {
vertical-align: middle;
......
......@@ -35,14 +35,14 @@ describe('Api', () => {
});
describe('group', () => {
it('fetches a group', (done) => {
it('fetches a group', done => {
const groupId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}`;
mock.onGet(expectedUrl).reply(200, {
name: 'test',
});
Api.group(groupId, (response) => {
Api.group(groupId, response => {
expect(response.name).toBe('test');
done();
});
......@@ -50,15 +50,17 @@ describe('Api', () => {
});
describe('groups', () => {
it('fetches groups', (done) => {
it('fetches groups', done => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups.json`;
mock.onGet(expectedUrl).reply(200, [{
mock.onGet(expectedUrl).reply(200, [
{
name: 'test',
}]);
},
]);
Api.groups(query, options, (response) => {
Api.groups(query, options, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
......@@ -67,14 +69,16 @@ describe('Api', () => {
});
describe('namespaces', () => {
it('fetches namespaces', (done) => {
it('fetches namespaces', done => {
const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/namespaces.json`;
mock.onGet(expectedUrl).reply(200, [{
mock.onGet(expectedUrl).reply(200, [
{
name: 'test',
}]);
},
]);
Api.namespaces(query, (response) => {
Api.namespaces(query, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
......@@ -83,31 +87,35 @@ describe('Api', () => {
});
describe('projects', () => {
it('fetches projects with membership when logged in', (done) => {
it('fetches projects with membership when logged in', done => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
window.gon.current_user_id = 1;
mock.onGet(expectedUrl).reply(200, [{
mock.onGet(expectedUrl).reply(200, [
{
name: 'test',
}]);
},
]);
Api.projects(query, options, (response) => {
Api.projects(query, options, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
});
});
it('fetches projects without membership when not logged in', (done) => {
it('fetches projects without membership when not logged in', done => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
mock.onGet(expectedUrl).reply(200, [{
mock.onGet(expectedUrl).reply(200, [
{
name: 'test',
}]);
},
]);
Api.projects(query, options, (response) => {
Api.projects(query, options, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
......@@ -115,8 +123,65 @@ describe('Api', () => {
});
});
describe('mergerequest', () => {
it('fetches a merge request', done => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}`;
mock.onGet(expectedUrl).reply(200, {
title: 'test',
});
Api.mergeRequest(projectPath, mergeRequestId)
.then(({ data }) => {
expect(data.title).toBe('test');
})
.then(done)
.catch(done.fail);
});
});
describe('mergerequest changes', () => {
it('fetches the changes of a merge request', done => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/changes`;
mock.onGet(expectedUrl).reply(200, {
title: 'test',
});
Api.mergeRequestChanges(projectPath, mergeRequestId)
.then(({ data }) => {
expect(data.title).toBe('test');
})
.then(done)
.catch(done.fail);
});
});
describe('mergerequest versions', () => {
it('fetches the versions of a merge request', done => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/versions`;
mock.onGet(expectedUrl).reply(200, [
{
id: 123,
},
]);
Api.mergeRequestVersions(projectPath, mergeRequestId)
.then(({ data }) => {
expect(data.length).toBe(1);
expect(data[0].id).toBe(123);
})
.then(done)
.catch(done.fail);
});
});
describe('newLabel', () => {
it('creates a new label', (done) => {
it('creates a new label', done => {
const namespace = 'some namespace';
const project = 'some project';
const labelData = { some: 'data' };
......@@ -124,36 +189,42 @@ describe('Api', () => {
const expectedData = {
label: labelData,
};
mock.onPost(expectedUrl).reply((config) => {
mock.onPost(expectedUrl).reply(config => {
expect(config.data).toBe(JSON.stringify(expectedData));
return [200, {
return [
200,
{
name: 'test',
}];
},
];
});
Api.newLabel(namespace, project, labelData, (response) => {
Api.newLabel(namespace, project, labelData, response => {
expect(response.name).toBe('test');
done();
});
});
it('creates a group label', (done) => {
it('creates a group label', done => {
const namespace = 'group/subgroup';
const labelData = { some: 'data' };
const expectedUrl = `${dummyUrlRoot}/groups/${namespace}/-/labels`;
const expectedData = {
label: labelData,
};
mock.onPost(expectedUrl).reply((config) => {
mock.onPost(expectedUrl).reply(config => {
expect(config.data).toBe(JSON.stringify(expectedData));
return [200, {
return [
200,
{
name: 'test',
}];
},
];
});
Api.newLabel(namespace, undefined, labelData, (response) => {
Api.newLabel(namespace, undefined, labelData, response => {
expect(response.name).toBe('test');
done();
});
......@@ -161,15 +232,17 @@ describe('Api', () => {
});
describe('groupProjects', () => {
it('fetches group projects', (done) => {
it('fetches group projects', done => {
const groupId = '123456';
const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
mock.onGet(expectedUrl).reply(200, [{
mock.onGet(expectedUrl).reply(200, [
{
name: 'test',
}]);
},
]);
Api.groupProjects(groupId, query, (response) => {
Api.groupProjects(groupId, query, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
......@@ -178,13 +251,13 @@ describe('Api', () => {
});
describe('licenseText', () => {
it('fetches a license text', (done) => {
it('fetches a license text', done => {
const licenseKey = "driver's license";
const data = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/licenses/${licenseKey}`;
mock.onGet(expectedUrl).reply(200, 'test');
Api.licenseText(licenseKey, data, (response) => {
Api.licenseText(licenseKey, data, response => {
expect(response).toBe('test');
done();
});
......@@ -192,12 +265,12 @@ describe('Api', () => {
});
describe('gitignoreText', () => {
it('fetches a gitignore text', (done) => {
it('fetches a gitignore text', done => {
const gitignoreKey = 'ignore git';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitignores/${gitignoreKey}`;
mock.onGet(expectedUrl).reply(200, 'test');
Api.gitignoreText(gitignoreKey, (response) => {
Api.gitignoreText(gitignoreKey, response => {
expect(response).toBe('test');
done();
});
......@@ -205,12 +278,12 @@ describe('Api', () => {
});
describe('gitlabCiYml', () => {
it('fetches a .gitlab-ci.yml', (done) => {
it('fetches a .gitlab-ci.yml', done => {
const gitlabCiYmlKey = 'Y CI ML';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitlab_ci_ymls/${gitlabCiYmlKey}`;
mock.onGet(expectedUrl).reply(200, 'test');
Api.gitlabCiYml(gitlabCiYmlKey, (response) => {
Api.gitlabCiYml(gitlabCiYmlKey, response => {
expect(response).toBe('test');
done();
});
......@@ -218,12 +291,12 @@ describe('Api', () => {
});
describe('dockerfileYml', () => {
it('fetches a Dockerfile', (done) => {
it('fetches a Dockerfile', done => {
const dockerfileYmlKey = 'a giant whale';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/dockerfiles/${dockerfileYmlKey}`;
mock.onGet(expectedUrl).reply(200, 'test');
Api.dockerfileYml(dockerfileYmlKey, (response) => {
Api.dockerfileYml(dockerfileYmlKey, response => {
expect(response).toBe('test');
done();
});
......@@ -231,12 +304,14 @@ describe('Api', () => {
});
describe('issueTemplate', () => {
it('fetches an issue template', (done) => {
it('fetches an issue template', done => {
const namespace = 'some namespace';
const project = 'some project';
const templateKey = ' template #%?.key ';
const templateType = 'template type';
const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent(templateKey)}`;
const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent(
templateKey,
)}`;
mock.onGet(expectedUrl).reply(200, 'test');
Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => {
......@@ -247,13 +322,15 @@ describe('Api', () => {
});
describe('users', () => {
it('fetches users', (done) => {
it('fetches users', done => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users.json`;
mock.onGet(expectedUrl).reply(200, [{
mock.onGet(expectedUrl).reply(200, [
{
name: 'test',
}]);
},
]);
Api.users(query, options)
.then(({ data }) => {
......
......@@ -11,6 +11,7 @@ describe('IDE changed file icon', () => {
vm = createComponent(component, {
file: {
tempFile: false,
changed: true,
},
});
});
......@@ -20,7 +21,7 @@ describe('IDE changed file icon', () => {
});
describe('changedIcon', () => {
it('equals file-modified when not a temp file', () => {
it('equals file-modified when not a temp file and has changes', () => {
expect(vm.changedIcon).toBe('file-modified');
});
......
......@@ -89,6 +89,20 @@ describe('RepoEditor', () => {
done();
});
});
it('calls createDiffInstance when viewer is a merge request diff', done => {
vm.$store.state.viewer = 'mrdiff';
spyOn(vm.editor, 'createDiffInstance');
vm.createEditorInstance();
vm.$nextTick(() => {
expect(vm.editor.createDiffInstance).toHaveBeenCalled();
done();
});
});
});
describe('setupEditor', () => {
......@@ -134,4 +148,48 @@ describe('RepoEditor', () => {
});
});
});
describe('setup editor for merge request viewing', () => {
beforeEach(done => {
// Resetting as the main test setup has already done it
vm.$destroy();
resetStore(vm.$store);
Editor.editorInstance.modelManager.dispose();
const f = {
...file(),
active: true,
tempFile: true,
html: 'testing',
mrChange: { diff: 'ABC' },
baseRaw: 'testing',
content: 'test',
};
const RepoEditor = Vue.extend(repoEditor);
vm = createComponentWithStore(RepoEditor, store, {
file: f,
});
vm.$store.state.openFiles.push(f);
vm.$store.state.entries[f.path] = f;
vm.$store.state.viewer = 'mrdiff';
vm.monaco = true;
vm.$mount();
monacoLoader(['vs/editor/editor.main'], () => {
setTimeout(done, 0);
});
});
it('attaches merge request model to editor when merge request diff', () => {
spyOn(vm.editor, 'attachMergeRequestModel').and.callThrough();
vm.setupEditor();
expect(vm.editor.attachMergeRequestModel).toHaveBeenCalledWith(vm.model);
});
});
});
......@@ -17,6 +17,7 @@ describe('RepoTabs', () => {
files: openedFiles,
viewer: 'editor',
hasChanges: false,
hasMergeRequest: false,
});
openedFiles[0].active = true;
......@@ -56,6 +57,7 @@ describe('RepoTabs', () => {
files: [],
viewer: 'editor',
hasChanges: false,
hasMergeRequest: false,
},
'#test-app',
);
......
......@@ -11,7 +11,10 @@ describe('Multi-file editor library model', () => {
spyOn(eventHub, '$on').and.callThrough();
monacoLoader(['vs/editor/editor.main'], () => {
model = new Model(monaco, file('path'));
const f = file('path');
f.mrChange = { diff: 'ABC' };
f.baseRaw = 'test';
model = new Model(monaco, f);
done();
});
......@@ -21,9 +24,10 @@ describe('Multi-file editor library model', () => {
model.dispose();
});
it('creates original model & new model', () => {
it('creates original model & base model & new model', () => {
expect(model.originalModel).not.toBeNull();
expect(model.model).not.toBeNull();
expect(model.baseModel).not.toBeNull();
});
it('adds eventHub listener', () => {
......@@ -51,6 +55,12 @@ describe('Multi-file editor library model', () => {
});
});
describe('getBaseModel', () => {
it('returns base model', () => {
expect(model.getBaseModel()).toBe(model.baseModel);
});
});
describe('setValue', () => {
it('updates models value', () => {
model.setValue('testing 123');
......
......@@ -143,6 +143,31 @@ describe('Multi-file editor library', () => {
});
});
describe('attachMergeRequestModel', () => {
let model;
beforeEach(() => {
instance.createDiffInstance(document.createElement('div'));
const f = file();
f.mrChanges = { diff: 'ABC' };
f.baseRaw = 'testing';
model = instance.createModel(f);
});
it('sets original & modified', () => {
spyOn(instance.instance, 'setModel');
instance.attachMergeRequestModel(model);
expect(instance.instance.setModel).toHaveBeenCalledWith({
original: model.getBaseModel(),
modified: model.getModel(),
});
});
});
describe('clearEditor', () => {
it('resets the editor model', () => {
instance.createInstance(document.createElement('div'));
......
......@@ -5,7 +5,7 @@ import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
import { file, resetStore } from '../../helpers';
describe('Multi-file store file actions', () => {
describe('IDE store file actions', () => {
beforeEach(() => {
spyOn(router, 'push');
});
......@@ -189,7 +189,7 @@ describe('Multi-file store file actions', () => {
it('calls the service', done => {
store
.dispatch('getFileData', localFile)
.dispatch('getFileData', { path: localFile.path })
.then(() => {
expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
......@@ -200,7 +200,7 @@ describe('Multi-file store file actions', () => {
it('sets the file data', done => {
store
.dispatch('getFileData', localFile)
.dispatch('getFileData', { path: localFile.path })
.then(() => {
expect(localFile.blamePath).toBe('blame_path');
......@@ -211,7 +211,7 @@ describe('Multi-file store file actions', () => {
it('sets document title', done => {
store
.dispatch('getFileData', localFile)
.dispatch('getFileData', { path: localFile.path })
.then(() => {
expect(document.title).toBe('testing getFileData');
......@@ -222,7 +222,7 @@ describe('Multi-file store file actions', () => {
it('sets the file as active', done => {
store
.dispatch('getFileData', localFile)
.dispatch('getFileData', { path: localFile.path })
.then(() => {
expect(localFile.active).toBeTruthy();
......@@ -231,9 +231,20 @@ describe('Multi-file store file actions', () => {
.catch(done.fail);
});
it('sets the file not as active if we pass makeFileActive false', done => {
store
.dispatch('getFileData', { path: localFile.path, makeFileActive: false })
.then(() => {
expect(localFile.active).toBeFalsy();
done();
})
.catch(done.fail);
});
it('adds the file to open files', done => {
store
.dispatch('getFileData', localFile)
.dispatch('getFileData', { path: localFile.path })
.then(() => {
expect(store.state.openFiles.length).toBe(1);
expect(store.state.openFiles[0].name).toBe(localFile.name);
......@@ -256,7 +267,7 @@ describe('Multi-file store file actions', () => {
it('calls getRawFileData service method', done => {
store
.dispatch('getRawFileData', tmpFile)
.dispatch('getRawFileData', { path: tmpFile.path })
.then(() => {
expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
......@@ -267,7 +278,7 @@ describe('Multi-file store file actions', () => {
it('updates file raw data', done => {
store
.dispatch('getRawFileData', tmpFile)
.dispatch('getRawFileData', { path: tmpFile.path })
.then(() => {
expect(tmpFile.raw).toBe('raw');
......@@ -275,6 +286,22 @@ describe('Multi-file store file actions', () => {
})
.catch(done.fail);
});
it('calls also getBaseRawFileData service method', done => {
spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw'));
tmpFile.mrChange = { new_file: false };
store
.dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' })
.then(() => {
expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA');
expect(tmpFile.baseRaw).toBe('baseraw');
done();
})
.catch(done.fail);
});
});
describe('changeFileContent', () => {
......
import store from '~/ide/stores';
import service from '~/ide/services';
import { resetStore } from '../../helpers';
describe('IDE store merge request actions', () => {
beforeEach(() => {
store.state.projects.abcproject = {
mergeRequests: {},
};
});
afterEach(() => {
resetStore(store);
});
describe('getMergeRequestData', () => {
beforeEach(() => {
spyOn(service, 'getProjectMergeRequestData').and.returnValue(
Promise.resolve({ data: { title: 'mergerequest' } }),
);
});
it('calls getProjectMergeRequestData service method', done => {
store
.dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
.then(() => {
expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1);
done();
})
.catch(done.fail);
});
it('sets the Merge Request Object', done => {
store
.dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
.then(() => {
expect(store.state.projects.abcproject.mergeRequests['1'].title).toBe('mergerequest');
expect(store.state.currentMergeRequestId).toBe(1);
done();
})
.catch(done.fail);
});
});
describe('getMergeRequestChanges', () => {
beforeEach(() => {
spyOn(service, 'getProjectMergeRequestChanges').and.returnValue(
Promise.resolve({ data: { title: 'mergerequest' } }),
);
store.state.projects.abcproject.mergeRequests['1'] = { changes: [] };
});
it('calls getProjectMergeRequestChanges service method', done => {
store
.dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
.then(() => {
expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith('abcproject', 1);
done();
})
.catch(done.fail);
});
it('sets the Merge Request Changes Object', done => {
store
.dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
.then(() => {
expect(store.state.projects.abcproject.mergeRequests['1'].changes.title).toBe(
'mergerequest',
);
done();
})
.catch(done.fail);
});
});
describe('getMergeRequestVersions', () => {
beforeEach(() => {
spyOn(service, 'getProjectMergeRequestVersions').and.returnValue(
Promise.resolve({ data: [{ id: 789 }] }),
);
store.state.projects.abcproject.mergeRequests['1'] = { versions: [] };
});
it('calls getProjectMergeRequestVersions service method', done => {
store
.dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
.then(() => {
expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith('abcproject', 1);
done();
})
.catch(done.fail);
});
it('sets the Merge Request Versions Object', done => {
store
.dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
.then(() => {
expect(store.state.projects.abcproject.mergeRequests['1'].versions.length).toBe(1);
done();
})
.catch(done.fail);
});
});
});
......@@ -68,9 +68,7 @@ describe('Multi-file store tree actions', () => {
expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js');
expect(projectTree.tree[1].type).toBe('blob');
expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob');
expect(projectTree.tree[0].tree[0].tree[0].name).toBe(
'fileinsubfolder.js',
);
expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js');
done();
})
......@@ -132,9 +130,7 @@ describe('Multi-file store tree actions', () => {
store
.dispatch('getLastCommitData', projectTree)
.then(() => {
expect(service.getTreeLastCommit).toHaveBeenCalledWith(
'lastcommitpath',
);
expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
done();
})
......@@ -160,9 +156,7 @@ describe('Multi-file store tree actions', () => {
.dispatch('getLastCommitData', projectTree)
.then(Vue.nextTick)
.then(() => {
expect(projectTree.tree[0].lastCommit.message).not.toBe(
'commit message',
);
expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message');
done();
})
......
......@@ -2,7 +2,7 @@ import * as getters from '~/ide/stores/getters';
import state from '~/ide/stores/state';
import { file } from '../helpers';
describe('Multi-file store getters', () => {
describe('IDE store getters', () => {
let localState;
beforeEach(() => {
......@@ -52,4 +52,24 @@ describe('Multi-file store getters', () => {
expect(modifiedFiles[0].name).toBe('added');
});
});
describe('currentMergeRequest', () => {
it('returns Current Merge Request', () => {
localState.currentProjectId = 'abcproject';
localState.currentMergeRequestId = 1;
localState.projects.abcproject = {
mergeRequests: {
1: { mergeId: 1 },
},
};
expect(getters.currentMergeRequest(localState).mergeId).toBe(1);
});
it('returns null if no active Merge Request was found', () => {
localState.currentProjectId = 'otherproject';
expect(getters.currentMergeRequest(localState)).toBeNull();
});
});
});
......@@ -2,7 +2,7 @@ import mutations from '~/ide/stores/mutations/file';
import state from '~/ide/stores/state';
import { file } from '../../helpers';
describe('Multi-file store file mutations', () => {
describe('IDE store file mutations', () => {
let localState;
let localFile;
......@@ -62,6 +62,8 @@ describe('Multi-file store file mutations', () => {
expect(localFile.rawPath).toBe('raw');
expect(localFile.binary).toBeTruthy();
expect(localFile.renderError).toBe('render_error');
expect(localFile.raw).toBeNull();
expect(localFile.baseRaw).toBeNull();
});
});
......@@ -76,6 +78,17 @@ describe('Multi-file store file mutations', () => {
});
});
describe('SET_FILE_BASE_RAW_DATA', () => {
it('sets raw data from base branch', () => {
mutations.SET_FILE_BASE_RAW_DATA(localState, {
file: localFile,
baseRaw: 'testing',
});
expect(localFile.baseRaw).toBe('testing');
});
});
describe('UPDATE_FILE_CONTENT', () => {
beforeEach(() => {
localFile.raw = 'test';
......@@ -112,6 +125,17 @@ describe('Multi-file store file mutations', () => {
});
});
describe('SET_FILE_MERGE_REQUEST_CHANGE', () => {
it('sets file mr change', () => {
mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, {
file: localFile,
mrChange: { diff: 'ABC' },
});
expect(localFile.mrChange.diff).toBe('ABC');
});
});
describe('DISCARD_FILE_CHANGES', () => {
beforeEach(() => {
localFile.content = 'test';
......
import mutations from '~/ide/stores/mutations/merge_request';
import state from '~/ide/stores/state';
describe('IDE store merge request mutations', () => {
let localState;
beforeEach(() => {
localState = state();
localState.projects = { abcproject: { mergeRequests: {} } };
mutations.SET_MERGE_REQUEST(localState, {
projectPath: 'abcproject',
mergeRequestId: 1,
mergeRequest: {
title: 'mr',
},
});
});
describe('SET_CURRENT_MERGE_REQUEST', () => {
it('sets current merge request', () => {
mutations.SET_CURRENT_MERGE_REQUEST(localState, 2);
expect(localState.currentMergeRequestId).toBe(2);
});
});
describe('SET_MERGE_REQUEST', () => {
it('setsmerge request data', () => {
const newMr = localState.projects.abcproject.mergeRequests[1];
expect(newMr.title).toBe('mr');
expect(newMr.active).toBeTruthy();
});
});
describe('SET_MERGE_REQUEST_CHANGES', () => {
it('sets merge request changes', () => {
mutations.SET_MERGE_REQUEST_CHANGES(localState, {
projectPath: 'abcproject',
mergeRequestId: 1,
changes: {
diff: 'abc',
},
});
const newMr = localState.projects.abcproject.mergeRequests[1];
expect(newMr.changes.diff).toBe('abc');
});
});
describe('SET_MERGE_REQUEST_VERSIONS', () => {
it('sets merge request versions', () => {
mutations.SET_MERGE_REQUEST_VERSIONS(localState, {
projectPath: 'abcproject',
mergeRequestId: 1,
versions: [{ id: 123 }],
});
const newMr = localState.projects.abcproject.mergeRequests[1];
expect(newMr.versions.length).toBe(1);
expect(newMr.versions[0].id).toBe(123);
});
});
});
......@@ -17,46 +17,58 @@ describe('MRWidgetHeader', () => {
describe('computed', () => {
describe('shouldShowCommitsBehindText', () => {
it('return true when there are divergedCommitsCount', () => {
vm = mountComponent(Component, { mr: {
vm = mountComponent(Component, {
mr: {
divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
targetBranch: 'master',
} });
statusPath: 'abc',
},
});
expect(vm.shouldShowCommitsBehindText).toEqual(true);
});
it('returns false where there are no divergedComits count', () => {
vm = mountComponent(Component, { mr: {
vm = mountComponent(Component, {
mr: {
divergedCommitsCount: 0,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
targetBranch: 'master',
} });
statusPath: 'abc',
},
});
expect(vm.shouldShowCommitsBehindText).toEqual(false);
});
});
describe('commitsText', () => {
it('returns singular when there is one commit', () => {
vm = mountComponent(Component, { mr: {
vm = mountComponent(Component, {
mr: {
divergedCommitsCount: 1,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
targetBranch: 'master',
} });
statusPath: 'abc',
},
});
expect(vm.commitsText).toEqual('1 commit behind');
});
it('returns plural when there is more than one commit', () => {
vm = mountComponent(Component, { mr: {
vm = mountComponent(Component, {
mr: {
divergedCommitsCount: 2,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
targetBranch: 'master',
} });
statusPath: 'abc',
},
});
expect(vm.commitsText).toEqual('2 commits behind');
});
......@@ -66,7 +78,8 @@ describe('MRWidgetHeader', () => {
describe('template', () => {
describe('common elements', () => {
beforeEach(() => {
vm = mountComponent(Component, { mr: {
vm = mountComponent(Component, {
mr: {
divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
......@@ -77,13 +90,15 @@ describe('MRWidgetHeader', () => {
isOpen: true,
emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath',
} });
statusPath: 'abc',
},
});
});
it('renders source branch link', () => {
expect(
vm.$el.querySelector('.js-source-branch').innerHTML,
).toEqual('<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>');
expect(vm.$el.querySelector('.js-source-branch').innerHTML).toEqual(
'<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
);
});
it('renders clipboard button', () => {
......@@ -101,7 +116,8 @@ describe('MRWidgetHeader', () => {
});
beforeEach(() => {
vm = mountComponent(Component, { mr: {
vm = mountComponent(Component, {
mr: {
divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
......@@ -112,7 +128,9 @@ describe('MRWidgetHeader', () => {
isOpen: true,
emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath',
} });
statusPath: 'abc',
},
});
});
it('renders checkout branch button with modal trigger', () => {
......@@ -123,28 +141,36 @@ describe('MRWidgetHeader', () => {
expect(button.getAttribute('data-toggle')).toEqual('modal');
});
it('renders web ide button', () => {
const button = vm.$el.querySelector('.js-web-ide');
expect(button.textContent.trim()).toEqual('Web IDE');
expect(button.getAttribute('href')).toEqual('undefined/-/ide/projectabc');
});
it('renders download dropdown with links', () => {
expect(
vm.$el.querySelector('.js-download-email-patches').textContent.trim(),
).toEqual('Email patches');
expect(vm.$el.querySelector('.js-download-email-patches').textContent.trim()).toEqual(
'Email patches',
);
expect(
vm.$el.querySelector('.js-download-email-patches').getAttribute('href'),
).toEqual('/mr/email-patches');
expect(vm.$el.querySelector('.js-download-email-patches').getAttribute('href')).toEqual(
'/mr/email-patches',
);
expect(
vm.$el.querySelector('.js-download-plain-diff').textContent.trim(),
).toEqual('Plain diff');
expect(vm.$el.querySelector('.js-download-plain-diff').textContent.trim()).toEqual(
'Plain diff',
);
expect(
vm.$el.querySelector('.js-download-plain-diff').getAttribute('href'),
).toEqual('/mr/plainDiffPath');
expect(vm.$el.querySelector('.js-download-plain-diff').getAttribute('href')).toEqual(
'/mr/plainDiffPath',
);
});
});
describe('with a closed merge request', () => {
beforeEach(() => {
vm = mountComponent(Component, { mr: {
vm = mountComponent(Component, {
mr: {
divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
......@@ -155,7 +181,9 @@ describe('MRWidgetHeader', () => {
isOpen: false,
emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath',
} });
statusPath: 'abc',
},
});
});
it('does not render checkout branch button with modal trigger', () => {
......@@ -165,19 +193,16 @@ describe('MRWidgetHeader', () => {
});
it('does not render download dropdown with links', () => {
expect(
vm.$el.querySelector('.js-download-email-patches'),
).toEqual(null);
expect(vm.$el.querySelector('.js-download-email-patches')).toEqual(null);
expect(
vm.$el.querySelector('.js-download-plain-diff'),
).toEqual(null);
expect(vm.$el.querySelector('.js-download-plain-diff')).toEqual(null);
});
});
describe('without diverged commits', () => {
beforeEach(() => {
vm = mountComponent(Component, { mr: {
vm = mountComponent(Component, {
mr: {
divergedCommitsCount: 0,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
......@@ -188,7 +213,9 @@ describe('MRWidgetHeader', () => {
isOpen: true,
emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath',
} });
statusPath: 'abc',
},
});
});
it('does not render diverged commits info', () => {
......@@ -198,7 +225,8 @@ describe('MRWidgetHeader', () => {
describe('with diverged commits', () => {
beforeEach(() => {
vm = mountComponent(Component, { mr: {
vm = mountComponent(Component, {
mr: {
divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
......@@ -209,11 +237,15 @@ describe('MRWidgetHeader', () => {
isOpen: true,
emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath',
} });
statusPath: 'abc',
},
});
});
it('renders diverged commits info', () => {
expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual('(12 commits behind)');
expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual(
'(12 commits behind)',
);
});
});
});
......
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