Commit 6b63e11d authored by Constance Okoghenun's avatar Constance Okoghenun

Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into boards-bundle-refactor

parents 7282dbea dfb14e4d
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
"import/no-commonjs": "error", "import/no-commonjs": "error",
"no-multiple-empty-lines": ["error", { "max": 1 }], "no-multiple-empty-lines": ["error", { "max": 1 }],
"promise/catch-or-return": "error", "promise/catch-or-return": "error",
"no-underscore-dangle": ["error", { "allow": ["__"]}], "no-underscore-dangle": ["error", { "allow": ["__", "_links"]}],
"vue/html-self-closing": ["error", { "vue/html-self-closing": ["error", {
"html": { "html": {
"void": "always", "void": "always",
......
...@@ -2,6 +2,25 @@ ...@@ -2,6 +2,25 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.4.4 (2018-02-16)
### Security (1 change)
- Update nokogiri to 1.8.2. !16807
### Fixed (9 changes)
- Fix 500 error when loading a merge request with an invalid comment. !16795
- Cleanup new branch/merge request form in issues. !16854
- Fix GitLab import leaving group_id on ProjectLabel. !16877
- Fix forking projects when no restricted visibility levels are defined applicationwide. !16881
- Resolve PrepareUntrackedUploads PostgreSQL syntax error. !17019
- Fixed error 500 when removing an identity with synced attributes and visiting the profile page. !17054
- Validate user namespace before saving so that errors persist on model.
- LDAP Person no longer throws exception on invalid entry.
- Fix JIRA not working when a trailing slash is included.
## 10.4.3 (2018-02-05) ## 10.4.3 (2018-02-05)
### Security (4 changes) ### Security (4 changes)
......
...@@ -75,6 +75,7 @@ export default class AjaxVariableList { ...@@ -75,6 +75,7 @@ export default class AjaxVariableList {
if (res.status === statusCodes.OK && res.data) { if (res.status === statusCodes.OK && res.data) {
this.updateRowsWithPersistedVariables(res.data.variables); this.updateRowsWithPersistedVariables(res.data.variables);
this.variableList.hideValues();
} else if (res.status === statusCodes.BAD_REQUEST) { } else if (res.status === statusCodes.BAD_REQUEST) {
// Validation failed // Validation failed
this.errorBox.innerHTML = generateErrorBoxContent(res.data); this.errorBox.innerHTML = generateErrorBoxContent(res.data);
......
...@@ -178,6 +178,10 @@ export default class VariableList { ...@@ -178,6 +178,10 @@ export default class VariableList {
this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled); this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled);
} }
hideValues() {
this.secretValues.updateDom(false);
}
getAllData() { getAllData() {
// Ignore the last empty row because we don't want to try persist // Ignore the last empty row because we don't want to try persist
// a blank variable and run into validation problems. // a blank variable and run into validation problems.
......
/* eslint-disable func-names, wrap-iife, consistent-return,
no-return-assign, no-param-reassign, one-var-declaration-per-line, no-unused-vars,
prefer-template, object-shorthand, prefer-arrow-callback */
import { pluralize } from './lib/utils/text_utility'; import { pluralize } from './lib/utils/text_utility';
import { localTimeAgo } from './lib/utils/datetime_utility'; import { localTimeAgo } from './lib/utils/datetime_utility';
import Pager from './pager'; import Pager from './pager';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
export default (function () { export default class CommitsList {
const CommitsList = {}; constructor(limit = 0) {
this.timer = null;
CommitsList.timer = null;
CommitsList.init = function (limit) {
this.$contentList = $('.content_list'); this.$contentList = $('.content_list');
$('body').on('click', '.day-commits-table li.commit', function (e) { Pager.init(parseInt(limit, 10), false, false, this.processCommits.bind(this));
if (e.target.nodeName !== 'A') {
location.href = $(this).attr('url');
e.stopPropagation();
return false;
}
});
Pager.init(parseInt(limit, 10), false, false, this.processCommits);
this.content = $('#commits-list'); this.content = $('#commits-list');
this.searchField = $('#commits-search'); this.searchField = $('#commits-search');
this.lastSearch = this.searchField.val(); this.lastSearch = this.searchField.val();
return this.initSearch(); this.initSearch();
}; }
CommitsList.initSearch = function () { initSearch() {
this.timer = null; this.timer = null;
return this.searchField.keyup((function (_this) { this.searchField.on('keyup', () => {
return function () { clearTimeout(this.timer);
clearTimeout(_this.timer); this.timer = setTimeout(this.filterResults.bind(this), 500);
return _this.timer = setTimeout(_this.filterResults, 500); });
}; }
})(this));
};
CommitsList.filterResults = function () { filterResults() {
const form = $('.commits-search-form'); const form = $('.commits-search-form');
const search = CommitsList.searchField.val(); const search = this.searchField.val();
if (search === CommitsList.lastSearch) return Promise.resolve(); if (search === this.lastSearch) return Promise.resolve();
const commitsUrl = form.attr('action') + '?' + form.serialize(); const commitsUrl = `${form.attr('action')}?${form.serialize()}`;
CommitsList.content.fadeTo('fast', 0.5); this.content.fadeTo('fast', 0.5);
const params = form.serializeArray().reduce((acc, obj) => Object.assign(acc, { const params = form.serializeArray().reduce((acc, obj) => Object.assign(acc, {
[obj.name]: obj.value, [obj.name]: obj.value,
}), {}); }), {});
...@@ -55,9 +39,9 @@ export default (function () { ...@@ -55,9 +39,9 @@ export default (function () {
params, params,
}) })
.then(({ data }) => { .then(({ data }) => {
CommitsList.lastSearch = search; this.lastSearch = search;
CommitsList.content.html(data.html); this.content.html(data.html);
CommitsList.content.fadeTo('fast', 1.0); this.content.fadeTo('fast', 1.0);
// Change url so if user reload a page - search results are saved // Change url so if user reload a page - search results are saved
history.replaceState({ history.replaceState({
...@@ -65,16 +49,16 @@ export default (function () { ...@@ -65,16 +49,16 @@ export default (function () {
}, document.title, commitsUrl); }, document.title, commitsUrl);
}) })
.catch(() => { .catch(() => {
CommitsList.content.fadeTo('fast', 1.0); this.content.fadeTo('fast', 1.0);
CommitsList.lastSearch = null; this.lastSearch = null;
}); });
}; }
// Prepare loaded data. // Prepare loaded data.
CommitsList.processCommits = (data) => { processCommits(data) {
let processedData = data; let processedData = data;
const $processedData = $(processedData); const $processedData = $(processedData);
const $commitsHeadersLast = CommitsList.$contentList.find('li.js-commit-header').last(); const $commitsHeadersLast = this.$contentList.find('li.js-commit-header').last();
const lastShownDay = $commitsHeadersLast.data('day'); const lastShownDay = $commitsHeadersLast.data('day');
const $loadedCommitsHeadersFirst = $processedData.filter('li.js-commit-header').first(); const $loadedCommitsHeadersFirst = $processedData.filter('li.js-commit-header').first();
const loadedShownDayFirst = $loadedCommitsHeadersFirst.data('day'); const loadedShownDayFirst = $loadedCommitsHeadersFirst.data('day');
...@@ -97,7 +81,5 @@ export default (function () { ...@@ -97,7 +81,5 @@ export default (function () {
localTimeAgo($processedData.find('.js-timeago')); localTimeAgo($processedData.find('.js-timeago'));
return processedData; return processedData;
}; }
}
return CommitsList;
})();
...@@ -230,6 +230,11 @@ var Dispatcher; ...@@ -230,6 +230,11 @@ var Dispatcher;
.then(callDefault) .then(callDefault)
.catch(fail); .catch(fail);
break; break;
case 'projects:services:edit':
import('./pages/projects/services/edit')
.then(callDefault)
.catch(fail);
break;
case 'projects:snippets:edit': case 'projects:snippets:edit':
case 'projects:snippets:update': case 'projects:snippets:update':
import('./pages/projects/snippets/edit') import('./pages/projects/snippets/edit')
...@@ -468,11 +473,6 @@ var Dispatcher; ...@@ -468,11 +473,6 @@ var Dispatcher;
.then(callDefault) .then(callDefault)
.catch(fail); .catch(fail);
break; break;
case 'users:show':
import('./pages/users/show')
.then(callDefault)
.catch(fail);
break;
case 'admin:conversational_development_index:show': case 'admin:conversational_development_index:show':
import('./pages/admin/conversational_development_index/show') import('./pages/admin/conversational_development_index/show')
.then(callDefault) .then(callDefault)
......
...@@ -59,8 +59,10 @@ class ImporterStatus { ...@@ -59,8 +59,10 @@ class ImporterStatus {
.catch(() => flash(__('An error occurred while importing project'))); .catch(() => flash(__('An error occurred while importing project')));
} }
setAutoUpdate() { autoUpdate() {
return setInterval(() => $.get(this.jobsUrl, data => $.each(data, (i, job) => { return axios.get(this.jobsUrl)
.then(({ data = [] }) => {
data.forEach((job) => {
const jobItem = $(`#project_${job.id}`); const jobItem = $(`#project_${job.id}`);
const statusField = jobItem.find('.job-status'); const statusField = jobItem.find('.job-status');
...@@ -81,7 +83,12 @@ class ImporterStatus { ...@@ -81,7 +83,12 @@ class ImporterStatus {
statusField.html(job.import_status); statusField.html(job.import_status);
break; break;
} }
})), 4000); });
});
}
setAutoUpdate() {
setInterval(this.autoUpdate.bind(this), 4000);
} }
} }
......
/* eslint-disable no-new */
import IntegrationSettingsForm from './integration_settings_form';
$(() => {
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
});
<script> <script>
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import Flash from '~/flash'; import createFlash from '~/flash';
import modal from '~/vue_shared/components/modal.vue'; import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
export default { export default {
components: { components: {
modal, GlModal,
}, },
props: { props: {
url: { url: {
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
}, },
computed: { computed: {
text() { text() {
return s__('AdminArea|You’re about to stop all jobs. This will halt all current jobs that are running.'); return s__('AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running.');
}, },
}, },
methods: { methods: {
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
redirectTo(response.request.responseURL); redirectTo(response.request.responseURL);
}) })
.catch((error) => { .catch((error) => {
Flash(s__('AdminArea|Stopping jobs failed')); createFlash(s__('AdminArea|Stopping jobs failed'));
throw error; throw error;
}); });
}, },
...@@ -37,11 +37,13 @@ ...@@ -37,11 +37,13 @@
</script> </script>
<template> <template>
<modal <gl-modal
id="stop-jobs-modal" id="stop-jobs-modal"
:title="s__('AdminArea|Stop all jobs?')" :header-title-text="s__('AdminArea|Stop all jobs?')"
:text="text" footer-primary-button-variant="danger"
kind="danger" :footer-primary-button-text="s__('AdminArea|Stop jobs')"
:primary-button-label="s__('AdminArea|Stop jobs')" @submit="onSubmit"
@submit="onSubmit" /> >
{{ text }}
</gl-modal>
</template> </template>
...@@ -8,7 +8,7 @@ Vue.use(Translate); ...@@ -8,7 +8,7 @@ Vue.use(Translate);
export default () => { export default () => {
const stopJobsButton = document.getElementById('stop-jobs-button'); const stopJobsButton = document.getElementById('stop-jobs-button');
if (stopJobsButton) {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el: '#stop-jobs-modal', el: '#stop-jobs-modal',
...@@ -26,4 +26,5 @@ export default () => { ...@@ -26,4 +26,5 @@ export default () => {
}); });
}, },
}); });
}
}; };
...@@ -3,7 +3,7 @@ import GpgBadges from '~/gpg_badges'; ...@@ -3,7 +3,7 @@ import GpgBadges from '~/gpg_badges';
import ShortcutsNavigation from '~/shortcuts_navigation'; import ShortcutsNavigation from '~/shortcuts_navigation';
export default () => { export default () => {
CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit); new CommitsList(document.querySelector('.js-project-commits-show').dataset.commitsLimit); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
GpgBadges.fetch(); GpgBadges.fetch();
}; };
import initForm from '../shared/init_form';
document.addEventListener('DOMContentLoaded', initForm);
import initForm from '../shared/init_form';
document.addEventListener('DOMContentLoaded', initForm);
import Vue from 'vue'; import Vue from 'vue';
import PipelineSchedulesCallout from './components/pipeline_schedules_callout.vue'; import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipeline-schedules-callout', el: '#pipeline-schedules-callout',
......
import initForm from '../shared/init_form';
document.addEventListener('DOMContentLoaded', initForm);
<script> <script>
import Vue from 'vue'; import Vue from 'vue';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import Translate from '../../vue_shared/translate'; import Translate from '../../../../../vue_shared/translate';
import illustrationSvg from '../icons/intro_illustration.svg'; import illustrationSvg from '../icons/intro_illustration.svg';
Vue.use(Translate); Vue.use(Translate);
......
import Vue from 'vue'; import Vue from 'vue';
import Translate from '../vue_shared/translate'; import Translate from '../../../../vue_shared/translate';
import GlFieldErrors from '../gl_field_errors'; import GlFieldErrors from '../../../../gl_field_errors';
import intervalPatternInput from './components/interval_pattern_input.vue'; import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown'; import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown'; import TargetBranchDropdown from './components/target_branch_dropdown';
import setupNativeFormVariableList from '../ci_variable_list/native_form_variable_list'; import setupNativeFormVariableList from '../../../../ci_variable_list/native_form_variable_list';
Vue.use(Translate); Vue.use(Translate);
...@@ -27,7 +27,7 @@ function initIntervalPatternInput() { ...@@ -27,7 +27,7 @@ function initIntervalPatternInput() {
}); });
} }
document.addEventListener('DOMContentLoaded', () => { export default () => {
/* Most of the form is written in haml, but for fields with more complex behaviors, /* Most of the form is written in haml, but for fields with more complex behaviors,
* you should mount individual Vue components here. If at some point components need * you should mount individual Vue components here. If at some point components need
* to share state, it may make sense to refactor the whole form to Vue */ * to share state, it may make sense to refactor the whole form to Vue */
...@@ -46,4 +46,4 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -46,4 +46,4 @@ document.addEventListener('DOMContentLoaded', () => {
container: $('.js-ci-variable-list-section'), container: $('.js-ci-variable-list-section'),
formField: 'schedule', formField: 'schedule',
}); });
}); };
import initForm from '../shared/init_form';
document.addEventListener('DOMContentLoaded', initForm);
import Chart from 'vendor/Chart';
const options = {
scaleOverlay: true,
responsive: true,
maintainAspectRatio: false,
};
const buildChart = (chartScope) => {
const data = {
labels: chartScope.labels,
datasets: [{
fillColor: '#707070',
strokeColor: '#707070',
pointColor: '#707070',
pointStrokeColor: '#EEE',
data: chartScope.totalValues,
},
{
fillColor: '#1aaa55',
strokeColor: '#1aaa55',
pointColor: '#1aaa55',
pointStrokeColor: '#fff',
data: chartScope.successValues,
},
],
};
const ctx = $(`#${chartScope.scope}Chart`).get(0).getContext('2d');
new Chart(ctx).Line(data, options);
};
document.addEventListener('DOMContentLoaded', () => {
const chartTimesData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML);
const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML);
const data = {
labels: chartTimesData.labels,
datasets: [{
fillColor: 'rgba(220,220,220,0.5)',
strokeColor: 'rgba(220,220,220,1)',
barStrokeWidth: 1,
barValueSpacing: 1,
barDatasetSpacing: 1,
data: chartTimesData.values,
}],
};
if (window.innerWidth < 768) {
// Scale fonts if window width lower than 768px (iPad portrait)
options.scaleFontSize = 8;
}
new Chart($('#build_timesChart').get(0).getContext('2d')).Bar(data, options);
chartsData.forEach(scope => buildChart(scope));
});
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
export default () => {
const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring');
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
if (prometheusSettingsWrapper) {
const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
prometheusMetrics.loadActiveMetrics();
}
};
import UserCallout from '~/user_callout';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import UserTabs from './user_tabs'; import UserTabs from './user_tabs';
...@@ -22,4 +23,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -22,4 +23,5 @@ document.addEventListener('DOMContentLoaded', () => {
const page = $('body').attr('data-page'); const page = $('body').attr('data-page');
const action = page.split(':')[1]; const action = page.split(':')[1];
initUserProfile(action); initUserProfile(action);
new UserCallout(); // eslint-disable-line no-new
}); });
import UserCallout from '~/user_callout';
export default () => new UserCallout();
import axios from '../lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import Activities from '../activities'; import Activities from '~/activities';
import { localTimeAgo } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import flash from '~/flash';
import ActivityCalendar from './activity_calendar'; import ActivityCalendar from './activity_calendar';
import { localTimeAgo } from '../lib/utils/datetime_utility';
import { __ } from '../locale';
import flash from '../flash';
/** /**
* UserTabs * UserTabs
......
import Chart from 'vendor/Chart';
document.addEventListener('DOMContentLoaded', () => {
const chartData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML);
const buildChart = (chartScope) => {
const data = {
labels: chartScope.labels,
datasets: [{
fillColor: '#707070',
strokeColor: '#707070',
pointColor: '#707070',
pointStrokeColor: '#EEE',
data: chartScope.totalValues,
},
{
fillColor: '#1aaa55',
strokeColor: '#1aaa55',
pointColor: '#1aaa55',
pointStrokeColor: '#fff',
data: chartScope.successValues,
},
],
};
const ctx = $(`#${chartScope.scope}Chart`).get(0).getContext('2d');
const options = {
scaleOverlay: true,
responsive: true,
maintainAspectRatio: false,
};
if (window.innerWidth < 768) {
// Scale fonts if window width lower than 768px (iPad portrait)
options.scaleFontSize = 8;
}
new Chart(ctx).Line(data, options);
};
chartData.forEach(scope => buildChart(scope));
});
import Chart from 'vendor/Chart';
document.addEventListener('DOMContentLoaded', () => {
const chartData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML);
const data = {
labels: chartData.labels,
datasets: [{
fillColor: 'rgba(220,220,220,0.5)',
strokeColor: 'rgba(220,220,220,1)',
barStrokeWidth: 1,
barValueSpacing: 1,
barDatasetSpacing: 1,
data: chartData.values,
}],
};
const ctx = $('#build_timesChart').get(0).getContext('2d');
const options = {
scaleOverlay: true,
responsive: true,
maintainAspectRatio: false,
};
if (window.innerWidth < 768) {
// Scale fonts if window width lower than 768px (iPad portrait)
options.scaleFontSize = 8;
}
new Chart(ctx).Bar(data, options);
});
import PrometheusMetrics from './prometheus_metrics';
$(() => {
const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
prometheusMetrics.loadActiveMetrics();
});
<script>
const buttonVariants = [
'danger',
'primary',
'success',
'warning',
];
export default {
name: 'GlModal',
props: {
id: {
type: String,
required: false,
default: null,
},
headerTitleText: {
type: String,
required: false,
default: '',
},
footerPrimaryButtonVariant: {
type: String,
required: false,
default: 'primary',
validator: value => buttonVariants.indexOf(value) !== -1,
},
footerPrimaryButtonText: {
type: String,
required: false,
default: '',
},
},
methods: {
emitCancel(event) {
this.$emit('cancel', event);
},
emitSubmit(event) {
this.$emit('submit', event);
},
},
};
</script>
<template>
<div
:id="id"
class="modal fade"
tabindex="-1"
role="dialog"
>
<div
class="modal-dialog"
role="document"
>
<div class="modal-content">
<div class="modal-header">
<slot name="header">
<button
type="button"
class="close"
data-dismiss="modal"
:aria-label="s__('Modal|Close')"
@click="emitCancel($event)"
>
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">
<slot name="title">
{{ headerTitleText }}
</slot>
</h4>
</slot>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button
type="button"
class="btn"
data-dismiss="modal"
@click="emitCancel($event)"
>
{{ s__('Modal|Cancel') }}
</button>
<button
type="button"
class="btn"
:class="`btn-${footerPrimaryButtonVariant}`"
data-dismiss="modal"
@click="emitSubmit($event)"
>
{{ footerPrimaryButtonText }}
</button>
</slot>
</div>
</div>
</div>
</div>
</template>
...@@ -255,8 +255,6 @@ ul.controls { ...@@ -255,8 +255,6 @@ ul.controls {
} }
.author_link { .author_link {
display: inline-block;
.avatar-inline { .avatar-inline {
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
......
...@@ -9,6 +9,7 @@ class Identity < ActiveRecord::Base ...@@ -9,6 +9,7 @@ class Identity < ActiveRecord::Base
validates :user_id, uniqueness: { scope: :provider } validates :user_id, uniqueness: { scope: :provider }
before_save :ensure_normalized_extern_uid, if: :extern_uid_changed? before_save :ensure_normalized_extern_uid, if: :extern_uid_changed?
after_destroy :clear_user_synced_attributes, if: :user_synced_attributes_metadata_from_provider?
scope :with_provider, ->(provider) { where(provider: provider) } scope :with_provider, ->(provider) { where(provider: provider) }
scope :with_extern_uid, ->(provider, extern_uid) do scope :with_extern_uid, ->(provider, extern_uid) do
...@@ -34,4 +35,12 @@ class Identity < ActiveRecord::Base ...@@ -34,4 +35,12 @@ class Identity < ActiveRecord::Base
self.extern_uid = Identity.normalize_uid(self.provider, self.extern_uid) self.extern_uid = Identity.normalize_uid(self.provider, self.extern_uid)
end end
def user_synced_attributes_metadata_from_provider?
user.user_synced_attributes_metadata&.provider == provider
end
def clear_user_synced_attributes
user.user_synced_attributes_metadata&.destroy
end
end end
...@@ -593,7 +593,15 @@ class Repository ...@@ -593,7 +593,15 @@ class Repository
def license_key def license_key
return unless exists? return unless exists?
# The licensee gem creates a Rugged object from the path:
# https://github.com/benbalter/licensee/blob/v8.7.0/lib/licensee/projects/git_project.rb
begin
Licensee.license(path).try(:key) Licensee.license(path).try(:key)
# Normally we would rescue Rugged::Error, but that is banned by lint-rugged
# and we need to migrate this endpoint to Gitaly:
# https://gitlab.com/gitlab-org/gitaly/issues/1026
rescue
end
end end
cache_method :license_key cache_method :license_key
......
...@@ -249,7 +249,7 @@ class User < ActiveRecord::Base ...@@ -249,7 +249,7 @@ class User < ActiveRecord::Base
def find_for_database_authentication(warden_conditions) def find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup conditions = warden_conditions.dup
if login = conditions.delete(:login) if login = conditions.delete(:login)
where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase) where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase.strip)
else else
find_by(conditions) find_by(conditions)
end end
......
...@@ -19,19 +19,10 @@ module Issues ...@@ -19,19 +19,10 @@ module Issues
# on rewriting notes (unfolding references) # on rewriting notes (unfolding references)
# #
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
# New issue tasks
#
@new_issue = create_new_issue @new_issue = create_new_issue
rewrite_notes update_new_issue
rewrite_issue_award_emoji update_old_issue
add_note_moved_from
# Old issue tasks
#
add_note_moved_to
close_issue
mark_as_moved
end end
notify_participants notify_participants
...@@ -41,6 +32,18 @@ module Issues ...@@ -41,6 +32,18 @@ module Issues
private private
def update_new_issue
rewrite_notes
rewrite_issue_award_emoji
add_note_moved_from
end
def update_old_issue
add_note_moved_to
close_issue
mark_as_moved
end
def create_new_issue def create_new_issue
new_params = { id: nil, iid: nil, label_ids: cloneable_label_ids, new_params = { id: nil, iid: nil, label_ids: cloneable_label_ids,
milestone_id: cloneable_milestone_id, milestone_id: cloneable_milestone_id,
......
...@@ -7,10 +7,9 @@ ...@@ -7,10 +7,9 @@
- build_path_proc = ->(scope) { admin_jobs_path(scope: scope) } - build_path_proc = ->(scope) { admin_jobs_path(scope: scope) }
= render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
.nav-controls
- if @all_builds.running_or_pending.any? - if @all_builds.running_or_pending.any?
#stop-jobs-modal #stop-jobs-modal
.nav-controls
%button#stop-jobs-button.btn.btn-danger{ data: { toggle: 'modal', %button#stop-jobs-button.btn.btn-danger{ data: { toggle: 'modal',
target: '#stop-jobs-modal', target: '#stop-jobs-modal',
url: cancel_all_admin_jobs_path } } url: cancel_all_admin_jobs_path } }
......
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
- if @project.avatar? - if @project.avatar?
%hr %hr
= link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _("Avatar will be removed. Are you sure?") }, method: :delete, class: "btn btn-danger btn-inverted" = link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _("Avatar will be removed. Are you sure?") }, method: :delete, class: "btn btn-danger btn-inverted"
= f.submit 'Save changes', class: "btn btn-success" = f.submit 'Save changes', class: "btn btn-success js-btn-save-general-project-settings"
%section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) } %section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'schedule_form'
= form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "form-horizontal js-pipeline-schedule-form" } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "form-horizontal js-pipeline-schedule-form" } do |f|
= form_errors(@schedule) = form_errors(@schedule)
.form-group .form-group
......
- breadcrumb_title _("Schedules") - breadcrumb_title _("Schedules")
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'schedules_index'
- @no_container = true - @no_container = true
- page_title _("Pipeline Schedules") - page_title _("Pipeline Schedules")
......
- @no_container = true - @no_container = true
- breadcrumb_title "CI / CD Charts" - breadcrumb_title "CI / CD Charts"
- page_title _("Charts"), _("Pipelines") - page_title _("Charts"), _("Pipelines")
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_d3')
= page_specific_javascript_bundle_tag('graphs')
%div{ class: container_class } %div{ class: container_class }
.sub-header-block .sub-header-block
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('pipelines_times')
%div %div
%p.light %p.light
= _("Commit duration in minutes for last 30 commits") = _("Commit duration in minutes for last 30 commits")
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('pipelines_charts')
%h4= _("Pipelines charts") %h4= _("Pipelines charts")
%p %p
&nbsp; &nbsp;
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('integrations')
.row.prepend-top-default.append-bottom-default .row.prepend-top-default.append-bottom-default
.col-lg-3 .col-lg-3
%h4.prepend-top-0 %h4.prepend-top-0
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('prometheus_metrics')
.row.prepend-top-default.append-bottom-default.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring .row.prepend-top-default.append-bottom-default.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring
.col-lg-3 .col-lg-3
%h4.prepend-top-0 %h4.prepend-top-0
......
...@@ -6,4 +6,4 @@ ...@@ -6,4 +6,4 @@
$(".project-edit-errors").html("#{escape_javascript(render('errors'))}"); $(".project-edit-errors").html("#{escape_javascript(render('errors'))}");
$('.save-project-loader').hide(); $('.save-project-loader').hide();
$('.project-edit-container').show(); $('.project-edit-container').show();
$('.edit-project .btn-save').enable(); $('.edit-project .js-btn-save-general-project-settings').enable();
...@@ -23,11 +23,11 @@ ...@@ -23,11 +23,11 @@
- if show_archive_options - if show_archive_options
%li.divider %li.divider
%li.js-filter-archived-projects %li.js-filter-archived-projects
= link_to group_children_path(@group, archived: nil), class: ("is-active" unless params[:archived].present?) do = link_to filter_groups_path(archived: nil), class: ("is-active" unless params[:archived].present?) do
Hide archived projects Hide archived projects
%li.js-filter-archived-projects %li.js-filter-archived-projects
= link_to group_children_path(@group, archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do = link_to filter_groups_path(archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do
Show archived projects Show archived projects
%li.js-filter-archived-projects %li.js-filter-archived-projects
= link_to group_children_path(@group, archived: 'only'), class: ("is-active" if params[:archived] == 'only') do = link_to filter_groups_path(archived: 'only'), class: ("is-active" if params[:archived] == 'only') do
Show archived projects only Show archived projects only
...@@ -4,9 +4,6 @@ ...@@ -4,9 +4,6 @@
- page_description @user.bio - page_description @user.bio
- header_title @user.name, user_path(@user) - header_title @user.name, user_path(@user)
- @no_container = true - @no_container = true
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_d3'
= webpack_bundle_tag 'users'
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
......
---
title: "Fix user avatar's vertical align on the issues and merge requests pages"
merge_request: 17072
author: Laszlo Karpati
type: fixed
title: Fix 404 when listing archived projects in a group where all projects have been archived
merge_request: 17077
author: Ashley Dumaine
type: fixed
---
title: API endpoint for importing a project export
merge_request: 17025
author:
type: added
---
title: Fix 500 error when loading a merge request with an invalid comment
merge_request: 16795
author:
type: fixed
---
title: Update nokogiri to 1.8.2
merge_request: 16807
author:
type: security
---
title: Fix GitLab import leaving group_id on ProjectLabel
merge_request: 16877
author:
type: fixed
---
title: Hide CI secret variable values after saving
merge_request: 17044
author:
type: changed
---
title: Allows project rename after validation error
merge_request: 17150
author:
type: fixed
---
title: Asciidoc now support inter-document cross references between files in repository
merge_request: 17125
author: Turo Soisenniemi
type: changed
---
title: Fix forking projects when no restricted visibility levels are defined applicationwide
merge_request: 16881
author:
type: fixed
---
title: Remove whitespace from the username/email sign in form field
merge_request: 17020
author: Peter lauck
type: changed
--- ---
title: Fix JIRA not working when a trailing slash is included title: Escape HTML entities in commit messages
merge_request: merge_request:
author: author:
type: fixed type: fixed
---
title: Validate user namespace before saving so that errors persist on model
merge_request:
author:
type: fixed
---
title: Fixed bug with unauthenticated requests through git ssh
merge_request: 17149
author:
type: fixed
---
title: Only check LFS integrity for first ref in a push to avoid timeout
merge_request: 17098
author:
type: performance
---
title: Resolve PrepareUntrackedUploads PostgreSQL syntax error
merge_request: 17019
author:
type: fixed
---
title: LDAP Person no longer throws exception on invalid entry
merge_request:
author:
type: fixed
---
title: Cleanup new branch/merge request form in issues
merge_request: 16854
author:
type: fixed
---
title: Add new modal Vue component
merge_request: 17108
author:
type: changed
...@@ -69,6 +69,7 @@ module Gitlab ...@@ -69,6 +69,7 @@ module Gitlab
# - Webhook URLs (:hook) # - Webhook URLs (:hook)
# - Sentry DSN (:sentry_dsn) # - Sentry DSN (:sentry_dsn)
# - Deploy keys (:key) # - Deploy keys (:key)
# - Secret variable values (:value)
config.filter_parameters += [/token$/, /password/, /secret/] config.filter_parameters += [/token$/, /password/, /secret/]
config.filter_parameters += %i( config.filter_parameters += %i(
certificate certificate
...@@ -80,6 +81,7 @@ module Gitlab ...@@ -80,6 +81,7 @@ module Gitlab
sentry_dsn sentry_dsn
trace trace
variables variables
value
) )
# Enable escaping HTML in JSON. # Enable escaping HTML in JSON.
......
...@@ -26,6 +26,7 @@ class Rack::Attack ...@@ -26,6 +26,7 @@ class Rack::Attack
throttle('throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req| throttle('throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req|
Gitlab::Throttle.settings.throttle_unauthenticated_enabled && Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
req.unauthenticated? && req.unauthenticated? &&
!req.api_internal_request? &&
req.ip req.ip
end end
...@@ -54,6 +55,10 @@ class Rack::Attack ...@@ -54,6 +55,10 @@ class Rack::Attack
path.start_with?('/api') path.start_with?('/api')
end end
def api_internal_request?
path =~ %r{^/api/v\d+/internal/}
end
def web_request? def web_request?
!api_request? !api_request?
end end
......
...@@ -67,7 +67,6 @@ var config = { ...@@ -67,7 +67,6 @@ var config = {
help: './help/help.js', help: './help/help.js',
how_to_merge: './how_to_merge.js', how_to_merge: './how_to_merge.js',
issue_show: './issue_show/index.js', issue_show: './issue_show/index.js',
integrations: './integrations',
job_details: './jobs/job_details_bundle.js', job_details: './jobs/job_details_bundle.js',
locale: './locale/index.js', locale: './locale/index.js',
main: './main.js', main: './main.js',
...@@ -78,19 +77,14 @@ var config = { ...@@ -78,19 +77,14 @@ var config = {
notes: './notes/index.js', notes: './notes/index.js',
pdf_viewer: './blob/pdf_viewer.js', pdf_viewer: './blob/pdf_viewer.js',
pipelines: './pipelines/pipelines_bundle.js', pipelines: './pipelines/pipelines_bundle.js',
pipelines_charts: './pipelines/pipelines_charts.js',
pipelines_details: './pipelines/pipeline_details_bundle.js', pipelines_details: './pipelines/pipeline_details_bundle.js',
pipelines_times: './pipelines/pipelines_times.js',
profile: './profile/profile_bundle.js', profile: './profile/profile_bundle.js',
project_import_gl: './projects/project_import_gitlab_project.js', project_import_gl: './projects/project_import_gitlab_project.js',
prometheus_metrics: './prometheus_metrics',
protected_branches: './protected_branches', protected_branches: './protected_branches',
protected_tags: './protected_tags', protected_tags: './protected_tags',
registry_list: './registry/index.js', registry_list: './registry/index.js',
ide: './ide/index.js', ide: './ide/index.js',
sidebar: './sidebar/sidebar_bundle.js', sidebar: './sidebar/sidebar_bundle.js',
schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
schedules_index: './pipeline_schedules/pipeline_schedules_index_bundle.js',
snippet: './snippet/snippet_bundle.js', snippet: './snippet/snippet_bundle.js',
sketch_viewer: './blob/sketch_viewer.js', sketch_viewer: './blob/sketch_viewer.js',
stl_viewer: './blob/stl_viewer.js', stl_viewer: './blob/stl_viewer.js',
...@@ -101,7 +95,6 @@ var config = { ...@@ -101,7 +95,6 @@ var config = {
vue_merge_request_widget: './vue_merge_request_widget/index.js', vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js', test: './test.js',
two_factor_auth: './two_factor_auth.js', two_factor_auth: './two_factor_auth.js',
users: './users/index.js',
webpack_runtime: './webpack.js', webpack_runtime: './webpack.js',
}, },
......
...@@ -9,7 +9,19 @@ created in snippets, wikis, and repos. ...@@ -9,7 +9,19 @@ created in snippets, wikis, and repos.
## PlantUML Server ## PlantUML Server
Before you can enable PlantUML in GitLab; you need to set up your own PlantUML Before you can enable PlantUML in GitLab; you need to set up your own PlantUML
server that will generate the diagrams. Installing and configuring your server that will generate the diagrams.
### Docker
With Docker, you can just run a container like this:
`docker run -d --name plantuml -p 8080:8080 plantuml/plantuml-server:tomcat`
The **PlantUML URL** will be the hostname of the server running the container.
### Debian/Ubuntu
Installing and configuring your
own PlantUML server is easy in Debian/Ubuntu distributions using Tomcat. own PlantUML server is easy in Debian/Ubuntu distributions using Tomcat.
First you need to create a `plantuml.war` file from the source code: First you need to create a `plantuml.war` file from the source code:
......
...@@ -56,7 +56,7 @@ new one, and attempting to pull a repo. ...@@ -56,7 +56,7 @@ new one, and attempting to pull a repo.
> **Warning:** Do not disable writes until SSH is confirmed to be working > **Warning:** Do not disable writes until SSH is confirmed to be working
perfectly, because the file will quickly become out-of-date. perfectly, because the file will quickly become out-of-date.
In the case of lookup failures (which are not uncommon), the `authorized_keys` In the case of lookup failures (which are common), the `authorized_keys`
file will still be scanned. So git SSH performance will still be slow for many file will still be scanned. So git SSH performance will still be slow for many
users as long as a large file exists. users as long as a large file exists.
......
...@@ -61,6 +61,21 @@ Before proceeding with the Pages configuration, you will need to: ...@@ -61,6 +61,21 @@ Before proceeding with the Pages configuration, you will need to:
NOTE: **Note:** NOTE: **Note:**
If your GitLab instance and the Pages daemon are deployed in a private network or behind a firewall, your GitLab Pages websites will only be accessible to devices/users that have access to the private network. If your GitLab instance and the Pages daemon are deployed in a private network or behind a firewall, your GitLab Pages websites will only be accessible to devices/users that have access to the private network.
### Add the domain to the Public Suffix List
The [Public Suffix List](https://publicsuffix.org) is used by browsers to
decide how to treat subdomains. If your GitLab instance allows members of the
public to create GitLab Pages sites, it also allows those users to create
subdomains on the pages domain (`example.io`). Adding the domain to the Public
Suffix List prevents browsers from accepting
[supercookies](https://en.wikipedia.org/wiki/HTTP_cookie#Supercookie),
among other things.
Follow [these instructions](https://publicsuffix.org/submit/) to submit your
GitLab Pages subdomain. For instance, if your domain is `example.io`, you should
request that `*.example.io` is added to the Public Suffix List. GitLab.com
added `*.gitlab.io` [in 2016](https://gitlab.com/gitlab-com/infrastructure/issues/230).
### DNS configuration ### DNS configuration
GitLab Pages expect to run on their own virtual host. In your DNS server/provider GitLab Pages expect to run on their own virtual host. In your DNS server/provider
......
...@@ -43,6 +43,7 @@ following locations: ...@@ -43,6 +43,7 @@ following locations:
- [Pipeline Schedules](pipeline_schedules.md) - [Pipeline Schedules](pipeline_schedules.md)
- [Projects](projects.md) including setting Webhooks - [Projects](projects.md) including setting Webhooks
- [Project Access Requests](access_requests.md) - [Project Access Requests](access_requests.md)
- [Project import/export](project_import_export.md)
- [Project Members](members.md) - [Project Members](members.md)
- [Project Snippets](project_snippets.md) - [Project Snippets](project_snippets.md)
- [Protected Branches](protected_branches.md) - [Protected Branches](protected_branches.md)
......
# Project import API
[Introduced][ce-41899] in GitLab 10.6
[See also the project import/export documentation](../user/project/settings/import_export.md)
## Import a file
```http
POST /projects/import
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
| `namespace` | integer/string | no | The ID or path of the namespace that the project will be imported to. Defaults to the current user's namespace |
| `file` | string | yes | The file to be uploaded |
| `path` | string | yes | Name and path for new project |
To upload a file from your filesystem, use the `--form` argument. This causes
cURL to post data using the header `Content-Type: multipart/form-data`.
The `file=` parameter must point to a file on your filesystem and be preceded
by `@`. For example:
```console
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "path=api-project" --form "file=@/path/to/file" https://gitlab.example.com/api/v4/projects/import
```
```json
{
"id": 1,
"description": null,
"name": "api-project",
"name_with_namespace": "Administrator / api-project",
"path": "api-project",
"path_with_namespace": "root/api-project",
"created_at": "2018-02-13T09:05:58.023Z",
"import_status": "scheduled"
}
```
## Import status
Get the status of an import.
```http
GET /projects/:id/import
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```console
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/import
```
Status can be one of `none`, `scheduled`, `failed`, `started`, or `finished`.
If the status is `failed`, it will include the import error message under `import_error`.
```json
{
"id": 1,
"description": "Itaque perspiciatis minima aspernatur corporis consequatur.",
"name": "Gitlab Test",
"name_with_namespace": "Gitlab Org / Gitlab Test",
"path": "gitlab-test",
"path_with_namespace": "gitlab-org/gitlab-test",
"created_at": "2017-08-29T04:36:44.383Z",
"import_status": "started"
}
```
[ce-41899]: https://gitlab.com/gitlab-org/gitlab-ce/issues/41899
...@@ -1330,6 +1330,10 @@ POST /projects/:id/housekeeping ...@@ -1330,6 +1330,10 @@ POST /projects/:id/housekeeping
Read more in the [Branches](branches.md) documentation. Read more in the [Branches](branches.md) documentation.
## Project Import/Export
Read more in the [Project import/export](project_import_export.md) documentation.
## Project members ## Project members
Read more in the [Project members](members.md) documentation. Read more in the [Project members](members.md) documentation.
...@@ -70,6 +70,8 @@ learn how to leverage its potential even more. ...@@ -70,6 +70,8 @@ learn how to leverage its potential even more.
- [Use SSH keys in your build environment](ssh_keys/README.md) - [Use SSH keys in your build environment](ssh_keys/README.md)
- [Trigger pipelines through the GitLab API](triggers/README.md) - [Trigger pipelines through the GitLab API](triggers/README.md)
- [Trigger pipelines on a schedule](../user/project/pipelines/schedules.md) - [Trigger pipelines on a schedule](../user/project/pipelines/schedules.md)
- [Kubernetes clusters](../user/project/clusters/index.md) - Integrate one or
more Kubernetes clusters to your project
## GitLab CI/CD for Docker ## GitLab CI/CD for Docker
......
# Components
## Contents
* [Dropdowns](#dropdowns)
* [Modals](#modals)
## Dropdowns
See also the [corresponding UX guide](../ux_guide/components.md#dropdowns).
### How to style a bootstrap dropdown
1. Use the HTML structure provided by the [docs][bootstrap-dropdowns]
1. Add a specific class to the top level `.dropdown` element
```Haml
.dropdown.my-dropdown
%button{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
%span.dropdown-toggle-text
Toggle Dropdown
= icon('chevron-down')
%ul.dropdown-menu
%li
%a
item!
```
Or use the helpers
```Haml
.dropdown.my-dropdown
= dropdown_toggle('Toogle!', { toggle: 'dropdown' })
= dropdown_content
%li
%a
item!
```
[bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns
## Modals
See also the [corresponding UX guide](../ux_guide/components.md#modals).
We have a reusable Vue component for modals: [vue_shared/components/gl-modal.vue](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/vue_shared/components/gl-modal.vue)
Here is an example of how to use it:
```html
<gl-modal
id="dogs-out-modal"
:header-title-text="s__('ModalExample|Let the dogs out?')"
footer-primary-button-variant="danger"
:footer-primary-button-text="s__('ModalExample|Let them out')"
@submit="letOut(theDogs)"
>
{{ s__('ModalExample|You’re about to let the dogs out.') }}
</gl-modal>
```
![example modal](img/gl-modal.png)
# Dropdowns This page has moved [here](components.md#dropdowns).
## How to style a bootstrap dropdown
1. Use the HTML structure provided by the [docs][bootstrap-dropdowns]
1. Add a specific class to the top level `.dropdown` element
```Haml
.dropdown.my-dropdown
%button{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
%span.dropdown-toggle-text
Toggle Dropdown
= icon('chevron-down')
%ul.dropdown-menu
%li
%a
item!
```
Or use the helpers
```Haml
.dropdown.my-dropdown
= dropdown_toggle('Toogle!', { toggle: 'dropdown' })
= dropdown_content
%li
%a
item!
```
[bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns
...@@ -21,6 +21,8 @@ Working with our frontend assets requires Node (v4.3 or greater) and Yarn ...@@ -21,6 +21,8 @@ Working with our frontend assets requires Node (v4.3 or greater) and Yarn
[jQuery][jquery] is used throughout the application's JavaScript, with [jQuery][jquery] is used throughout the application's JavaScript, with
[Vue.js][vue] for particularly advanced, dynamic elements. [Vue.js][vue] for particularly advanced, dynamic elements.
We also use [Axios][axios] to handle all of our network requests.
### Browser Support ### Browser Support
For our currently-supported browsers, see our [requirements][requirements]. For our currently-supported browsers, see our [requirements][requirements].
...@@ -77,8 +79,10 @@ Axios specific practices and gotchas. ...@@ -77,8 +79,10 @@ Axios specific practices and gotchas.
## [Icons](icons.md) ## [Icons](icons.md)
How we use SVG for our Icons. How we use SVG for our Icons.
## [Dropdowns](dropdowns.md) ## [Components](components.md)
How we use dropdowns.
How we use UI components.
--- ---
## Style Guides ## Style Guides
...@@ -122,6 +126,7 @@ The [externalization part of the guide](../i18n/externalization.md) explains the ...@@ -122,6 +126,7 @@ The [externalization part of the guide](../i18n/externalization.md) explains the
[webpack]: https://webpack.js.org/ [webpack]: https://webpack.js.org/
[jquery]: https://jquery.com/ [jquery]: https://jquery.com/
[vue]: http://vuejs.org/ [vue]: http://vuejs.org/
[axios]: https://github.com/axios/axios
[airbnb-js-style-guide]: https://github.com/airbnb/javascript [airbnb-js-style-guide]: https://github.com/airbnb/javascript
[scss-lint]: https://github.com/brigade/scss-lint [scss-lint]: https://github.com/brigade/scss-lint
[install]: ../../install/installation.md#4-node [install]: ../../install/installation.md#4-node
......
...@@ -27,6 +27,17 @@ Gitlab::Profiler.profile('/my-user') ...@@ -27,6 +27,17 @@ Gitlab::Profiler.profile('/my-user')
# Returns a RubyProf::Profile where 100 seconds is spent in UsersController#show # Returns a RubyProf::Profile where 100 seconds is spent in UsersController#show
``` ```
For routes that require authorization you will need to provide a user to
`Gitlab::Profiler`. You can do this like so:
```ruby
Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first)
```
The user you provide will need to have a [personal access
token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html) in
the GitLab instance.
Passing a `logger:` keyword argument to `Gitlab::Profiler.profile` will send Passing a `logger:` keyword argument to `Gitlab::Profiler.profile` will send
ActiveRecord and ActionController log output to that logger. Further options are ActiveRecord and ActionController log output to that logger. Further options are
documented with the method source. documented with the method source.
......
...@@ -2,12 +2,7 @@ ...@@ -2,12 +2,7 @@
![GCP landing page](img/gcp_landing.png) ![GCP landing page](img/gcp_landing.png)
The fastest way to get started on [Google Cloud Platform (GCP)][gcp] is through Gettung started with GitLab on a [Google Cloud Platform (GCP)][gcp] instance is quick and easy.
the [Google Cloud Launcher][launcher] program.
GitLab's official Google Launcher apps:
1. [GitLab Community Edition](https://console.cloud.google.com/launcher/details/gitlab-public/gitlab-community-edition?project=gitlab-public)
2. [GitLab Enterprise Edition](https://console.cloud.google.com/launcher/details/gitlab-public/gitlab-enterprise-edition?project=gitlab-public)
## Prerequisites ## Prerequisites
...@@ -17,84 +12,52 @@ There are only two prerequisites in order to install GitLab on GCP: ...@@ -17,84 +12,52 @@ There are only two prerequisites in order to install GitLab on GCP:
1. You need to sign up for the GCP program. If this is your first time, Google 1. You need to sign up for the GCP program. If this is your first time, Google
gives you [$300 credit for free][freetrial] to consume over a 60-day period. gives you [$300 credit for free][freetrial] to consume over a 60-day period.
Once you have performed those two steps, you can visit the Once you have performed those two steps, you can [create a VM](#creating-the-vm).
[GCP launcher console][console] which has a list of all the things you can
deploy on GCP.
![GCP launcher console](img/gcp_launcher_console_home_page.png)
The next step is to find and install GitLab.
## Configuring and deploying the VM ## Creating the VM
To deploy GitLab on GCP you need to follow five simple steps: To deploy GitLab on GCP you need to follow five simple steps:
1. Go to https://cloud.google.com/launcher and login with your Google credentials 1. Go to https://console.cloud.google.com/compute/instances and login with your Google credentials.
1. Search for GitLab from GitLab Inc. (not the same as Bitnami) and click on
the tile.
![Search for GitLab](img/gcp_search_for_gitlab.png) 1. Click on **Create**
1. In the next page, you can see an overview of the GitLab VM as well as some ![Search for GitLab](img/launch_vm.png)
estimated costs. Click the **Launch on Compute Engine** button to choose the
hardware and network settings.
![Launch on Compute Engine](img/gcp_gitlab_overview.png) 1. On the next page, you can select the type of VM as well as the
estimated costs. Provide the name of the instance, desired datacenter, and machine type. Note that GitLab recommends at least 2 vCPU's and 4GB of RAM.
1. In the settings page you can choose things like the datacenter where your GitLab ![Launch on Compute Engine](img/vm_details.png)
server will be hosted, the number of CPUs and amount of RAM, the disk size
and type, etc. Read GitLab's [requirements documentation][req] for more
details on what to choose depending on your needs.
![Deploy settings](img/new_gitlab_deployment_settings.png) 1. Click **Change** under Boot disk to select the size, type, and desired operating system. GitLab supports a [variety of linux operating systems][req], including Ubuntu and Debian. Click **Select** when finished.
1. As a last step, hit **Deploy** when ready. The process will finish in a few ![Deploy in progress](img/boot_disk.png)
seconds.
![Deploy in progress](img/gcp_gitlab_being_deployed.png) 1. As a last step allow HTTP and HTTPS traffic, then click **Create**. The process will finish in a few seconds.
## Installing GitLab
## Visiting GitLab for the first time After a few seconds, the instance will be created and available to log in. The next step is to install GitLab onto the instance.
After a few seconds, GitLab will be successfully deployed and you should be ![Deploy settings](img/vm_created.png)
able to see the IP address that Google assigned to the VM, as well as the
credentials to the GitLab admin account.
![Deploy settings](img/gitlab_deployed_page.png) 1. Make a note of the IP address of the instance, as you will need that in a later step.
1. Click on the SSH button to connect to the instance.
1. A new window will appear, with you logged into the instance.
1. Click on the IP under **Site address** to visit GitLab. ![GitLab first sign in](img/ssh_terminal.png)
1. Accept the self-signed certificate that Google automatically deployed in
order to securely reach GitLab's login page.
1. Use the username and password that are present in the Google console page
to login into GitLab and click **Sign in**.
![GitLab first sign in](img/gitlab_first_sign_in.png) 1. Next, follow the instructions for installing GitLab for the operating system you choose, at https://about.gitlab.com/installation/. You can use the IP address from the step above, as the hostname.
Congratulations! GitLab is now installed and you can access it via your browser, 1. Congratulations! GitLab is now installed and you can access it via your browser. To finish installation, open the URL in your browser and provide the initial administrator password. The username for this account is `root`.
but we're not done yet. There are some steps you need to take in order to have
a fully functional GitLab installation. ![GitLab first sign in](img/first_signin.png)
## Next steps ## Next steps
These are the most important next steps to take after you installed GitLab for These are the most important next steps to take after you installed GitLab for
the first time. the first time.
### Changing the admin password and email
Google assigned a random password for the GitLab admin account and you should
change it ASAP:
1. Visit the GitLab admin page through the link in the Google console under
**Admin URL**.
1. Find the Administrator user under the **Users** page and hit **Edit**.
1. Change the email address to a real one and enter a new password.
![Change GitLab admin password](img/change_admin_passwd_email.png)
1. Hit **Save changes** for the changes to take effect.
1. After changing the password, you will be signed out from GitLab. Use the
new credentials to login again.
### Assigning a static IP ### Assigning a static IP
By default, Google assigns an ephemeral IP to your instance. It is strongly By default, Google assigns an ephemeral IP to your instance. It is strongly
...@@ -112,7 +75,7 @@ here's how you configure GitLab to be aware of the change: ...@@ -112,7 +75,7 @@ here's how you configure GitLab to be aware of the change:
1. SSH into the VM. You can easily use the **SSH** button in the Google console 1. SSH into the VM. You can easily use the **SSH** button in the Google console
and a new window will pop up. and a new window will pop up.
![SSH button](img/ssh_via_button.png) ![SSH button](img/vm_created.png)
In the future you might want to set up [connecting with an SSH key][ssh] In the future you might want to set up [connecting with an SSH key][ssh]
instead. instead.
...@@ -161,7 +124,6 @@ Kerberos, etc. Here are some documents you might be interested in reading: ...@@ -161,7 +124,6 @@ Kerberos, etc. Here are some documents you might be interested in reading:
- [GitLab Pages configuration](https://docs.gitlab.com/ce/administration/pages/index.html) - [GitLab Pages configuration](https://docs.gitlab.com/ce/administration/pages/index.html)
- [GitLab Container Registry configuration](https://docs.gitlab.com/ce/administration/container_registry.html) - [GitLab Container Registry configuration](https://docs.gitlab.com/ce/administration/container_registry.html)
[console]: https://console.cloud.google.com/launcher "GCP launcher console"
[freetrial]: https://console.cloud.google.com/freetrial "GCP free trial" [freetrial]: https://console.cloud.google.com/freetrial "GCP free trial"
[ip]: https://cloud.google.com/compute/docs/configure-instance-ip-addresses#promote_ephemeral_ip "Configuring an Instance's IP Addresses" [ip]: https://cloud.google.com/compute/docs/configure-instance-ip-addresses#promote_ephemeral_ip "Configuring an Instance's IP Addresses"
[gcp]: https://cloud.google.com/ "Google Cloud Platform" [gcp]: https://cloud.google.com/ "Google Cloud Platform"
......
...@@ -95,7 +95,9 @@ Auto Deploy, and Auto Monitoring will be silently skipped. ...@@ -95,7 +95,9 @@ Auto Deploy, and Auto Monitoring will be silently skipped.
The Auto DevOps base domain is required if you want to make use of [Auto The Auto DevOps base domain is required if you want to make use of [Auto
Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It is defined Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It is defined
under the project's CI/CD settings while [enabling Auto DevOps](#enabling-auto-devops). either under the project's CI/CD settings while
[enabling Auto DevOps](#enabling-auto-devops) or in instance-wide settings in
the CI/CD section.
It can also be set at the project or group level as a variable, `AUTO_DEVOPS_DOMAIN`. It can also be set at the project or group level as a variable, `AUTO_DEVOPS_DOMAIN`.
A wildcard DNS A record matching the base domain is required, for example, A wildcard DNS A record matching the base domain is required, for example,
......
...@@ -5,20 +5,23 @@ ...@@ -5,20 +5,23 @@
Connect your project to Google Kubernetes Engine (GKE) or an existing Kubernetes Connect your project to Google Kubernetes Engine (GKE) or an existing Kubernetes
cluster in a few steps. cluster in a few steps.
With a cluster associated to your project, you can use Review Apps, deploy your ## Overview
applications, run your pipelines, and much more, in an easy way.
With a Kubernetes cluster associated to your project, you can use
[Review Apps](../../../ci/review_apps/index.md), deploy your applications, run
your pipelines, and much more, in an easy way.
There are two options when adding a new cluster to your project; either associate There are two options when adding a new cluster to your project; either associate
your account with Google Kubernetes Engine (GKE) so that you can [create new your account with Google Kubernetes Engine (GKE) so that you can [create new
clusters](#adding-and-creating-a-new-gke-cluster-via-gitlab) from within GitLab, clusters](#adding-and-creating-a-new-gke-cluster-via-gitlab) from within GitLab,
or provide the credentials to an [existing Kubernetes cluster](#adding-an-existing-kubernetes-cluster). or provide the credentials to an [existing Kubernetes cluster](#adding-an-existing-kubernetes-cluster).
## Prerequisites ## Adding and creating a new GKE cluster via GitLab
In order to be able to manage your Kubernetes cluster through GitLab, the NOTE: **Note:**
following prerequisites must be met. You need Master [permissions] and above to access the Kubernetes page.
**For a cluster hosted on GKE:** Before proceeding, make sure the following requirements are met:
- The [Google authentication integration](../../../integration/google.md) must - The [Google authentication integration](../../../integration/google.md) must
be enabled in GitLab at the instance level. If that's not the case, ask your be enabled in GitLab at the instance level. If that's not the case, ask your
...@@ -28,30 +31,16 @@ following prerequisites must be met. ...@@ -28,30 +31,16 @@ following prerequisites must be met.
account](https://cloud.google.com/billing/docs/how-to/manage-billing-account) account](https://cloud.google.com/billing/docs/how-to/manage-billing-account)
must be set up and that you have to have permissions to access it. must be set up and that you have to have permissions to access it.
- You must have Master [permissions] in order to be able to access the - You must have Master [permissions] in order to be able to access the
**Cluster** page. **Kubernetes** page.
- You must have [Cloud Billing API](https://cloud.google.com/billing/) enabled - You must have [Cloud Billing API](https://cloud.google.com/billing/) enabled
- You must have [Resource Manager - You must have [Resource Manager
API](https://cloud.google.com/resource-manager/) API](https://cloud.google.com/resource-manager/)
**For an existing Kubernetes cluster:** If all of the above requirements are met, you can proceed to create and add a
new Kubernetes cluster that will be hosted on GKE to your project:
- Since the cluster is already created, there are no prerequisites.
---
If all of the above requirements are met, you can proceed to add a new Kubernetes
cluster.
## Adding and creating a new GKE cluster via GitLab 1. Navigate to your project's **CI/CD > Kubernetes** page.
1. Click on **Add Kubernetes cluster**.
NOTE: **Note:**
You need Master [permissions] and above to access the Clusters page.
Before proceeding, make sure all [prerequisites](#prerequisites) are met.
To add a new cluster hosted on GKE to your project:
1. Navigate to your project's **CI/CD > Clusters** page.
1. Click on **Add cluster**.
1. Click on **Create with GKE**. 1. Click on **Create with GKE**.
1. Connect your Google account if you haven't done already by clicking the 1. Connect your Google account if you haven't done already by clicking the
**Sign in with Google** button. **Sign in with Google** button.
...@@ -66,7 +55,7 @@ To add a new cluster hosted on GKE to your project: ...@@ -66,7 +55,7 @@ To add a new cluster hosted on GKE to your project:
- **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types) - **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types)
of the Virtual Machine instance that the cluster will be based on. of the Virtual Machine instance that the cluster will be based on.
- **Environment scope** - The [associated environment](#setting-the-environment-scope) to this cluster. - **Environment scope** - The [associated environment](#setting-the-environment-scope) to this cluster.
1. Finally, click the **Create cluster** button. 1. Finally, click the **Create Kubernetes cluster** button.
After a few moments, your cluster should be created. If something goes wrong, After a few moments, your cluster should be created. If something goes wrong,
you will be notified. you will be notified.
...@@ -77,14 +66,14 @@ enable the Cluster integration. ...@@ -77,14 +66,14 @@ enable the Cluster integration.
## Adding an existing Kubernetes cluster ## Adding an existing Kubernetes cluster
NOTE: **Note:** NOTE: **Note:**
You need Master [permissions] and above to access the Clusters page. You need Master [permissions] and above to access the Kubernetes page.
To add an existing Kubernetes cluster to your project: To add an existing Kubernetes cluster to your project:
1. Navigate to your project's **CI/CD > Clusters** page. 1. Navigate to your project's **CI/CD > Kubernetes** page.
1. Click on **Add cluster**. 1. Click on **Add Kuberntes cluster**.
1. Click on **Add an existing cluster** and fill in the details: 1. Click on **Add an existing Kubernetes cluster** and fill in the details:
- **Cluster name** (required) - The name you wish to give the cluster. - **Kubernetes cluster name** (required) - The name you wish to give the cluster.
- **Environment scope** (required)- The - **Environment scope** (required)- The
[associated environment](#setting-the-environment-scope) to this cluster. [associated environment](#setting-the-environment-scope) to this cluster.
- **API URL** (required) - - **API URL** (required) -
...@@ -112,15 +101,13 @@ To add an existing Kubernetes cluster to your project: ...@@ -112,15 +101,13 @@ To add an existing Kubernetes cluster to your project:
- If you or someone created a secret specifically for the project, usually - If you or someone created a secret specifically for the project, usually
with limited permissions, the secret's namespace and project namespace may with limited permissions, the secret's namespace and project namespace may
be the same. be the same.
1. Finally, click the **Create cluster** button. 1. Finally, click the **Create Kuberntes cluster** button.
The Kubernetes service takes the following parameters:
After a few moments, your cluster should be created. If something goes wrong, After a few moments, your cluster should be created. If something goes wrong,
you will be notified. you will be notified.
You can now proceed to install some pre-defined applications and then You can now proceed to install some pre-defined applications and then
enable the Cluster integration. enable the Kubernetes cluster integration.
## Installing applications ## Installing applications
...@@ -139,7 +126,7 @@ added directly to your configured cluster. Those applications are needed for ...@@ -139,7 +126,7 @@ added directly to your configured cluster. Those applications are needed for
NOTE: **Note:** NOTE: **Note:**
You need a load balancer installed in your cluster in order to obtain the You need a load balancer installed in your cluster in order to obtain the
external IP address with the following procedure. It can be deployed using the external IP address with the following procedure. It can be deployed using the
**Ingress** application described in the previous section. [**Ingress** application](#installing-appplications).
In order to publish your web application, you first need to find the external IP In order to publish your web application, you first need to find the external IP
address associated to your load balancer. address associated to your load balancer.
...@@ -153,7 +140,8 @@ the `gcloud` command in a local terminal or using the **Cloud Shell**. ...@@ -153,7 +140,8 @@ the `gcloud` command in a local terminal or using the **Cloud Shell**.
If the cluster is not on GKE, follow the specific instructions for your If the cluster is not on GKE, follow the specific instructions for your
Kubernetes provider to configure `kubectl` with the right credentials. Kubernetes provider to configure `kubectl` with the right credentials.
If you installed the Ingress using the **Applications** section, run the following command: If you installed the Ingress [via the **Applications**](#installing-applications),
run the following command:
```bash ```bash
kubectl get svc --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip} ' kubectl get svc --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
...@@ -171,9 +159,14 @@ your deployed applications. ...@@ -171,9 +159,14 @@ your deployed applications.
## Setting the environment scope ## Setting the environment scope
When adding more than one clusters, you need to differentiate them with an NOTE: **Note:**
environment scope. The environment scope associates clusters and This is only available for [GitLab Premium][ee] where you can add more than
[environments](../../../ci/environments.md) in an 1:1 relationship similar to how the one Kubernetes cluster.
When adding more than one Kubernetes clusters to your project, you need to
differentiate them with an environment scope. The environment scope associates
clusters and [environments](../../../ci/environments.md) in an 1:1 relationship
similar to how the
[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-secret-variables) [environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-secret-variables)
work. work.
...@@ -183,7 +176,7 @@ cluster in a project, and a validation error will occur if otherwise. ...@@ -183,7 +176,7 @@ cluster in a project, and a validation error will occur if otherwise.
--- ---
For example, let's say the following clusters exist in a project: For example, let's say the following Kubernetes clusters exist in a project:
| Cluster | Environment scope | | Cluster | Environment scope |
| ---------- | ------------------- | | ---------- | ------------------- |
...@@ -231,8 +224,7 @@ With GitLab Premium, you can associate more than one Kubernetes clusters to your ...@@ -231,8 +224,7 @@ With GitLab Premium, you can associate more than one Kubernetes clusters to your
project. That way you can have different clusters for different environments, project. That way you can have different clusters for different environments,
like dev, staging, production, etc. like dev, staging, production, etc.
To add another cluster, follow the same steps as described in [adding a Simply add another cluster, like you did the first time, and make sure to
Kubernetes cluster](#adding-a-kubernetes-cluster) and make sure to
[set an environment scope](#setting-the-environment-scope) that will [set an environment scope](#setting-the-environment-scope) that will
differentiate the new cluster with the rest. differentiate the new cluster with the rest.
...@@ -240,45 +232,42 @@ differentiate the new cluster with the rest. ...@@ -240,45 +232,42 @@ differentiate the new cluster with the rest.
The Kubernetes cluster integration exposes the following The Kubernetes cluster integration exposes the following
[deployment variables](../../../ci/variables/README.md#deployment-variables) in the [deployment variables](../../../ci/variables/README.md#deployment-variables) in the
GitLab CI/CD build environment: GitLab CI/CD build environment.
- `KUBE_URL` - Equal to the API URL. | Variable | Description |
- `KUBE_TOKEN` - The Kubernetes token. | -------- | ----------- |
- `KUBE_NAMESPACE` - The Kubernetes namespace is auto-generated if not specified. | `KUBE_URL` | Equal to the API URL. |
The default value is `<project_name>-<project_id>`. You can overwrite it to | `KUBE_TOKEN` | The Kubernetes token. |
use different one if needed, otherwise the `KUBE_NAMESPACE` variable will | `KUBE_NAMESPACE` | The Kubernetes namespace is auto-generated if not specified. The default value is `<project_name>-<project_id>`. You can overwrite it to use different one if needed, otherwise the `KUBE_NAMESPACE` variable will receive the default value. |
receive the default value. | `KUBE_CA_PEM_FILE` | Only present if a custom CA bundle was specified. Path to a file containing PEM data. |
- `KUBE_CA_PEM_FILE` - Only present if a custom CA bundle was specified. Path | `KUBE_CA_PEM` | (**deprecated**) Only if a custom CA bundle was specified. Raw PEM data. |
to a file containing PEM data. | `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. |
- `KUBE_CA_PEM` (deprecated) - Only if a custom CA bundle was specified. Raw PEM data.
- `KUBECONFIG` - Path to a file containing `kubeconfig` for this deployment. ## Enabling or disabling the Kubernetes cluster integration
CA bundle would be embedded if specified.
## Enabling or disabling the Cluster integration
After you have successfully added your cluster information, you can enable the After you have successfully added your cluster information, you can enable the
Cluster integration: Kubernetes cluster integration:
1. Click the "Enabled/Disabled" switch 1. Click the "Enabled/Disabled" switch
1. Hit **Save** for the changes to take effect 1. Hit **Save** for the changes to take effect
You can now start using your Kubernetes cluster for your deployments. You can now start using your Kubernetes cluster for your deployments.
To disable the Cluster integration, follow the same procedure. To disable the Kubernetes cluster integration, follow the same procedure.
## Removing the Cluster integration ## Removing the Kubernetes cluster integration
NOTE: **Note:** NOTE: **Note:**
You need Master [permissions] and above to remove a cluster integration. You need Master [permissions] and above to remove a Kubernetes cluster integration.
NOTE: **Note:** NOTE: **Note:**
When you remove a cluster, you only remove its relation to GitLab, not the When you remove a cluster, you only remove its relation to GitLab, not the
cluster itself. To remove the cluster, you can do so by visiting the GKE cluster itself. To remove the cluster, you can do so by visiting the GKE
dashboard or using `kubectl`. dashboard or using `kubectl`.
To remove the Cluster integration from your project, simply click on the To remove the Kubernetes cluster integration from your project, simply click on the
**Remove integration** button. You will then be able to follow the procedure **Remove integration** button. You will then be able to follow the procedure
and [add a cluster](#adding-a-cluster) again. and add a Kubernetes cluster again.
## What you can get with the Kubernetes integration ## What you can get with the Kubernetes integration
......
...@@ -18,7 +18,7 @@ documentation. ...@@ -18,7 +18,7 @@ documentation.
> **Important:** > **Important:**
For security reasons, when using the command line, we strongly recommend For security reasons, when using the command line, we strongly recommend
you to [connect with GitLab via SSH](../../../ssh/README.md). that you [connect with GitLab via SSH](../../../ssh/README.md).
## Files ## Files
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
> in the import side is required to map the users, based on email or username. > in the import side is required to map the users, based on email or username.
> Otherwise, a supplementary comment is left to mention the original author and > Otherwise, a supplementary comment is left to mention the original author and
> the MRs, notes or issues will be owned by the importer. > the MRs, notes or issues will be owned by the importer.
> - Control project Import/Export with the [API](../../../api/project_import_export.md).
Existing projects running on any GitLab instance or GitLab.com can be exported Existing projects running on any GitLab instance or GitLab.com can be exported
with all their related data and be moved into a new GitLab instance. with all their related data and be moved into a new GitLab instance.
......
@profile
Feature: Profile
Background:
Given I sign in as a user
Scenario: I look at my profile
Given I visit profile page
Then I should see my profile info
@javascript
Scenario: I can see groups I belong to
Given I have group with projects
When I visit profile page
And I click on my profile picture
Then I should see my user page
And I should see groups I belong to
Scenario: I edit profile
Given I visit profile page
Then I change my profile info
And I should see new profile info
Scenario: I change my password without old one
Given I visit profile password page
When I try change my password w/o old one
Then I should see a missing password error message
And I should be redirected to password page
Scenario: I change my password
Given I visit profile password page
Then I change my password
And I should be redirected to sign in page
Scenario: I edit my avatar
Given I visit profile page
Then I change my avatar
And I should see new avatar
And I should see the "Remove avatar" button
And I should see the gravatar host link
Scenario: I remove my avatar
Given I visit profile page
And I have an avatar
When I remove my avatar
Then I should see my gravatar
And I should not see the "Remove avatar" button
And I should see the gravatar host link
Scenario: My password is expired
Given my password is expired
And I am not an ldap user
Given I visit profile password page
Then I redirected to expired password page
And I submit new password
And I redirected to sign in page
Scenario: I unsuccessfully change my password
Given I visit profile password page
When I unsuccessfully change my password
Then I should see a password error message
Scenario: I visit history tab
Given I logout
And I sign in via the UI
And I have activity
When I visit Authentication log page
Then I should see my activity
Scenario: I visit my user page
When I visit profile page
And I click on my profile picture
Then I should see my user page
Scenario: I can manage application
Given I visit profile applications page
Then I should see application form
Then I fill application form out and submit
And I see application
Then I click edit
And I see edit application form
Then I change name of application and submit
And I see that application was changed
Then I visit profile applications page
And I click to remove application
Then I see that application is removed
class Spinach::Features::Profile < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
step 'I should see my profile info' do
expect(page).to have_content "This information will appear on your profile"
end
step 'I change my profile info' do
fill_in 'user_skype', with: 'testskype'
fill_in 'user_linkedin', with: 'testlinkedin'
fill_in 'user_twitter', with: 'testtwitter'
fill_in 'user_website_url', with: 'testurl'
fill_in 'user_location', with: 'Ukraine'
fill_in 'user_bio', with: 'I <3 GitLab'
fill_in 'user_organization', with: 'GitLab'
click_button 'Update profile settings'
@user.reload
end
step 'I should see new profile info' do
expect(@user.skype).to eq 'testskype'
expect(@user.linkedin).to eq 'testlinkedin'
expect(@user.twitter).to eq 'testtwitter'
expect(@user.website_url).to eq 'testurl'
expect(@user.bio).to eq 'I <3 GitLab'
expect(@user.organization).to eq 'GitLab'
expect(find('#user_location').value).to eq 'Ukraine'
end
step 'I change my avatar' do
attach_file(:user_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
click_button "Update profile settings"
@user.reload
end
step 'I should see new avatar' do
expect(@user.avatar).to be_instance_of AvatarUploader
expect(@user.avatar.url).to eq "/uploads/-/system/user/avatar/#{@user.id}/banana_sample.gif"
end
step 'I should see the "Remove avatar" button' do
expect(page).to have_link("Remove avatar")
end
step 'I have an avatar' do
attach_file(:user_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
click_button "Update profile settings"
@user.reload
end
step 'I remove my avatar' do
click_link "Remove avatar"
@user.reload
end
step 'I should see my gravatar' do
expect(@user.avatar?).to eq false
end
step 'I should not see the "Remove avatar" button' do
expect(page).not_to have_link("Remove avatar")
end
step 'I should see the gravatar host link' do
expect(page).to have_link("gravatar.com")
end
step 'I try change my password w/o old one' do
page.within '.update-password' do
fill_in "user_password", with: "22233344"
fill_in "user_password_confirmation", with: "22233344"
click_button "Save password"
end
end
step 'I change my password' do
page.within '.update-password' do
fill_in "user_current_password", with: "12345678"
fill_in "user_password", with: "22233344"
fill_in "user_password_confirmation", with: "22233344"
click_button "Save password"
end
end
step 'I unsuccessfully change my password' do
page.within '.update-password' do
fill_in "user_current_password", with: "12345678"
fill_in "user_password", with: "password"
fill_in "user_password_confirmation", with: "confirmation"
click_button "Save password"
end
end
step "I should see a missing password error message" do
page.within ".flash-container" do
expect(page).to have_content "You must provide a valid current password"
end
end
step "I should see a password error message" do
page.within '.alert-danger' do
expect(page).to have_content "Password confirmation doesn't match"
end
end
step 'I have activity' do
create(:closed_issue_event, author: current_user)
end
step 'I should see my activity' do
expect(page).to have_content "Signed in with standard authentication"
end
step 'my password is expired' do
current_user.update_attributes(password_expires_at: Time.now - 1.hour)
end
step "I am not an ldap user" do
current_user.identities.delete
expect(current_user.ldap_user?).to eq false
end
step 'I redirected to expired password page' do
expect(current_path).to eq new_profile_password_path
end
step 'I submit new password' do
fill_in :user_current_password, with: '12345678'
fill_in :user_password, with: '12345678'
fill_in :user_password_confirmation, with: '12345678'
click_button "Set new password"
end
step 'I redirected to sign in page' do
expect(current_path).to eq new_user_session_path
end
step 'I should be redirected to password page' do
expect(current_path).to eq edit_profile_password_path
end
step 'I should be redirected to account page' do
expect(current_path).to eq profile_account_path
end
step 'I click on my profile picture' do
find(:css, '.header-user-dropdown-toggle').click
page.within ".header-user" do
click_link "Profile"
end
end
step 'I should see my user page' do
page.within ".cover-block" do
expect(page).to have_content current_user.name
expect(page).to have_content current_user.username
end
end
step 'I have group with projects' do
@group = create(:group)
@group.add_owner(current_user)
@project = create(:project, :repository, namespace: @group)
@event = create(:closed_issue_event, project: @project)
@project.add_master(current_user)
end
step 'I should see groups I belong to' do
page.within ".content" do
click_link "Groups"
end
page.within "#groups" do
expect(page).to have_content @group.name
end
end
step 'I should see application form' do
expect(page).to have_content "Add new application"
end
step 'I fill application form out and submit' do
fill_in :doorkeeper_application_name, with: 'test'
fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
click_on "Save application"
end
step 'I see application' do
expect(page).to have_content "Application: test"
expect(page).to have_content "Application Id"
expect(page).to have_content "Secret"
end
step 'I click edit' do
click_on "Edit"
end
step 'I see edit application form' do
expect(page).to have_content "Edit application"
end
step 'I change name of application and submit' do
expect(page).to have_content "Edit application"
fill_in :doorkeeper_application_name, with: 'test_changed'
click_on "Save application"
end
step 'I see that application was changed' do
expect(page).to have_content "test_changed"
expect(page).to have_content "Application Id"
expect(page).to have_content "Secret"
end
step 'I click to remove application' do
page.within '.oauth-applications' do
click_on "Destroy"
end
end
step "I see that application is removed" do
expect(page.find(".oauth-applications")).not_to have_content "test_changed"
end
end
...@@ -20,15 +20,16 @@ Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file } ...@@ -20,15 +20,16 @@ Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file }
Spinach.hooks.before_run do Spinach.hooks.before_run do
include RSpec::Mocks::ExampleMethods include RSpec::Mocks::ExampleMethods
include ActiveJob::TestHelper
include FactoryBot::Syntax::Methods
include GitlabRoutingHelper
RSpec::Mocks.setup RSpec::Mocks.setup
TestEnv.init(mailer: false) TestEnv.init(mailer: false)
# skip pre-receive hook check so we can use # skip pre-receive hook check so we can use
# web editor and merge # web editor and merge
TestEnv.disable_pre_receive TestEnv.disable_pre_receive
include FactoryBot::Syntax::Methods
include GitlabRoutingHelper
end end
Spinach.hooks.after_scenario do |scenario_data, step_definitions| Spinach.hooks.after_scenario do |scenario_data, step_definitions|
......
...@@ -138,6 +138,7 @@ module API ...@@ -138,6 +138,7 @@ module API
mount ::API::PagesDomains mount ::API::PagesDomains
mount ::API::Pipelines mount ::API::Pipelines
mount ::API::PipelineSchedules mount ::API::PipelineSchedules
mount ::API::ProjectImport
mount ::API::ProjectHooks mount ::API::ProjectHooks
mount ::API::Projects mount ::API::Projects
mount ::API::ProjectMilestones mount ::API::ProjectMilestones
......
...@@ -91,6 +91,13 @@ module API ...@@ -91,6 +91,13 @@ module API
expose :created_at expose :created_at
end end
class ProjectImportStatus < ProjectIdentity
expose :import_status
# TODO: Use `expose_nil` once we upgrade the grape-entity gem
expose :import_error, if: lambda { |status, _ops| status.import_error }
end
class BasicProjectDetails < ProjectIdentity class BasicProjectDetails < ProjectIdentity
include ::API::ProjectsRelationBuilder include ::API::ProjectsRelationBuilder
......
module API
class ProjectImport < Grape::API
include PaginationParams
helpers do
def import_params
declared_params(include_missing: false)
end
def file_is_valid?
import_params[:file] && import_params[:file]['tempfile'].respond_to?(:read)
end
def validate_file!
render_api_error!('The file is invalid', 400) unless file_is_valid?
end
end
before do
forbidden! unless Gitlab::CurrentSettings.import_sources.include?('gitlab_project')
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
params do
requires :path, type: String, desc: 'The new project path and name'
requires :file, type: File, desc: 'The project export file to be imported'
optional :namespace, type: String, desc: "The ID or name of the namespace that the project will be imported into. Defaults to the current user's namespace."
end
desc 'Create a new project import' do
detail 'This feature was introduced in GitLab 10.6.'
success Entities::ProjectImportStatus
end
post 'import' do
validate_file!
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42437')
namespace = if import_params[:namespace]
find_namespace!(import_params[:namespace])
else
current_user.namespace
end
project_params = {
path: import_params[:path],
namespace_id: namespace.id,
file: import_params[:file]['tempfile']
}
project = ::Projects::GitlabProjectsImportService.new(current_user, project_params).execute
render_api_error!(project.errors.full_messages&.first, 400) unless project.saved?
present project, with: Entities::ProjectImportStatus
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
desc 'Get a project export status' do
detail 'This feature was introduced in GitLab 10.6.'
success Entities::ProjectImportStatus
end
get ':id/import' do
present user_project, with: Entities::ProjectImportStatus
end
end
end
end
...@@ -5,7 +5,7 @@ module Banzai ...@@ -5,7 +5,7 @@ module Banzai
# Text filter that escapes these HTML entities: & " < > # Text filter that escapes these HTML entities: & " < >
class HtmlEntityFilter < HTML::Pipeline::TextFilter class HtmlEntityFilter < HTML::Pipeline::TextFilter
def call def call
ERB::Util.html_escape_once(text) ERB::Util.html_escape(text)
end end
end end
end end
......
...@@ -8,7 +8,8 @@ module Gitlab ...@@ -8,7 +8,8 @@ module Gitlab
module Asciidoc module Asciidoc
DEFAULT_ADOC_ATTRS = [ DEFAULT_ADOC_ATTRS = [
'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab', 'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab',
'env-gitlab', 'source-highlighter=html-pipeline', 'icons=font' 'env-gitlab', 'source-highlighter=html-pipeline', 'icons=font',
'outfilesuffix=.adoc'
].freeze ].freeze
# Public: Converts the provided Asciidoc markup into HTML. # Public: Converts the provided Asciidoc markup into HTML.
......
...@@ -16,11 +16,11 @@ module Gitlab ...@@ -16,11 +16,11 @@ module Gitlab
lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".' lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
}.freeze }.freeze
attr_reader :user_access, :project, :skip_authorization, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name
def initialize( def initialize(
change, user_access:, project:, skip_authorization: false, change, user_access:, project:, skip_authorization: false,
protocol: skip_lfs_integrity_check: false, protocol:
) )
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref) @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref) @branch_name = Gitlab::Git.branch_name(@ref)
...@@ -28,6 +28,7 @@ module Gitlab ...@@ -28,6 +28,7 @@ module Gitlab
@user_access = user_access @user_access = user_access
@project = project @project = project
@skip_authorization = skip_authorization @skip_authorization = skip_authorization
@skip_lfs_integrity_check = skip_lfs_integrity_check
@protocol = protocol @protocol = protocol
end end
...@@ -37,7 +38,7 @@ module Gitlab ...@@ -37,7 +38,7 @@ module Gitlab
push_checks push_checks
branch_checks branch_checks
tag_checks tag_checks
lfs_objects_exist_check lfs_objects_exist_check unless skip_lfs_integrity_check
commits_check unless skip_commits_check commits_check unless skip_commits_check
true true
......
...@@ -238,19 +238,22 @@ module Gitlab ...@@ -238,19 +238,22 @@ module Gitlab
changes_list = Gitlab::ChangesList.new(changes) changes_list = Gitlab::ChangesList.new(changes)
# Iterate over all changes to find if user allowed all of them to be applied # Iterate over all changes to find if user allowed all of them to be applied
changes_list.each do |change| changes_list.each.with_index do |change, index|
first_change = index == 0
# If user does not have access to make at least one change, cancel all # If user does not have access to make at least one change, cancel all
# push by allowing the exception to bubble up # push by allowing the exception to bubble up
check_single_change_access(change) check_single_change_access(change, skip_lfs_integrity_check: !first_change)
end end
end end
def check_single_change_access(change) def check_single_change_access(change, skip_lfs_integrity_check: false)
Checks::ChangeAccess.new( Checks::ChangeAccess.new(
change, change,
user_access: user_access, user_access: user_access,
project: project, project: project,
skip_authorization: deploy_key?, skip_authorization: deploy_key?,
skip_lfs_integrity_check: skip_lfs_integrity_check,
protocol: protocol protocol: protocol
).exec ).exec
end end
......
...@@ -13,7 +13,7 @@ module Gitlab ...@@ -13,7 +13,7 @@ module Gitlab
authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_wiki_code) authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_wiki_code)
end end
def check_single_change_access(change) def check_single_change_access(change, _options = {})
unless user_access.can_do_action?(:create_wiki) unless user_access.can_do_action?(:create_wiki)
raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki] raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
end end
......
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
end end
def self.servers def self.servers
Gitlab.config.ldap.servers.values Gitlab.config.ldap['servers']&.values || []
end end
def self.available_servers def self.available_servers
......
...@@ -198,9 +198,11 @@ module Gitlab ...@@ -198,9 +198,11 @@ module Gitlab
end end
def update_profile def update_profile
clear_user_synced_attributes_metadata
return unless sync_profile_from_provider? || creating_linked_ldap_user? return unless sync_profile_from_provider? || creating_linked_ldap_user?
metadata = gl_user.user_synced_attributes_metadata || gl_user.build_user_synced_attributes_metadata metadata = gl_user.build_user_synced_attributes_metadata
if sync_profile_from_provider? if sync_profile_from_provider?
UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key| UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key|
...@@ -221,6 +223,10 @@ module Gitlab ...@@ -221,6 +223,10 @@ module Gitlab
end end
end end
def clear_user_synced_attributes_metadata
gl_user&.user_synced_attributes_metadata&.destroy
end
def log def log
Gitlab::AppLogger Gitlab::AppLogger
end end
......
...@@ -45,6 +45,7 @@ module Gitlab ...@@ -45,6 +45,7 @@ module Gitlab
if user if user
private_token ||= user.personal_access_tokens.active.pluck(:token).first private_token ||= user.personal_access_tokens.active.pluck(:token).first
raise 'Your user must have a personal_access_token' unless private_token
end end
headers['Private-Token'] = private_token if private_token headers['Private-Token'] = private_token if private_token
......
...@@ -16,5 +16,54 @@ unless Rails.env.production? ...@@ -16,5 +16,54 @@ unless Rails.env.production?
task :javascript do task :javascript do
Rake::Task['eslint'].invoke Rake::Task['eslint'].invoke
end end
desc "GitLab | lint | Run several lint checks"
task :all do
status = 0
%w[
config_lint
haml_lint
scss_lint
flay
gettext:lint
lint:static_verification
].each do |task|
pid = Process.fork do
rd, wr = IO.pipe
stdout = $stdout.dup
stderr = $stderr.dup
$stdout.reopen(wr)
$stderr.reopen(wr)
begin
begin
Rake::Task[task].invoke
rescue RuntimeError # The haml_lint tasks raise a RuntimeError
exit(1)
end
rescue SystemExit => ex
msg = "*** Rake task #{task} failed with the following error(s):"
raise ex
ensure
$stdout.reopen(stdout)
$stderr.reopen(stderr)
wr.close
if msg
warn "\n#{msg}\n\n"
IO.copy_stream(rd, $stderr)
else
IO.copy_stream(rd, $stdout)
end
end
end
Process.waitpid(pid)
status += $?.exitstatus
end
exit(status)
end
end end
end end
...@@ -45,6 +45,10 @@ module QA ...@@ -45,6 +45,10 @@ module QA
end end
def new_merge_request def new_merge_request
wait(reload: true) do
has_css?(element_selector_css(:create_merge_request))
end
click_element :create_merge_request click_element :create_merge_request
end end
......
...@@ -7,7 +7,7 @@ require_relative '../lib/gitlab/popen/runner' ...@@ -7,7 +7,7 @@ require_relative '../lib/gitlab/popen/runner'
def emit_warnings(static_analysis) def emit_warnings(static_analysis)
static_analysis.warned_results.each do |result| static_analysis.warned_results.each do |result|
puts puts
puts "**** #{result.cmd.join(' ')} had the following warnings:" puts "**** #{result.cmd.join(' ')} had the following warning(s):"
puts puts
puts result.stderr puts result.stderr
puts puts
...@@ -17,7 +17,7 @@ end ...@@ -17,7 +17,7 @@ end
def emit_errors(static_analysis) def emit_errors(static_analysis)
static_analysis.failed_results.each do |result| static_analysis.failed_results.each do |result|
puts puts
puts "**** #{result.cmd.join(' ')} failed with the following error:" puts "**** #{result.cmd.join(' ')} failed with the following error(s):"
puts puts
puts result.stdout puts result.stdout
puts result.stderr puts result.stderr
...@@ -26,15 +26,10 @@ def emit_errors(static_analysis) ...@@ -26,15 +26,10 @@ def emit_errors(static_analysis)
end end
tasks = [ tasks = [
%w[bundle exec rake config_lint], %w[bin/rake lint:all],
%w[bundle exec rake flay],
%w[bundle exec rake haml_lint],
%w[bundle exec rake scss_lint],
%w[bundle exec license_finder], %w[bundle exec license_finder],
%w[yarn run eslint], %w[yarn run eslint],
%w[bundle exec rubocop --parallel], %w[bundle exec rubocop --parallel],
%w[bundle exec rake gettext:lint],
%w[bundle exec rake lint:static_verification],
%w[scripts/lint-conflicts.sh], %w[scripts/lint-conflicts.sh],
%w[scripts/lint-rugged] %w[scripts/lint-rugged]
] ]
......
require 'spec_helper' require 'spec_helper'
describe 'Profile > Password' do describe 'Profile > Password' do
context 'Password authentication enabled' do let(:user) { create(:user) }
let(:user) { create(:user, password_automatically_set: true) }
before do
sign_in(user)
visit edit_profile_password_path
end
def fill_passwords(password, confirmation) def fill_passwords(password, confirmation)
fill_in 'New password', with: password fill_in 'New password', with: password
...@@ -16,6 +10,14 @@ describe 'Profile > Password' do ...@@ -16,6 +10,14 @@ describe 'Profile > Password' do
click_button 'Save password' click_button 'Save password'
end end
context 'Password authentication enabled' do
let(:user) { create(:user, password_automatically_set: true) }
before do
sign_in(user)
visit edit_profile_password_path
end
context 'User with password automatically set' do context 'User with password automatically set' do
describe 'User puts different passwords in the field and in the confirmation' do describe 'User puts different passwords in the field and in the confirmation' do
it 'shows an error message' do it 'shows an error message' do
...@@ -73,4 +75,64 @@ describe 'Profile > Password' do ...@@ -73,4 +75,64 @@ describe 'Profile > Password' do
end end
end end
end end
context 'Change passowrd' do
before do
sign_in(user)
visit(edit_profile_password_path)
end
it 'does not change user passowrd without old one' do
page.within '.update-password' do
fill_passwords('22233344', '22233344')
end
page.within '.flash-container' do
expect(page).to have_content 'You must provide a valid current password'
end
end
it 'does not change password with invalid old password' do
page.within '.update-password' do
fill_in 'user_current_password', with: 'invalid'
fill_passwords('password', 'confirmation')
end
page.within '.flash-container' do
expect(page).to have_content 'You must provide a valid current password'
end
end
it 'changes user password' do
page.within '.update-password' do
fill_in "user_current_password", with: user.password
fill_passwords('22233344', '22233344')
end
expect(current_path).to eq new_user_session_path
end
end
context 'when password is expired' do
before do
sign_in(user)
user.update_attributes(password_expires_at: 1.hour.ago)
user.identities.delete
expect(user.ldap_user?).to eq false
end
it 'needs change user password' do
visit edit_profile_password_path
expect(current_path).to eq new_profile_password_path
fill_in :user_current_password, with: user.password
fill_in :user_password, with: '12345678'
fill_in :user_password_confirmation, with: '12345678'
click_button 'Set new password'
expect(current_path).to eq new_user_session_path
end
end
end end
require 'spec_helper'
describe 'User edit profile' do
let(:user) { create(:user) }
before do
sign_in(user)
visit(profile_path)
end
it 'changes user profile' do
fill_in 'user_skype', with: 'testskype'
fill_in 'user_linkedin', with: 'testlinkedin'
fill_in 'user_twitter', with: 'testtwitter'
fill_in 'user_website_url', with: 'testurl'
fill_in 'user_location', with: 'Ukraine'
fill_in 'user_bio', with: 'I <3 GitLab'
fill_in 'user_organization', with: 'GitLab'
click_button 'Update profile settings'
expect(user.reload).to have_attributes(
skype: 'testskype',
linkedin: 'testlinkedin',
twitter: 'testtwitter',
website_url: 'testurl',
bio: 'I <3 GitLab',
organization: 'GitLab'
)
expect(find('#user_location').value).to eq 'Ukraine'
expect(page).to have_content('Profile was successfully updated')
end
context 'user avatar' do
before do
attach_file(:user_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif'))
click_button 'Update profile settings'
end
it 'changes user avatar' do
expect(page).to have_link('Remove avatar')
user.reload
expect(user.avatar).to be_instance_of AvatarUploader
expect(user.avatar.url).to eq "/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif"
end
it 'removes user avatar' do
click_link 'Remove avatar'
user.reload
expect(user.avatar?).to eq false
expect(page).not_to have_link('Remove avatar')
expect(page).to have_link('gravatar.com')
end
end
end
require 'spec_helper'
describe 'User manages applications' do
let(:user) { create(:user) }
before do
sign_in(user)
visit applications_profile_path
end
it 'manages applications' do
expect(page).to have_content 'Add new application'
fill_in :doorkeeper_application_name, with: 'test'
fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
click_on 'Save application'
expect(page).to have_content 'Application: test'
expect(page).to have_content 'Application Id'
expect(page).to have_content 'Secret'
click_on 'Edit'
expect(page).to have_content 'Edit application'
fill_in :doorkeeper_application_name, with: 'test_changed'
click_on 'Save application'
expect(page).to have_content 'test_changed'
expect(page).to have_content 'Application Id'
expect(page).to have_content 'Secret'
visit applications_profile_path
page.within '.oauth-applications' do
click_on 'Destroy'
end
expect(page.find('.oauth-applications')).not_to have_content 'test_changed'
end
end
...@@ -3,13 +3,28 @@ require 'spec_helper' ...@@ -3,13 +3,28 @@ require 'spec_helper'
describe 'User visits the authentication log' do describe 'User visits the authentication log' do
let(:user) { create(:user) } let(:user) { create(:user) }
context 'when user signed in' do
before do before do
sign_in(user) sign_in(user)
visit(audit_log_profile_path)
end end
it 'shows correct menu item' do it 'shows correct menu item' do
visit(audit_log_profile_path)
expect(page).to have_active_navigation('Authentication log') expect(page).to have_active_navigation('Authentication log')
end end
end
context 'when user has activity' do
before do
create(:closed_issue_event, author: user)
gitlab_sign_in(user)
end
it 'shows user activity' do
visit(audit_log_profile_path)
expect(page).to have_content 'Signed in with standard authentication'
end
end
end end
...@@ -5,20 +5,58 @@ describe 'User visits their profile' do ...@@ -5,20 +5,58 @@ describe 'User visits their profile' do
before do before do
sign_in(user) sign_in(user)
visit(profile_path)
end end
it 'shows correct menu item' do it 'shows correct menu item' do
visit(profile_path)
expect(page).to have_active_navigation('Profile') expect(page).to have_active_navigation('Profile')
end end
describe 'profile settings', :js do it 'shows profile info' do
it 'saves updates' do visit(profile_path)
fill_in 'user_bio', with: 'bio'
click_button 'Update profile settings' expect(page).to have_content "This information will appear on your profile"
end
context 'when user has groups' do
let(:group) do
create :group do |group|
group.add_owner(user)
end
end
let!(:project) do
create(:project, :repository, namespace: group) do |project|
create(:closed_issue_event, project: project)
project.add_master(user)
end
end
def click_on_profile_picture
find(:css, '.header-user-dropdown-toggle').click
page.within ".header-user" do
click_link "Profile"
end
end
it 'shows user groups', :js do
visit(profile_path)
click_on_profile_picture
page.within ".cover-block" do
expect(page).to have_content user.name
expect(page).to have_content user.username
end
expect(page).to have_content('Profile was successfully updated') page.within ".content" do
click_link "Groups"
end
page.within "#groups" do
expect(page).to have_content group.name
end
end end
end end
end end
...@@ -20,5 +20,9 @@ describe EventsHelper do ...@@ -20,5 +20,9 @@ describe EventsHelper do
it 'handles nil values' do it 'handles nil values' do
expect(helper.event_commit_title(nil)).to eq('') expect(helper.event_commit_title(nil)).to eq('')
end end
it 'does not escape HTML entities' do
expect(helper.event_commit_title("foo & bar")).to eq("foo & bar")
end
end end
end end
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import AjaxFormVariableList from '~/ci_variable_list/ajax_variable_list'; import AjaxFormVariableList from '~/ci_variable_list/ajax_variable_list';
const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/variables'; const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/variables';
const HIDE_CLASS = 'hide';
describe('AjaxFormVariableList', () => { describe('AjaxFormVariableList', () => {
preloadFixtures('projects/ci_cd_settings.html.raw'); preloadFixtures('projects/ci_cd_settings.html.raw');
...@@ -45,16 +47,16 @@ describe('AjaxFormVariableList', () => { ...@@ -45,16 +47,16 @@ describe('AjaxFormVariableList', () => {
const loadingIcon = saveButton.querySelector('.js-secret-variables-save-loading-icon'); const loadingIcon = saveButton.querySelector('.js-secret-variables-save-loading-icon');
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => { mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => {
expect(loadingIcon.classList.contains('hide')).toEqual(false); expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(false);
return [200, {}]; return [200, {}];
}); });
expect(loadingIcon.classList.contains('hide')).toEqual(true); expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true);
ajaxVariableList.onSaveClicked() ajaxVariableList.onSaveClicked()
.then(() => { .then(() => {
expect(loadingIcon.classList.contains('hide')).toEqual(true); expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true);
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
...@@ -78,11 +80,11 @@ describe('AjaxFormVariableList', () => { ...@@ -78,11 +80,11 @@ describe('AjaxFormVariableList', () => {
it('hides any previous error box', (done) => { it('hides any previous error box', (done) => {
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200); mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200);
expect(errorBox.classList.contains('hide')).toEqual(true); expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
ajaxVariableList.onSaveClicked() ajaxVariableList.onSaveClicked()
.then(() => { .then(() => {
expect(errorBox.classList.contains('hide')).toEqual(true); expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
...@@ -103,17 +105,39 @@ describe('AjaxFormVariableList', () => { ...@@ -103,17 +105,39 @@ describe('AjaxFormVariableList', () => {
.catch(done.fail); .catch(done.fail);
}); });
it('hides secret values', (done) => {
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, {});
const row = container.querySelector('.js-row:first-child');
const valueInput = row.querySelector('.js-ci-variable-input-value');
const valuePlaceholder = row.querySelector('.js-secret-value-placeholder');
valueInput.value = 'bar';
$(valueInput).trigger('input');
expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(true);
expect(valueInput.classList.contains(HIDE_CLASS)).toBe(false);
ajaxVariableList.onSaveClicked()
.then(() => {
expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(false);
expect(valueInput.classList.contains(HIDE_CLASS)).toBe(true);
})
.then(done)
.catch(done.fail);
});
it('shows error box with validation errors', (done) => { it('shows error box with validation errors', (done) => {
const validationError = 'some validation error'; const validationError = 'some validation error';
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(400, [ mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(400, [
validationError, validationError,
]); ]);
expect(errorBox.classList.contains('hide')).toEqual(true); expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
ajaxVariableList.onSaveClicked() ajaxVariableList.onSaveClicked()
.then(() => { .then(() => {
expect(errorBox.classList.contains('hide')).toEqual(false); expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(false);
expect(errorBox.textContent.trim().replace(/\n+\s+/m, ' ')).toEqual(`Validation failed ${validationError}`); expect(errorBox.textContent.trim().replace(/\n+\s+/m, ' ')).toEqual(`Validation failed ${validationError}`);
}) })
.then(done) .then(done)
...@@ -123,11 +147,11 @@ describe('AjaxFormVariableList', () => { ...@@ -123,11 +147,11 @@ describe('AjaxFormVariableList', () => {
it('shows flash message when request fails', (done) => { it('shows flash message when request fails', (done) => {
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(500); mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(500);
expect(errorBox.classList.contains('hide')).toEqual(true); expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
ajaxVariableList.onSaveClicked() ajaxVariableList.onSaveClicked()
.then(() => { .then(() => {
expect(errorBox.classList.contains('hide')).toEqual(true); expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
...@@ -170,9 +194,9 @@ describe('AjaxFormVariableList', () => { ...@@ -170,9 +194,9 @@ describe('AjaxFormVariableList', () => {
const valueInput = row.querySelector('.js-ci-variable-input-value'); const valueInput = row.querySelector('.js-ci-variable-input-value');
keyInput.value = 'foo'; keyInput.value = 'foo';
keyInput.dispatchEvent(new Event('input')); $(keyInput).trigger('input');
valueInput.value = 'bar'; valueInput.value = 'bar';
valueInput.dispatchEvent(new Event('input')); $(valueInput).trigger('input');
expect(idInput.value).toEqual(''); expect(idInput.value).toEqual('');
......
import VariableList from '~/ci_variable_list/ci_variable_list'; import VariableList from '~/ci_variable_list/ci_variable_list';
import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper'; import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
const HIDE_CLASS = 'hide';
describe('VariableList', () => { describe('VariableList', () => {
preloadFixtures('pipeline_schedules/edit.html.raw'); preloadFixtures('pipeline_schedules/edit.html.raw');
preloadFixtures('pipeline_schedules/edit_with_variables.html.raw'); preloadFixtures('pipeline_schedules/edit_with_variables.html.raw');
...@@ -92,14 +94,14 @@ describe('VariableList', () => { ...@@ -92,14 +94,14 @@ describe('VariableList', () => {
const $inputValue = $row.find('.js-ci-variable-input-value'); const $inputValue = $row.find('.js-ci-variable-input-value');
const $placeholder = $row.find('.js-secret-value-placeholder'); const $placeholder = $row.find('.js-secret-value-placeholder');
expect($placeholder.hasClass('hide')).toBe(false); expect($placeholder.hasClass(HIDE_CLASS)).toBe(false);
expect($inputValue.hasClass('hide')).toBe(true); expect($inputValue.hasClass(HIDE_CLASS)).toBe(true);
// Reveal values // Reveal values
$wrapper.find('.js-secret-value-reveal-button').click(); $wrapper.find('.js-secret-value-reveal-button').click();
expect($placeholder.hasClass('hide')).toBe(true); expect($placeholder.hasClass(HIDE_CLASS)).toBe(true);
expect($inputValue.hasClass('hide')).toBe(false); expect($inputValue.hasClass(HIDE_CLASS)).toBe(false);
}); });
}); });
}); });
...@@ -179,4 +181,35 @@ describe('VariableList', () => { ...@@ -179,4 +181,35 @@ describe('VariableList', () => {
expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3); expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
}); });
}); });
describe('hideValues', () => {
beforeEach(() => {
loadFixtures('projects/ci_cd_settings.html.raw');
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
container: $wrapper,
formField: 'variables',
});
variableList.init();
});
it('should hide value input and show placeholder stars', () => {
const $row = $wrapper.find('.js-row');
const $inputValue = $row.find('.js-ci-variable-input-value');
const $placeholder = $row.find('.js-secret-value-placeholder');
$row.find('.js-ci-variable-input-value')
.val('foo')
.trigger('input');
expect($placeholder.hasClass(HIDE_CLASS)).toBe(true);
expect($inputValue.hasClass(HIDE_CLASS)).toBe(false);
variableList.hideValues();
expect($placeholder.hasClass(HIDE_CLASS)).toBe(false);
expect($inputValue.hasClass(HIDE_CLASS)).toBe(true);
});
});
}); });
...@@ -4,6 +4,8 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -4,6 +4,8 @@ import axios from '~/lib/utils/axios_utils';
import CommitsList from '~/commits'; import CommitsList from '~/commits';
describe('Commits List', () => { describe('Commits List', () => {
let commitsList;
beforeEach(() => { beforeEach(() => {
setFixtures(` setFixtures(`
<form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master"> <form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master">
...@@ -11,6 +13,7 @@ describe('Commits List', () => { ...@@ -11,6 +13,7 @@ describe('Commits List', () => {
</form> </form>
<ol id="commits-list"></ol> <ol id="commits-list"></ol>
`); `);
commitsList = new CommitsList(25);
}); });
it('should be defined', () => { it('should be defined', () => {
...@@ -19,7 +22,7 @@ describe('Commits List', () => { ...@@ -19,7 +22,7 @@ describe('Commits List', () => {
describe('processCommits', () => { describe('processCommits', () => {
it('should join commit headers', () => { it('should join commit headers', () => {
CommitsList.$contentList = $(` commitsList.$contentList = $(`
<div> <div>
<li class="commit-header" data-day="2016-09-20"> <li class="commit-header" data-day="2016-09-20">
<span class="day">20 Sep, 2016</span> <span class="day">20 Sep, 2016</span>
...@@ -39,7 +42,7 @@ describe('Commits List', () => { ...@@ -39,7 +42,7 @@ describe('Commits List', () => {
// The last commit header should be removed // The last commit header should be removed
// since the previous one has the same data-day value. // since the previous one has the same data-day value.
expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0); expect(commitsList.processCommits(data).find('li.commit-header').length).toBe(0);
}); });
}); });
...@@ -48,8 +51,7 @@ describe('Commits List', () => { ...@@ -48,8 +51,7 @@ describe('Commits List', () => {
let mock; let mock;
beforeEach(() => { beforeEach(() => {
CommitsList.init(25); commitsList.searchField.val('');
CommitsList.searchField.val('');
spyOn(history, 'replaceState').and.stub(); spyOn(history, 'replaceState').and.stub();
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
...@@ -66,11 +68,11 @@ describe('Commits List', () => { ...@@ -66,11 +68,11 @@ describe('Commits List', () => {
}); });
it('should save the last search string', (done) => { it('should save the last search string', (done) => {
CommitsList.searchField.val('GitLab'); commitsList.searchField.val('GitLab');
CommitsList.filterResults() commitsList.filterResults()
.then(() => { .then(() => {
expect(ajaxSpy).toHaveBeenCalled(); expect(ajaxSpy).toHaveBeenCalled();
expect(CommitsList.lastSearch).toEqual('GitLab'); expect(commitsList.lastSearch).toEqual('GitLab');
done(); done();
}) })
...@@ -78,10 +80,10 @@ describe('Commits List', () => { ...@@ -78,10 +80,10 @@ describe('Commits List', () => {
}); });
it('should not make ajax call if the input does not change', (done) => { it('should not make ajax call if the input does not change', (done) => {
CommitsList.filterResults() commitsList.filterResults()
.then(() => { .then(() => {
expect(ajaxSpy).not.toHaveBeenCalled(); expect(ajaxSpy).not.toHaveBeenCalled();
expect(CommitsList.lastSearch).toEqual(''); expect(commitsList.lastSearch).toEqual('');
done(); done();
}) })
......
...@@ -3,9 +3,18 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -3,9 +3,18 @@ import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
describe('Importer Status', () => { describe('Importer Status', () => {
describe('addToImport', () => {
let instance; let instance;
let mock; let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('addToImport', () => {
const importUrl = '/import_url'; const importUrl = '/import_url';
beforeEach(() => { beforeEach(() => {
...@@ -21,11 +30,6 @@ describe('Importer Status', () => { ...@@ -21,11 +30,6 @@ describe('Importer Status', () => {
spyOn(ImporterStatus.prototype, 'initStatusPage').and.callFake(() => {}); spyOn(ImporterStatus.prototype, 'initStatusPage').and.callFake(() => {});
spyOn(ImporterStatus.prototype, 'setAutoUpdate').and.callFake(() => {}); spyOn(ImporterStatus.prototype, 'setAutoUpdate').and.callFake(() => {});
instance = new ImporterStatus('', importUrl); instance = new ImporterStatus('', importUrl);
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
}); });
it('sets table row to active after post request', (done) => { it('sets table row to active after post request', (done) => {
...@@ -44,4 +48,60 @@ describe('Importer Status', () => { ...@@ -44,4 +48,60 @@ describe('Importer Status', () => {
.catch(done.fail); .catch(done.fail);
}); });
}); });
describe('autoUpdate', () => {
const jobsUrl = '/jobs_url';
beforeEach(() => {
const div = document.createElement('div');
div.innerHTML = `
<div id="project_1">
<div class="job-status">
</div>
</div>
`;
document.body.appendChild(div);
spyOn(ImporterStatus.prototype, 'initStatusPage').and.callFake(() => {});
spyOn(ImporterStatus.prototype, 'setAutoUpdate').and.callFake(() => {});
instance = new ImporterStatus(jobsUrl);
});
function setupMock(importStatus) {
mock.onGet(jobsUrl).reply(200, [{
id: 1,
import_status: importStatus,
}]);
}
function expectJobStatus(done, status) {
instance.autoUpdate()
.then(() => {
expect(document.querySelector('#project_1').innerText.trim()).toEqual(status);
done();
})
.catch(done.fail);
}
it('sets the job status to done', (done) => {
setupMock('finished');
expectJobStatus(done, 'done');
});
it('sets the job status to scheduled', (done) => {
setupMock('scheduled');
expectJobStatus(done, 'scheduled');
});
it('sets the job status to started', (done) => {
setupMock('started');
expectJobStatus(done, 'started');
});
it('sets the job status to custom status', (done) => {
setupMock('custom status');
expectJobStatus(done, 'custom status');
});
});
}); });
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import IntervalPatternInput from '~/pipeline_schedules/components/interval_pattern_input.vue'; import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
Vue.use(Translate); Vue.use(Translate);
......
import Vue from 'vue'; import Vue from 'vue';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import PipelineSchedulesCallout from '~/pipeline_schedules/components/pipeline_schedules_callout.vue'; import PipelineSchedulesCallout from '~/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue';
const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout); const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
const cookieKey = 'pipeline_schedules_callout_dismissed'; const cookieKey = 'pipeline_schedules_callout_dismissed';
......
import Vue from 'vue';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
const modalComponent = Vue.extend(GlModal);
describe('GlModal', () => {
let vm;
afterEach(() => {
vm.$destroy();
});
describe('props', () => {
describe('with id', () => {
const props = {
id: 'my-modal',
};
beforeEach(() => {
vm = mountComponent(modalComponent, props);
});
it('assigns the id to the modal', () => {
expect(vm.$el.id).toBe(props.id);
});
});
describe('without id', () => {
beforeEach(() => {
vm = mountComponent(modalComponent, { });
});
it('does not add an id attribute to the modal', () => {
expect(vm.$el.hasAttribute('id')).toBe(false);
});
});
describe('with headerTitleText', () => {
const props = {
headerTitleText: 'my title text',
};
beforeEach(() => {
vm = mountComponent(modalComponent, props);
});
it('sets the modal title', () => {
const modalTitle = vm.$el.querySelector('.modal-title');
expect(modalTitle.innerHTML.trim()).toBe(props.headerTitleText);
});
});
describe('with footerPrimaryButtonVariant', () => {
const props = {
footerPrimaryButtonVariant: 'danger',
};
beforeEach(() => {
vm = mountComponent(modalComponent, props);
});
it('sets the primary button class', () => {
const primaryButton = vm.$el.querySelector('.modal-footer button:last-of-type');
expect(primaryButton).toHaveClass(`btn-${props.footerPrimaryButtonVariant}`);
});
});
describe('with footerPrimaryButtonText', () => {
const props = {
footerPrimaryButtonText: 'my button text',
};
beforeEach(() => {
vm = mountComponent(modalComponent, props);
});
it('sets the primary button text', () => {
const primaryButton = vm.$el.querySelector('.modal-footer button:last-of-type');
expect(primaryButton.innerHTML.trim()).toBe(props.footerPrimaryButtonText);
});
});
});
it('works with data-toggle="modal"', (done) => {
setFixtures(`
<button id="modal-button" data-toggle="modal" data-target="#my-modal"></button>
<div id="modal-container"></div>
`);
const modalContainer = document.getElementById('modal-container');
const modalButton = document.getElementById('modal-button');
vm = mountComponent(modalComponent, {
id: 'my-modal',
}, modalContainer);
$(vm.$el).on('shown.bs.modal', () => done());
modalButton.click();
});
describe('methods', () => {
const dummyEvent = 'not really an event';
beforeEach(() => {
vm = mountComponent(modalComponent, { });
spyOn(vm, '$emit');
});
describe('emitCancel', () => {
it('emits a cancel event', () => {
vm.emitCancel(dummyEvent);
expect(vm.$emit).toHaveBeenCalledWith('cancel', dummyEvent);
});
});
describe('emitSubmit', () => {
it('emits a submit event', () => {
vm.emitSubmit(dummyEvent);
expect(vm.$emit).toHaveBeenCalledWith('submit', dummyEvent);
});
});
});
describe('slots', () => {
const slotContent = 'this should go into the slot';
const modalWithSlot = (slotName) => {
let template;
if (slotName) {
template = `
<gl-modal>
<template slot="${slotName}">${slotContent}</template>
</gl-modal>
`;
} else {
template = `<gl-modal>${slotContent}</gl-modal>`;
}
return Vue.extend({
components: {
GlModal,
},
template,
});
};
describe('default slot', () => {
beforeEach(() => {
vm = mountComponent(modalWithSlot());
});
it('sets the modal body', () => {
const modalBody = vm.$el.querySelector('.modal-body');
expect(modalBody.innerHTML).toBe(slotContent);
});
});
describe('header slot', () => {
beforeEach(() => {
vm = mountComponent(modalWithSlot('header'));
});
it('sets the modal header', () => {
const modalHeader = vm.$el.querySelector('.modal-header');
expect(modalHeader.innerHTML).toBe(slotContent);
});
});
describe('title slot', () => {
beforeEach(() => {
vm = mountComponent(modalWithSlot('title'));
});
it('sets the modal title', () => {
const modalTitle = vm.$el.querySelector('.modal-title');
expect(modalTitle.innerHTML).toBe(slotContent);
});
});
describe('footer slot', () => {
beforeEach(() => {
vm = mountComponent(modalWithSlot('footer'));
});
it('sets the modal footer', () => {
const modalFooter = vm.$el.querySelector('.modal-footer');
expect(modalFooter.innerHTML).toBe(slotContent);
});
});
});
});
...@@ -3,17 +3,12 @@ require 'spec_helper' ...@@ -3,17 +3,12 @@ require 'spec_helper'
describe Banzai::Filter::HtmlEntityFilter do describe Banzai::Filter::HtmlEntityFilter do
include FilterSpecHelper include FilterSpecHelper
let(:unescaped) { 'foo <strike attr="foo">&&&</strike>' } let(:unescaped) { 'foo <strike attr="foo">&&amp;&</strike>' }
let(:escaped) { 'foo &lt;strike attr=&quot;foo&quot;&gt;&amp;&amp;&amp;&lt;/strike&gt;' } let(:escaped) { 'foo &lt;strike attr=&quot;foo&quot;&gt;&amp;&amp;amp;&amp;&lt;/strike&gt;' }
it 'converts common entities to their HTML-escaped equivalents' do it 'converts common entities to their HTML-escaped equivalents' do
output = filter(unescaped) output = filter(unescaped)
expect(output).to eq(escaped) expect(output).to eq(escaped)
end end
it 'does not double-escape' do
escaped = ERB::Util.html_escape("Merge branch 'blabla' into 'master'")
expect(filter(escaped)).to eq(escaped)
end
end end
...@@ -95,6 +95,14 @@ module Gitlab ...@@ -95,6 +95,14 @@ module Gitlab
expect(render(input, context)).to include('<p><code data-math-style="inline" class="code math js-render-math">2+2</code> is 4</p>') expect(render(input, context)).to include('<p><code data-math-style="inline" class="code math js-render-math">2+2</code> is 4</p>')
end end
end end
context 'outfilesuffix' do
it 'defaults to adoc' do
output = render("Inter-document reference <<README.adoc#>>", context)
expect(output).to include("a href=\"README.adoc\"")
end
end
end end
def render(*args) def render(*args)
......
...@@ -18,8 +18,9 @@ describe Gitlab::GitAccess do ...@@ -18,8 +18,9 @@ describe Gitlab::GitAccess do
redirected_path: redirected_path) redirected_path: redirected_path)
end end
let(:push_access_check) { access.check('git-receive-pack', '_any') } let(:changes) { '_any' }
let(:pull_access_check) { access.check('git-upload-pack', '_any') } let(:push_access_check) { access.check('git-receive-pack', changes) }
let(:pull_access_check) { access.check('git-upload-pack', changes) }
describe '#check with single protocols allowed' do describe '#check with single protocols allowed' do
def disable_protocol(protocol) def disable_protocol(protocol)
...@@ -646,6 +647,20 @@ describe Gitlab::GitAccess do ...@@ -646,6 +647,20 @@ describe Gitlab::GitAccess do
end end
end end
describe 'check LFS integrity' do
let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master', '6f6d7e7ed 570e7b2ab refs/heads/feature'] }
before do
project.add_developer(user)
end
it 'checks LFS integrity only for first change' do
expect_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).exactly(1).times
push_access_check
end
end
describe '#check_push_access!' do describe '#check_push_access!' do
before do before do
merge_into_protected_branch merge_into_protected_branch
......
...@@ -5,6 +5,14 @@ describe Gitlab::LDAP::Config do ...@@ -5,6 +5,14 @@ describe Gitlab::LDAP::Config do
let(:config) { described_class.new('ldapmain') } let(:config) { described_class.new('ldapmain') }
describe '.servers' do
it 'returns empty array if no server information is available' do
allow(Gitlab.config).to receive(:ldap).and_return('enabled' => false)
expect(described_class.servers).to eq []
end
end
describe '#initialize' do describe '#initialize' do
it 'requires a provider' do it 'requires a provider' do
expect { described_class.new }.to raise_error ArgumentError expect { described_class.new }.to raise_error ArgumentError
......
...@@ -724,6 +724,10 @@ describe Gitlab::OAuth::User do ...@@ -724,6 +724,10 @@ describe Gitlab::OAuth::User do
it "does not update the user location" do it "does not update the user location" do
expect(gl_user.location).not_to eq(info_hash[:address][:country]) expect(gl_user.location).not_to eq(info_hash[:address][:country])
end end
it 'does not create associated user synced attributes metadata' do
expect(gl_user.user_synced_attributes_metadata).to be_nil
end
end end
end end
......
...@@ -53,6 +53,15 @@ describe Gitlab::Profiler do ...@@ -53,6 +53,15 @@ describe Gitlab::Profiler do
described_class.profile('/', user: user) described_class.profile('/', user: user)
end end
context 'when providing a user without a personal access token' do
it 'raises an error' do
user = double(:user)
allow(user).to receive_message_chain(:personal_access_tokens, :active, :pluck).and_return([])
expect { described_class.profile('/', user: user) }.to raise_error('Your user must have a personal_access_token')
end
end
it 'uses the private_token for auth if both it and user are set' do it 'uses the private_token for auth if both it and user are set' do
user = double(:user) user = double(:user)
user_token = 'user' user_token = 'user'
......
...@@ -70,5 +70,38 @@ describe Identity do ...@@ -70,5 +70,38 @@ describe Identity do
end end
end end
end end
context 'after_destroy' do
let!(:user) { create(:user) }
let(:ldap_identity) { create(:identity, provider: 'ldapmain', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', user: user) }
let(:ldap_user_synced_attributes) { { provider: 'ldapmain', name_synced: true, email_synced: true } }
let(:other_provider_user_synced_attributes) { { provider: 'other', name_synced: true, email_synced: true } }
describe 'if user synced attributes metadada provider' do
context 'matches the identity provider ' do
it 'removes the user synced attributes' do
user.create_user_synced_attributes_metadata(ldap_user_synced_attributes)
expect(user.user_synced_attributes_metadata.provider).to eq 'ldapmain'
ldap_identity.destroy
expect(user.reload.user_synced_attributes_metadata).to be_nil
end
end
context 'does not matche the identity provider' do
it 'does not remove the user synced attributes' do
user.create_user_synced_attributes_metadata(other_provider_user_synced_attributes)
expect(user.user_synced_attributes_metadata.provider).to eq 'other'
ldap_identity.destroy
expect(user.reload.user_synced_attributes_metadata.provider).to eq 'other'
end
end
end
end
end end
end end
...@@ -873,6 +873,18 @@ describe Repository do ...@@ -873,6 +873,18 @@ describe Repository do
expect(repository.license_key).to be_nil expect(repository.license_key).to be_nil
end end
it 'returns nil when the commit SHA does not exist' do
allow(repository.head_commit).to receive(:sha).and_return('1' * 40)
expect(repository.license_key).to be_nil
end
it 'returns nil when master does not exist' do
repository.rm_branch(user, 'master')
expect(repository.license_key).to be_nil
end
it 'returns the license key' do it 'returns the license key' do
repository.create_file(user, 'LICENSE', repository.create_file(user, 'LICENSE',
Licensee::License.new('mit').content, Licensee::License.new('mit').content,
......
...@@ -893,6 +893,14 @@ describe User do ...@@ -893,6 +893,14 @@ describe User do
end end
end end
describe '.find_for_database_authentication' do
it 'strips whitespace from login' do
user = create(:user)
expect(described_class.find_for_database_authentication({ login: " #{user.username} " })).to eq user
end
end
describe '.find_by_any_email' do describe '.find_by_any_email' do
it 'finds by primary email' do it 'finds by primary email' do
user = create(:user, email: 'foo@example.com') user = create(:user, email: 'foo@example.com')
......
require 'spec_helper'
describe API::ProjectImport do
let(:export_path) { "#{Dir.tmpdir}/project_export_spec" }
let(:user) { create(:user) }
let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
let(:namespace) { create(:group) }
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
namespace.add_owner(user)
end
after do
FileUtils.rm_rf(export_path, secure: true)
end
describe 'POST /projects/import' do
it 'schedules an import using a namespace' do
stub_import(namespace)
post api('/projects/import', user), path: 'test-import', file: fixture_file_upload(file), namespace: namespace.id
expect(response).to have_gitlab_http_status(201)
end
it 'schedules an import using the namespace path' do
stub_import(namespace)
post api('/projects/import', user), path: 'test-import', file: fixture_file_upload(file), namespace: namespace.full_path
expect(response).to have_gitlab_http_status(201)
end
it 'schedules an import at the user namespace level' do
stub_import(user.namespace)
post api('/projects/import', user), path: 'test-import2', file: fixture_file_upload(file)
expect(response).to have_gitlab_http_status(201)
end
it 'schedules an import at the user namespace level' do
expect_any_instance_of(Project).not_to receive(:import_schedule)
expect(::Projects::CreateService).not_to receive(:new)
post api('/projects/import', user), namespace: 'nonexistent', path: 'test-import2', file: fixture_file_upload(file)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Namespace Not Found')
end
it 'does not schedule an import if the user has no permission to the namespace' do
expect_any_instance_of(Project).not_to receive(:import_schedule)
post(api('/projects/import', create(:user)),
path: 'test-import3',
file: fixture_file_upload(file),
namespace: namespace.full_path)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Namespace Not Found')
end
it 'does not schedule an import if the user uploads no valid file' do
expect_any_instance_of(Project).not_to receive(:import_schedule)
post api('/projects/import', user), path: 'test-import3', file: './random/test'
expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('file is invalid')
end
def stub_import(namespace)
expect_any_instance_of(Project).to receive(:import_schedule)
expect(::Projects::CreateService).to receive(:new).with(user, hash_including(namespace_id: namespace.id)).and_call_original
end
end
describe 'GET /projects/:id/import' do
it 'returns the import status' do
project = create(:project, import_status: 'started')
project.add_master(user)
get api("/projects/#{project.id}/import", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to include('import_status' => 'started')
end
it 'returns the import status and the error if failed' do
project = create(:project, import_status: 'failed', import_error: 'error')
project.add_master(user)
get api("/projects/#{project.id}/import", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to include('import_status' => 'failed',
'import_error' => 'error')
end
end
end
...@@ -22,6 +22,7 @@ describe 'Rack Attack global throttles' do ...@@ -22,6 +22,7 @@ describe 'Rack Attack global throttles' do
let(:url_that_does_not_require_authentication) { '/users/sign_in' } let(:url_that_does_not_require_authentication) { '/users/sign_in' }
let(:url_that_requires_authentication) { '/dashboard/snippets' } let(:url_that_requires_authentication) { '/dashboard/snippets' }
let(:url_api_internal) { '/api/v4/internal/check' }
let(:api_partial_url) { '/todos' } let(:api_partial_url) { '/todos' }
around do |example| around do |example|
...@@ -172,6 +173,15 @@ describe 'Rack Attack global throttles' do ...@@ -172,6 +173,15 @@ describe 'Rack Attack global throttles' do
get url_that_does_not_require_authentication get url_that_does_not_require_authentication
expect(response).to have_http_status 200 expect(response).to have_http_status 200
end end
context 'when the request is to the api internal endpoints' do
it 'allows requests over the rate limit' do
(1 + requests_per_period).times do
get url_api_internal, secret_token: Gitlab::Shell.secret_token
expect(response).to have_http_status 200
end
end
end
end end
context 'when the throttle is disabled' do context 'when the throttle is disabled' do
......
module FixtureHelpers module FixtureHelpers
def fixture_file(filename) def fixture_file(filename, dir: '')
return '' if filename.blank? return '' if filename.blank?
File.read(expand_fixture_path(filename)) File.read(expand_fixture_path(filename, dir: dir))
end end
def expand_fixture_path(filename) def expand_fixture_path(filename, dir: '')
File.expand_path(Rails.root.join('spec/fixtures/', filename)) File.expand_path(Rails.root.join(dir, 'spec', 'fixtures', filename))
end end
end end
......
...@@ -321,7 +321,15 @@ production: ...@@ -321,7 +321,15 @@ production:
# Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable"
SAST_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') SAST_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
docker run --volume "$PWD:/code" \ # Deprecation notice for CONFIDENCE_LEVEL variable
if [ -z "$SAST_CONFIDENCE_LEVEL" -a "$CONFIDENCE_LEVEL" ]; then
SAST_CONFIDENCE_LEVEL="$CONFIDENCE_LEVEL"
echo "WARNING: CONFIDENCE_LEVEL is deprecated and MUST be replaced with SAST_CONFIDENCE_LEVEL"
fi
docker run --env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}" \
--env SAST_DISABLE_REMOTE_CHECKS="${SAST_DISABLE_REMOTE_CHECKS:-false}" \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/sast:$SAST_VERSION" /app/bin/run /code "registry.gitlab.com/gitlab-org/security-products/sast:$SAST_VERSION" /app/bin/run /code
;; ;;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment