diff options
60 files changed, 1129 insertions, 112 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a1232f02ec0..ef263a3f106 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -307,6 +307,8 @@ cloud-native-image: before_script: [] stage: build allow_failure: true + variables: + GIT_DEPTH: "1" cache: {} before_script: - gem install gitlab --no-rdoc --no-ri diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 8ad3d18b302..eb919241318 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -23,6 +23,8 @@ const Api = { commitPath: '/api/:version/projects/:id/repository/commits', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', createBranchPath: '/api/:version/projects/:id/repository/branches', + pipelinesPath: '/api/:version/projects/:id/pipelines', + pipelineJobsPath: '/api/:version/projects/:id/pipelines/:pipeline_id/jobs', group(groupId, callback) { const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); @@ -222,6 +224,20 @@ const Api = { }); }, + pipelines(projectPath, params = {}) { + const url = Api.buildUrl(this.pipelinesPath).replace(':id', encodeURIComponent(projectPath)); + + return axios.get(url, { params }); + }, + + pipelineJobs(projectPath, pipelineId, params = {}) { + const url = Api.buildUrl(this.pipelineJobsPath) + .replace(':id', encodeURIComponent(projectPath)) + .replace(':pipeline_id', pipelineId); + + return axios.get(url, { params }); + }, + buildUrl(url) { let urlRoot = ''; if (gon.relative_url_root != null) { diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue index ea2b13a8b21..cabb3f59b17 100644 --- a/app/assets/javascripts/ide/components/file_finder/index.vue +++ b/app/assets/javascripts/ide/components/file_finder/index.vue @@ -39,12 +39,10 @@ export default { return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS); } - return fuzzaldrinPlus - .filter(this.allBlobs, searchText, { - key: 'path', - maxResults: MAX_FILE_FINDER_RESULTS, - }) - .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt); + return fuzzaldrinPlus.filter(this.allBlobs, searchText, { + key: 'path', + maxResults: MAX_FILE_FINDER_RESULTS, + }); }, filteredBlobsLength() { return this.filteredBlobs.length; diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 6c373a92776..1ec69adce09 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -52,7 +52,10 @@ export default { methods: { ...mapActions(['toggleFileFinder']), mousetrapStopCallback(e, el, combo) { - if (combo === 't' && el.classList.contains('dropdown-input-field')) { + if ( + (combo === 't' && el.classList.contains('dropdown-input-field')) || + el.classList.contains('inputarea') + ) { return true; } else if (combo === 'command+p' || combo === 'ctrl+p') { return false; diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index f8678b602ac..f2178c06c10 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -43,9 +43,13 @@ export default { }, }, watch: { - file(oldVal, newVal) { + file(newVal, oldVal) { + if (oldVal.pending) { + this.removePendingTab(oldVal); + } + // Compare key to allow for files opened in review mode to be cached differently - if (newVal.key !== this.file.key) { + if (oldVal.key !== this.file.key) { this.initMonaco(); if (this.currentActivityView !== activityBarViews.edit) { @@ -99,6 +103,7 @@ export default { 'setFileViewMode', 'setFileEOL', 'updateViewer', + 'removePendingTab', ]), initMonaco() { if (this.shouldHideEditor) return; diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index adca85dc65b..a21cec4e8d8 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -41,7 +41,7 @@ const router = new VueRouter({ component: EmptyRouterComponent, children: [ { - path: ':targetmode(edit|tree|blob)/:branch/*', + path: ':targetmode(edit|tree|blob)/*', component: EmptyRouterComponent, }, { @@ -63,23 +63,27 @@ router.beforeEach((to, from, next) => { .then(() => { const fullProjectId = `${to.params.namespace}/${to.params.project}`; - if (to.params.branch) { - store.dispatch('setCurrentBranchId', to.params.branch); + const baseSplit = to.params[0].split('/-/'); + const branchId = baseSplit[0].slice(-1) === '/' ? baseSplit[0].slice(0, -1) : baseSplit[0]; + + if (branchId) { + const basePath = baseSplit.length > 1 ? baseSplit[1] : ''; + + store.dispatch('setCurrentBranchId', branchId); store.dispatch('getBranchData', { projectId: fullProjectId, - branchId: to.params.branch, + branchId, }); store .dispatch('getFiles', { projectId: fullProjectId, - branchId: to.params.branch, + branchId, }) .then(() => { - if (to.params[0]) { - const path = - to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0]; + if (basePath) { + const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; const treeEntryKey = Object.keys(store.state.entries).find( key => key === path && !store.state.entries[key].pending, ); diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index b6baa693104..13aea91d8ba 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -63,7 +63,9 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive const file = state.entries[path]; commit(types.TOGGLE_LOADING, { entry: file }); return service - .getFileData(`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url}`) + .getFileData( + `${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`, + ) .then(res => { const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); setPageTitle(pageTitle); diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js index 7c82ce7976b..699710055e3 100644 --- a/app/assets/javascripts/ide/stores/index.js +++ b/app/assets/javascripts/ide/stores/index.js @@ -5,6 +5,7 @@ import * as actions from './actions'; import * as getters from './getters'; import mutations from './mutations'; import commitModule from './modules/commit'; +import pipelines from './modules/pipelines'; Vue.use(Vuex); @@ -15,5 +16,6 @@ export default new Vuex.Store({ getters, modules: { commit: commitModule, + pipelines, }, }); diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index b85246b2502..cd25c3060f2 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -204,17 +204,23 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo dispatch('updateViewer', 'editor', { root: true }); router.push( - `/project/${rootState.currentProjectId}/blob/${getters.branchName}/${ + `/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${ rootGetters.activeFile.path }`, ); } }) .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)) - .then(() => dispatch('refreshLastCommitData', { - projectId: rootState.currentProjectId, - branchId: rootState.currentBranchId, - }, { root: true })); + .then(() => + dispatch( + 'refreshLastCommitData', + { + projectId: rootState.currentProjectId, + branchId: rootState.currentBranchId, + }, + { root: true }, + ), + ); }) .catch(err => { let errMsg = __('Error committing changes. Please try again.'); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js new file mode 100644 index 00000000000..07f7b201f2e --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js @@ -0,0 +1,49 @@ +import { __ } from '../../../../locale'; +import Api from '../../../../api'; +import flash from '../../../../flash'; +import * as types from './mutation_types'; + +export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE); +export const receiveLatestPipelineError = ({ commit }) => { + flash(__('There was an error loading latest pipeline')); + commit(types.RECEIVE_LASTEST_PIPELINE_ERROR); +}; +export const receiveLatestPipelineSuccess = ({ commit }, pipeline) => + commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, pipeline); + +export const fetchLatestPipeline = ({ dispatch, rootState }, sha) => { + dispatch('requestLatestPipeline'); + + return Api.pipelines(rootState.currentProjectId, { sha, per_page: '1' }) + .then(({ data }) => { + dispatch('receiveLatestPipelineSuccess', data.pop()); + }) + .catch(() => dispatch('receiveLatestPipelineError')); +}; + +export const requestJobs = ({ commit }) => commit(types.REQUEST_JOBS); +export const receiveJobsError = ({ commit }) => { + flash(__('There was an error loading jobs')); + commit(types.RECEIVE_JOBS_ERROR); +}; +export const receiveJobsSuccess = ({ commit }, data) => commit(types.RECEIVE_JOBS_SUCCESS, data); + +export const fetchJobs = ({ dispatch, state, rootState }, page = '1') => { + dispatch('requestJobs'); + + Api.pipelineJobs(rootState.currentProjectId, state.latestPipeline.id, { + page, + }) + .then(({ data, headers }) => { + const nextPage = headers && headers['x-next-page']; + + dispatch('receiveJobsSuccess', data); + + if (nextPage) { + dispatch('fetchJobs', nextPage); + } + }) + .catch(() => dispatch('receiveJobsError')); +}; + +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js new file mode 100644 index 00000000000..d6c91f5b64d --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js @@ -0,0 +1,7 @@ +export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline; + +export const failedJobs = state => + state.stages.reduce( + (acc, stage) => acc.concat(stage.jobs.filter(job => job.status === 'failed')), + [], + ); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/index.js b/app/assets/javascripts/ide/stores/modules/pipelines/index.js new file mode 100644 index 00000000000..b44c3141b81 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/index.js @@ -0,0 +1,12 @@ +import state from './state'; +import * as actions from './actions'; +import mutations from './mutations'; +import * as getters from './getters'; + +export default { + namespaced: true, + state: state(), + actions, + mutations, + getters, +}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js new file mode 100644 index 00000000000..6b5701670a6 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js @@ -0,0 +1,7 @@ +export const REQUEST_LATEST_PIPELINE = 'REQUEST_LATEST_PIPELINE'; +export const RECEIVE_LASTEST_PIPELINE_ERROR = 'RECEIVE_LASTEST_PIPELINE_ERROR'; +export const RECEIVE_LASTEST_PIPELINE_SUCCESS = 'RECEIVE_LASTEST_PIPELINE_SUCCESS'; + +export const REQUEST_JOBS = 'REQUEST_JOBS'; +export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR'; +export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS'; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js new file mode 100644 index 00000000000..2b16e57b386 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js @@ -0,0 +1,53 @@ +/* eslint-disable no-param-reassign */ +import * as types from './mutation_types'; + +export default { + [types.REQUEST_LATEST_PIPELINE](state) { + state.isLoadingPipeline = true; + }, + [types.RECEIVE_LASTEST_PIPELINE_ERROR](state) { + state.isLoadingPipeline = false; + }, + [types.RECEIVE_LASTEST_PIPELINE_SUCCESS](state, pipeline) { + state.isLoadingPipeline = false; + + if (pipeline) { + state.latestPipeline = { + id: pipeline.id, + status: pipeline.status, + }; + } + }, + [types.REQUEST_JOBS](state) { + state.isLoadingJobs = true; + }, + [types.RECEIVE_JOBS_ERROR](state) { + state.isLoadingJobs = false; + }, + [types.RECEIVE_JOBS_SUCCESS](state, jobs) { + state.isLoadingJobs = false; + + state.stages = jobs.reduce((acc, job) => { + let stage = acc.find(s => s.title === job.stage); + + if (!stage) { + stage = { + title: job.stage, + jobs: [], + }; + + acc.push(stage); + } + + stage.jobs = stage.jobs.concat({ + id: job.id, + name: job.name, + status: job.status, + stage: job.stage, + duration: job.duration, + }); + + return acc; + }, state.stages); + }, +}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/state.js b/app/assets/javascripts/ide/stores/modules/pipelines/state.js new file mode 100644 index 00000000000..6f22542aaea --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/state.js @@ -0,0 +1,6 @@ +export default () => ({ + isLoadingPipeline: false, + isLoadingJobs: false, + latestPipeline: null, + stages: [], +}); diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js index d249b05f47c..0a1c253c637 100644 --- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js +++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js @@ -26,7 +26,7 @@ self.addEventListener('message', e => { id: folderPath, name: folderName, path: folderPath, - url: `/${projectId}/tree/${branchId}/${folderPath}/`, + url: `/${projectId}/tree/${branchId}/-/${folderPath}/`, type: 'tree', parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`, tempFile, @@ -64,7 +64,7 @@ self.addEventListener('message', e => { id: path, name: blobName, path, - url: `/${projectId}/blob/${branchId}/${path}`, + url: `/${projectId}/blob/${branchId}/-/${path}`, type: 'blob', parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`, tempFile, diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index c3d94d63c13..b7624cf490a 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -2,10 +2,7 @@ import $ from 'jquery'; import timeago from 'timeago.js'; import dateFormat from 'vendor/date.format'; import { pluralize } from './text_utility'; -import { - languageCode, - s__, -} from '../../locale'; +import { languageCode, s__ } from '../../locale'; window.timeago = timeago; window.dateFormat = dateFormat; @@ -17,11 +14,37 @@ window.dateFormat = dateFormat; * * @param {Boolean} abbreviated */ -const getMonthNames = (abbreviated) => { +const getMonthNames = abbreviated => { if (abbreviated) { - return [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')]; + return [ + s__('Jan'), + s__('Feb'), + s__('Mar'), + s__('Apr'), + s__('May'), + s__('Jun'), + s__('Jul'), + s__('Aug'), + s__('Sep'), + s__('Oct'), + s__('Nov'), + s__('Dec'), + ]; } - return [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')]; + return [ + s__('January'), + s__('February'), + s__('March'), + s__('April'), + s__('May'), + s__('June'), + s__('July'), + s__('August'), + s__('September'), + s__('October'), + s__('November'), + s__('December'), + ]; }; /** @@ -29,7 +52,8 @@ const getMonthNames = (abbreviated) => { * @param {date} date * @returns {String} */ -export const getDayName = date => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()]; +export const getDayName = date => + ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()]; /** * @example @@ -55,7 +79,7 @@ export function getTimeago() { if (!timeagoInstance) { const localeRemaining = function getLocaleRemaining(number, index) { return [ - [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')], + [s__('Timeago|less than a minute ago'), s__('Timeago|right now')], [s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')], [s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')], [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], @@ -73,7 +97,7 @@ export function getTimeago() { }; const locale = function getLocale(number, index) { return [ - [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')], + [s__('Timeago|less than a minute ago'), s__('Timeago|right now')], [s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')], [s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')], [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], @@ -102,7 +126,7 @@ export function getTimeago() { * For the given element, renders a timeago instance. * @param {jQuery} $els */ -export const renderTimeago = ($els) => { +export const renderTimeago = $els => { const timeagoEls = $els || document.querySelectorAll('.js-timeago-render'); // timeago.js sets timeouts internally for each timeago value to be updated in real time @@ -119,7 +143,8 @@ export const localTimeAgo = ($timeagoEls, setTimeago = true) => { if (setTimeago) { // Recreate with custom template $(el).tooltip({ - template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>', + template: + '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>', }); } @@ -141,7 +166,9 @@ export const timeFor = (time, expiredLabel) => { if (new Date(time) < new Date()) { return expiredLabel || s__('Timeago|Past due'); } - return getTimeago().format(time, `${timeagoLanguageCode}-remaining`).trim(); + return getTimeago() + .format(time, `${timeagoLanguageCode}-remaining`) + .trim(); }; export const getDayDifference = (a, b) => { @@ -161,7 +188,7 @@ export const getDayDifference = (a, b) => { export function timeIntervalInWords(intervalInSeconds) { const secondsInteger = parseInt(intervalInSeconds, 10); const minutes = Math.floor(secondsInteger / 60); - const seconds = secondsInteger - (minutes * 60); + const seconds = secondsInteger - minutes * 60; let text = ''; if (minutes >= 1) { @@ -178,8 +205,34 @@ export function dateInWords(date, abbreviated = false, hideYear = false) { const month = date.getMonth(); const year = date.getFullYear(); - const monthNames = [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')]; - const monthNamesAbbr = [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')]; + const monthNames = [ + s__('January'), + s__('February'), + s__('March'), + s__('April'), + s__('May'), + s__('June'), + s__('July'), + s__('August'), + s__('September'), + s__('October'), + s__('November'), + s__('December'), + ]; + const monthNamesAbbr = [ + s__('Jan'), + s__('Feb'), + s__('Mar'), + s__('Apr'), + s__('May'), + s__('Jun'), + s__('Jul'), + s__('Aug'), + s__('Sep'), + s__('Oct'), + s__('Nov'), + s__('Dec'), + ]; const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month]; @@ -210,7 +263,7 @@ export const monthInWords = (date, abbreviated = false) => { * * @param {Date} date */ -export const totalDaysInMonth = (date) => { +export const totalDaysInMonth = date => { if (!date) { return 0; } @@ -223,12 +276,20 @@ export const totalDaysInMonth = (date) => { * * @param {Date} date */ -export const getSundays = (date) => { +export const getSundays = date => { if (!date) { return []; } - const daysToSunday = ['Saturday', 'Friday', 'Thursday', 'Wednesday', 'Tuesday', 'Monday', 'Sunday']; + const daysToSunday = [ + 'Saturday', + 'Friday', + 'Thursday', + 'Wednesday', + 'Tuesday', + 'Monday', + 'Sunday', + ]; const month = date.getMonth(); const year = date.getFullYear(); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js index db1d09eb2f2..70f185e3656 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js @@ -351,7 +351,7 @@ import Cookies from 'js-cookie'; }, getCommitButtonText() { - const initial = 'Commit conflict resolution'; + const initial = 'Commit to source branch'; const inProgress = 'Committing...'; return this.state ? this.state.isSubmitting ? inProgress : initial : initial; diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 04bde64c752..3d5ed9ef3c5 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -286,6 +286,14 @@ $colors: ( } .resolve-conflicts-form { - padding-top: $gl-padding; + h4 { + margin-top: 0; + } + + .resolve-info { + @media (max-width: $screen-md-max) { + margin-bottom: $gl-padding; + } + } } } diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index aa4569500b8..f5d94ad96a1 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -2,6 +2,11 @@ require 'digest/md5' require 'uri' module ApplicationHelper + # See https://docs.gitlab.com/ee/development/ee_features.html#code-in-app-views + def render_if_exists(partial, locals = {}) + render(partial, locals) if lookup_context.exists?(partial, [], true) + end + # Check if a particular controller is the current one # # args - One or more controller names to check diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index e7a36e20050..3db28fd6da3 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -17,7 +17,9 @@ module BlobHelper end def ide_edit_path(project = @project, ref = @ref, path = @path, options = {}) - "#{ide_path}/project#{url_for([project, "edit", "blob", id: [ref, path], script_name: "/"])}" + segments = [ide_path, 'project', project.full_path, 'edit', ref] + segments.concat(['-', path]) if path.present? + File.join(segments) end def edit_blob_button(project = @project, ref = @ref, path = @path, options = {}) @@ -331,7 +333,6 @@ module BlobHelper if !on_top_of_branch?(project, ref) edit_disabled_button_tag(text, common_classes) # This condition only applies to users who are logged in - # Web IDE (Beta) requires the user to have this feature enabled elsif !current_user || (current_user && can_modify_blob?(blob, project, ref)) edit_link_tag(text, edit_path, common_classes) elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project) diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 26cbfd784ad..84248f9590b 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -3,6 +3,7 @@ require "gemnasium/gitlab_service" class GemnasiumService < Service prop_accessor :token, :api_key validates :token, :api_key, presence: true, if: :activated? + validate :deprecation_validation def title 'Gemnasium' @@ -27,6 +28,18 @@ class GemnasiumService < Service %w(push) end + def deprecated? + true + end + + def deprecation_message + "Gemnasium has been acquired by GitLab in January 2018. Since May 15, 2018, the service provided by Gemnasium is no longer available." + end + + def deprecation_validation + errors[:base] << deprecation_message + end + def execute(data) return unless supported_events.include?(data[:object_kind]) diff --git a/app/models/service.rb b/app/models/service.rb index f7e3f7590ad..831c2ea1141 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -253,7 +253,6 @@ class Service < ActiveRecord::Base emails_on_push external_wiki flowdock - gemnasium hipchat irker jira diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb index 7c4a79f555e..3025029755c 100644 --- a/app/services/boards/issues/create_service.rb +++ b/app/services/boards/issues/create_service.rb @@ -10,11 +10,15 @@ module Boards end def execute - create_issue(params.merge(label_ids: [list.label_id])) + create_issue(params.merge(issue_params)) end private + def issue_params + { label_ids: [list.label_id] } + end + def board @board ||= parent.boards.find(params.delete(:board_id)) end diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 41ef646fc0e..cfa1d9d0f0c 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -2,6 +2,8 @@ - breadcrumb_title "Dashboard" %div{ class: container_class } + = render_if_exists "admin/licenses/breakdown", license: @license + .admin-dashboard.prepend-top-default .row .col-sm-4 @@ -20,6 +22,7 @@ %h3.text-center Users: = approximate_count_with_delimiters(User) + = render_if_exists 'users_statistics' %hr = link_to 'New user', new_admin_user_path, class: "btn btn-new" .col-sm-4 @@ -97,6 +100,9 @@ = reply_email %span.light.pull-right = boolean_to_icon Gitlab::IncomingEmail.enabled? + + = render_if_exists 'elastic_and_geo' + - container_reg = "Container Registry" %p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") } = container_reg @@ -144,6 +150,9 @@ GitLab Pages %span.pull-right = Gitlab::Pages::VERSION + + = render_if_exists 'geo' + %p Ruby %span.pull-right diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml index 964dc40a213..e6205f24ae6 100644 --- a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml +++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml @@ -11,6 +11,6 @@ Showing %strong.cred {{conflictsCountText}} between - %strong {{conflictsData.sourceBranch}} + %strong.ref-name {{conflictsData.sourceBranch}} and - %strong {{conflictsData.targetBranch}} + %strong.ref-name {{conflictsData.targetBranch}} diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml index 13026b7566a..b86a87a1fc6 100644 --- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml +++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml @@ -1,13 +1,21 @@ +- branch_name = link_to @merge_request.source_branch, project_tree_path(@merge_request.project, @merge_request.source_branch), class: "ref-name" +- translation =_('You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}') % { use_ours: '<code>Use Ours</code>', use_theirs: '<code>Use Theirs</code>', branch_name: branch_name } + +%hr .form-horizontal.resolve-conflicts-form .form-group - %label.col-sm-2.control-label{ "for" => "commit-message" } - #{ _('Commit message') } - .col-sm-10 + .col-md-4 + %h4= _('Resolve conflicts on source branch') + .resolve-info + = translation.html_safe + .col-md-8 + %label.label-light{ "for" => "commit-message" } + #{ _('Commit message') } .commit-message-container .max-width-marker %textarea.form-control.js-commit-message#commit-message{ "v-model" => "conflictsData.commitMessage", "rows" => "5" } .form-group - .col-sm-offset-2.col-sm-10 + .col-md-offset-4.col-md-8 .row .col-xs-6 %button.btn.btn-success.js-submit-button{ type: "button", "@click" => "commit()", ":disabled" => "!readyToCommit" } diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 8587d3b0c0d..fc8ebfa1fb1 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -82,7 +82,7 @@ - if can_collaborate = succeed " " do - = link_to ide_edit_path(@project, @id, ""), class: 'btn btn-default' do + = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default' do = _('Web IDE') = render 'projects/buttons/download', project: @project, ref: @ref diff --git a/changelogs/unreleased/46354-deprecate_gemnasium_service.yml b/changelogs/unreleased/46354-deprecate_gemnasium_service.yml new file mode 100644 index 00000000000..c5ead45d883 --- /dev/null +++ b/changelogs/unreleased/46354-deprecate_gemnasium_service.yml @@ -0,0 +1,5 @@ +--- +title: Deprecate Gemnasium project service +merge_request: 18954 +author: +type: deprecated diff --git a/changelogs/unreleased/issue-25256.yml b/changelogs/unreleased/issue-25256.yml new file mode 100644 index 00000000000..e981b5f9a8f --- /dev/null +++ b/changelogs/unreleased/issue-25256.yml @@ -0,0 +1,5 @@ +--- +title: Don't trim incoming emails that create new issues +merge_request: +author: Cameron Crockett +type: fixed diff --git a/changelogs/unreleased/winh-make-it-right-now.yml b/changelogs/unreleased/winh-make-it-right-now.yml new file mode 100644 index 00000000000..7b386c0b332 --- /dev/null +++ b/changelogs/unreleased/winh-make-it-right-now.yml @@ -0,0 +1,5 @@ +--- +title: Use "right now" for short time periods +merge_request: 19095 +author: +type: changed diff --git a/config/webpack.config.js b/config/webpack.config.js index cfeae801e7b..27050e7069d 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -9,6 +9,7 @@ const CompressionPlugin = require('compression-webpack-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const ROOT_PATH = path.resolve(__dirname, '..'); +const CACHE_PATH = path.join(ROOT_PATH, 'tmp/cache'); const IS_PRODUCTION = process.env.NODE_ENV === 'production'; const IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1; const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost'; @@ -17,6 +18,9 @@ const DEV_SERVER_LIVERELOAD = IS_DEV_SERVER && process.env.DEV_SERVER_LIVERELOAD const WEBPACK_REPORT = process.env.WEBPACK_REPORT; const NO_COMPRESSION = process.env.NO_COMPRESSION; +const VUE_VERSION = require('vue/package.json').version; +const VUE_LOADER_VERSION = require('vue-loader/package.json').version; + let autoEntriesCount = 0; let watchAutoEntries = []; const defaultEntries = ['./main']; @@ -99,12 +103,21 @@ module.exports = { exclude: path => /node_modules|vendor[\\/]assets/.test(path) && !/\.vue\.js/.test(path), loader: 'babel-loader', options: { - cacheDirectory: path.join(ROOT_PATH, 'tmp/cache/babel-loader'), + cacheDirectory: path.join(CACHE_PATH, 'babel-loader'), }, }, { test: /\.vue$/, loader: 'vue-loader', + options: { + cacheDirectory: path.join(CACHE_PATH, 'vue-loader'), + cacheIdentifier: [ + process.env.NODE_ENV || 'development', + webpack.version, + VUE_VERSION, + VUE_LOADER_VERSION, + ].join('|'), + }, }, { test: /\.svg$/, diff --git a/db/post_migrate/20180514161336_remove_gemnasium_service.rb b/db/post_migrate/20180514161336_remove_gemnasium_service.rb new file mode 100644 index 00000000000..6d7806e8daa --- /dev/null +++ b/db/post_migrate/20180514161336_remove_gemnasium_service.rb @@ -0,0 +1,15 @@ +class RemoveGemnasiumService < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + disable_statement_timeout + + execute("DELETE FROM services WHERE type='GemnasiumService';") + end + + def down + # noop + end +end diff --git a/doc/api/services.md b/doc/api/services.md index ec632125325..f23303ef836 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -405,6 +405,13 @@ GET /projects/:id/services/flowdock Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities. +CAUTION: **Warning:** +Gemnasium service integration has been deprecated in GitLab 11.0. Gemnasium has been +[acquired by GitLab](https://about.gitlab.com/press/releases/2018-01-30-gemnasium-acquisition.html) +in January 2018 and since May 15, 2018, the service provided by Gemnasium is no longer available. +You can [migrate from Gemnasium to GitLab](https://docs.gitlab.com/ee/user/project/import/gemnasium.html) +to keep monitoring your dependencies. + ### Create/Edit Gemnasium service Set Gemnasium service for a project. diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index 4873090a2d4..057a4094aed 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -53,6 +53,9 @@ stub_licensed_features(variable_environment_scope: true) EE-specific comments should not be backported to CE. +**Note:** This is only meant as a workaround, we should follow up and +resolve this soon. + ### Detection of EE-only files For each commit (except on `master`), the `ee-files-location-check` CI job tries @@ -105,11 +108,14 @@ is applied not only to models. Here's a list of other examples: - `ee/app/services/foo/create_service.rb` - `ee/app/validators/foo_attr_validator.rb` - `ee/app/workers/foo_worker.rb` +- `ee/app/views/foo.html.haml` +- `ee/app/views/foo/_bar.html.haml` This works because for every path that are present in CE's eager-load/auto-load paths, we add the same `ee/`-prepended path in [`config/application.rb`]. +This also applies to views. -[`config/application.rb`]: https://gitlab.com/gitlab-org/gitlab-ee/blob/d278b76d6600a0e27d8019a0be27971ba23ab640/config/application.rb#L41-51 +[`config/application.rb`]: https://gitlab.com/gitlab-org/gitlab-ee/blob/925d3d4ebc7a2c72964ce97623ae41b8af12538d/config/application.rb#L42-52 ### EE features based on CE features @@ -359,9 +365,37 @@ Blocks of code that are EE-specific should be moved to partials. This avoids conflicts with big chunks of HAML code that that are not fun to resolve when you add the indentation to the equation. -EE-specific views should be placed in `ee/app/views/ee/`, using extra +EE-specific views should be placed in `ee/app/views/`, using extra sub-directories if appropriate. +Instead of using regular `render`, we should use `render_if_exists`, which +will not render anything if it cannot find the specific partial. We use this +so that we could put `render_if_exists` in CE, keeping code the same between +CE and EE. + +Also, it should search for the EE partial first, and then CE partial, and +then if nothing found, render nothing. + +This has two uses: + +- CE renders nothing, and EE renders its EE partial. +- CE renders its CE partial, and EE renders its EE partial, while the view + file stays the same. + +The advantages of this: + +- Minimal code difference between CE and EE. +- Very clear hints about where we're extending EE views while reading CE codes. +- Whenever we want to show something different in CE, we could just add CE + partials. Same applies the other way around. If we just use + `render_if_exists`, it would be very easy to change the content in EE. + +The disadvantage of this: + +- Slightly more work while developing EE features, because now we need to + port `render_if_exists` to CE. +- If we have typos in the partial name, it would be silently ignored. + ### Code in `lib/` Place EE-specific logic in the top-level `EE` module namespace. Namespace the diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md index 9496d6f2ce0..074eeb729e3 100644 --- a/doc/user/project/integrations/project_services.md +++ b/doc/user/project/integrations/project_services.md @@ -34,7 +34,7 @@ Click on the service links to see further configuration instructions and details | [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients | | External Wiki | Replaces the link to the internal wiki with a link to an external wiki | | Flowdock | Flowdock is a collaboration web app for technical teams | -| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | +| Gemnasium _(Has been deprecated in GitLab 11.0)_ | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | | [HipChat](hipchat.md) | Private group chat and IM | | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | | [JIRA](jira.md) | JIRA issue tracker | diff --git a/doc/user/project/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md index 68c49054e47..ecbc8534eea 100644 --- a/doc/user/project/merge_requests/resolve_conflicts.md +++ b/doc/user/project/merge_requests/resolve_conflicts.md @@ -29,7 +29,7 @@ The merge conflict resolution editor allows for more complex merge conflicts, which require the user to manually modify a file in order to resolve a conflict, to be solved right form the GitLab interface. Use the **Edit inline** button to open the editor. Once you're sure about your changes, hit the -**Commit conflict resolution** button. +**Commit to source branch** button. ![Merge conflict editor](img/merge_conflict_editor.png) diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb index 05a60deb7d3..764f93f6d3d 100644 --- a/lib/gitlab/email/handler/create_issue_handler.rb +++ b/lib/gitlab/email/handler/create_issue_handler.rb @@ -47,7 +47,7 @@ module Gitlab project, author, title: mail.subject, - description: message + description: message_including_reply ).execute end end diff --git a/lib/gitlab/email/handler/reply_processing.rb b/lib/gitlab/email/handler/reply_processing.rb index da5ff350549..38b1425364f 100644 --- a/lib/gitlab/email/handler/reply_processing.rb +++ b/lib/gitlab/email/handler/reply_processing.rb @@ -16,8 +16,12 @@ module Gitlab @message ||= process_message end - def process_message - message = ReplyParser.new(mail).execute.strip + def message_including_reply + @message_with_reply ||= process_message(trim_reply: false) + end + + def process_message(**kwargs) + message = ReplyParser.new(mail, **kwargs).execute.strip add_attachments(message) end diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb index 01c28d051ee..ae6b84607d6 100644 --- a/lib/gitlab/email/reply_parser.rb +++ b/lib/gitlab/email/reply_parser.rb @@ -4,8 +4,9 @@ module Gitlab class ReplyParser attr_accessor :message - def initialize(message) + def initialize(message, trim_reply: true) @message = message + @trim_reply = trim_reply end def execute @@ -13,7 +14,9 @@ module Gitlab encoding = body.encoding - body = EmailReplyTrimmer.trim(body) + if @trim_reply + body = EmailReplyTrimmer.trim(body) + end return '' unless body diff --git a/lib/gitlab/git/gitlab_projects.rb b/lib/gitlab/git/gitlab_projects.rb index 68373460d23..00c943fdb25 100644 --- a/lib/gitlab/git/gitlab_projects.rb +++ b/lib/gitlab/git/gitlab_projects.rb @@ -53,7 +53,7 @@ module Gitlab # Import project via git clone --bare # URL must be publicly cloneable def import_project(source, timeout) - Gitlab::GitalyClient.migrate(:import_repository) do |is_enabled| + Gitlab::GitalyClient.migrate(:import_repository, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled gitaly_import_repository(source) else diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 2be604a5b13..1a21625a322 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -1048,7 +1048,7 @@ module Gitlab return @info_attributes if @info_attributes content = - gitaly_migrate(:get_info_attributes) do |is_enabled| + gitaly_migrate(:get_info_attributes, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled gitaly_repository_client.info_attributes else @@ -1334,7 +1334,7 @@ module Gitlab end def squash_in_progress?(squash_id) - gitaly_migrate(:squash_in_progress) do |is_enabled| + gitaly_migrate(:squash_in_progress, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled gitaly_repository_client.squash_in_progress?(squash_id) else diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index d75a5f15c29..1ab8c4e0229 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -131,7 +131,7 @@ module Gitlab def page_formatted_data(title:, dir: nil, version: nil) version = version&.id - @repository.gitaly_migrate(:wiki_page_formatted_data) do |is_enabled| + @repository.gitaly_migrate(:wiki_page_formatted_data, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled gitaly_wiki_client.get_formatted_data(title: title, dir: dir, version: version) else diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 14228b18332..608d2a584ba 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-05-16 18:52+0200\n" -"PO-Revision-Date: 2018-05-16 18:52+0200\n" +"POT-Creation-Date: 2018-05-21 12:38-0700\n" +"PO-Revision-Date: 2018-05-21 12:38-0700\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -82,6 +82,9 @@ msgstr[1] "" msgid "%{loadingIcon} Started" msgstr "" +msgid "%{lock_path} is locked by GitLab User %{lock_user_id}" +msgstr "" + msgid "%{nip_domain} can be used as an alternative to a custom domain." msgstr "" @@ -1447,7 +1450,7 @@ msgstr "" msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images." msgstr "" -msgid "ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images." +msgid "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images." msgstr "" msgid "Continuous Integration and Deployment" @@ -2487,6 +2490,9 @@ msgstr "" msgid "Lock %{issuableDisplayName}" msgstr "" +msgid "Lock not found" +msgstr "" + msgid "Lock to current projects" msgstr "" @@ -3407,6 +3413,9 @@ msgstr "" msgid "Reset runners registration token" msgstr "" +msgid "Resolve conflicts on source branch" +msgstr "" + msgid "Resolve discussion" msgstr "" @@ -4278,6 +4287,9 @@ msgstr "" msgid "Toggle Sidebar" msgstr "" +msgid "Toggle discussion" +msgstr "" + msgid "Toggle sidebar" msgstr "" @@ -4599,12 +4611,21 @@ msgstr "" msgid "You can only edit files when you are on a branch" msgstr "" +msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}" +msgstr "" + msgid "You cannot write to this read-only GitLab instance." msgstr "" +msgid "You have no permissions" +msgstr "" + msgid "You have reached your project limit" msgstr "" +msgid "You must have master access to force delete a lock" +msgstr "" + msgid "You must sign in to star a project" msgstr "" diff --git a/package.json b/package.json index 9231a216163..1382afd41d6 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "blackst0ne-mermaid": "^7.1.0-fixed", "bootstrap-sass": "^3.3.6", "brace-expansion": "^1.1.8", + "cache-loader": "^1.2.2", "chart.js": "1.0.2", "classlist-polyfill": "^1.2.0", "clipboard": "^1.7.1", @@ -83,7 +84,7 @@ "url-loader": "^1.0.1", "visibilityjs": "^1.2.4", "vue": "^2.5.16", - "vue-loader": "^15.0.12", + "vue-loader": "^15.2.0", "vue-resource": "^1.5.0", "vue-router": "^3.0.1", "vue-template-compiler": "^2.5.16", diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb index 19995559fae..25a57f7d379 100644 --- a/spec/features/merge_request/user_resolves_conflicts_spec.rb +++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb @@ -27,7 +27,7 @@ describe 'Merge request > User resolves conflicts', :js do end end - find_button('Commit conflict resolution').send_keys(:return) + find_button('Commit to source branch').send_keys(:return) expect(page).to have_content('All merge conflicts were resolved') merge_request.reload_diff @@ -71,7 +71,7 @@ describe 'Merge request > User resolves conflicts', :js do execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).setValue("Gregor Samsa woke from troubled dreams");') end - find_button('Commit conflict resolution').send_keys(:return) + find_button('Commit to source branch').send_keys(:return) expect(page).to have_content('All merge conflicts were resolved') merge_request.reload_diff @@ -145,7 +145,7 @@ describe 'Merge request > User resolves conflicts', :js do execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("Gregor Samsa woke from troubled dreams");') end - click_button 'Commit conflict resolution' + click_button 'Commit to source branch' expect(page).to have_content('All merge conflicts were resolved') diff --git a/spec/fixtures/emails/valid_new_issue_with_quote.eml b/spec/fixtures/emails/valid_new_issue_with_quote.eml new file mode 100644 index 00000000000..0caf8ed4e9e --- /dev/null +++ b/spec/fixtures/emails/valid_new_issue_with_quote.eml @@ -0,0 +1,25 @@ +Return-Path: <jake@adventuretime.ooo> +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog <jake@adventuretime.ooo> +To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +Subject: New Issue by email +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 + +The reply by email functionality should be extended to allow creating a new issue by email. +even when the email is forwarded to the project which may include lines that begin with ">" + +there should be a quote below this line: + +> this is a quote
\ No newline at end of file diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index 8de615ad8c2..a3e010c3206 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -258,13 +258,19 @@ describe BlobHelper do it 'returns full IDE path' do Rails.application.routes.default_url_options[:script_name] = nil - expect(helper.ide_edit_path(project, "master", "")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master/") + expect(helper.ide_edit_path(project, "master", "")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master") + end + + it 'returns full IDE path with second -' do + Rails.application.routes.default_url_options[:script_name] = nil + + expect(helper.ide_edit_path(project, "testing/slashes", "readme.md")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/slashes/-/readme.md") end it 'returns IDE path without relative_url_root' do Rails.application.routes.default_url_options[:script_name] = "/gitlab" - expect(helper.ide_edit_path(project, "master", "")).to eq("/gitlab/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master/") + expect(helper.ide_edit_path(project, "master", "")).to eq("/gitlab/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master") end end end diff --git a/spec/javascripts/helpers/vuex_action_helper.js b/spec/javascripts/helpers/vuex_action_helper.js index 83f29d1b0c2..d6ab0aeeed7 100644 --- a/spec/javascripts/helpers/vuex_action_helper.js +++ b/spec/javascripts/helpers/vuex_action_helper.js @@ -55,7 +55,7 @@ export default (action, payload, state, expectedMutations, expectedActions, done }; // call the action with mocked store and arguments - action({ commit, state, dispatch }, payload); + action({ commit, state, dispatch, rootState: state }, payload); // check if no mutations should have been dispatched if (expectedMutations.length === 0) { diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js index 6f580e1f7af..045a60e56a0 100644 --- a/spec/javascripts/ide/components/ide_spec.js +++ b/spec/javascripts/ide/components/ide_spec.js @@ -107,5 +107,11 @@ describe('ide component', () => { vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'), ).toBe(true); }); + + it('stops callback in monaco editor', () => { + setFixtures('<div class="inputarea"></div>'); + + expect(vm.mousetrapStopCallback(null, document.querySelector('.inputarea'), 't')).toBe(true); + }); }); }); diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js index ff500acd849..d3f80e6f9c0 100644 --- a/spec/javascripts/ide/components/repo_editor_spec.js +++ b/spec/javascripts/ide/components/repo_editor_spec.js @@ -346,4 +346,24 @@ describe('RepoEditor', () => { }); }); }); + + it('calls removePendingTab when old file is pending', done => { + spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true); + spyOn(vm, 'removePendingTab'); + + vm.file.pending = true; + + vm + .$nextTick() + .then(() => { + vm.file = file('testing'); + + return vm.$nextTick(); + }) + .then(() => { + expect(vm.removePendingTab).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); }); diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js index c059862b9d1..7e641c7984b 100644 --- a/spec/javascripts/ide/mock_data.js +++ b/spec/javascripts/ide/mock_data.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export export const projectData = { id: 1, name: 'abcproject', @@ -14,3 +13,49 @@ export const projectData = { mergeRequests: {}, merge_requests_enabled: true, }; + +export const pipelines = [ + { + id: 1, + ref: 'master', + sha: '123', + status: 'failed', + }, + { + id: 2, + ref: 'master', + sha: '213', + status: 'success', + }, +]; + +export const jobs = [ + { + id: 1, + name: 'test', + status: 'failed', + stage: 'test', + duration: 1, + }, + { + id: 2, + name: 'test 2', + status: 'failed', + stage: 'test', + duration: 1, + }, + { + id: 3, + name: 'test 3', + status: 'failed', + stage: 'test', + duration: 1, + }, + { + id: 4, + name: 'test 3', + status: 'failed', + stage: 'build', + duration: 1, + }, +]; diff --git a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js new file mode 100644 index 00000000000..85fbcf8084b --- /dev/null +++ b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js @@ -0,0 +1,289 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import actions, { + requestLatestPipeline, + receiveLatestPipelineError, + receiveLatestPipelineSuccess, + fetchLatestPipeline, + requestJobs, + receiveJobsError, + receiveJobsSuccess, + fetchJobs, +} from '~/ide/stores/modules/pipelines/actions'; +import state from '~/ide/stores/modules/pipelines/state'; +import * as types from '~/ide/stores/modules/pipelines/mutation_types'; +import testAction from '../../../../helpers/vuex_action_helper'; +import { pipelines, jobs } from '../../../mock_data'; + +describe('IDE pipelines actions', () => { + let mockedState; + let mock; + + beforeEach(() => { + mockedState = state(); + mock = new MockAdapter(axios); + + gon.api_version = 'v4'; + mockedState.currentProjectId = 'test/project'; + }); + + afterEach(() => { + mock.restore(); + }); + + describe('requestLatestPipeline', () => { + it('commits request', done => { + testAction( + requestLatestPipeline, + null, + mockedState, + [{ type: types.REQUEST_LATEST_PIPELINE }], + [], + done, + ); + }); + }); + + describe('receiveLatestPipelineError', () => { + it('commits error', done => { + testAction( + receiveLatestPipelineError, + null, + mockedState, + [{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }], + [], + done, + ); + }); + + it('creates flash message', () => { + const flashSpy = spyOnDependency(actions, 'flash'); + + receiveLatestPipelineError({ commit() {} }); + + expect(flashSpy).toHaveBeenCalled(); + }); + }); + + describe('receiveLatestPipelineSuccess', () => { + it('commits pipeline', done => { + testAction( + receiveLatestPipelineSuccess, + pipelines[0], + mockedState, + [{ type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: pipelines[0] }], + [], + done, + ); + }); + }); + + describe('fetchLatestPipeline', () => { + describe('success', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(200, pipelines); + }); + + it('dispatches request', done => { + testAction( + fetchLatestPipeline, + '123', + mockedState, + [], + [{ type: 'requestLatestPipeline' }, { type: 'receiveLatestPipelineSuccess' }], + done, + ); + }); + + it('dispatches success with latest pipeline', done => { + testAction( + fetchLatestPipeline, + '123', + mockedState, + [], + [ + { type: 'requestLatestPipeline' }, + { type: 'receiveLatestPipelineSuccess', payload: pipelines[0] }, + ], + done, + ); + }); + + it('calls axios with correct params', () => { + const apiSpy = spyOn(axios, 'get').and.callThrough(); + + fetchLatestPipeline({ dispatch() {}, rootState: state }, '123'); + + expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), { + params: { + sha: '123', + per_page: '1', + }, + }); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(500); + }); + + it('dispatches error', done => { + testAction( + fetchLatestPipeline, + '123', + mockedState, + [], + [{ type: 'requestLatestPipeline' }, { type: 'receiveLatestPipelineError' }], + done, + ); + }); + }); + }); + + describe('requestJobs', () => { + it('commits request', done => { + testAction(requestJobs, null, mockedState, [{ type: types.REQUEST_JOBS }], [], done); + }); + }); + + describe('receiveJobsError', () => { + it('commits error', done => { + testAction( + receiveJobsError, + null, + mockedState, + [{ type: types.RECEIVE_JOBS_ERROR }], + [], + done, + ); + }); + + it('creates flash message', () => { + const flashSpy = spyOnDependency(actions, 'flash'); + + receiveJobsError({ commit() {} }); + + expect(flashSpy).toHaveBeenCalled(); + }); + }); + + describe('receiveJobsSuccess', () => { + it('commits jobs', done => { + testAction( + receiveJobsSuccess, + jobs, + mockedState, + [{ type: types.RECEIVE_JOBS_SUCCESS, payload: jobs }], + [], + done, + ); + }); + }); + + describe('fetchJobs', () => { + let page = ''; + + beforeEach(() => { + mockedState.latestPipeline = pipelines[0]; + }); + + describe('success', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines\/(.*)\/jobs/).replyOnce(() => [ + 200, + jobs, + { + 'x-next-page': page, + }, + ]); + }); + + it('dispatches request', done => { + testAction( + fetchJobs, + null, + mockedState, + [], + [{ type: 'requestJobs' }, { type: 'receiveJobsSuccess' }], + done, + ); + }); + + it('dispatches success with latest pipeline', done => { + testAction( + fetchJobs, + null, + mockedState, + [], + [{ type: 'requestJobs' }, { type: 'receiveJobsSuccess', payload: jobs }], + done, + ); + }); + + it('dispatches twice for both pages', done => { + page = '2'; + + testAction( + fetchJobs, + null, + mockedState, + [], + [ + { type: 'requestJobs' }, + { type: 'receiveJobsSuccess', payload: jobs }, + { type: 'fetchJobs', payload: '2' }, + { type: 'requestJobs' }, + { type: 'receiveJobsSuccess', payload: jobs }, + ], + done, + ); + }); + + it('calls axios with correct URL', () => { + const apiSpy = spyOn(axios, 'get').and.callThrough(); + + fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState }); + + expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', { + params: { page: '1' }, + }); + }); + + it('calls axios with page next page', () => { + const apiSpy = spyOn(axios, 'get').and.callThrough(); + + fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState }); + + expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', { + params: { page: '1' }, + }); + + page = '2'; + + fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState }, page); + + expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', { + params: { page: '2' }, + }); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(500); + }); + + it('dispatches error', done => { + testAction( + fetchJobs, + null, + mockedState, + [], + [{ type: 'requestJobs' }, { type: 'receiveJobsError' }], + done, + ); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js b/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js new file mode 100644 index 00000000000..b2a7e8a9025 --- /dev/null +++ b/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js @@ -0,0 +1,71 @@ +import * as getters from '~/ide/stores/modules/pipelines/getters'; +import state from '~/ide/stores/modules/pipelines/state'; + +describe('IDE pipeline getters', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe('hasLatestPipeline', () => { + it('returns false when loading is true', () => { + mockedState.isLoadingPipeline = true; + + expect(getters.hasLatestPipeline(mockedState)).toBe(false); + }); + + it('returns false when pipelines is null', () => { + mockedState.latestPipeline = null; + + expect(getters.hasLatestPipeline(mockedState)).toBe(false); + }); + + it('returns false when loading is true & pipelines is null', () => { + mockedState.latestPipeline = null; + mockedState.isLoadingPipeline = true; + + expect(getters.hasLatestPipeline(mockedState)).toBe(false); + }); + + it('returns true when loading is false & pipelines is an object', () => { + mockedState.latestPipeline = { + id: 1, + }; + mockedState.isLoadingPipeline = false; + + expect(getters.hasLatestPipeline(mockedState)).toBe(true); + }); + }); + + describe('failedJobs', () => { + it('returns array of failed jobs', () => { + mockedState.stages = [ + { + title: 'test', + jobs: [{ id: 1, status: 'failed' }, { id: 2, status: 'success' }], + }, + { + title: 'build', + jobs: [{ id: 3, status: 'failed' }, { id: 4, status: 'failed' }], + }, + ]; + + expect(getters.failedJobs(mockedState).length).toBe(3); + expect(getters.failedJobs(mockedState)).toEqual([ + { + id: 1, + status: jasmine.anything(), + }, + { + id: 3, + status: jasmine.anything(), + }, + { + id: 4, + status: jasmine.anything(), + }, + ]); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js new file mode 100644 index 00000000000..8262e916243 --- /dev/null +++ b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js @@ -0,0 +1,120 @@ +import mutations from '~/ide/stores/modules/pipelines/mutations'; +import state from '~/ide/stores/modules/pipelines/state'; +import * as types from '~/ide/stores/modules/pipelines/mutation_types'; +import { pipelines, jobs } from '../../../mock_data'; + +describe('IDE pipelines mutations', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe(types.REQUEST_LATEST_PIPELINE, () => { + it('sets loading to true', () => { + mutations[types.REQUEST_LATEST_PIPELINE](mockedState); + + expect(mockedState.isLoadingPipeline).toBe(true); + }); + }); + + describe(types.RECEIVE_LASTEST_PIPELINE_ERROR, () => { + it('sets loading to false', () => { + mutations[types.RECEIVE_LASTEST_PIPELINE_ERROR](mockedState); + + expect(mockedState.isLoadingPipeline).toBe(false); + }); + }); + + describe(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, () => { + it('sets loading to false on success', () => { + mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, pipelines[0]); + + expect(mockedState.isLoadingPipeline).toBe(false); + }); + + it('sets latestPipeline', () => { + mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, pipelines[0]); + + expect(mockedState.latestPipeline).toEqual({ + id: pipelines[0].id, + status: pipelines[0].status, + }); + }); + + it('does not set latest pipeline if pipeline is null', () => { + mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null); + + expect(mockedState.latestPipeline).toEqual(null); + }); + }); + + describe(types.REQUEST_JOBS, () => { + it('sets jobs loading to true', () => { + mutations[types.REQUEST_JOBS](mockedState); + + expect(mockedState.isLoadingJobs).toBe(true); + }); + }); + + describe(types.RECEIVE_JOBS_ERROR, () => { + it('sets jobs loading to false', () => { + mutations[types.RECEIVE_JOBS_ERROR](mockedState); + + expect(mockedState.isLoadingJobs).toBe(false); + }); + }); + + describe(types.RECEIVE_JOBS_SUCCESS, () => { + it('sets jobs loading to false on success', () => { + mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs); + + expect(mockedState.isLoadingJobs).toBe(false); + }); + + it('sets stages', () => { + mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs); + + expect(mockedState.stages.length).toBe(2); + expect(mockedState.stages).toEqual([ + { + title: 'test', + jobs: jasmine.anything(), + }, + { + title: 'build', + jobs: jasmine.anything(), + }, + ]); + }); + + it('sets jobs in stages', () => { + mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs); + + expect(mockedState.stages[0].jobs.length).toBe(3); + expect(mockedState.stages[1].jobs.length).toBe(1); + expect(mockedState.stages).toEqual([ + { + title: jasmine.anything(), + jobs: jobs.filter(job => job.stage === 'test').map(job => ({ + id: job.id, + name: job.name, + status: job.status, + stage: job.stage, + duration: job.duration, + })), + }, + { + title: jasmine.anything(), + jobs: jobs.filter(job => job.stage === 'build').map(job => ({ + id: job.id, + name: job.name, + status: job.status, + stage: job.stage, + duration: job.duration, + })), + }, + ]); + }); + }); +}); diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb index 452249210b0..154ab4b3856 100644 --- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb @@ -46,6 +46,20 @@ describe Gitlab::Email::Handler::CreateIssueHandler do expect(issue.description).to eq('') end end + + context "when there are quotes in email" do + let(:email_raw) { fixture_file("emails/valid_new_issue_with_quote.eml") } + + it "creates a new issue" do + expect { receiver.execute }.to change { project.issues.count }.by(1) + issue = project.issues.last + + expect(issue.author).to eq(user) + expect(issue.title).to eq('New Issue by email') + expect(issue.description).to include('reply by email') + expect(issue.description).to include('> this is a quote') + end + end end context "something is wrong" do diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb index e21a998adfe..0989188f7ee 100644 --- a/spec/lib/gitlab/email/reply_parser_spec.rb +++ b/spec/lib/gitlab/email/reply_parser_spec.rb @@ -3,8 +3,8 @@ require "spec_helper" # Inspired in great part by Discourse's Email::Receiver describe Gitlab::Email::ReplyParser do describe '#execute' do - def test_parse_body(mail_string) - described_class.new(Mail::Message.new(mail_string)).execute + def test_parse_body(mail_string, params = {}) + described_class.new(Mail::Message.new(mail_string), params).execute end it "returns an empty string if the message is blank" do @@ -212,5 +212,19 @@ describe Gitlab::Email::ReplyParser do it "does not wrap links with no href in unnecessary brackets" do expect(test_parse_body(fixture_file("emails/html_empty_link.eml"))).to eq("no brackets!") end + + it "does not trim reply if trim_reply option is false" do + expect(test_parse_body(fixture_file("emails/valid_new_issue_with_quote.eml"), { trim_reply: false })) + .to eq( + <<-BODY.strip_heredoc.chomp + The reply by email functionality should be extended to allow creating a new issue by email. + even when the email is forwarded to the project which may include lines that begin with ">" + + there should be a quote below this line: + + > this is a quote + BODY + ) + end end end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 6d63749296e..4f64f2bd6b4 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -6760,26 +6760,6 @@ "wiki_page_events": true }, { - "id": 92, - "title": "Gemnasium", - "project_id": 5, - "created_at": "2016-06-14T15:01:51.202Z", - "updated_at": "2016-06-14T15:01:51.202Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "GemnasiumService", - "category": "common", - "default": false, - "wiki_page_events": true - }, - { "id": 91, "title": "Flowdock", "project_id": 5, diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb index 4c61bc0af95..6e417323e40 100644 --- a/spec/models/project_services/gemnasium_service_spec.rb +++ b/spec/models/project_services/gemnasium_service_spec.rb @@ -26,24 +26,49 @@ describe GemnasiumService do end end + describe "deprecated?" do + let(:project) { create(:project, :repository) } + let(:gemnasium_service) { described_class.new } + + before do + allow(gemnasium_service).to receive_messages( + project_id: project.id, + project: project, + service_hook: true, + token: 'verySecret', + api_key: 'GemnasiumUserApiKey' + ) + end + + it "is true" do + expect(gemnasium_service.deprecated?).to be true + end + + it "can't create a new service" do + expect(gemnasium_service.save).to be false + expect(gemnasium_service.errors[:base].first) + .to eq('Gemnasium has been acquired by GitLab in January 2018. Since May 15, 2018, the service provided by Gemnasium is no longer available.') + end + end + describe "Execute" do let(:user) { create(:user) } let(:project) { create(:project, :repository) } + let(:gemnasium_service) { described_class.new } + let(:sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) } before do - @gemnasium_service = described_class.new - allow(@gemnasium_service).to receive_messages( + allow(gemnasium_service).to receive_messages( project_id: project.id, project: project, service_hook: true, token: 'verySecret', api_key: 'GemnasiumUserApiKey' ) - @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user) end it "calls Gemnasium service" do expect(Gemnasium::GitlabService).to receive(:execute).with(an_instance_of(Hash)).once - @gemnasium_service.execute(@sample_data) + gemnasium_service.execute(sample_data) end end end diff --git a/yarn.lock b/yarn.lock index 21eff9f70c2..e0d46609f7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1481,6 +1481,15 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +cache-loader@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cache-loader/-/cache-loader-1.2.2.tgz#6d5c38ded959a09cc5d58190ab5af6f73bd353f5" + dependencies: + loader-utils "^1.1.0" + mkdirp "^0.5.1" + neo-async "^2.5.0" + schema-utils "^0.4.2" + cacheable-request@^2.1.1: version "2.1.4" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" @@ -7689,7 +7698,7 @@ sax@~1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828" -schema-utils@^0.4.0, schema-utils@^0.4.3, schema-utils@^0.4.4, schema-utils@^0.4.5: +schema-utils@^0.4.0, schema-utils@^0.4.2, schema-utils@^0.4.3, schema-utils@^0.4.4, schema-utils@^0.4.5: version "0.4.5" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e" dependencies: @@ -8957,9 +8966,9 @@ vue-hot-reload-api@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.0.tgz#97976142405d13d8efae154749e88c4e358cf926" -vue-loader@^15.0.12: - version "15.0.12" - resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.0.12.tgz#9221e88f1c4f7657d425e40c676cd25671d5d294" +vue-loader@^15.2.0: + version "15.2.0" + resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.2.0.tgz#5a8138e490a1040942d2f10ae68fa72b5a923364" dependencies: "@vue/component-compiler-utils" "^1.2.1" hash-sum "^1.0.2" |