summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml12
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--app/assets/javascripts/api.js16
-rw-r--r--app/assets/javascripts/ide/components/file_finder/index.vue10
-rw-r--r--app/assets/javascripts/ide/components/ide.vue5
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue9
-rw-r--r--app/assets/javascripts/ide/ide_router.js20
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js4
-rw-r--r--app/assets/javascripts/ide/stores/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js16
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js49
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/getters.js7
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/index.js12
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js7
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutations.js53
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/state.js6
-rw-r--r--app/assets/javascripts/ide/stores/workers/files_decorator_worker.js4
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js99
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js2
-rw-r--r--app/assets/stylesheets/pages/commits.scss4
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss10
-rw-r--r--app/helpers/blob_helper.rb5
-rw-r--r--app/models/project_services/gemnasium_service.rb13
-rw-r--r--app/models/repository.rb20
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/user.rb27
-rw-r--r--app/views/projects/merge_requests/conflicts/_commit_stats.html.haml4
-rw-r--r--app/views/projects/merge_requests/conflicts/_submit_form.html.haml16
-rw-r--r--app/views/projects/tree/_tree_header.html.haml2
-rw-r--r--changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml6
-rw-r--r--changelogs/unreleased/46354-deprecate_gemnasium_service.yml5
-rw-r--r--changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml5
-rw-r--r--changelogs/unreleased/fix-unverified-hover-state.yml5
-rw-r--r--changelogs/unreleased/winh-make-it-right-now.yml5
-rw-r--r--config/webpack.config.js15
-rw-r--r--db/migrate/20180521171529_increase_mysql_text_limit_for_gpg_keys.rb2
-rw-r--r--db/migrate/gpg_keys_limits_to_mysql.rb15
-rw-r--r--db/post_migrate/20180514161336_remove_gemnasium_service.rb15
-rw-r--r--db/schema.rb2
-rw-r--r--doc/api/services.md7
-rw-r--r--doc/user/project/integrations/project_services.md2
-rw-r--r--doc/user/project/merge_requests/resolve_conflicts.md2
-rw-r--r--lib/gitlab/file_detector.rb2
-rw-r--r--lib/gitlab/git/gitlab_projects.rb2
-rw-r--r--lib/gitlab/git/repository.rb2
-rw-r--r--lib/gitlab/git/wiki.rb2
-rw-r--r--lib/tasks/migrate/add_limits_mysql.rake2
-rw-r--r--locale/gitlab.pot27
-rw-r--r--package.json3
-rwxr-xr-xscripts/no-ee-check7
-rw-r--r--spec/features/merge_request/user_resolves_conflicts_spec.rb6
-rw-r--r--spec/helpers/blob_helper_spec.rb10
-rw-r--r--spec/javascripts/helpers/vuex_action_helper.js2
-rw-r--r--spec/javascripts/ide/components/ide_spec.js6
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js20
-rw-r--r--spec/javascripts/ide/mock_data.js47
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/actions_spec.js289
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/getters_spec.js71
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js120
-rw-r--r--spec/lib/gitlab/import_export/project.json20
-rw-r--r--spec/models/project_services/gemnasium_service_spec.rb33
-rw-r--r--spec/models/repository_spec.rb14
-rw-r--r--spec/models/user_spec.rb88
-rw-r--r--spec/requests/api/users_spec.rb27
-rw-r--r--yarn.lock17
65 files changed, 1183 insertions, 157 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ced51cf8225..a1232f02ec0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -855,3 +855,15 @@ gitlab_git_test:
cache: {}
script:
- spec/support/prepare-gitlab-git-test-for-commit --check-for-changes
+
+no_ee_check:
+ <<: *dedicated-runner
+ <<: *except-docs-and-qa
+ variables:
+ SETUP_DB: "false"
+ before_script: []
+ cache: {}
+ script:
+ - scripts/no-ee-check
+ only:
+ - //@gitlab-org/gitlab-ce
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 6aba2b245a8..fae6e3d04b2 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-4.2.0
+4.2.1
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/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 944996159d7..0a27c6e0a25 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -329,6 +329,10 @@
&.invalid {
@include status-color($gray-dark, $gray, $gray-darkest);
border-color: $gray-darkest;
+
+ &:not(span):hover {
+ color: $gray;
+ }
}
}
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/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/repository.rb b/app/models/repository.rb
index 44c6bff6b66..0e1bf11d7c0 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -596,7 +596,7 @@ class Repository
cache_method :gitlab_ci_yml
def xcode_project?
- file_on_head(:xcode_config).present?
+ file_on_head(:xcode_config, :tree).present?
end
cache_method :xcode_project?
@@ -920,11 +920,21 @@ class Repository
end
end
- def file_on_head(type)
- if head = tree(:head)
- head.blobs.find do |blob|
- Gitlab::FileDetector.type_of(blob.path) == type
+ def file_on_head(type, object_type = :blob)
+ return unless head = tree(:head)
+
+ objects =
+ case object_type
+ when :blob
+ head.blobs
+ when :tree
+ head.trees
+ else
+ raise ArgumentError, "Object type #{object_type} is not supported"
end
+
+ objects.find do |object|
+ Gitlab::FileDetector.type_of(object.path) == type
end
end
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/models/user.rb b/app/models/user.rb
index 5a4c373705b..0a838d34054 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -165,8 +165,7 @@ class User < ActiveRecord::Base
validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
before_validation :sanitize_attrs
- before_validation :set_notification_email, if: :email_changed?
- before_save :set_notification_email, if: :email_changed? # in case validation is skipped
+ before_validation :set_notification_email, if: :new_record?
before_validation :set_public_email, if: :public_email_changed?
before_save :set_public_email, if: :public_email_changed? # in case validation is skipped
before_save :ensure_incoming_email_token
@@ -179,8 +178,21 @@ class User < ActiveRecord::Base
after_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook
after_destroy :remove_key_cache
- after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') }
- after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
+ after_commit(on: :update) do
+ if previous_changes.key?('email')
+ # Grab previous_email here since previous_changes changes after
+ # #update_emails_with_primary_email and #update_notification_email are called
+ previous_email = previous_changes[:email][0]
+
+ update_emails_with_primary_email(previous_email)
+ update_invalid_gpg_signatures
+
+ if previous_email == notification_email
+ self.notification_email = email
+ save
+ end
+ end
+ end
after_initialize :set_projects_limit
@@ -546,8 +558,7 @@ class User < ActiveRecord::Base
# hash and `_was` variables getting munged.
# By using an `after_commit` instead of `after_update`, we avoid the recursive callback
# scenario, though it then requires us to use the `previous_changes` hash
- def update_emails_with_primary_email
- previous_email = previous_changes[:email][0] # grab this before the DestroyService is called
+ def update_emails_with_primary_email(previous_email)
primary_email_record = emails.find_by(email: email)
Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
@@ -772,13 +783,13 @@ class User < ActiveRecord::Base
end
def set_notification_email
- if notification_email.blank? || !all_emails.include?(notification_email)
+ if notification_email.blank? || all_emails.exclude?(notification_email)
self.notification_email = email
end
end
def set_public_email
- if public_email.blank? || !all_emails.include?(public_email)
+ if public_email.blank? || all_emails.exclude?(public_email)
self.public_email = ''
end
end
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/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml b/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml
new file mode 100644
index 00000000000..2b4727c5f03
--- /dev/null
+++ b/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml
@@ -0,0 +1,6 @@
+---
+title: Fix an issue where the notification email address would be set to an unconfirmed
+ email address
+merge_request: 18474
+author:
+type: fixed
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/ab-46530-mediumtext-for-gpg-keys.yml b/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml
new file mode 100644
index 00000000000..88ef62ebc0e
--- /dev/null
+++ b/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml
@@ -0,0 +1,5 @@
+---
+title: Increase text limit for GPG keys (mysql only).
+merge_request: 19069
+author:
+type: other
diff --git a/changelogs/unreleased/fix-unverified-hover-state.yml b/changelogs/unreleased/fix-unverified-hover-state.yml
new file mode 100644
index 00000000000..003138f9821
--- /dev/null
+++ b/changelogs/unreleased/fix-unverified-hover-state.yml
@@ -0,0 +1,5 @@
+---
+title: Unverified hover state color changed to black
+merge_request:
+author:
+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/migrate/20180521171529_increase_mysql_text_limit_for_gpg_keys.rb b/db/migrate/20180521171529_increase_mysql_text_limit_for_gpg_keys.rb
new file mode 100644
index 00000000000..df84898003f
--- /dev/null
+++ b/db/migrate/20180521171529_increase_mysql_text_limit_for_gpg_keys.rb
@@ -0,0 +1,2 @@
+# rubocop:disable all
+require_relative 'gpg_keys_limits_to_mysql'
diff --git a/db/migrate/gpg_keys_limits_to_mysql.rb b/db/migrate/gpg_keys_limits_to_mysql.rb
new file mode 100644
index 00000000000..780340d0564
--- /dev/null
+++ b/db/migrate/gpg_keys_limits_to_mysql.rb
@@ -0,0 +1,15 @@
+class IncreaseMysqlTextLimitForGpgKeys < ActiveRecord::Migration
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ return unless Gitlab::Database.mysql?
+
+ change_column :gpg_keys, :key, :text, limit: 16.megabytes - 1
+ end
+
+ def down
+ # no-op
+ end
+end
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/db/schema.rb b/db/schema.rb
index c2e97f93160..37d336b9928 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180517082340) do
+ActiveRecord::Schema.define(version: 20180521171529) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
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/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/file_detector.rb b/lib/gitlab/file_detector.rb
index 77af7a868d0..49bc9c0b671 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -14,7 +14,7 @@ module Gitlab
avatar: /\Alogo\.(png|jpg|gif)\z/,
issue_template: %r{\A\.gitlab/issue_templates/[^/]+\.md\z},
merge_request_template: %r{\A\.gitlab/merge_request_templates/[^/]+\.md\z},
- xcode_config: %r{\A[^/]*\.(xcodeproj|xcworkspace)\z},
+ xcode_config: %r{\A[^/]*\.(xcodeproj|xcworkspace)(/.+)?\z},
# Configuration files
gitignore: '.gitignore',
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 224cd50c02e..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
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/lib/tasks/migrate/add_limits_mysql.rake b/lib/tasks/migrate/add_limits_mysql.rake
index c6204f89de4..9b05876034c 100644
--- a/lib/tasks/migrate/add_limits_mysql.rake
+++ b/lib/tasks/migrate/add_limits_mysql.rake
@@ -2,6 +2,7 @@ require Rails.root.join('db/migrate/limits_to_mysql')
require Rails.root.join('db/migrate/markdown_cache_limits_to_mysql')
require Rails.root.join('db/migrate/merge_request_diff_file_limits_to_mysql')
require Rails.root.join('db/migrate/limits_ci_build_trace_chunks_raw_data_for_mysql')
+require Rails.root.join('db/migrate/gpg_keys_limits_to_mysql')
desc "GitLab | Add limits to strings in mysql database"
task add_limits_mysql: :environment do
@@ -10,4 +11,5 @@ task add_limits_mysql: :environment do
MarkdownCacheLimitsToMysql.new.up
MergeRequestDiffFileLimitsToMysql.new.up
LimitsCiBuildTraceChunksRawDataForMysql.new.up
+ IncreaseMysqlTextLimitForGpgKeys.new.up
end
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/scripts/no-ee-check b/scripts/no-ee-check
new file mode 100755
index 00000000000..29d319dc822
--- /dev/null
+++ b/scripts/no-ee-check
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+ee_path = File.join(File.expand_path(__dir__), '../ee')
+
+if Dir.exist?(ee_path)
+ puts 'The repository contains /ee directory. There should be no /ee directory in CE repo.'
+ exit 1
+end
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/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/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/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 6bc148a1392..0ccf55bd895 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -2031,27 +2031,27 @@ describe Repository do
describe '#xcode_project?' do
before do
- allow(repository).to receive(:tree).with(:head).and_return(double(:tree, blobs: [blob]))
+ allow(repository).to receive(:tree).with(:head).and_return(double(:tree, trees: [tree]))
end
- context 'when the root contains a *.xcodeproj file' do
- let(:blob) { double(:blob, path: 'Foo.xcodeproj') }
+ context 'when the root contains a *.xcodeproj directory' do
+ let(:tree) { double(:tree, path: 'Foo.xcodeproj') }
it 'returns true' do
expect(repository.xcode_project?).to be_truthy
end
end
- context 'when the root contains a *.xcworkspace file' do
- let(:blob) { double(:blob, path: 'Foo.xcworkspace') }
+ context 'when the root contains a *.xcworkspace directory' do
+ let(:tree) { double(:tree, path: 'Foo.xcworkspace') }
it 'returns true' do
expect(repository.xcode_project?).to be_truthy
end
end
- context 'when the root contains no XCode config file' do
- let(:blob) { double(:blob, path: 'subdir/Foo.xcworkspace') }
+ context 'when the root contains no Xcode config directory' do
+ let(:tree) { double(:tree, path: 'Foo') }
it 'returns false' do
expect(repository.xcode_project?).to be_falsey
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 684fa030baf..6a2f4a39f09 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -393,24 +393,6 @@ describe User do
end
describe 'after commit hook' do
- describe '.update_invalid_gpg_signatures' do
- let(:user) do
- create(:user, email: 'tula.torphy@abshire.ca').tap do |user|
- user.skip_reconfirmation!
- end
- end
-
- it 'does nothing when the name is updated' do
- expect(user).not_to receive(:update_invalid_gpg_signatures)
- user.update_attributes!(name: 'Bette')
- end
-
- it 'synchronizes the gpg keys when the email is updated' do
- expect(user).to receive(:update_invalid_gpg_signatures).at_most(:twice)
- user.update_attributes!(email: 'shawnee.ritchie@denesik.com')
- end
- end
-
describe '#update_emails_with_primary_email' do
before do
@user = create(:user, email: 'primary@example.com').tap do |user|
@@ -450,6 +432,76 @@ describe User do
expect(@user.emails.first.confirmed_at).not_to eq nil
end
end
+
+ describe '#update_notification_email' do
+ # Regression: https://gitlab.com/gitlab-org/gitlab-ce/issues/22846
+ context 'when changing :email' do
+ let(:user) { create(:user) }
+ let(:new_email) { 'new-email@example.com' }
+
+ it 'sets :unconfirmed_email' do
+ expect do
+ user.tap { |u| u.update!(email: new_email) }.reload
+ end.to change(user, :unconfirmed_email).to(new_email)
+ end
+
+ it 'does not change :notification_email' do
+ expect do
+ user.tap { |u| u.update!(email: new_email) }.reload
+ end.not_to change(user, :notification_email)
+ end
+
+ it 'updates :notification_email to the new email once confirmed' do
+ user.update!(email: new_email)
+
+ expect do
+ user.tap(&:confirm).reload
+ end.to change(user, :notification_email).to eq(new_email)
+ end
+
+ context 'and :notification_email is set to a secondary email' do
+ let!(:email_attrs) { attributes_for(:email, :confirmed, user: user) }
+ let(:secondary) { create(:email, :confirmed, email: 'secondary@example.com', user: user) }
+
+ before do
+ user.emails.create(email_attrs)
+ user.tap { |u| u.update!(notification_email: email_attrs[:email]) }.reload
+ end
+
+ it 'does not change :notification_email to :email' do
+ expect do
+ user.tap { |u| u.update!(email: new_email) }.reload
+ end.not_to change(user, :notification_email)
+ end
+
+ it 'does not change :notification_email to :email once confirmed' do
+ user.update!(email: new_email)
+
+ expect do
+ user.tap(&:confirm).reload
+ end.not_to change(user, :notification_email)
+ end
+ end
+ end
+ end
+
+ describe '#update_invalid_gpg_signatures' do
+ let(:user) do
+ create(:user, email: 'tula.torphy@abshire.ca').tap do |user|
+ user.skip_reconfirmation!
+ end
+ end
+
+ it 'does nothing when the name is updated' do
+ expect(user).not_to receive(:update_invalid_gpg_signatures)
+ user.update_attributes!(name: 'Bette')
+ end
+
+ it 'synchronizes the gpg keys when the email is updated' do
+ expect(user).to receive(:update_invalid_gpg_signatures).at_most(:twice)
+ user.update_attributes!(email: 'shawnee.ritchie@denesik.com')
+ end
+ end
end
describe '#update_tracked_fields!', :clean_gitlab_redis_shared_state do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index e8196980a8c..05637eb0729 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -488,10 +488,6 @@ describe API::Users do
describe "PUT /users/:id" do
let!(:admin_user) { create(:admin) }
- before do
- admin
- end
-
it "updates user with new bio" do
put api("/users/#{user.id}", admin), { bio: 'new test bio' }
@@ -525,27 +521,28 @@ describe API::Users do
expect(json_response['avatar_url']).to include(user.avatar_path)
end
- it 'updates user with his own email' do
- put api("/users/#{user.id}", admin), email: user.email
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['email']).to eq(user.email)
- expect(user.reload.email).to eq(user.email)
- end
-
it 'updates user with a new email' do
+ old_email = user.email
+ old_notification_email = user.notification_email
put api("/users/#{user.id}", admin), email: 'new@email.com'
+ user.reload
+
expect(response).to have_gitlab_http_status(200)
- expect(user.reload.notification_email).to eq('new@email.com')
+ expect(user).to be_confirmed
+ expect(user.email).to eq(old_email)
+ expect(user.notification_email).to eq(old_notification_email)
+ expect(user.unconfirmed_email).to eq('new@email.com')
end
it 'skips reconfirmation when requested' do
- put api("/users/#{user.id}", admin), { skip_reconfirmation: true }
+ put api("/users/#{user.id}", admin), email: 'new@email.com', skip_reconfirmation: true
user.reload
- expect(user.confirmed_at).to be_present
+ expect(response).to have_gitlab_http_status(200)
+ expect(user).to be_confirmed
+ expect(user.email).to eq('new@email.com')
end
it 'updates user with his own username' do
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"