Commit 54cf7dac authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into list-multiple-clusters

* master: (53 commits)
  Updated timeago.js
  Prevent 500 error when inspecting job after trigger was removed
  Add classList polyfill for IE classList.toggle(.., force)
  Use feature flag instead of application settigns to control if method calls should be instrumented
  Rename wip to worker_id
  add missing schema entry and application settigns helper
  Use only real duration to measure method call performance via Prometheus
  check method timing threshold when observing method performance
  Add changelog for #15558
  Reenable prometheus metrics
  Move prometheus middle ware to prometheus initialized.
  Fix WIP system note not being created
  Update CHANGELOG.md for 10.2.2
  Fix hashed storage for attachments bugs
  Rename fetch_refs to refmap
  Clean up repository fetch and mirror methods
  Fix pulling and pushing using a personal access token with the sudo scope
  Allow password authentication to be disabled entirely
  Simplify the DB changes checklist
  Added Rubocop config for background migrations
  ...
parents 0bee0186 7c1e54d5
Remove this section and replace it with a description of your MR. Also follow the Add a description of your merge request here. Merge requests without an adequate
checklist below and check off any tasks that are done. If a certain task can not description will not be reviewed until one is added.
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.
## Database Checklist ## Database Checklist
...@@ -23,34 +8,23 @@ When adding migrations: ...@@ -23,34 +8,23 @@ When adding migrations:
- [ ] Updated `db/schema.rb` - [ ] Updated `db/schema.rb`
- [ ] Added a `down` method so the migration can be reverted - [ ] Added a `down` method so the migration can be reverted
- [ ] Added the output of the migration(s) to the MR body - [ ] 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)
- [ ] 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
When adding or modifying queries to improve performance: When adding or modifying queries to improve performance:
- [ ] Included the raw SQL queries of the relevant queries - [ ] Included data that shows the performance improvement, preferably in the form of a benchmark
- [ ] Included the output of `EXPLAIN ANALYZE` and execution timings of the - [ ] Included the output of `EXPLAIN (ANALYZE, BUFFERS)` of the relevant queries
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
When adding foreign keys to existing tables: 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 - [ ] Removed any instances of `dependent: ...` that may no longer be necessary
When adding tables: When adding tables:
- [ ] Ordered columns based on their type sizes in descending order - [ ] 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 if necessary - [ ] Added foreign keys to any columns pointing to data in other tables
- [ ] Added indexes if necessary - [ ] 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: When removing columns, tables, indexes or other structures:
...@@ -64,8 +38,6 @@ When removing columns, tables, indexes or other structures: ...@@ -64,8 +38,6 @@ When removing columns, tables, indexes or other structures:
- [ ] API support added - [ ] API support added
- [ ] Tests added for this feature/bug - [ ] Tests added for this feature/bug
- Review - Review
- [ ] Has been reviewed by UX
- [ ] Has been reviewed by Frontend
- [ ] Has been reviewed by Backend - [ ] Has been reviewed by Backend
- [ ] Has been reviewed by Database - [ ] Has been reviewed by Database
- [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html)
......
...@@ -2,6 +2,35 @@ ...@@ -2,6 +2,35 @@
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.2.2 (2017-11-23)
### Fixed (5 changes)
- Label addition/removal are not going to be redacted wrongfully in the API. !15080
- Fix bitbucket wiki import with hashed storage enabled. !15490
- Impersonation no longer gets stuck on password change. !15497
- Fix blank states using old css.
- Fix promoting milestone updating all issuables without milestone.
### Performance (3 changes)
- Update Issue Boards to fetch the notification subscription status asynchronously.
- Update composite pipelines index to include "id".
- Use arrays in Pipeline#latest_builds_with_artifacts.
### Other (2 changes)
- Don't move repositories and attachments for projects using hashed storage. !15479
- Add logs for monitoring the merge process.
## 10.2.1 (2017-11-22)
### Fixed (1 change)
- Force disable Prometheus metrics.
## 10.2.0 (2017-11-22) ## 10.2.0 (2017-11-22)
### Security (4 changes) ### Security (4 changes)
......
...@@ -283,7 +283,7 @@ group :metrics do ...@@ -283,7 +283,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false gem 'influxdb', '~> 0.2', require: false
# Prometheus # Prometheus
gem 'prometheus-client-mmap', '~>0.7.0.beta18' gem 'prometheus-client-mmap', '~> 0.7.0.beta36'
gem 'raindrops', '~> 0.18' gem 'raindrops', '~> 0.18'
end end
......
...@@ -488,7 +488,7 @@ GEM ...@@ -488,7 +488,7 @@ GEM
mini_mime (0.1.4) mini_mime (0.1.4)
mini_portile2 (2.3.0) mini_portile2 (2.3.0)
minitest (5.7.0) minitest (5.7.0)
mmap2 (2.2.7) mmap2 (2.2.9)
mousetrap-rails (1.4.6) mousetrap-rails (1.4.6)
multi_json (1.12.2) multi_json (1.12.2)
multi_xml (0.6.0) multi_xml (0.6.0)
...@@ -625,8 +625,8 @@ GEM ...@@ -625,8 +625,8 @@ GEM
parser parser
unparser unparser
procto (0.0.3) procto (0.0.3)
prometheus-client-mmap (0.7.0.beta18) prometheus-client-mmap (0.7.0.beta36)
mmap2 (~> 2.2, >= 2.2.7) mmap2 (~> 2.2, >= 2.2.9)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
...@@ -1111,7 +1111,7 @@ DEPENDENCIES ...@@ -1111,7 +1111,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3) peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2) pg (~> 0.18.2)
premailer-rails (~> 1.9.7) premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta18) prometheus-client-mmap (~> 0.7.0.beta36)
pry-byebug (~> 3.4.1) pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1) rack-attack (~> 4.4.1)
......
...@@ -10,6 +10,7 @@ import 'core-js/fn/string/from-code-point'; ...@@ -10,6 +10,7 @@ import 'core-js/fn/string/from-code-point';
import 'core-js/fn/symbol'; import 'core-js/fn/symbol';
// Browser polyfills // Browser polyfills
import 'classlist-polyfill';
import './polyfills/custom_event'; import './polyfills/custom_event';
import './polyfills/element'; import './polyfills/element';
import './polyfills/event'; import './polyfills/event';
......
<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 Visibility from 'visibilityjs';
import Flash from '../../flash';
import EnvironmentsService from '../services/environments_service';
import environmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils';
import eventHub from '../event_hub';
import Poll from '../../lib/utils/poll';
import environmentsMixin from '../mixins/environments_mixin';
export default {
components: {
environmentTable,
tablePagination,
loadingIcon,
},
mixins: [
environmentsMixin,
],
data() {
const environmentsData = document.querySelector('#environments-list-view').dataset;
const store = new EnvironmentsStore();
return {
store,
state: store.state,
visibility: 'available',
isLoading: false,
cssContainerClass: environmentsData.cssClass,
endpoint: environmentsData.environmentsDataEndpoint,
canCreateDeployment: environmentsData.canCreateDeployment,
canReadEnvironment: environmentsData.canReadEnvironment,
canCreateEnvironment: environmentsData.canCreateEnvironment,
projectEnvironmentsPath: environmentsData.projectEnvironmentsPath,
projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
isMakingRequest: false,
// Pagination Properties,
paginationInformation: {},
pageNumber: 1,
};
},
computed: {
scope() {
return getParameterByName('scope');
},
canReadEnvironmentParsed() {
return convertPermissionToBoolean(this.canReadEnvironment);
},
canCreateDeploymentParsed() {
return convertPermissionToBoolean(this.canCreateDeployment);
},
canCreateEnvironmentParsed() {
return convertPermissionToBoolean(this.canCreateEnvironment);
},
},
/**
* Fetches all the environments and stores them.
* Toggles loading property.
*/
created() {
const scope = getParameterByName('scope') || this.visibility;
const page = getParameterByName('page') || this.pageNumber;
this.service = new EnvironmentsService(this.endpoint);
const poll = new Poll({
resource: this.service,
method: 'get',
data: { scope, page },
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: (isMakingRequest) => {
this.isMakingRequest = isMakingRequest;
},
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
eventHub.$on('toggleFolder', this.toggleFolder);
eventHub.$on('postAction', this.postAction);
},
beforeDestroy() {
eventHub.$off('toggleFolder');
eventHub.$off('postAction');
},
methods: {
toggleFolder(folder) {
this.store.toggleFolder(folder);
if (!folder.isOpen) {
this.fetchChildEnvironments(folder, true);
}
},
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
* @return {String}
*/
changePage(pageNumber) {
const param = setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
fetchEnvironments() {
const scope = getParameterByName('scope') || this.visibility;
const page = getParameterByName('page') || this.pageNumber;
this.isLoading = true;
return this.service.get({ scope, page })
.then(this.successCallback)
.catch(this.errorCallback);
},
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(() => {
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.');
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
});
},
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.'));
}
},
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));
}
},
errorCallback() {
this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.');
},
},
};
</script>
<template>
<div :class="cssContainerClass">
<div class="top-area">
<ul
v-if="!isLoading"
class="nav-links">
<li :class="{ active: scope === null || scope === 'available' }">
<a :href="projectEnvironmentsPath">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li>
<li :class="{ active : scope === 'stopped' }">
<a :href="projectStoppedEnvironmentsPath">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
<div
v-if="canCreateEnvironmentParsed && !isLoading"
class="nav-controls">
<a
:href="newEnvironmentPath"
class="btn btn-create">
New environment
</a>
</div>
</div>
<div class="environments-container">
<loading-icon
label="Loading environments"
size="3"
v-if="isLoading"
/>
<div
class="blank-state-row"
v-if="!isLoading && state.environments.length === 0">
<div class="blank-state-center">
<h2 class="blank-state-title js-blank-state-title">
You don't have any environments right now.
</h2>
<p class="blank-state-text">
Environments are places where code gets deployed, such as staging or production.
<br />
<a :href="helpPagePath">
Read more about environments
</a>
</p>
<a
v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create js-new-environment-button">
New environment
</a>
</div>
</div>
<div
class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<environment-table
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
/>
</div>
<table-pagination
v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation" />
</div>
</div>
</template>
<script> <script>
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale';
/** /**
* Renders the external url link in environments table. * Renders the external url link in environments table.
...@@ -18,7 +19,7 @@ export default { ...@@ -18,7 +19,7 @@ export default {
computed: { computed: {
title() { title() {
return 'Open'; return s__('Environments|Open');
}, },
}, },
}; };
......
...@@ -432,7 +432,7 @@ export default { ...@@ -432,7 +432,7 @@ export default {
v-if="!model.isFolder" v-if="!model.isFolder"
class="table-mobile-header" class="table-mobile-header"
role="rowheader"> role="rowheader">
Environment {{s__("Environments|Environment")}}
</div> </div>
<a <a
v-if="!model.isFolder" v-if="!model.isFolder"
...@@ -505,7 +505,7 @@ export default { ...@@ -505,7 +505,7 @@ export default {
<div <div
role="rowheader" role="rowheader"
class="table-mobile-header"> class="table-mobile-header">
Commit {{s__("Environments|Commit")}}
</div> </div>
<div <div
v-if="hasLastDeploymentKey" v-if="hasLastDeploymentKey"
...@@ -521,7 +521,7 @@ export default { ...@@ -521,7 +521,7 @@ export default {
<div <div
v-if="!hasLastDeploymentKey" v-if="!hasLastDeploymentKey"
class="commit-title table-mobile-content"> class="commit-title table-mobile-content">
No deployments yet {{s__("Environments|No deployments yet")}}
</div> </div>
</div> </div>
...@@ -531,7 +531,7 @@ export default { ...@@ -531,7 +531,7 @@ export default {
<div <div
role="rowheader" role="rowheader"
class="table-mobile-header"> class="table-mobile-header">
Updated {{s__("Environments|Updated")}}
</div> </div>
<span <span
v-if="canShowDate" v-if="canShowDate"
......
...@@ -34,6 +34,7 @@ export default { ...@@ -34,6 +34,7 @@ export default {
:aria-label="title"> :aria-label="title">
<i <i
class="fa fa-area-chart" class="fa fa-area-chart"
aria-hidden="true" /> aria-hidden="true"
/>
</a> </a>
</template> </template>
...@@ -48,10 +48,10 @@ export default { ...@@ -48,10 +48,10 @@ export default {
:disabled="isLoading"> :disabled="isLoading">
<span v-if="isLastDeployment"> <span v-if="isLastDeployment">
Re-deploy {{s__("Environments|Re-deploy")}}
</span> </span>
<span v-else> <span v-else>
Rollback {{s__("Environments|Rollback")}}
</span> </span>
<loading-icon v-if="isLoading" /> <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,12 +2,12 @@ ...@@ -2,12 +2,12 @@
/** /**
* Render environments table. * Render environments table.
*/ */
import EnvironmentTableRowComponent from './environment_item.vue'; import environmentItem from './environment_item.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default { export default {
components: { components: {
'environment-item': EnvironmentTableRowComponent, environmentItem,
loadingIcon, loadingIcon,
}, },
...@@ -42,19 +42,19 @@ export default { ...@@ -42,19 +42,19 @@ export default {
<div class="ci-table" role="grid"> <div class="ci-table" role="grid">
<div class="gl-responsive-table-row table-row-header" role="row"> <div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section section-10 environments-name" role="columnheader"> <div class="table-section section-10 environments-name" role="columnheader">
Environment {{s__("Environments|Environment")}}
</div> </div>
<div class="table-section section-10 environments-deploy" role="columnheader"> <div class="table-section section-10 environments-deploy" role="columnheader">
Deployment {{s__("Environments|Deployment")}}
</div> </div>
<div class="table-section section-15 environments-build" role="columnheader"> <div class="table-section section-15 environments-build" role="columnheader">
Job {{s__("Environments|Job")}}
</div> </div>
<div class="table-section section-25 environments-commit" role="columnheader"> <div class="table-section section-25 environments-commit" role="columnheader">
Commit {{s__("Environments|Commit")}}
</div> </div>
<div class="table-section section-10 environments-date" role="columnheader"> <div class="table-section section-10 environments-date" role="columnheader">
Updated {{s__("Environments|Updated")}}
</div> </div>
</div> </div>
<template <template
...@@ -86,7 +86,7 @@ export default { ...@@ -86,7 +86,7 @@ export default {
<a <a
:href="folderUrl(model)" :href="folderUrl(model)"
class="btn btn-default"> class="btn btn-default">
Show all {{s__("Environments|Show all")}}
</a> </a>
</div> </div>
</div> </div>
......
import Vue from 'vue'; 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({ document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#environments-list-view', el: '#environments-list-view',
components: { 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 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);
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#environments-folder-list-view', el: '#environments-folder-list-view',
components: { components: {
'environments-folder-app': EnvironmentsFolderComponent, 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,
},
});
}, },
render: createElement => createElement('environments-folder-app'),
})); }));
<script> <script>
import Visibility from 'visibilityjs'; import environmentsMixin from '../mixins/environments_mixin';
import Flash from '../../flash'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import EnvironmentsService from '../services/environments_service';
import environmentTable from '../components/environments_table.vue'; export default {
import EnvironmentsStore from '../stores/environments_store'; props: {
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; endpoint: {
import tablePagination from '../../vue_shared/components/table_pagination.vue'; type: String,
import Poll from '../../lib/utils/poll'; required: true,
import eventHub from '../event_hub'; },
import environmentsMixin from '../mixins/environments_mixin'; folderName: {
import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils'; type: String,
required: true,
export default { },
components: { cssContainerClass: {
environmentTable, type: String,
tablePagination, required: true,
loadingIcon, },
}, canCreateDeployment: {
type: Boolean,
mixins: [ required: true,
environmentsMixin, },
], canReadEnvironment: {
type: Boolean,
data() { required: true,
const environmentsData = document.querySelector('#environments-folder-list-view').dataset;
const store = new EnvironmentsStore();
const pathname = window.location.pathname;
const endpoint = `${pathname}.json`;
const folderName = pathname.substr(pathname.lastIndexOf('/') + 1);
return {
store,
folderName,
endpoint,
state: store.state,
visibility: 'available',
isLoading: false,
cssContainerClass: environmentsData.cssClass,
canCreateDeployment: environmentsData.canCreateDeployment,
canReadEnvironment: environmentsData.canReadEnvironment,
// Pagination Properties,
paginationInformation: {},
pageNumber: 1,
};
},
computed: {
scope() {
return getParameterByName('scope');
},
canReadEnvironmentParsed() {
return convertPermissionToBoolean(this.canReadEnvironment);
},
canCreateDeploymentParsed() {
return convertPermissionToBoolean(this.canCreateDeployment);
},
/**
* URL to link in the stopped tab.
*
* @return {String}
*/
stoppedPath() {
return `${window.location.pathname}?scope=stopped`;
},
/**
* URL to link in the available tab.
*
* @return {String}
*/
availablePath() {
return window.location.pathname;
},
},
/**
* Fetches all the environments and stores them.
* Toggles loading property.
*/
created() {
const scope = getParameterByName('scope') || this.visibility;
const page = getParameterByName('page') || this.pageNumber;
this.service = new EnvironmentsService(this.endpoint);
const poll = new Poll({
resource: this.service,
method: 'get',
data: { scope, page },
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: (isMakingRequest) => {
this.isMakingRequest = isMakingRequest;
}, },
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
eventHub.$on('postAction', this.postAction);
},
beforeDestroyed() {
eventHub.$off('postAction');
},
methods: {
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
*/
changePage(pageNumber) {
const param = setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
fetchEnvironments() {
const scope = getParameterByName('scope') || this.visibility;
const page = getParameterByName('page') || this.pageNumber;
this.isLoading = true;
return this.service.get({ scope, page })
.then(this.successCallback)
.catch(this.errorCallback);
},
successCallback(resp) {
this.saveData(resp);
},
errorCallback() {
this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.');
}, },
mixins: [
postAction(endpoint) { environmentsMixin,
if (!this.isMakingRequest) { CIPaginationMixin,
this.isLoading = true; ],
methods: {
this.service.postAction(endpoint) successCallback(resp) {
.then(() => this.fetchEnvironments()) this.saveData(resp);
.catch(() => new Flash('An error occurred while making the request.')); },
}
}, },
}, };
};
</script> </script>
<template> <template>
<div :class="cssContainerClass"> <div :class="cssContainerClass">
...@@ -171,56 +43,23 @@ export default { ...@@ -171,56 +43,23 @@ export default {
v-if="!isLoading"> v-if="!isLoading">
<h4 class="js-folder-name environments-folder-name"> <h4 class="js-folder-name environments-folder-name">
Environments / <b>{{folderName}}</b> {{s__("Environments|Environments")}} / <b>{{folderName}}</b>
</h4> </h4>
<ul class="nav-links"> <tabs
<li :class="{ active: scope === null || scope === 'available' }"> :tabs="tabs"
<a @onChangeTab="onChangeTab"
:href="availablePath" scope="environments"
class="js-available-environments-folder-tab">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li>
<li :class="{ active : scope === 'stopped' }">
<a
:href="stoppedPath"
class="js-stopped-environments-folder-tab">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
</div>
<div class="environments-container">
<loading-icon
label="Loading environments"
v-if="isLoading"
size="3"
/> />
<div
class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<environment-table
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
/>
<table-pagination
v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation"/>
</div>
</div> </div>
<container
:is-loading="isLoading"
:environments="state.environments"
:pagination="state.paginationInformation"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
@onChangePage="onChangePage"
/>
</div> </div>
</template> </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 { 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: { methods: {
saveData(resp) { saveData(resp) {
const headers = resp.headers; const headers = resp.headers;
return resp.json().then((response) => { return resp.json().then((response) => {
this.isLoading = false; this.isLoading = false;
this.store.storeAvailableCount(response.available_count); if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
this.store.storeStoppedCount(response.stopped_count); this.store.storeAvailableCount(response.available_count);
this.store.storeEnvironments(response.environments); this.store.storeStoppedCount(response.stopped_count);
this.store.setPagination(headers); 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');
}, },
}; };
...@@ -36,7 +36,12 @@ export default class EnvironmentsStore { ...@@ -36,7 +36,12 @@ export default class EnvironmentsStore {
storeEnvironments(environments = []) { storeEnvironments(environments = []) {
const filteredEnvironments = environments.map((env) => { const filteredEnvironments = environments.map((env) => {
const oldEnvironmentState = this.state.environments 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 = {}; let filtered = {};
......
...@@ -4,9 +4,11 @@ import tooltip from '../../vue_shared/directives/tooltip'; ...@@ -4,9 +4,11 @@ import tooltip from '../../vue_shared/directives/tooltip';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { COMMON_STR } from '../constants'; import { COMMON_STR } from '../constants';
import Icon from '../../vue_shared/components/icon.vue';
export default { export default {
components: { components: {
Icon,
PopupDialog, PopupDialog,
}, },
directives: { directives: {
...@@ -63,9 +65,9 @@ export default { ...@@ -63,9 +65,9 @@ export default {
:aria-label="editBtnTitle" :aria-label="editBtnTitle"
data-container="body" data-container="body"
class="edit-group btn no-expand"> class="edit-group btn no-expand">
<i <icon
class="fa fa-cogs" name="settings">
aria-hidden="true"/> </icon>
</a> </a>
<a <a
v-tooltip v-tooltip
......
...@@ -269,46 +269,6 @@ export const parseIntPagination = paginationInformation => ({ ...@@ -269,46 +269,6 @@ export const parseIntPagination = paginationInformation => ({
previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10), 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. * Given a string of query parameters creates an object.
* *
......
...@@ -69,8 +69,6 @@ import './project_import'; ...@@ -69,8 +69,6 @@ import './project_import';
import './projects_dropdown'; import './projects_dropdown';
import './projects_list'; import './projects_list';
import './syntax_highlight'; import './syntax_highlight';
import './render_math';
import './render_mermaid';
import './render_gfm'; import './render_gfm';
import './right_sidebar'; import './right_sidebar';
import './search'; import './search';
......
import Vue from 'vue'; import axios from '../../lib/utils/axios_utils';
import VueResource from 'vue-resource';
import statusCodes from '../../lib/utils/http_status'; import statusCodes from '../../lib/utils/http_status';
import { backOff } from '../../lib/utils/common_utils'; import { backOff } from '../../lib/utils/common_utils';
Vue.use(VueResource);
const MAX_REQUESTS = 3; const MAX_REQUESTS = 3;
function backOffRequest(makeRequestCallback) { function backOffRequest(makeRequestCallback) {
...@@ -32,8 +29,8 @@ export default class MonitoringService { ...@@ -32,8 +29,8 @@ export default class MonitoringService {
} }
getGraphsData() { getGraphsData() {
return backOffRequest(() => Vue.http.get(this.metricsEndpoint)) return backOffRequest(() => axios.get(this.metricsEndpoint))
.then(resp => resp.json()) .then(resp => resp.data)
.then((response) => { .then((response) => {
if (!response || !response.data) { if (!response || !response.data) {
throw new Error('Unexpected metrics data response from prometheus endpoint'); throw new Error('Unexpected metrics data response from prometheus endpoint');
...@@ -43,8 +40,8 @@ export default class MonitoringService { ...@@ -43,8 +40,8 @@ export default class MonitoringService {
} }
getDeploymentData() { getDeploymentData() {
return backOffRequest(() => Vue.http.get(this.deploymentEndpoint)) return backOffRequest(() => axios.get(this.deploymentEndpoint))
.then(resp => resp.json()) .then(resp => resp.data)
.then((response) => { .then((response) => {
if (!response || !response.deployments) { if (!response || !response.deployments) {
throw new Error('Unexpected deployment data response from prometheus endpoint'); throw new Error('Unexpected deployment data response from prometheus endpoint');
......
...@@ -3,15 +3,14 @@ ...@@ -3,15 +3,14 @@
import PipelinesService from '../services/pipelines_service'; import PipelinesService from '../services/pipelines_service';
import pipelinesMixin from '../mixins/pipelines'; import pipelinesMixin from '../mixins/pipelines';
import tablePagination from '../../vue_shared/components/table_pagination.vue'; 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 navigationControls from './nav_controls.vue';
import { import {
convertPermissionToBoolean, convertPermissionToBoolean,
getParameterByName, getParameterByName,
historyPushState,
buildUrlWithCurrentLocation,
parseQueryStringIntoObject, parseQueryStringIntoObject,
} from '../../lib/utils/common_utils'; } from '../../lib/utils/common_utils';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default { export default {
props: { props: {
...@@ -36,6 +35,7 @@ ...@@ -36,6 +35,7 @@
}, },
mixins: [ mixins: [
pipelinesMixin, pipelinesMixin,
CIPaginationMixin,
], ],
data() { data() {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
...@@ -170,22 +170,8 @@ ...@@ -170,22 +170,8 @@
* - Update the internal state * - Update the internal state
*/ */
updateContent(parameters) { updateContent(parameters) {
// stop polling this.updateInternalState(parameters);
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;
// fetch new data // fetch new data
return this.service.getPipelines(this.requestData) return this.service.getPipelines(this.requestData)
.then((response) => { .then((response) => {
...@@ -203,14 +189,6 @@ ...@@ -203,14 +189,6 @@
this.poll.restart(); 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> </script>
...@@ -235,6 +213,7 @@ ...@@ -235,6 +213,7 @@
<navigation-tabs <navigation-tabs
:tabs="tabs" :tabs="tabs"
@onChangeTab="onChangeTab" @onChangeTab="onChangeTab"
scope="pipelines"
/> />
<navigation-controls <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 // Render Gitlab flavoured Markdown
// //
// Delegates to syntax highlight and render math & mermaid diagrams. // Delegates to syntax highlight and render math & mermaid diagrams.
// //
(function() { $.fn.renderGFM = function renderGFM() {
$.fn.renderGFM = function() { this.find('.js-syntax-highlight').syntaxHighlight();
this.find('.js-syntax-highlight').syntaxHighlight(); renderMath(this.find('.js-render-math'));
this.find('.js-render-math').renderMath(); renderMermaid(this.find('.js-render-mermaid'));
this.find('.js-render-mermaid').renderMermaid(); return this;
return this; };
};
$(() => $('body').renderGFM()); $(() => $('body').renderGFM());
}).call(window);
/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len, no-console */
/* global katex */ /* global katex */
// Renders math using KaTeX in any element with the // Renders math using KaTeX in any element with the
...@@ -8,49 +7,45 @@ ...@@ -8,49 +7,45 @@
// //
// <code class="js-render-math"></div> // <code class="js-render-math"></div>
// //
(function() {
// Only load once // Only load once
var katexLoaded = false; let katexLoaded = false;
// Loop over all math elements and render math // Loop over all math elements and render math
var renderWithKaTeX = function (elements) { function renderWithKaTeX(elements) {
elements.each(function () { elements.each(function katexElementsLoop() {
var mathNode = $('<span></span>'); const mathNode = $('<span></span>');
var $this = $(this); const $this = $(this);
var display = $this.attr('data-math-style') === 'display'; const display = $this.attr('data-math-style') === 'display';
try { try {
katex.render($this.text(), mathNode.get(0), { displayMode: display }); katex.render($this.text(), mathNode.get(0), { displayMode: display });
mathNode.insertAfter($this); mathNode.insertAfter($this);
$this.remove(); $this.remove();
} catch (err) { } catch (err) {
// What can we do?? throw err;
console.log(err.message); }
} });
}); }
};
$.fn.renderMath = function() { export default function renderMath($els) {
var $this = this; if (!$els.length) return;
if ($this.length === 0) return;
if (katexLoaded) renderWithKaTeX($this); if (katexLoaded) {
else { renderWithKaTeX($els);
// Request CSS file so it is in the cache } else {
$.get(gon.katex_css_url, function() { $.get(gon.katex_css_url, () => {
var css = $('<link>', const css = $('<link>', {
{ rel: 'stylesheet', rel: 'stylesheet',
type: 'text/css', type: 'text/css',
href: gon.katex_css_url, href: gon.katex_css_url,
}); });
css.appendTo('head'); css.appendTo('head');
// Load KaTeX js // Load KaTeX js
$.getScript(gon.katex_js_url, function() { $.getScript(gon.katex_js_url, () => {
katexLoaded = true; katexLoaded = true;
renderWithKaTeX($this); // Run KaTeX renderWithKaTeX($els); // Run KaTeX
});
}); });
} });
}; }
}).call(window); }
...@@ -14,8 +14,8 @@ ...@@ -14,8 +14,8 @@
import Flash from './flash'; import Flash from './flash';
$.fn.renderMermaid = function renderMermaid() { export default function renderMermaid($els) {
if (this.length === 0) return; if (!$els.length) return;
import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid').then((mermaid) => { import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid').then((mermaid) => {
mermaid.initialize({ mermaid.initialize({
...@@ -23,8 +23,10 @@ $.fn.renderMermaid = function renderMermaid() { ...@@ -23,8 +23,10 @@ $.fn.renderMermaid = function renderMermaid() {
theme: 'neutral', theme: 'neutral',
}); });
mermaid.init(undefined, this); $els.each((i, el) => {
mermaid.init(undefined, el);
});
}).catch((err) => { }).catch((err) => {
Flash(`Can't load mermaid module: ${err}`); Flash(`Can't load mermaid module: ${err}`);
}); });
}; }
import Flash from './flash'; import Flash from './flash';
import { __, s__ } from './locale'; import { __, s__ } from './locale';
import { spriteIcon } from './lib/utils/common_utils';
export default class Star { export default class Star {
constructor() { constructor() {
...@@ -7,16 +8,18 @@ export default class Star { ...@@ -7,16 +8,18 @@ export default class Star {
.on('ajax:success', function handleSuccess(e, data) { .on('ajax:success', function handleSuccess(e, data) {
const $this = $(this); const $this = $(this);
const $starSpan = $this.find('span'); const $starSpan = $this.find('span');
const $starIcon = $this.find('i'); const $startIcon = $this.find('svg');
function toggleStar(isStarred) { function toggleStar(isStarred) {
$this.parent().find('.star-count').text(data.star_count); $this.parent().find('.star-count').text(data.star_count);
if (isStarred) { if (isStarred) {
$starSpan.removeClass('starred').text(s__('StarProject|Star')); $starSpan.removeClass('starred').text(s__('StarProject|Star'));
$starIcon.removeClass('fa-star').addClass('fa-star-o'); $startIcon.remove();
$this.prepend(spriteIcon('star-o'));
} else { } else {
$starSpan.addClass('starred').text(__('Unstar')); $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 { ...@@ -14,7 +14,7 @@ export default {
statusObj() { statusObj() {
return { return {
group: this.status, group: this.status,
icon: `icon_status_${this.status}`, icon: `status_${this.status}`,
}; };
}, },
}, },
......
<script> <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 { export default {
name: 'PipelineNavigationTabs', name: 'NavigationTabs',
props: { props: {
tabs: { tabs: {
type: Array, type: Array,
required: true, required: true,
}, },
scope: {
type: String,
required: false,
default: '',
},
}, },
mounted() { mounted() {
$(document).trigger('init.scrolling-tabs'); $(document).trigger('init.scrolling-tabs');
...@@ -34,7 +59,7 @@ ...@@ -34,7 +59,7 @@
<a <a
role="button" role="button"
@click="onTabClick(tab)" @click="onTabClick(tab)"
:class="`js-pipelines-tab-${tab.scope}`" :class="`js-${scope}-tab-${tab.scope}`"
> >
{{ tab.name }} {{ 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 { ...@@ -364,6 +364,18 @@ span.idiff {
float: none; float: none;
} }
} }
@media (max-width: $screen-xs-max) {
display: block;
.file-actions {
white-space: normal;
.btn-group {
padding-top: 5px;
}
}
}
} }
.is-stl-loading { .is-stl-loading {
......
...@@ -196,7 +196,7 @@ class ApplicationController < ActionController::Base ...@@ -196,7 +196,7 @@ class ApplicationController < ActionController::Base
end end
def check_password_expiration 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 password_expires_at = current_user&.password_expires_at
......
...@@ -150,7 +150,7 @@ module IssuableCollections ...@@ -150,7 +150,7 @@ module IssuableCollections
when 'MergeRequest' when 'MergeRequest'
[ [
:source_project, :target_project, :author, :assignee, :labels, :milestone, :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
end end
......
...@@ -8,6 +8,7 @@ module PreviewMarkdown ...@@ -8,6 +8,7 @@ module PreviewMarkdown
case controller_name case controller_name
when 'wikis' then { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] } when 'wikis' then { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
when 'snippets' then { skip_project_check: true } when 'snippets' then { skip_project_check: true }
when 'groups' then { group: group }
else {} else {}
end end
......
...@@ -51,7 +51,7 @@ class InvitesController < ApplicationController ...@@ -51,7 +51,7 @@ class InvitesController < ApplicationController
return if current_user return if current_user
notice = "To accept this invitation, sign in" 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 << "." notice << "."
store_location_for :user, request.fullpath store_location_for :user, request.fullpath
......
...@@ -140,7 +140,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -140,7 +140,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
label = Gitlab::OAuth::Provider.label_for(oauth['provider']) label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed." 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." message << " Create a GitLab account first, and then connect it to your #{label} account."
end end
......
class PasswordsController < Devise::PasswordsController class PasswordsController < Devise::PasswordsController
include Gitlab::CurrentSettings
before_action :resource_from_email, only: [:create] 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] before_action :throttle_reset, only: [:create]
def edit def edit
...@@ -25,7 +27,7 @@ class PasswordsController < Devise::PasswordsController ...@@ -25,7 +27,7 @@ class PasswordsController < Devise::PasswordsController
def update def update
super do |resource| super do |resource|
if resource.valid? && resource.require_password_creation? if resource.valid? && resource.password_automatically_set?
resource.update_attribute(:password_automatically_set, false) resource.update_attribute(:password_automatically_set, false)
end end
end end
...@@ -38,11 +40,15 @@ class PasswordsController < Devise::PasswordsController ...@@ -38,11 +40,15 @@ class PasswordsController < Devise::PasswordsController
self.resource = resource_class.find_by_email(email) self.resource = resource_class.find_by_email(email)
end end
def prevent_ldap_reset def check_password_authentication_available
return unless resource&.ldap_user? 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), redirect_to after_sending_reset_password_instructions_path_for(resource_name),
alert: "Cannot reset password for LDAP user." alert: "Password authentication is unavailable."
end end
def throttle_reset def throttle_reset
......
...@@ -77,7 +77,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController ...@@ -77,7 +77,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
end end
def authorize_change_password! def authorize_change_password!
render_404 if @user.ldap_user? render_404 unless @user.allow_password_authentication?
end end
def user_params def user_params
......
...@@ -34,6 +34,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -34,6 +34,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
folder_environments = project.environments.where(environment_type: params[:id]) folder_environments = project.environments.where(environment_type: params[:id])
@environments = folder_environments.with_state(params[:scope] || :available) @environments = folder_environments.with_state(params[:scope] || :available)
.order(:name) .order(:name)
@folder = params[:id]
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -63,7 +63,7 @@ class SessionsController < Devise::SessionsController ...@@ -63,7 +63,7 @@ class SessionsController < Devise::SessionsController
user = User.admins.last 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| Users::UpdateService.new(current_user, user: user).execute do |user|
@token = user.generate_reset_token @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:
...@@ -3,9 +3,9 @@ module ApplicationSettingsHelper ...@@ -3,9 +3,9 @@ module ApplicationSettingsHelper
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
delegate :gravatar_enabled?, delegate :allow_signup?,
:signup_enabled?, :gravatar_enabled?,
:password_authentication_enabled?, :password_authentication_enabled_for_web?,
:akismet_enabled?, :akismet_enabled?,
:koding_enabled?, :koding_enabled?,
to: :current_application_settings to: :current_application_settings
...@@ -203,7 +203,7 @@ module ApplicationSettingsHelper ...@@ -203,7 +203,7 @@ module ApplicationSettingsHelper
:metrics_port, :metrics_port,
:metrics_sample_interval, :metrics_sample_interval,
:metrics_timeout, :metrics_timeout,
:password_authentication_enabled, :password_authentication_enabled_for_web,
:performance_bar_allowed_group_id, :performance_bar_allowed_group_id,
:performance_bar_enabled, :performance_bar_enabled,
:plantuml_enabled, :plantuml_enabled,
......
...@@ -58,12 +58,12 @@ module ButtonHelper ...@@ -58,12 +58,12 @@ module ButtonHelper
def http_clone_button(project, placement = 'right', append_link: true) def http_clone_button(project, placement = 'right', append_link: true)
klass = 'http-selector' 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 protocol = gitlab_config.protocol.upcase
tooltip_title = 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 } _("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol }
else else
_("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol } _("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol }
......
...@@ -234,11 +234,11 @@ module ProjectsHelper ...@@ -234,11 +234,11 @@ module ProjectsHelper
def show_no_password_message? def show_no_password_message?
cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && 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 end
def link_to_set_password 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 link_to s_('SetPasswordToCloneLink|set a password'), edit_profile_password_path
else else
link_to s_('CreateTokenToCloneLink|create a personal access token'), profile_personal_access_tokens_path link_to s_('CreateTokenToCloneLink|create a personal access token'), profile_personal_access_tokens_path
......
...@@ -276,7 +276,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -276,7 +276,8 @@ class ApplicationSetting < ActiveRecord::Base
koding_url: nil, koding_url: nil,
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_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, performance_bar_allowed_group_id: nil,
rsa_key_restriction: 0, rsa_key_restriction: 0,
plantuml_enabled: false, plantuml_enabled: false,
...@@ -474,6 +475,14 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -474,6 +475,14 @@ class ApplicationSetting < ActiveRecord::Base
has_attribute?(attr_name) ? public_send(attr_name) : FORBIDDEN_KEY_VALUE # rubocop:disable GitlabSecurity/PublicSend has_attribute?(attr_name) ? public_send(attr_name) : FORBIDDEN_KEY_VALUE # rubocop:disable GitlabSecurity/PublicSend
end 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 private
def ensure_uuid! def ensure_uuid!
......
...@@ -243,7 +243,7 @@ module Ci ...@@ -243,7 +243,7 @@ module Ci
@merge_request ||= @merge_request ||=
begin begin
merge_requests = MergeRequest.includes(:merge_request_diff) merge_requests = MergeRequest.includes(:latest_merge_request_diff)
.where(source_branch: ref, .where(source_branch: ref,
source_project: pipeline.project) source_project: pipeline.project)
.reorder(iid: :desc) .reorder(iid: :desc)
......
...@@ -109,12 +109,12 @@ class Commit ...@@ -109,12 +109,12 @@ class Commit
@link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/) @link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/)
end end
def to_reference(from_project = nil, full: false) def to_reference(from = nil, full: false)
commit_reference(from_project, id, full: full) commit_reference(from, id, full: full)
end end
def reference_link_text(from_project = nil, full: false) def reference_link_text(from = nil, full: false)
commit_reference(from_project, short_id, full: full) commit_reference(from, short_id, full: full)
end end
def diff_line_count def diff_line_count
...@@ -381,8 +381,8 @@ class Commit ...@@ -381,8 +381,8 @@ class Commit
private private
def commit_reference(from_project, referable_commit_id, full: false) def commit_reference(from, referable_commit_id, full: false)
reference = project.to_reference(from_project, full: full) reference = project.to_reference(from, full: full)
if reference.present? if reference.present?
"#{reference}#{self.class.reference_prefix}#{referable_commit_id}" "#{reference}#{self.class.reference_prefix}#{referable_commit_id}"
......
...@@ -89,8 +89,8 @@ class CommitRange ...@@ -89,8 +89,8 @@ class CommitRange
alias_method :id, :to_s alias_method :id, :to_s
def to_reference(from_project = nil, full: false) def to_reference(from = nil, full: false)
project_reference = project.to_reference(from_project, full: full) project_reference = project.to_reference(from, full: full)
if project_reference.present? if project_reference.present?
project_reference + self.class.reference_prefix + self.id project_reference + self.class.reference_prefix + self.id
...@@ -99,8 +99,8 @@ class CommitRange ...@@ -99,8 +99,8 @@ class CommitRange
end end
end end
def reference_link_text(from_project = nil) def reference_link_text(from = nil)
project_reference = project.to_reference(from_project) project_reference = project.to_reference(from)
reference = ref_from + notation + ref_to reference = ref_from + notation + ref_to
if project_reference.present? if project_reference.present?
......
...@@ -345,4 +345,11 @@ module Issuable ...@@ -345,4 +345,11 @@ module Issuable
def first_contribution? def first_contribution?
false false
end end
##
# Overriden in MergeRequest
#
def wipless_title_changed(old_title)
old_title != title
end
end end
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 ...@@ -31,11 +31,11 @@ module Mentionable
# #
# By default this will be the class name and the result of calling # By default this will be the class name and the result of calling
# `to_reference` on the object. # `to_reference` on the object.
def gfm_reference(from_project = nil) def gfm_reference(from = nil)
# "MergeRequest" > "merge_request" > "Merge request" > "merge request" # "MergeRequest" > "merge_request" > "Merge request" > "merge request"
friendly_name = self.class.to_s.underscore.humanize.downcase friendly_name = self.class.to_s.underscore.humanize.downcase
"#{friendly_name} #{to_reference(from_project)}" "#{friendly_name} #{to_reference(from)}"
end end
# The GFM reference to this Mentionable, which shouldn't be included in its #references. # The GFM reference to this Mentionable, which shouldn't be included in its #references.
......
...@@ -7,7 +7,7 @@ module Referable ...@@ -7,7 +7,7 @@ module Referable
# Returns the String necessary to reference this object in Markdown # 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. # This should be overridden by the including class.
# #
...@@ -17,12 +17,12 @@ module Referable ...@@ -17,12 +17,12 @@ module Referable
# Issue.last.to_reference(other_project) # => "cross-project#1" # Issue.last.to_reference(other_project) # => "cross-project#1"
# #
# Returns a String # Returns a String
def to_reference(_from_project = nil, full:) def to_reference(_from = nil, full:)
'' ''
end end
def reference_link_text(from_project = nil) def reference_link_text(from = nil)
to_reference(from_project) to_reference(from)
end end
included do included do
......
...@@ -38,11 +38,11 @@ class ExternalIssue ...@@ -38,11 +38,11 @@ class ExternalIssue
@project.id @project.id
end end
def to_reference(_from_project = nil, full: nil) def to_reference(_from = nil, full: nil)
id id
end end
def reference_link_text(from_project = nil) def reference_link_text(from = nil)
return "##{id}" if id =~ /^\d+$/ return "##{id}" if id =~ /^\d+$/
id id
......
...@@ -97,7 +97,7 @@ class Group < Namespace ...@@ -97,7 +97,7 @@ class Group < Namespace
end end
end end
def to_reference(_from_project = nil, full: nil) def to_reference(_from = nil, full: nil)
"#{self.class.reference_prefix}#{full_path}" "#{self.class.reference_prefix}#{full_path}"
end end
......
...@@ -165,12 +165,12 @@ class Label < ActiveRecord::Base ...@@ -165,12 +165,12 @@ class Label < ActiveRecord::Base
# #
# Returns a String # 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) format_reference = label_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}" reference = "#{self.class.reference_prefix}#{format_reference}"
if from_project if from
"#{from_project.to_reference(target_project, full: full)}#{reference}" "#{from.to_reference(target_project, full: full)}#{reference}"
else else
reference reference
end end
......
...@@ -5,6 +5,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -5,6 +5,8 @@ class MergeRequest < ActiveRecord::Base
include Referable include Referable
include IgnorableColumn include IgnorableColumn
include TimeTrackable include TimeTrackable
include ManualInverseAssociation
include EachBatch
ignore_column :locked_at, ignore_column :locked_at,
:ref_fetched :ref_fetched
...@@ -14,9 +16,28 @@ class MergeRequest < ActiveRecord::Base ...@@ -14,9 +16,28 @@ class MergeRequest < ActiveRecord::Base
belongs_to :merge_user, class_name: "User" belongs_to :merge_user, class_name: "User"
has_many :merge_request_diffs has_many :merge_request_diffs
has_one :merge_request_diff, has_one :merge_request_diff,
-> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request -> { 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" belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
...@@ -167,6 +188,22 @@ class MergeRequest < ActiveRecord::Base ...@@ -167,6 +188,22 @@ class MergeRequest < ActiveRecord::Base
where("merge_requests.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection where("merge_requests.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end 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 WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
def self.work_in_progress?(title) def self.work_in_progress?(title)
...@@ -181,6 +218,12 @@ class MergeRequest < ActiveRecord::Base ...@@ -181,6 +218,12 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}" work_in_progress?(title) ? title : "WIP: #{title}"
end end
# Verifies if title has changed not taking into account WIP prefix
# for merge requests.
def wipless_title_changed(old_title)
self.class.wipless_title(old_title) != self.wipless_title
end
def hook_attrs def hook_attrs
Gitlab::HookData::MergeRequestBuilder.new(self).build Gitlab::HookData::MergeRequestBuilder.new(self).build
end end
......
...@@ -2,6 +2,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -2,6 +2,7 @@ class MergeRequestDiff < ActiveRecord::Base
include Sortable include Sortable
include Importable include Importable
include Gitlab::EncodingHelper include Gitlab::EncodingHelper
include ManualInverseAssociation
# Prevent store of diff if commits amount more then 500 # Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100 COMMITS_SAFE_SIZE = 100
...@@ -10,6 +11,8 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -10,6 +11,8 @@ class MergeRequestDiff < ActiveRecord::Base
VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta].freeze VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta].freeze
belongs_to :merge_request 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_files, -> { order(:merge_request_diff_id, :relative_order) }
has_many :merge_request_diff_commits, -> { 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 ...@@ -194,7 +197,7 @@ class MergeRequestDiff < ActiveRecord::Base
end end
def latest? def latest?
self == merge_request.merge_request_diff self.id == merge_request.latest_merge_request_diff_id
end end
def compare_with(sha) def compare_with(sha)
......
...@@ -162,18 +162,18 @@ class Milestone < ActiveRecord::Base ...@@ -162,18 +162,18 @@ class Milestone < ActiveRecord::Base
# Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1" # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1"
# Milestone.first.to_reference(same_namespace_project) # => "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) format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}" reference = "#{self.class.reference_prefix}#{format_reference}"
if project if project
"#{project.to_reference(from_project, full: full)}#{reference}" "#{project.to_reference(from, full: full)}#{reference}"
else else
reference reference
end end
end end
def reference_link_text(from_project = nil) def reference_link_text(from = nil)
self.title self.title
end end
......
...@@ -760,10 +760,10 @@ class Project < ActiveRecord::Base ...@@ -760,10 +760,10 @@ class Project < ActiveRecord::Base
end end
end end
def to_human_reference(from_project = nil) def to_human_reference(from = nil)
if cross_namespace_reference?(from_project) if cross_namespace_reference?(from)
name_with_namespace name_with_namespace
elsif cross_project_reference?(from_project) elsif cross_project_reference?(from)
name name
end end
end end
......
...@@ -909,19 +909,13 @@ class Repository ...@@ -909,19 +909,13 @@ class Repository
end end
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) branch = Gitlab::Git::Branch.find(self, branch_or_name)
if branch if branch
@root_ref_sha ||= commit(root_ref).sha @root_ref_sha ||= commit(root_ref).sha
same_head = branch.target == @root_ref_sha same_head = branch.target == @root_ref_sha
merged = merged = ancestor?(branch.target, @root_ref_sha)
if pre_loaded_merged_branches
pre_loaded_merged_branches.include?(branch.name)
else
ancestor?(branch.target, @root_ref_sha)
end
!same_head && merged !same_head && merged
else else
nil nil
...@@ -972,6 +966,19 @@ class Repository ...@@ -972,6 +966,19 @@ class Repository
run_git(args).first.lines.map(&:strip) run_git(args).first.lines.map(&:strip)
end end
def fetch_as_mirror(url, forced: false, refmap: :all_refs, remote_name: nil)
unless remote_name
remote_name = "tmp-#{SecureRandom.hex}"
tmp_remote_name = true
end
add_remote(remote_name, url)
set_remote_as_mirror(remote_name, refmap: refmap)
fetch_remote(remote_name, forced: forced)
ensure
remove_remote(remote_name) if tmp_remote_name
end
def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false) def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false)
gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags) gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags)
end end
...@@ -1069,6 +1076,10 @@ class Repository ...@@ -1069,6 +1076,10 @@ class Repository
raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref) raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref)
end end
def repository_storage_path
@project.repository_storage_path
end
private private
# TODO Generice finder, later split this on finders by Ref or Oid # TODO Generice finder, later split this on finders by Ref or Oid
...@@ -1134,10 +1145,6 @@ class Repository ...@@ -1134,10 +1145,6 @@ class Repository
raw_repository.run_git_with_timeout(args, Gitlab::Git::Popen::FAST_GIT_PROCESS_TIMEOUT).first.strip raw_repository.run_git_with_timeout(args, Gitlab::Git::Popen::FAST_GIT_PROCESS_TIMEOUT).first.strip
end end
def repository_storage_path
@project.repository_storage_path
end
def initialize_raw_repository def initialize_raw_repository
Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, is_wiki)) Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, is_wiki))
end end
......
...@@ -75,11 +75,11 @@ class Snippet < ActiveRecord::Base ...@@ -75,11 +75,11 @@ class Snippet < ActiveRecord::Base
@link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/) @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end end
def to_reference(from_project = nil, full: false) def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{id}" reference = "#{self.class.reference_prefix}#{id}"
if project.present? if project.present?
"#{project.to_reference(from_project, full: full)}#{reference}" "#{project.to_reference(from, full: full)}#{reference}"
else else
reference reference
end end
......
...@@ -437,7 +437,7 @@ class User < ActiveRecord::Base ...@@ -437,7 +437,7 @@ class User < ActiveRecord::Base
username username
end 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}" "#{self.class.reference_prefix}#{username}"
end end
...@@ -633,18 +633,34 @@ class User < ActiveRecord::Base ...@@ -633,18 +633,34 @@ class User < ActiveRecord::Base
count.zero? && Gitlab::ProtocolAccess.allowed?('ssh') count.zero? && Gitlab::ProtocolAccess.allowed?('ssh')
end end
def require_password_creation? def require_password_creation_for_web?
password_automatically_set? && allow_password_authentication? allow_password_authentication_for_web? && password_automatically_set?
end
def require_password_creation_for_git?
allow_password_authentication_for_git? && password_automatically_set?
end end
def require_personal_access_token_creation_for_git_auth? 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? PersonalAccessTokensFinder.new(user: self, impersonation: false, state: 'active').execute.none?
end 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? 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 end
def can_change_username? def can_change_username?
......
...@@ -41,6 +41,14 @@ module Issuable ...@@ -41,6 +41,14 @@ module Issuable
end end
end end
def create_wip_note(old_title)
return unless issuable.is_a?(MergeRequest)
if MergeRequest.work_in_progress?(old_title) != issuable.work_in_progress?
SystemNoteService.handle_merge_request_wip(issuable, issuable.project, current_user)
end
end
def create_labels_note(old_labels) def create_labels_note(old_labels)
added_labels = issuable.labels - old_labels added_labels = issuable.labels - old_labels
removed_labels = old_labels - issuable.labels removed_labels = old_labels - issuable.labels
...@@ -49,7 +57,11 @@ module Issuable ...@@ -49,7 +57,11 @@ module Issuable
end end
def create_title_change_note(old_title) def create_title_change_note(old_title)
SystemNoteService.change_title(issuable, issuable.project, current_user, old_title) create_wip_note(old_title)
if issuable.wipless_title_changed(old_title)
SystemNoteService.change_title(issuable, issuable.project, current_user, old_title)
end
end end
def create_description_change_note def create_description_change_note
......
...@@ -4,20 +4,6 @@ module MergeRequests ...@@ -4,20 +4,6 @@ module MergeRequests
SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, state, nil) SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, state, nil)
end end
def create_title_change_note(issuable, old_title)
removed_wip = MergeRequest.work_in_progress?(old_title) && !issuable.work_in_progress?
added_wip = !MergeRequest.work_in_progress?(old_title) && issuable.work_in_progress?
changed_title = MergeRequest.wipless_title(old_title) != issuable.wipless_title
if removed_wip
SystemNoteService.remove_merge_request_wip(issuable, issuable.project, current_user)
elsif added_wip
SystemNoteService.add_merge_request_wip(issuable, issuable.project, current_user)
end
super if changed_title
end
def hook_data(merge_request, action, old_rev: nil, old_labels: [], old_assignees: [], old_total_time_spent: nil) def hook_data(merge_request, action, old_rev: nil, old_labels: [], old_assignees: [], old_total_time_spent: nil)
hook_data = merge_request.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent) hook_data = merge_request.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
hook_data[:object_attributes][:action] = action hook_data[:object_attributes][:action] = action
......
...@@ -35,7 +35,7 @@ module MergeRequests ...@@ -35,7 +35,7 @@ module MergeRequests
# target branch manually # target branch manually
def close_merge_requests def close_merge_requests
commit_ids = @commits.map(&:id) 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(&:diff_head_commit)
merge_requests = merge_requests.select do |merge_request| merge_requests = merge_requests.select do |merge_request|
......
...@@ -51,10 +51,13 @@ module Projects ...@@ -51,10 +51,13 @@ module Projects
def import_repository def import_repository
begin begin
if project.gitea_import? refmap = importer_class.try(:refmap) if has_importer?
fetch_repository
if refmap
project.ensure_repository
project.repository.fetch_as_mirror(project.import_url, refmap: refmap)
else else
clone_repository gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, project.import_url)
end end
rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e
# Expire cache to prevent scenarios such as: # Expire cache to prevent scenarios such as:
...@@ -66,17 +69,6 @@ module Projects ...@@ -66,17 +69,6 @@ module Projects
end end
end end
def clone_repository
gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, project.import_url)
end
def fetch_repository
project.ensure_repository
project.repository.add_remote(project.import_type, project.import_url)
project.repository.set_remote_as_mirror(project.import_type)
project.repository.fetch_remote(project.import_type, forced: true)
end
def import_data def import_data
return unless has_importer? return unless has_importer?
......
# 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 # 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 # internal data model (separate access levels). The translation code is non-trivial, and so
# lives in this service. # lives in this service.
module ProtectedBranches module ProtectedBranches
class ApiCreateService < BaseService class LegacyApiCreateService < BaseService
def execute def execute
push_access_level = push_access_level =
if params.delete(:developers_can_push) 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 # 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 # internal data model (separate access levels). The translation code is non-trivial, and so
# lives in this service. # lives in this service.
module ProtectedBranches module ProtectedBranches
class ApiUpdateService < BaseService class LegacyApiUpdateService < BaseService
def execute(protected_branch) def execute(protected_branch)
@developers_can_push = params.delete(:developers_can_push) @developers_can_push = params.delete(:developers_can_push)
@developers_can_merge = params.delete(:developers_can_merge) @developers_can_merge = params.delete(:developers_can_merge)
......
...@@ -241,14 +241,10 @@ module SystemNoteService ...@@ -241,14 +241,10 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, project, author, body, action: 'merge')) create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
end end
def remove_merge_request_wip(noteable, project, author) def handle_merge_request_wip(noteable, project, author)
body = 'unmarked as a **Work In Progress**' prefix = noteable.work_in_progress? ? "marked" : "unmarked"
create_note(NoteSummary.new(noteable, project, author, body, action: 'title')) body = "#{prefix} as a **Work In Progress**"
end
def add_merge_request_wip(noteable, project, author)
body = 'marked as a **Work In Progress**'
create_note(NoteSummary.new(noteable, project, author, body, action: 'title')) create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
end end
......
...@@ -34,7 +34,7 @@ module Users ...@@ -34,7 +34,7 @@ module Users
private private
def can_create_user? 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 end
# Allowed params for creating a user (admins only) # Allowed params for creating a user (admins only)
......
...@@ -160,9 +160,22 @@ ...@@ -160,9 +160,22 @@
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
.checkbox .checkbox
= f.label :password_authentication_enabled do = f.label :password_authentication_enabled_for_web do
= f.check_box :password_authentication_enabled = f.check_box :password_authentication_enabled_for_web
Sign-in enabled 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? - if omniauth_enabled? && button_based_providers.any?
.form-group .form-group
= f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2' = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2'
......
...@@ -45,10 +45,10 @@ ...@@ -45,10 +45,10 @@
.well-segment.admin-well.admin-well-features .well-segment.admin-well.admin-well-features
%h4 Features %h4 Features
- sign_up = "Sign up" - 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 = sign_up
%span.light.pull-right %span.light.pull-right
= boolean_to_icon signup_enabled? = boolean_to_icon allow_signup?
- ldap = "LDAP" - ldap = "LDAP"
%p{ "aria-label" => "#{ldap}: status " + (Gitlab.config.ldap.enabled ? "on" : "off") } %p{ "aria-label" => "#{ldap}: status " + (Gitlab.config.ldap.enabled ? "on" : "off") }
= ldap = ldap
......
...@@ -6,15 +6,15 @@ ...@@ -6,15 +6,15 @@
- else - else
= render 'devise/shared/tabs_normal' = render 'devise/shared/tabs_normal'
.tab-content .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' = render 'devise/shared/signin_box'
-# Signup only makes sense if you can also sign-in -# Signup only makes sense if you can also sign-in
- if password_authentication_enabled? && signup_enabled? - if allow_signup?
= render 'devise/shared/signup_box' = render 'devise/shared/signup_box'
-# Show a message if none of the mechanisms above are enabled -# 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 %div
No authentication methods configured. No authentication methods configured.
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<%= link_to "Sign in", new_session_path(resource_name), class: "btn" %><br /> <%= link_to "Sign in", new_session_path(resource_name), class: "btn" %><br />
<% end -%> <% 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 /> <%= link_to "Sign up", new_registration_path(resource_name) %><br />
<% end -%> <% end -%>
......
...@@ -7,12 +7,12 @@ ...@@ -7,12 +7,12 @@
.login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && !crowd_enabled?) } .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && !crowd_enabled?) }
.login-body .login-body
= render 'devise/sessions/new_ldap', server: server = 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-box.tab-pane{ id: 'ldap-standard', role: 'tabpanel' }
.login-body .login-body
= render 'devise/sessions/new_base' = 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-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' }
.login-body .login-body
= render 'devise/sessions/new_base' = render 'devise/sessions/new_base'
...@@ -5,9 +5,9 @@ ...@@ -5,9 +5,9 @@
- @ldap_servers.each_with_index do |server, i| - @ldap_servers.each_with_index do |server, i|
%li{ class: active_when(i.zero? && !crowd_enabled?) } %li{ class: active_when(i.zero? && !crowd_enabled?) }
= link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab' = link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab'
- if password_authentication_enabled? - if password_authentication_enabled_for_web?
%li %li
= link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab' = link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab'
- if password_authentication_enabled? && signup_enabled? - if allow_signup?
%li %li
= link_to 'Register', '#register-pane', 'data-toggle' => 'tab' = link_to 'Register', '#register-pane', 'data-toggle' => 'tab'
%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist' } %ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist' }
%li.active{ role: 'presentation' } %li.active{ role: 'presentation' }
%a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in
- if password_authentication_enabled? && signup_enabled? - if allow_signup?
%li{ role: 'presentation' } %li{ role: 'presentation' }
%a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register
...@@ -10,6 +10,10 @@ ...@@ -10,6 +10,10 @@
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; } img { -ms-interpolation-mode: bicubic; }
.hidden {
display: none !important;
visibility: hidden !important;
}
/* iOS BLUE LINKS */ /* iOS BLUE LINKS */
a[x-apple-data-detectors] { a[x-apple-data-detectors] {
......
...@@ -73,7 +73,7 @@ ...@@ -73,7 +73,7 @@
= link_to profile_emails_path do = link_to profile_emails_path do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('Emails') } #{ _('Emails') }
- unless current_user.ldap_user? - if current_user.allow_password_authentication?
= nav_link(controller: :passwords) do = nav_link(controller: :passwords) do
= link_to edit_profile_password_path do = link_to edit_profile_password_path do
.nav-icon-container .nav-icon-container
......
%p %p
Hi #{@user['name']}! Hi #{@user['name']}!
%p %p
- if Gitlab.config.gitlab.signup_enabled - if current_application_settings.allow_signup?
Your account has been created successfully. Your account has been created successfully.
- else - else
The Administrator created an account for you. Now you are a member of the company GitLab application. The Administrator created an account for you. Now you are a member of the company GitLab application.
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
.js-file-title.file-title-flex-parent .js-file-title.file-title-flex-parent
= render 'projects/blob/header_content', blob: blob = render 'projects/blob/header_content', blob: blob
.file-actions.hidden-xs .file-actions
= render 'projects/blob/viewer_switcher', blob: blob unless blame = render 'projects/blob/viewer_switcher', blob: blob unless blame
.btn-group{ role: "group" }< .btn-group{ role: "group" }<
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
- if @branches.any? - if @branches.any?
%ul.content-list.all-branches %ul.content-list.all-branches
- @branches.each do |branch| - @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' = paginate @branches, theme: 'gitlab'
- else - else
.nothing-here-block .nothing-here-block
......
- if current_user - if current_user
= link_to toggle_star_project_path(@project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do = link_to toggle_star_project_path(@project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do
- if current_user.starred?(@project) - if current_user.starred?(@project)
= icon('star') = sprite_icon('star')
%span.starred= _('Unstar') %span.starred= _('Unstar')
- else - else
= icon('star-o') = sprite_icon('star-o')
%span= s_('StarProject|Star') %span= s_('StarProject|Star')
.count-with-arrow .count-with-arrow
%span.arrow %span.arrow
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
- else - else
= link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: _('You must sign in to star a project') do = 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') } #{ s_('StarProject|Star') }
.count-with-arrow .count-with-arrow
%span.arrow %span.arrow
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
= page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag("environments_folder") = 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, "can-read-environment" => can?(current_user, :read_environment, @project).to_s,
"css-class" => container_class } } "css-class" => container_class } }
...@@ -3,15 +3,13 @@ ...@@ -3,15 +3,13 @@
- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
- content_for :page_specific_javascripts do - 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") = page_specific_javascript_bundle_tag("environments")
#environments-list-view{ data: { environments_data: environments_list_data, #environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
"can-read-environment" => can?(current_user, :read_environment, @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, "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), "new-environment-path" => new_project_environment_path(@project),
"help-page-path" => help_page_path("ci/environments"), "help-page-path" => help_page_path("ci/environments"),
"css-class" => container_class } } "css-class" => container_class } }
...@@ -44,9 +44,10 @@ ...@@ -44,9 +44,10 @@
%h4.title %h4.title
Trigger Trigger
%p - if @build.trigger_request&.trigger&.short_token
%span.build-light-text Token: %p
#{@build.trigger_request.trigger.short_token} %span.build-light-text Token:
#{@build.trigger_request.trigger.short_token}
- if @build.trigger_variables.any? - if @build.trigger_variables.any?
%p %p
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
.controls.hidden-xs .controls.hidden-xs
- if can?(current_user, :admin_group, group) - if can?(current_user, :admin_group, group)
= link_to edit_group_path(group), class: "btn" do = 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 = 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') = icon('sign-out')
......
...@@ -9,7 +9,7 @@ class PipelineScheduleWorker ...@@ -9,7 +9,7 @@ class PipelineScheduleWorker
pipeline = Ci::CreatePipelineService.new(schedule.project, pipeline = Ci::CreatePipelineService.new(schedule.project,
schedule.owner, schedule.owner,
ref: schedule.ref) 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? schedule.deactivate! unless pipeline.persisted?
rescue => e rescue => e
......
---
title: Impersonation no longer gets stuck on password change.
merge_request: 15497
author:
type: fixed
---
title: Add edit button to mobile file view
merge_request: 15199
author: Travis Miller
type: added
--- ---
title: Fix promoting milestone updating all issuables without milestone title: Prevent 500 error when inspecting job after trigger was removed
merge_request: merge_request:
author: author:
type: fixed type: fixed
---
title: Update Issue Boards to fetch the notification subscription status asynchronously
merge_request:
author:
type: performance
---
title: Label addition/removal are not going to be redacted wrongfully in the API.
merge_request: 15080
author:
type: fixed
---
title: Fix bitbucket wiki import with hashed storage enabled
merge_request: 15490
author:
type: fixed
---
title: Don't move repositories and attachments for projects using hashed storage
merge_request: 15479
author:
type: other
---
title: Fix pulling and pushing using a personal access token with the sudo scope
merge_request:
author:
type: fixed
---
title: Allow password authentication to be disabled entirely
merge_request: 15223
author: Markus Koller
type: changed
---
title: Fix hashed storage for Import/Export uploads
merge_request: 15482
author:
type: fixed
---
title: Avoid deactivation when pipeline schedules execute a branch includes `[ci skip]`
comment
merge_request: 15405
author:
type: fixed
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.
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