Commit d38949ee authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'ce/master' into ce-to-ee-2017-11-22

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parents b49fab30 d22c8857
Remove this section and replace it with a description of your MR. Also follow the
checklist below and check off any tasks that are done. If a certain task can not
be done you should explain so in the MR body. You are free to remove any
sections that do not apply to your MR.
When gathering statistics (e.g. the output of `EXPLAIN ANALYZE`) you should make
sure your database has enough data. Having around 10 000 rows in the tables
being queries should provide a reasonable estimate of how a query will behave.
Also make sure that PostgreSQL uses the following settings:
* `random_page_cost`: `1`
* `work_mem`: `16MB`
* `maintenance_work_mem`: at least `64MB`
* `shared_buffers`: at least `256MB`
If you have access to GitLab.com's staging environment you should also run your
measurements there, and include the results in this MR.
Add a description of your merge request here. Merge requests without an adequate
description will not be reviewed until one is added.
## Database Checklist
......@@ -23,34 +8,23 @@ When adding migrations:
- [ ] Updated `db/schema.rb`
- [ ] Added a `down` method so the migration can be reverted
- [ ] Added the output of the migration(s) to the MR body
- [ ] Added the execution time of the migration(s) to the MR body
- [ ] Added tests for the migration in `spec/migrations` if necessary (e.g. when
migrating data)
- [ ] Made sure the migration won't interfere with a running GitLab cluster,
for example by disabling transactions for long running migrations
- [ ] Added tests for the migration in `spec/migrations` if necessary (e.g. when migrating data)
When adding or modifying queries to improve performance:
- [ ] Included the raw SQL queries of the relevant queries
- [ ] Included the output of `EXPLAIN ANALYZE` and execution timings of the
relevant queries
- [ ] Added tests for the relevant changes
When adding indexes:
- [ ] Described the need for these indexes in the MR body
- [ ] Made sure existing indexes can not be reused instead
- [ ] Included data that shows the performance improvement, preferably in the form of a benchmark
- [ ] Included the output of `EXPLAIN (ANALYZE, BUFFERS)` of the relevant queries
When adding foreign keys to existing tables:
- [ ] Included a migration to remove orphaned rows in the source table
- [ ] Included a migration to remove orphaned rows in the source table before adding the foreign key
- [ ] Removed any instances of `dependent: ...` that may no longer be necessary
When adding tables:
- [ ] Ordered columns based on their type sizes in descending order
- [ ] Added foreign keys if necessary
- [ ] Added indexes if necessary
- [ ] Ordered columns based on the [Ordering Table Columns](https://docs.gitlab.com/ee/development/ordering_table_columns.html#ordering-table-columns) guidelines
- [ ] Added foreign keys to any columns pointing to data in other tables
- [ ] Added indexes for fields that are used in statements such as WHERE, ORDER BY, GROUP BY, and JOINs
When removing columns, tables, indexes or other structures:
......@@ -64,8 +38,6 @@ When removing columns, tables, indexes or other structures:
- [ ] API support added
- [ ] Tests added for this feature/bug
- Review
- [ ] Has been reviewed by UX
- [ ] Has been reviewed by Frontend
- [ ] Has been reviewed by Backend
- [ ] Has been reviewed by Database
- [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html)
......
<script>
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import environmentTable from '../components/environments_table.vue';
export default {
props: {
isLoading: {
type: Boolean,
required: true,
},
environments: {
type: Array,
required: true,
},
pagination: {
type: Object,
required: true,
},
canCreateDeployment: {
type: Boolean,
required: true,
},
canReadEnvironment: {
type: Boolean,
required: true,
},
},
components: {
environmentTable,
loadingIcon,
tablePagination,
},
methods: {
onChangePage(page) {
this.$emit('onChangePage', page);
},
},
};
</script>
<template>
<div class="environments-container">
<loading-icon
label="Loading environments"
v-if="isLoading"
size="3"
/>
<slot name="emptyState"></slot>
<div
class="table-holder"
v-if="!isLoading && environments.length > 0">
<environment-table
:environments="environments"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
/>
<table-pagination
v-if="pagination && pagination.totalPages > 1"
:change="onChangePage"
:pageInfo="pagination"
/>
</div>
</div>
</template>
<script>
export default {
name: 'environmentsEmptyState',
props: {
newPath: {
type: String,
required: true,
},
canCreateEnvironment: {
type: Boolean,
required: true,
},
helpPath: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="blank-state-row">
<div class="blank-state-center">
<h2 class="blank-state-title js-blank-state-title">
{{s__("Environments|You don't have any environments right now.")}}
</h2>
<p class="blank-state-text">
{{s__("Environments|Environments are places where code gets deployed, such as staging or production.")}}
<br />
<a :href="helpPath">
{{s__("Environments|Read more about environments")}}
</a>
</p>
<a
v-if="canCreateEnvironment"
:href="newPath"
class="btn btn-create js-new-environment-button">
{{s__("Environments|New environment")}}
</a>
</div>
</div>
</template>
<script>
import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale';
/**
* Renders the external url link in environments table.
......@@ -18,7 +19,7 @@ export default {
computed: {
title() {
return 'Open';
return s__('Environments|Open');
},
},
};
......
......@@ -430,7 +430,7 @@ export default {
v-if="!model.isFolder"
class="table-mobile-header"
role="rowheader">
Environment
{{s__("Environments|Environment")}}
</div>
<span
class="deploy-board-icon"
......@@ -520,7 +520,7 @@ export default {
<div
role="rowheader"
class="table-mobile-header">
Commit
{{s__("Environments|Commit")}}
</div>
<div
v-if="hasLastDeploymentKey"
......@@ -536,7 +536,7 @@ export default {
<div
v-if="!hasLastDeploymentKey"
class="commit-title table-mobile-content">
No deployments yet
{{s__("Environments|No deployments yet")}}
</div>
</div>
......@@ -546,7 +546,7 @@ export default {
<div
role="rowheader"
class="table-mobile-header">
Updated
{{s__("Environments|Updated")}}
</div>
<span
v-if="canShowDate"
......
......@@ -34,6 +34,7 @@ export default {
:aria-label="title">
<i
class="fa fa-area-chart"
aria-hidden="true" />
aria-hidden="true"
/>
</a>
</template>
......@@ -48,10 +48,10 @@ export default {
:disabled="isLoading">
<span v-if="isLastDeployment">
Re-deploy
{{s__("Environments|Re-deploy")}}
</span>
<span v-else>
Rollback
{{s__("Environments|Rollback")}}
</span>
<loading-icon v-if="isLoading" />
......
<script>
import Flash from '../../flash';
import { s__ } from '../../locale';
import emptyState from './empty_state.vue';
import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default {
props: {
endpoint: {
type: String,
required: true,
},
canCreateEnvironment: {
type: Boolean,
required: true,
},
canCreateDeployment: {
type: Boolean,
required: true,
},
canReadEnvironment: {
type: Boolean,
required: true,
},
cssContainerClass: {
type: String,
required: true,
},
newEnvironmentPath: {
type: String,
required: true,
},
helpPagePath: {
type: String,
required: true,
},
},
components: {
emptyState,
},
mixins: [
CIPaginationMixin,
environmentsMixin,
],
created() {
eventHub.$on('toggleFolder', this.toggleFolder);
},
beforeDestroy() {
eventHub.$off('toggleFolder');
},
methods: {
toggleFolder(folder) {
this.store.toggleFolder(folder);
if (!folder.isOpen) {
this.fetchChildEnvironments(folder, true);
}
},
fetchChildEnvironments(folder, showLoader = false) {
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
this.service.getFolderContent(folder.folder_path)
.then(resp => resp.json())
.then(response => this.store.setfolderContent(folder, response.environments))
.then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
.catch(() => {
Flash(s__('Environments|An error occurred while fetching the environments.'));
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
});
},
successCallback(resp) {
this.saveData(resp);
// We need to verify if any folder is open to also update it
const openFolders = this.store.getOpenFolders();
if (openFolders.length) {
openFolders.forEach(folder => this.fetchChildEnvironments(folder));
}
},
},
};
</script>
<template>
<div :class="cssContainerClass">
<div class="top-area">
<tabs
:tabs="tabs"
@onChangeTab="onChangeTab"
scope="environments"
/>
<div
v-if="canCreateEnvironment && !isLoading"
class="nav-controls">
<a
:href="newEnvironmentPath"
class="btn btn-create">
{{s__("Environments|New environment")}}
</a>
</div>
</div>
<container
:is-loading="isLoading"
:environments="state.environments"
:pagination="state.paginationInformation"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
@onChangePage="onChangePage"
>
<empty-state
slot="emptyState"
v-if="!isLoading && state.environments.length === 0"
:new-path="newEnvironmentPath"
:help-path="helpPagePath"
:can-create-environment="canCreateEnvironment"
/>
</container>
</div>
</template>
......@@ -2,14 +2,22 @@
/**
* Render environments table.
*/
<<<<<<< HEAD
import EnvironmentTableRowComponent from './environment_item.vue';
import DeployBoard from './deploy_board_component.vue';
=======
import environmentItem from './environment_item.vue';
>>>>>>> ce/master
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {
<<<<<<< HEAD
'environment-item': EnvironmentTableRowComponent,
DeployBoard,
=======
environmentItem,
>>>>>>> ce/master
loadingIcon,
},
......@@ -44,19 +52,19 @@ export default {
<div class="ci-table" role="grid">
<div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section section-10 environments-name" role="columnheader">
Environment
{{s__("Environments|Environment")}}
</div>
<div class="table-section section-10 environments-deploy" role="columnheader">
Deployment
{{s__("Environments|Deployment")}}
</div>
<div class="table-section section-15 environments-build" role="columnheader">
Job
{{s__("Environments|Job")}}
</div>
<div class="table-section section-25 environments-commit" role="columnheader">
Commit
{{s__("Environments|Commit")}}
</div>
<div class="table-section section-10 environments-date" role="columnheader">
Updated
{{s__("Environments|Updated")}}
</div>
</div>
<template
......@@ -98,7 +106,7 @@ export default {
<a
:href="folderUrl(model)"
class="btn btn-default">
Show all
{{s__("Environments|Show all")}}
</a>
</div>
</div>
......
import Vue from 'vue';
import EnvironmentsComponent from './components/environment.vue';
import environmentsComponent from './components/environments_app.vue';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import Translate from '../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#environments-list-view',
components: {
'environments-table-app': EnvironmentsComponent,
environmentsComponent,
},
data() {
const environmentsData = document.querySelector(this.$options.el).dataset;
return {
endpoint: environmentsData.environmentsDataEndpoint,
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
cssContainerClass: environmentsData.cssClass,
canCreateEnvironment: convertPermissionToBoolean(environmentsData.canCreateEnvironment),
canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment),
canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment),
};
},
render(createElement) {
return createElement('environments-component', {
props: {
endpoint: this.endpoint,
newEnvironmentPath: this.newEnvironmentPath,
helpPagePath: this.helpPagePath,
cssContainerClass: this.cssContainerClass,
canCreateEnvironment: this.canCreateEnvironment,
canCreateDeployment: this.canCreateDeployment,
canReadEnvironment: this.canReadEnvironment,
},
});
},
render: createElement => createElement('environments-table-app'),
}));
import Vue from 'vue';
import EnvironmentsFolderComponent from './environments_folder_view.vue';
import environmentsFolderApp from './environments_folder_view.vue';
import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
import Translate from '../../vue_shared/translate';
Vue.use(Translate);
<<<<<<< HEAD
document.addEventListener('DOMContentLoaded', () => {
// eslint-disable-next-line no-new
new Vue({
......@@ -11,3 +16,33 @@ document.addEventListener('DOMContentLoaded', () => {
render: createElement => createElement('environments-folder-app'),
});
});
=======
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#environments-folder-list-view',
components: {
environmentsFolderApp,
},
data() {
const environmentsData = document.querySelector(this.$options.el).dataset;
return {
endpoint: environmentsData.endpoint,
folderName: environmentsData.folderName,
cssContainerClass: environmentsData.cssClass,
canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment),
canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment),
};
},
render(createElement) {
return createElement('environments-folder-app', {
props: {
endpoint: this.endpoint,
folderName: this.folderName,
cssContainerClass: this.cssContainerClass,
canCreateDeployment: this.canCreateDeployment,
canReadEnvironment: this.canReadEnvironment,
},
});
},
}));
>>>>>>> ce/master
<script>
<<<<<<< HEAD
import Visibility from 'visibilityjs';
import Flash from '../../flash';
import EnvironmentsService from '../services/environments_service';
......@@ -162,19 +163,44 @@ export default {
this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.');
=======
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default {
props: {
endpoint: {
type: String,
required: true,
},
folderName: {
type: String,
required: true,
},
cssContainerClass: {
type: String,
required: true,
},
canCreateDeployment: {
type: Boolean,
required: true,
},
canReadEnvironment: {
type: Boolean,
required: true,
},
>>>>>>> ce/master
},
postAction(endpoint) {
if (!this.isMakingRequest) {
this.isLoading = true;
this.service.postAction(endpoint)
.then(() => this.fetchEnvironments())
.catch(() => new Flash('An error occurred while making the request.'));
}
mixins: [
environmentsMixin,
CIPaginationMixin,
],
methods: {
successCallback(resp) {
this.saveData(resp);
},
},
},
};
};
</script>
<template>
<div :class="cssContainerClass">
......@@ -183,9 +209,10 @@ export default {
v-if="!isLoading">
<h4 class="js-folder-name environments-folder-name">
Environments / <b>{{folderName}}</b>
{{s__("Environments|Environments")}} / <b>{{folderName}}</b>
</h4>
<<<<<<< HEAD
<ul class="nav-links">
<li :class="{ 'active': scope === null || scope === 'available' }">
<a
......@@ -236,6 +263,22 @@ export default {
:change="changePage"
:pageInfo="state.paginationInformation"/>
</div>
=======
<tabs
:tabs="tabs"
@onChangeTab="onChangeTab"
scope="environments"
/>
>>>>>>> ce/master
</div>
<container
:is-loading="isLoading"
:environments="state.environments"
:pagination="state.paginationInformation"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
@onChangePage="onChangePage"
/>
</div>
</template>
/**
* Common code between environmets app and folder view
*/
import Visibility from 'visibilityjs';
import Poll from '../../lib/utils/poll';
import {
getParameterByName,
parseQueryStringIntoObject,
} from '../../lib/utils/common_utils';
import { s__ } from '../../locale';
import Flash from '../../flash';
import eventHub from '../event_hub';
import EnvironmentsStore from '../stores/environments_store';
import EnvironmentsService from '../services/environments_service';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import environmentTable from '../components/environments_table.vue';
import tabs from '../../vue_shared/components/navigation_tabs.vue';
import container from '../components/container.vue';
export default {
components: {
environmentTable,
container,
loadingIcon,
tabs,
tablePagination,
},
data() {
const store = new EnvironmentsStore();
return {
store,
state: store.state,
isLoading: false,
isMakingRequest: false,
scope: getParameterByName('scope') || 'available',
page: getParameterByName('page') || '1',
requestData: {},
};
},
methods: {
saveData(resp) {
const headers = resp.headers;
return resp.json().then((response) => {
this.isLoading = false;
this.store.storeAvailableCount(response.available_count);
this.store.storeStoppedCount(response.stopped_count);
this.store.storeEnvironments(response.environments);
this.store.setPagination(headers);
if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
this.store.storeAvailableCount(response.available_count);
this.store.storeStoppedCount(response.stopped_count);
this.store.storeEnvironments(response.environments);
this.store.setPagination(headers);
}
});
},
/**
* Handles URL and query parameter changes.
* When the user uses the pagination or the tabs,
* - update URL
* - Make API request to the server with new parameters
* - Update the polling function
* - Update the internal state
*/
updateContent(parameters) {
this.updateInternalState(parameters);
// fetch new data
return this.service.get(this.requestData)
.then(response => this.successCallback(response))
.then(() => {
// restart polling
this.poll.restart({ data: this.requestData });
})
.catch(() => {
this.errorCallback();
// restart polling
this.poll.restart();
});
},
errorCallback() {
this.isLoading = false;
Flash(s__('Environments|An error occurred while fetching the environments.'));
},
postAction(endpoint) {
if (!this.isMakingRequest) {
this.isLoading = true;
this.service.postAction(endpoint)
.then(() => this.fetchEnvironments())
.catch(() => {
this.isLoading = false;
Flash(s__('Environments|An error occurred while making the request.'));
});
}
},
fetchEnvironments() {
this.isLoading = true;
return this.service.get(this.requestData)
.then(this.successCallback)
.catch(this.errorCallback);
},
},
computed: {
tabs() {
return [
{
name: s__('Available'),
scope: 'available',
count: this.state.availableCounter,
isActive: this.scope === 'available',
},
{
name: s__('Stopped'),
scope: 'stopped',
count: this.state.stoppedCounter,
isActive: this.scope === 'stopped',
},
];
},
},
/**
* Fetches all the environments and stores them.
* Toggles loading property.
*/
created() {
this.service = new EnvironmentsService(this.endpoint);
this.requestData = { page: this.page, scope: this.scope };
this.poll = new Poll({
resource: this.service,
method: 'get',
data: this.requestData,
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: (isMakingRequest) => {
this.isMakingRequest = isMakingRequest;
},
});
if (!Visibility.hidden()) {
this.isLoading = true;
this.poll.makeRequest();
} else {
this.fetchEnvironments();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
eventHub.$on('postAction', this.postAction);
},
beforeDestroyed() {
eventHub.$off('postAction');
},
};
......@@ -44,7 +44,12 @@ export default class EnvironmentsStore {
storeEnvironments(environments = []) {
const filteredEnvironments = environments.map((env) => {
const oldEnvironmentState = this.state.environments
.find(element => element.id === env.latest.id) || {};
.find((element) => {
if (env.latest) {
return element.id === env.latest.id;
}
return element.id === env.id;
}) || {};
let filtered = {};
......
......@@ -4,9 +4,11 @@ import tooltip from '../../vue_shared/directives/tooltip';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
import Icon from '../../vue_shared/components/icon.vue';
export default {
components: {
Icon,
PopupDialog,
},
directives: {
......@@ -63,9 +65,9 @@ export default {
:aria-label="editBtnTitle"
data-container="body"
class="edit-group btn no-expand">
<i
class="fa fa-cogs"
aria-hidden="true"/>
<icon
name="settings">
</icon>
</a>
<a
v-tooltip
......
......@@ -269,46 +269,6 @@ export const parseIntPagination = paginationInformation => ({
previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10),
});
/**
* Updates the search parameter of a URL given the parameter and value provided.
*
* If no search params are present we'll add it.
* If param for page is already present, we'll update it
* If there are params but not for the given one, we'll add it at the end.
* Returns the new search parameters.
*
* @param {String} param
* @param {Number|String|Undefined|Null} value
* @return {String}
*/
export const setParamInURL = (param, value) => {
let search;
const locationSearch = window.location.search;
if (locationSearch.length) {
const parameters = locationSearch.substring(1, locationSearch.length)
.split('&')
.reduce((acc, element) => {
const val = element.split('=');
// eslint-disable-next-line no-param-reassign
acc[val[0]] = decodeURIComponent(val[1]);
return acc;
}, {});
parameters[param] = value;
const toString = Object.keys(parameters)
.map(val => `${val}=${encodeURIComponent(parameters[val])}`)
.join('&');
search = `?${toString}`;
} else {
search = `?${param}=${value}`;
}
return search;
};
/**
* Given a string of query parameters creates an object.
*
......
......@@ -71,8 +71,6 @@ import './project_import';
import './projects_dropdown';
import './projects_list';
import './syntax_highlight';
import './render_math';
import './render_mermaid';
import './render_gfm';
import './right_sidebar';
import './search';
......
......@@ -3,15 +3,14 @@
import PipelinesService from '../services/pipelines_service';
import pipelinesMixin from '../mixins/pipelines';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import navigationTabs from './navigation_tabs.vue';
import navigationTabs from '../../vue_shared/components/navigation_tabs.vue';
import navigationControls from './nav_controls.vue';
import {
convertPermissionToBoolean,
getParameterByName,
historyPushState,
buildUrlWithCurrentLocation,
parseQueryStringIntoObject,
} from '../../lib/utils/common_utils';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default {
props: {
......@@ -36,6 +35,7 @@
},
mixins: [
pipelinesMixin,
CIPaginationMixin,
],
data() {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
......@@ -170,22 +170,8 @@
* - Update the internal state
*/
updateContent(parameters) {
// stop polling
this.poll.stop();
this.updateInternalState(parameters);
const queryString = Object.keys(parameters).map((parameter) => {
const value = parameters[parameter];
// update internal state for UI
this[parameter] = value;
return `${parameter}=${encodeURIComponent(value)}`;
}).join('&');
// update polling parameters
this.requestData = parameters;
historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
this.isLoading = true;
// fetch new data
return this.service.getPipelines(this.requestData)
.then((response) => {
......@@ -203,14 +189,6 @@
this.poll.restart();
});
},
onChangeTab(scope) {
this.updateContent({ scope, page: '1' });
},
onChangePage(page) {
/* URLS parameters are strings, we need to parse to match types */
this.updateContent({ scope: this.scope, page: Number(page).toString() });
},
},
};
</script>
......@@ -235,6 +213,7 @@
<navigation-tabs
:tabs="tabs"
@onChangeTab="onChangeTab"
scope="pipelines"
/>
<navigation-controls
......
/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len */
import renderMath from './render_math';
import renderMermaid from './render_mermaid';
// Render Gitlab flavoured Markdown
//
// Delegates to syntax highlight and render math & mermaid diagrams.
//
(function() {
$.fn.renderGFM = function() {
this.find('.js-syntax-highlight').syntaxHighlight();
this.find('.js-render-math').renderMath();
this.find('.js-render-mermaid').renderMermaid();
return this;
};
$.fn.renderGFM = function renderGFM() {
this.find('.js-syntax-highlight').syntaxHighlight();
renderMath(this.find('.js-render-math'));
renderMermaid(this.find('.js-render-mermaid'));
return this;
};
$(() => $('body').renderGFM());
}).call(window);
$(() => $('body').renderGFM());
/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len, no-console */
/* global katex */
// Renders math using KaTeX in any element with the
......@@ -8,49 +7,45 @@
//
// <code class="js-render-math"></div>
//
(function() {
// Only load once
var katexLoaded = false;
let katexLoaded = false;
// Loop over all math elements and render math
var renderWithKaTeX = function (elements) {
elements.each(function () {
var mathNode = $('<span></span>');
var $this = $(this);
// Loop over all math elements and render math
function renderWithKaTeX(elements) {
elements.each(function katexElementsLoop() {
const mathNode = $('<span></span>');
const $this = $(this);
var display = $this.attr('data-math-style') === 'display';
try {
katex.render($this.text(), mathNode.get(0), { displayMode: display });
mathNode.insertAfter($this);
$this.remove();
} catch (err) {
// What can we do??
console.log(err.message);
}
});
};
const display = $this.attr('data-math-style') === 'display';
try {
katex.render($this.text(), mathNode.get(0), { displayMode: display });
mathNode.insertAfter($this);
$this.remove();
} catch (err) {
throw err;
}
});
}
$.fn.renderMath = function() {
var $this = this;
if ($this.length === 0) return;
export default function renderMath($els) {
if (!$els.length) return;
if (katexLoaded) renderWithKaTeX($this);
else {
// Request CSS file so it is in the cache
$.get(gon.katex_css_url, function() {
var css = $('<link>',
{ rel: 'stylesheet',
type: 'text/css',
href: gon.katex_css_url,
});
css.appendTo('head');
if (katexLoaded) {
renderWithKaTeX($els);
} else {
$.get(gon.katex_css_url, () => {
const css = $('<link>', {
rel: 'stylesheet',
type: 'text/css',
href: gon.katex_css_url,
});
css.appendTo('head');
// Load KaTeX js
$.getScript(gon.katex_js_url, function() {
katexLoaded = true;
renderWithKaTeX($this); // Run KaTeX
});
// Load KaTeX js
$.getScript(gon.katex_js_url, () => {
katexLoaded = true;
renderWithKaTeX($els); // Run KaTeX
});
}
};
}).call(window);
});
}
}
......@@ -14,8 +14,8 @@
import Flash from './flash';
$.fn.renderMermaid = function renderMermaid() {
if (this.length === 0) return;
export default function renderMermaid($els) {
if (!$els.length) return;
import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid').then((mermaid) => {
mermaid.initialize({
......@@ -23,8 +23,10 @@ $.fn.renderMermaid = function renderMermaid() {
theme: 'neutral',
});
mermaid.init(undefined, this);
$els.each((i, el) => {
mermaid.init(undefined, el);
});
}).catch((err) => {
Flash(`Can't load mermaid module: ${err}`);
});
};
}
import Flash from './flash';
import { __, s__ } from './locale';
import { spriteIcon } from './lib/utils/common_utils';
export default class Star {
constructor() {
......@@ -7,16 +8,18 @@ export default class Star {
.on('ajax:success', function handleSuccess(e, data) {
const $this = $(this);
const $starSpan = $this.find('span');
const $starIcon = $this.find('i');
const $startIcon = $this.find('svg');
function toggleStar(isStarred) {
$this.parent().find('.star-count').text(data.star_count);
if (isStarred) {
$starSpan.removeClass('starred').text(s__('StarProject|Star'));
$starIcon.removeClass('fa-star').addClass('fa-star-o');
$startIcon.remove();
$this.prepend(spriteIcon('star-o'));
} else {
$starSpan.addClass('starred').text(__('Unstar'));
$starIcon.removeClass('fa-star-o').addClass('fa-star');
$startIcon.remove();
$this.prepend(spriteIcon('star'));
}
}
......
......@@ -14,7 +14,7 @@ export default {
statusObj() {
return {
group: this.status,
icon: `icon_status_${this.status}`,
icon: `status_${this.status}`,
};
},
},
......
<script>
/**
* Given an array of tabs, renders non linked bootstrap tabs.
* When a tab is clicked it will trigger an event and provide the clicked scope.
*
* This component is used in apps that handle the API call.
* If you only need to change the URL this component should not be used.
*
* @example
* <navigation-tabs
* :tabs="[
* {
* name: String,
* scope: String,
* count: Number || Undefined,
* isActive: Boolean,
* },
* ]"
* @onChangeTab="onChangeTab"
* />
*/
export default {
name: 'PipelineNavigationTabs',
name: 'NavigationTabs',
props: {
tabs: {
type: Array,
required: true,
},
scope: {
type: String,
required: false,
default: '',
},
},
mounted() {
$(document).trigger('init.scrolling-tabs');
......@@ -34,7 +59,7 @@
<a
role="button"
@click="onTabClick(tab)"
:class="`js-pipelines-tab-${tab.scope}`"
:class="`js-${scope}-tab-${tab.scope}`"
>
{{ tab.name }}
......
/**
* API callbacks for pagination and tabs
* shared between Pipelines and Environments table.
*
* Components need to have `scope`, `page` and `requestData`
*/
import {
historyPushState,
buildUrlWithCurrentLocation,
} from '../../lib/utils/common_utils';
export default {
methods: {
onChangeTab(scope) {
this.updateContent({ scope, page: '1' });
},
onChangePage(page) {
/* URLS parameters are strings, we need to parse to match types */
this.updateContent({ scope: this.scope, page: Number(page).toString() });
},
updateInternalState(parameters) {
// stop polling
this.poll.stop();
const queryString = Object.keys(parameters).map((parameter) => {
const value = parameters[parameter];
// update internal state for UI
this[parameter] = value;
return `${parameter}=${encodeURIComponent(value)}`;
}).join('&');
// update polling parameters
this.requestData = parameters;
historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
this.isLoading = true;
},
},
};
......@@ -364,6 +364,18 @@ span.idiff {
float: none;
}
}
@media (max-width: $screen-xs-max) {
display: block;
.file-actions {
white-space: normal;
.btn-group {
padding-top: 5px;
}
}
}
}
.is-stl-loading {
......
......@@ -204,7 +204,7 @@ class ApplicationController < ActionController::Base
end
def check_password_expiration
return if session[:impersonator_id] || current_user&.ldap_user?
return if session[:impersonator_id] || !current_user&.allow_password_authentication?
password_expires_at = current_user&.password_expires_at
......
......@@ -152,7 +152,7 @@ module IssuableCollections
when 'MergeRequest'
[
:source_project, :target_project, :author, :assignee, :labels, :milestone,
head_pipeline: :project, target_project: :namespace, merge_request_diff: :merge_request_diff_commits
head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
]
end
end
......
......@@ -8,6 +8,7 @@ module PreviewMarkdown
case controller_name
when 'wikis' then { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
when 'snippets' then { skip_project_check: true }
when 'groups' then { group: group }
else {}
end
......
......@@ -51,7 +51,7 @@ class InvitesController < ApplicationController
return if current_user
notice = "To accept this invitation, sign in"
notice << " or create an account" if current_application_settings.signup_enabled?
notice << " or create an account" if current_application_settings.allow_signup?
notice << "."
store_location_for :user, request.fullpath
......
......@@ -154,7 +154,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed."
if current_application_settings.signup_enabled?
if current_application_settings.allow_signup?
message << " Create a GitLab account first, and then connect it to your #{label} account."
end
......
class PasswordsController < Devise::PasswordsController
include Gitlab::CurrentSettings
before_action :resource_from_email, only: [:create]
before_action :prevent_ldap_reset, only: [:create]
before_action :check_password_authentication_available, only: [:create]
before_action :throttle_reset, only: [:create]
prepend EE::PasswordsController
......@@ -27,7 +29,7 @@ class PasswordsController < Devise::PasswordsController
def update
super do |resource|
if resource.valid? && resource.require_password_creation?
if resource.valid? && resource.password_automatically_set?
resource.update_attribute(:password_automatically_set, false)
end
end
......@@ -40,11 +42,15 @@ class PasswordsController < Devise::PasswordsController
self.resource = resource_class.find_by_email(email)
end
def prevent_ldap_reset
return unless resource&.ldap_user?
def check_password_authentication_available
if resource
return if resource.allow_password_authentication?
else
return if current_application_settings.password_authentication_enabled?
end
redirect_to after_sending_reset_password_instructions_path_for(resource_name),
alert: "Cannot reset password for LDAP user."
alert: "Password authentication is unavailable."
end
def throttle_reset
......
......@@ -77,7 +77,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
end
def authorize_change_password!
render_404 if @user.ldap_user?
render_404 unless @user.allow_password_authentication?
end
def user_params
......
......@@ -35,6 +35,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
folder_environments = project.environments.where(environment_type: params[:id])
@environments = folder_environments.with_state(params[:scope] || :available)
.order(:name)
@folder = params[:id]
respond_to do |format|
format.html
......
......@@ -64,7 +64,7 @@ class SessionsController < Devise::SessionsController
user = User.admins.last
return unless user && user.require_password_creation?
return unless user && user.require_password_creation_for_web?
Users::UpdateService.new(current_user, user: user).execute do |user|
@token = user.generate_reset_token
......
# :nocov:
if Rails.env.test?
class UnicornTestController < ActionController::Base
def pid
render plain: Process.pid.to_s
end
def kill
Process.kill(params[:signal], Process.pid)
render plain: 'Bye!'
end
end
end
# :nocov:
......@@ -4,9 +4,9 @@ module ApplicationSettingsHelper
include Gitlab::CurrentSettings
delegate :gravatar_enabled?,
:signup_enabled?,
:password_authentication_enabled?,
delegate :allow_signup?,
:gravatar_enabled?,
:password_authentication_enabled_for_web?,
:akismet_enabled?,
:koding_enabled?,
to: :current_application_settings
......@@ -204,7 +204,7 @@ module ApplicationSettingsHelper
:metrics_port,
:metrics_sample_interval,
:metrics_timeout,
:password_authentication_enabled,
:password_authentication_enabled_for_web,
:performance_bar_allowed_group_id,
:performance_bar_enabled,
:plantuml_enabled,
......
......@@ -58,12 +58,12 @@ module ButtonHelper
def http_clone_button(project, placement = 'right', append_link: true)
klass = 'http-selector'
klass << ' has-tooltip' if current_user.try(:require_password_creation?) || current_user.try(:require_personal_access_token_creation_for_git_auth?)
klass << ' has-tooltip' if current_user.try(:require_extra_setup_for_git_auth?)
protocol = gitlab_config.protocol.upcase
tooltip_title =
if current_user.try(:require_password_creation?)
if current_user.try(:require_password_creation_for_git?)
_("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol }
else
_("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol }
......
......@@ -236,11 +236,11 @@ module ProjectsHelper
def show_no_password_message?
cookies[:hide_no_password_message].blank? && !current_user.hide_no_password &&
( current_user.require_password_creation? || current_user.require_personal_access_token_creation_for_git_auth? )
current_user.require_extra_setup_for_git_auth?
end
def link_to_set_password
if current_user.require_password_creation?
if current_user.require_password_creation_for_git?
link_to s_('SetPasswordToCloneLink|set a password'), edit_profile_password_path
else
link_to s_('CreateTokenToCloneLink|create a personal access token'), profile_personal_access_tokens_path
......
......@@ -289,7 +289,8 @@ class ApplicationSetting < ActiveRecord::Base
koding_url: nil,
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
password_authentication_enabled: Settings.gitlab['password_authentication_enabled'],
password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'],
password_authentication_enabled_for_git: true,
performance_bar_allowed_group_id: nil,
rsa_key_restriction: 0,
plantuml_enabled: false,
......@@ -521,6 +522,14 @@ class ApplicationSetting < ActiveRecord::Base
has_attribute?(attr_name) ? public_send(attr_name) : FORBIDDEN_KEY_VALUE # rubocop:disable GitlabSecurity/PublicSend
end
def allow_signup?
signup_enabled? && password_authentication_enabled_for_web?
end
def password_authentication_enabled?
password_authentication_enabled_for_web? || password_authentication_enabled_for_git?
end
private
def ensure_uuid!
......
......@@ -247,7 +247,7 @@ module Ci
@merge_request ||=
begin
merge_requests = MergeRequest.includes(:merge_request_diff)
merge_requests = MergeRequest.includes(:latest_merge_request_diff)
.where(source_branch: ref,
source_project: pipeline.project)
.reorder(iid: :desc)
......
......@@ -109,12 +109,12 @@ class Commit
@link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/)
end
def to_reference(from_project = nil, full: false)
commit_reference(from_project, id, full: full)
def to_reference(from = nil, full: false)
commit_reference(from, id, full: full)
end
def reference_link_text(from_project = nil, full: false)
commit_reference(from_project, short_id, full: full)
def reference_link_text(from = nil, full: false)
commit_reference(from, short_id, full: full)
end
def diff_line_count
......@@ -381,8 +381,8 @@ class Commit
private
def commit_reference(from_project, referable_commit_id, full: false)
reference = project.to_reference(from_project, full: full)
def commit_reference(from, referable_commit_id, full: false)
reference = project.to_reference(from, full: full)
if reference.present?
"#{reference}#{self.class.reference_prefix}#{referable_commit_id}"
......
......@@ -89,8 +89,8 @@ class CommitRange
alias_method :id, :to_s
def to_reference(from_project = nil, full: false)
project_reference = project.to_reference(from_project, full: full)
def to_reference(from = nil, full: false)
project_reference = project.to_reference(from, full: full)
if project_reference.present?
project_reference + self.class.reference_prefix + self.id
......@@ -99,8 +99,8 @@ class CommitRange
end
end
def reference_link_text(from_project = nil)
project_reference = project.to_reference(from_project)
def reference_link_text(from = nil)
project_reference = project.to_reference(from)
reference = ref_from + notation + ref_to
if project_reference.present?
......
module ManualInverseAssociation
extend ActiveSupport::Concern
module ClassMethods
def manual_inverse_association(association, inverse)
define_method(association) do |*args|
super(*args).tap do |value|
next unless value
child_association = value.association(inverse)
child_association.set_inverse_instance(self)
child_association.target = self
end
end
end
end
end
......@@ -31,11 +31,11 @@ module Mentionable
#
# By default this will be the class name and the result of calling
# `to_reference` on the object.
def gfm_reference(from_project = nil)
def gfm_reference(from = nil)
# "MergeRequest" > "merge_request" > "Merge request" > "merge request"
friendly_name = self.class.to_s.underscore.humanize.downcase
"#{friendly_name} #{to_reference(from_project)}"
"#{friendly_name} #{to_reference(from)}"
end
# The GFM reference to this Mentionable, which shouldn't be included in its #references.
......
......@@ -7,7 +7,7 @@ module Referable
# Returns the String necessary to reference this object in Markdown
#
# from_project - Refering Project object
# from - Referring parent object
#
# This should be overridden by the including class.
#
......@@ -17,12 +17,12 @@ module Referable
# Issue.last.to_reference(other_project) # => "cross-project#1"
#
# Returns a String
def to_reference(_from_project = nil, full:)
def to_reference(_from = nil, full:)
''
end
def reference_link_text(from_project = nil)
to_reference(from_project)
def reference_link_text(from = nil)
to_reference(from)
end
included do
......
......@@ -38,11 +38,11 @@ class ExternalIssue
@project.id
end
def to_reference(_from_project = nil, full: nil)
def to_reference(_from = nil, full: nil)
id
end
def reference_link_text(from_project = nil)
def reference_link_text(from = nil)
return "##{id}" if id =~ /^\d+$/
id
......
......@@ -115,7 +115,7 @@ class Group < Namespace
end
end
def to_reference(_from_project = nil, full: nil)
def to_reference(_from = nil, full: nil)
"#{self.class.reference_prefix}#{full_path}"
end
......
......@@ -168,12 +168,12 @@ class Label < ActiveRecord::Base
#
# Returns a String
#
def to_reference(from_project = nil, target_project: nil, format: :id, full: false)
def to_reference(from = nil, target_project: nil, format: :id, full: false)
format_reference = label_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
if from_project
"#{from_project.to_reference(target_project, full: full)}#{reference}"
if from
"#{from.to_reference(target_project, full: full)}#{reference}"
else
reference
end
......
......@@ -6,6 +6,8 @@ class MergeRequest < ActiveRecord::Base
include Elastic::MergeRequestsSearch
include IgnorableColumn
include TimeTrackable
include ManualInverseAssociation
include EachBatch
ignore_column :locked_at,
:ref_fetched
......@@ -18,9 +20,28 @@ class MergeRequest < ActiveRecord::Base
belongs_to :merge_user, class_name: "User"
has_many :merge_request_diffs
has_one :merge_request_diff,
-> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
belongs_to :latest_merge_request_diff, class_name: 'MergeRequestDiff'
manual_inverse_association :latest_merge_request_diff, :merge_request
# This is the same as latest_merge_request_diff unless:
# 1. There are arguments - in which case we might be trying to force-reload.
# 2. This association is already loaded.
# 3. The latest diff does not exist.
#
# The second one in particular is important - MergeRequestDiff#merge_request
# is the inverse of MergeRequest#merge_request_diff, which means it may not be
# the latest diff, because we could have loaded any diff from this particular
# MR. If we haven't already loaded a diff, then it's fine to load the latest.
def merge_request_diff(*args)
fallback = latest_merge_request_diff if args.empty? && !association(:merge_request_diff).loaded?
fallback || super
end
belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......@@ -172,6 +193,22 @@ class MergeRequest < ActiveRecord::Base
where("merge_requests.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end
# This is used after project import, to reset the IDs to the correct
# values. It is not intended to be called without having already scoped the
# relation.
def self.set_latest_merge_request_diff_ids!
update = '
latest_merge_request_diff_id = (
SELECT MAX(id)
FROM merge_request_diffs
WHERE merge_requests.id = merge_request_diffs.merge_request_id
)'.squish
self.each_batch do |batch|
batch.update_all(update)
end
end
WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
def self.work_in_progress?(title)
......
......@@ -2,6 +2,7 @@ class MergeRequestDiff < ActiveRecord::Base
include Sortable
include Importable
include Gitlab::EncodingHelper
include ManualInverseAssociation
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100
......@@ -10,6 +11,8 @@ class MergeRequestDiff < ActiveRecord::Base
VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta].freeze
belongs_to :merge_request
manual_inverse_association :merge_request, :merge_request_diff
has_many :merge_request_diff_files, -> { order(:merge_request_diff_id, :relative_order) }
has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) }
......@@ -194,7 +197,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def latest?
self == merge_request.merge_request_diff
self.id == merge_request.latest_merge_request_diff_id
end
def compare_with(sha)
......
......@@ -166,18 +166,18 @@ class Milestone < ActiveRecord::Base
# Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1"
# Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
#
def to_reference(from_project = nil, format: :name, full: false)
def to_reference(from = nil, format: :name, full: false)
format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
if project
"#{project.to_reference(from_project, full: full)}#{reference}"
"#{project.to_reference(from, full: full)}#{reference}"
else
reference
end
end
def reference_link_text(from_project = nil)
def reference_link_text(from = nil)
self.title
end
......
......@@ -761,10 +761,10 @@ class Project < ActiveRecord::Base
end
end
def to_human_reference(from_project = nil)
if cross_namespace_reference?(from_project)
def to_human_reference(from = nil)
if cross_namespace_reference?(from)
name_with_namespace
elsif cross_project_reference?(from_project)
elsif cross_project_reference?(from)
name
end
end
......
......@@ -917,19 +917,13 @@ class Repository
end
end
def merged_to_root_ref?(branch_or_name, pre_loaded_merged_branches = nil)
def merged_to_root_ref?(branch_or_name)
branch = Gitlab::Git::Branch.find(self, branch_or_name)
if branch
@root_ref_sha ||= commit(root_ref).sha
same_head = branch.target == @root_ref_sha
merged =
if pre_loaded_merged_branches
pre_loaded_merged_branches.include?(branch.name)
else
ancestor?(branch.target, @root_ref_sha)
end
merged = ancestor?(branch.target, @root_ref_sha)
!same_head && merged
else
nil
......
......@@ -76,11 +76,11 @@ class Snippet < ActiveRecord::Base
@link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end
def to_reference(from_project = nil, full: false)
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{id}"
if project.present?
"#{project.to_reference(from_project, full: full)}#{reference}"
"#{project.to_reference(from, full: full)}#{reference}"
else
reference
end
......
......@@ -456,7 +456,7 @@ class User < ActiveRecord::Base
username
end
def to_reference(_from_project = nil, target_project: nil, full: nil)
def to_reference(_from = nil, target_project: nil, full: nil)
"#{self.class.reference_prefix}#{username}"
end
......@@ -652,18 +652,34 @@ class User < ActiveRecord::Base
count.zero? && Gitlab::ProtocolAccess.allowed?('ssh')
end
def require_password_creation?
password_automatically_set? && allow_password_authentication?
def require_password_creation_for_web?
allow_password_authentication_for_web? && password_automatically_set?
end
def require_password_creation_for_git?
allow_password_authentication_for_git? && password_automatically_set?
end
def require_personal_access_token_creation_for_git_auth?
return false if current_application_settings.password_authentication_enabled? || ldap_user?
return false if allow_password_authentication_for_git? || ldap_user?
PersonalAccessTokensFinder.new(user: self, impersonation: false, state: 'active').execute.none?
end
def require_extra_setup_for_git_auth?
require_password_creation_for_git? || require_personal_access_token_creation_for_git_auth?
end
def allow_password_authentication?
!ldap_user? && current_application_settings.password_authentication_enabled?
allow_password_authentication_for_web? || allow_password_authentication_for_git?
end
def allow_password_authentication_for_web?
current_application_settings.password_authentication_enabled_for_web? && !ldap_user?
end
def allow_password_authentication_for_git?
current_application_settings.password_authentication_enabled_for_git? && !ldap_user?
end
def can_change_username?
......
......@@ -36,7 +36,7 @@ module MergeRequests
# target branch manually
def close_merge_requests
commit_ids = @commits.map(&:id)
merge_requests = @project.merge_requests.preload(:merge_request_diff).opened.where(target_branch: @branch_name).to_a
merge_requests = @project.merge_requests.preload(:latest_merge_request_diff).opened.where(target_branch: @branch_name).to_a
merge_requests = merge_requests.select(&:diff_head_commit)
merge_requests = merge_requests.select do |merge_request|
......
# The protected branches API still uses the `developers_can_push` and `developers_can_merge`
# The branches#protect API still uses the `developers_can_push` and `developers_can_merge`
# flags for backward compatibility, and so performs translation between that format and the
# internal data model (separate access levels). The translation code is non-trivial, and so
# lives in this service.
module ProtectedBranches
class ApiCreateService < BaseService
class LegacyApiCreateService < BaseService
def execute
push_access_level =
if params.delete(:developers_can_push)
......
# The protected branches API still uses the `developers_can_push` and `developers_can_merge`
# The branches#protect API still uses the `developers_can_push` and `developers_can_merge`
# flags for backward compatibility, and so performs translation between that format and the
# internal data model (separate access levels). The translation code is non-trivial, and so
# lives in this service.
module ProtectedBranches
class ApiUpdateService < BaseService
class LegacyApiUpdateService < BaseService
def execute(protected_branch)
@developers_can_push = params.delete(:developers_can_push)
@developers_can_merge = params.delete(:developers_can_merge)
......
......@@ -35,7 +35,7 @@ module Users
private
def can_create_user?
(current_user.nil? && current_application_settings.signup_enabled?) || current_user&.admin?
(current_user.nil? && current_application_settings.allow_signup?) || current_user&.admin?
end
# Allowed params for creating a user (admins only)
......
......@@ -190,9 +190,22 @@
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :password_authentication_enabled do
= f.check_box :password_authentication_enabled
Sign-in enabled
= f.label :password_authentication_enabled_for_web do
= f.check_box :password_authentication_enabled_for_web
Password authentication enabled for web interface
.help-block
When disabled, an external authentication provider must be used.
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :password_authentication_enabled_for_git do
= f.check_box :password_authentication_enabled_for_git
Password authentication enabled for Git over HTTP(S)
.help-block
When disabled, a Personal Access Token
- if Gitlab::LDAP::Config.enabled?
or LDAP password
must be used to authenticate.
- if omniauth_enabled? && button_based_providers.any?
.form-group
= f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2'
......
......@@ -48,10 +48,10 @@
.well-segment.admin-well.admin-well-features
%h4 Features
- sign_up = "Sign up"
%p{ "aria-label" => "#{sign_up}: status " + (signup_enabled? ? "on" : "off") }
%p{ "aria-label" => "#{sign_up}: status " + (allow_signup? ? "on" : "off") }
= sign_up
%span.light.pull-right
= boolean_to_icon signup_enabled?
= boolean_to_icon allow_signup?
- ldap = "LDAP"
%p{ "aria-label" => "#{ldap}: status " + (Gitlab.config.ldap.enabled ? "on" : "off") }
= ldap
......
......@@ -6,15 +6,15 @@
- else
= render 'devise/shared/tabs_normal'
.tab-content
- if password_authentication_enabled? || ldap_enabled? || crowd_enabled?
- if password_authentication_enabled_for_web? || ldap_enabled? || crowd_enabled?
= render 'devise/shared/signin_box'
-# Signup only makes sense if you can also sign-in
- if password_authentication_enabled? && signup_enabled?
- if allow_signup?
= render 'devise/shared/signup_box'
-# Show a message if none of the mechanisms above are enabled
- if !password_authentication_enabled? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
- if !password_authentication_enabled_for_web? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
%div
No authentication methods configured.
......
......@@ -2,7 +2,7 @@
<%= link_to "Sign in", new_session_path(resource_name), class: "btn" %><br />
<% end -%>
<%- if devise_mapping.registerable? && controller_name != 'registrations' && gitlab_config.signup_enabled %>
<%- if devise_mapping.registerable? && controller_name != 'registrations' && allow_signup? %>
<%= link_to "Sign up", new_registration_path(resource_name) %><br />
<% end -%>
......
......@@ -13,12 +13,12 @@
.login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && !crowd_enabled?) }
.login-body
= render 'devise/sessions/new_ldap', server: server
- if password_authentication_enabled?
- if password_authentication_enabled_for_web?
.login-box.tab-pane{ id: 'ldap-standard', role: 'tabpanel' }
.login-body
= render 'devise/sessions/new_base'
- elsif password_authentication_enabled?
- elsif password_authentication_enabled_for_web?
.login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' }
.login-body
= render 'devise/sessions/new_base'
......@@ -8,9 +8,9 @@
- @ldap_servers.each_with_index do |server, i|
%li{ class: active_when(i.zero? && !crowd_enabled?) }
= link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab'
- if password_authentication_enabled?
- if password_authentication_enabled_for_web?
%li
= link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab'
- if password_authentication_enabled? && signup_enabled?
- if allow_signup?
%li
= link_to 'Register', '#register-pane', 'data-toggle' => 'tab'
%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist' }
%li.active{ role: 'presentation' }
%a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in
- if password_authentication_enabled? && signup_enabled?
- if allow_signup?
%li{ role: 'presentation' }
%a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register
......@@ -10,6 +10,10 @@
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; }
.hidden {
display: none !important;
visibility: hidden !important;
}
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
......
......@@ -85,7 +85,7 @@
= link_to profile_emails_path do
%strong.fly-out-top-item-name
#{ _('Emails') }
- unless current_user.ldap_user?
- if current_user.allow_password_authentication?
= nav_link(controller: :passwords) do
= link_to edit_profile_password_path do
.nav-icon-container
......
%p
Hi #{@user['name']}!
%p
- if Gitlab.config.gitlab.signup_enabled
- if current_application_settings.allow_signup?
Your account has been created successfully.
- else
The Administrator created an account for you. Now you are a member of the company GitLab application.
......
......@@ -2,7 +2,7 @@
.js-file-title.file-title-flex-parent
= render 'projects/blob/header_content', blob: blob
.file-actions.hidden-xs
.file-actions
= render 'projects/blob/viewer_switcher', blob: blob unless blame
.btn-group{ role: "group" }<
......
......@@ -41,7 +41,7 @@
- if @branches.any?
%ul.content-list.all-branches
- @branches.each do |branch|
= render "projects/branches/branch", branch: branch, merged: @repository.merged_to_root_ref?(branch, @merged_branch_names)
= render "projects/branches/branch", branch: branch, merged: @merged_branch_names.include?(branch.name)
= paginate @branches, theme: 'gitlab'
- else
.nothing-here-block
......
- if current_user
= link_to toggle_star_project_path(@project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do
- if current_user.starred?(@project)
= icon('star')
= sprite_icon('star')
%span.starred= _('Unstar')
- else
= icon('star-o')
= sprite_icon('star-o')
%span= s_('StarProject|Star')
.count-with-arrow
%span.arrow
......@@ -13,7 +13,7 @@
- else
= link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: _('You must sign in to star a project') do
= icon('star')
= sprite_icon('star')
#{ s_('StarProject|Star') }
.count-with-arrow
%span.arrow
......
......@@ -5,6 +5,8 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag("environments_folder")
#environments-folder-list-view{ data: { "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
#environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json),
"folder-name" => @folder,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
"can-read-environment" => can?(current_user, :read_environment, @project).to_s,
"css-class" => container_class } }
......@@ -3,15 +3,13 @@
- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag("common_vue")
= page_specific_javascript_bundle_tag("environments")
#environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
"can-read-environment" => can?(current_user, :read_environment, @project).to_s,
"can-create-environment" => can?(current_user, :create_environment, @project).to_s,
"project-environments-path" => project_environments_path(@project),
"project-stopped-environments-path" => project_environments_path(@project, scope: :stopped),
"new-environment-path" => new_project_environment_path(@project),
"help-page-path" => help_page_path("ci/environments"),
"css-class" => container_class } }
......@@ -9,7 +9,7 @@
.controls.hidden-xs
- if can?(current_user, :admin_group, group)
= link_to edit_group_path(group), class: "btn" do
= icon('cogs')
= sprite_icon('settings')
= link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: s_("GroupsTree|Leave this group") do
= icon('sign-out')
......
......@@ -9,7 +9,7 @@ class PipelineScheduleWorker
pipeline = Ci::CreatePipelineService.new(schedule.project,
schedule.owner,
ref: schedule.ref)
.execute(:schedule, save_on_errors: false, schedule: schedule)
.execute(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
schedule.deactivate! unless pipeline.persisted?
rescue => e
......
---
title: Add edit button to mobile file view
merge_request: 15199
author: Travis Miller
type: added
---
title: Allow password authentication to be disabled entirely
merge_request: 15223
author: Markus Koller
type: changed
---
title: Avoid deactivation when pipeline schedules execute a branch includes `[ci skip]`
comment
merge_request: 15405
author:
type: fixed
---
title: Fix link text from group context
merge_request:
author:
type: fixed
---
title: Make finding most recent merge request diffs more efficient
merge_request:
author:
type: performance
......@@ -287,7 +287,7 @@ rescue ArgumentError # no user configured
end
Settings.gitlab['time_zone'] ||= nil
Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].nil?
Settings.gitlab['password_authentication_enabled'] ||= true if Settings.gitlab['password_authentication_enabled'].nil?
Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil?
Settings.gitlab['restricted_visibility_levels'] = Settings.__send__(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], [])
Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil?
Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil?
......
......@@ -115,7 +115,5 @@ Rails.application.routes.draw do
root to: "root#index"
draw :test if Rails.env.test?
get '*unmatched_route', to: 'application#route_not_found'
end
get '/unicorn_test/pid' => 'unicorn_test#pid'
post '/unicorn_test/kill' => 'unicorn_test#kill'
class RenameApplicationSettingsPasswordAuthenticationEnabledToPasswordAuthenticationEnabledForWeb < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
rename_column_concurrently :application_settings, :password_authentication_enabled, :password_authentication_enabled_for_web
end
def down
cleanup_concurrent_column_rename :application_settings, :password_authentication_enabled_for_web, :password_authentication_enabled
end
end
class AddPasswordAuthenticationEnabledForGitToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :application_settings, :password_authentication_enabled_for_git, :boolean, default: true, null: false
end
end
# This is identical to the stolen background migration, which already has specs.
class PopulateMergeRequestsLatestMergeRequestDiffIdTakeTwo < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
BATCH_SIZE = 1_000
class MergeRequest < ActiveRecord::Base
self.table_name = 'merge_requests'
include ::EachBatch
end
disable_ddl_transaction!
def up
Gitlab::BackgroundMigration.steal('PopulateMergeRequestsLatestMergeRequestDiffId')
update = '
latest_merge_request_diff_id = (
SELECT MAX(id)
FROM merge_request_diffs
WHERE merge_requests.id = merge_request_diffs.merge_request_id
)'.squish
MergeRequest.where(latest_merge_request_diff_id: nil).each_batch(of: BATCH_SIZE) do |relation|
relation.update_all(update)
end
end
end
class AddEnvironmentScopeToClusters < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:clusters, :environment_scope, :string, default: '*')
end
def down
remove_column(:clusters, :environment_scope)
end
end
......@@ -74,7 +74,6 @@ class MigrateGcpClustersToNewClustersArchitectures < ActiveRecord::Migration
encrypted_access_token_iv: gcp_cluster.encrypted_gcp_token_iv
},
platform_kubernetes_attributes: {
cluster_id: gcp_cluster.id,
api_url: api_url(gcp_cluster.endpoint),
ca_cert: gcp_cluster.ca_cert,
namespace: gcp_cluster.project_namespace,
......
class CleanupApplicationSettingsPasswordAuthenticationEnabledRename < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
cleanup_concurrent_column_rename :application_settings, :password_authentication_enabled, :password_authentication_enabled_for_web
end
def down
rename_column_concurrently :application_settings, :password_authentication_enabled_for_web, :password_authentication_enabled
end
end
......@@ -147,11 +147,14 @@ ActiveRecord::Schema.define(version: 20171121144800) do
t.boolean "authorized_keys_enabled", default: true, null: false
t.boolean "help_page_hide_commercial_content", default: false
t.string "help_page_support_url"
<<<<<<< HEAD
t.boolean "slack_app_enabled", default: false
t.string "slack_app_id"
t.string "slack_app_secret"
t.string "slack_app_verification_token"
t.boolean "password_authentication_enabled"
=======
>>>>>>> ce/master
t.integer "performance_bar_allowed_group_id"
t.boolean "allow_group_owners_to_manage_ldap", default: true, null: false
t.boolean "hashed_storage_enabled", default: false, null: false
......@@ -173,6 +176,8 @@ ActiveRecord::Schema.define(version: 20171121144800) do
t.boolean "throttle_authenticated_web_enabled", default: false, null: false
t.integer "throttle_authenticated_web_requests_per_period", default: 7200, null: false
t.integer "throttle_authenticated_web_period_in_seconds", default: 3600, null: false
t.boolean "password_authentication_enabled_for_web"
t.boolean "password_authentication_enabled_for_git", default: true
end
create_table "approvals", force: :cascade do |t|
......@@ -614,6 +619,7 @@ ActiveRecord::Schema.define(version: 20171121144800) do
t.datetime_with_timezone "updated_at", null: false
t.boolean "enabled", default: true
t.string "name", null: false
t.string "environment_scope", default: "*", null: false
end
add_index "clusters", ["enabled"], name: "index_clusters_on_enabled", using: :btree
......
......@@ -39,6 +39,12 @@ immediately block all access.
>**Note**: GitLab EE supports a configurable sync time, with a default
of one hour.
## Git password authentication
LDAP-enabled users can always authenticate with Git using their GitLab username
or email and LDAP password, even if password authentication for Git is disabled
in the application settings.
## Configuration
To enable LDAP integration you need to add your LDAP server settings in
......
# Repository storages
This document was moved to a [new location](repository_storage_paths.md).
This document was moved to [another location](repository_storage_paths.md).
......@@ -25,7 +25,7 @@ Example response:
"id" : 1,
"default_branch_protection" : 2,
"restricted_visibility_levels" : [],
"password_authentication_enabled" : true,
"password_authentication_enabled_for_web" : true,
"after_sign_out_path" : null,
"max_attachment_size" : 10,
"user_oauth_applications" : true,
......@@ -130,7 +130,8 @@ PUT /application/settings
| `metrics_port` | integer | no | The UDP port to use for connecting to InfluxDB |
| `metrics_sample_interval` | integer | yes (if `metrics_enabled` is `true`) | The sampling interval in seconds. |
| `metrics_timeout` | integer | yes (if `metrics_enabled` is `true`) | The amount of seconds after which InfluxDB will time out. |
| `password_authentication_enabled` | boolean | no | Enable authentication via a GitLab account password. Default is `true`. |
| `password_authentication_enabled_for_web` | boolean | no | Enable authentication for the web interface via a GitLab account password. Default is `true`. |
| `password_authentication_enabled_for_git` | boolean | no | Enable authentication for Git over HTTP(S) via a GitLab account password. Default is `true`. |
| `performance_bar_allowed_group_id` | string | no | The group that is allowed to enable the performance bar |
| `performance_bar_enabled` | boolean | no | Allow enabling the performance bar |
| `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. |
......@@ -182,7 +183,7 @@ Example response:
"id": 1,
"default_projects_limit": 100000,
"signup_enabled": true,
"password_authentication_enabled": true,
"password_authentication_enabled_for_web": true,
"gravatar_enabled": true,
"sign_in_text": "",
"created_at": "2015-06-12T15:51:55.432Z",
......
......@@ -303,10 +303,10 @@ GitLab.com or http://docs.gitlab.com. Make sure this is discussed with the
Documentation team beforehand.
If you indeed need to change a document's location, do NOT remove the old
document, but rather put a text in it that points to the new location, like:
document, but rather replace all of its contents with a new line:
```
This document was moved to [path/to/new_doc.md](path/to/new_doc.md).
This document was moved to [another location](path/to/new_doc.md).
```
where `path/to/new_doc.md` is the relative path to the root directory `doc/`.
......@@ -320,7 +320,7 @@ For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
1. Replace the contents of `doc/workflow/lfs/lfs_administration.md` with:
```
This document was moved to [administration/lfs.md](../../administration/lfs.md).
This document was moved to [another location](../../administration/lfs.md).
```
1. Find and replace any occurrences of the old location with the new one.
......
......@@ -12,8 +12,9 @@ in the project's default branch.
If a commit message or merge request description contains a sentence matching
a certain regular expression, all issues referenced from the matched text will
be closed. This happens when the commit is pushed to a project's **default**
branch, or when a commit or merge request is merged into it.
be closed. This happens when the commit is pushed to a project's
[**default** branch](../repository/branches/index.md#default-branch), or when a
commit or merge request is merged into it.
## Default closing pattern value
......
This document was moved to a [new location](../../user/project/import/index.md).
This document was moved to [another location](../../user/project/import/index.md).
This document was moved to a [new location](../../user/project/import/bitbucket.md).
This document was moved to [another location](../../user/project/import/bitbucket.md).
This document was moved to a [new location](../../user/project/import/fogbugz.md).
This document was moved to [another location](../../user/project/import/fogbugz.md).
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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