Merge remote-tracking branch 'origin/master' into droplab-templating-xss-fix
This commit is contained in:
commit
6a6a672037
|
@ -49,7 +49,7 @@ eslint-report.html
|
||||||
/tags
|
/tags
|
||||||
/tmp/*
|
/tmp/*
|
||||||
/vendor/bundle/*
|
/vendor/bundle/*
|
||||||
/builds/*
|
/builds*
|
||||||
/shared/*
|
/shared/*
|
||||||
/.gitlab_workhorse_secret
|
/.gitlab_workhorse_secret
|
||||||
/webpack-report/
|
/webpack-report/
|
||||||
|
|
|
@ -59,7 +59,7 @@ stages:
|
||||||
|
|
||||||
.only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql
|
.only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql
|
||||||
only:
|
only:
|
||||||
- /\-(?i)mysql$/
|
- /mysql/
|
||||||
- master@gitlab-org/gitlab-ce
|
- master@gitlab-org/gitlab-ce
|
||||||
- master@gitlab/gitlabhq
|
- master@gitlab/gitlabhq
|
||||||
- tags@gitlab-org/gitlab-ce
|
- tags@gitlab-org/gitlab-ce
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
5.0.2
|
5.0.3
|
||||||
|
|
|
@ -716,7 +716,7 @@ GEM
|
||||||
rack
|
rack
|
||||||
shoulda-matchers (2.8.0)
|
shoulda-matchers (2.8.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
sidekiq (4.2.7)
|
sidekiq (4.2.10)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
connection_pool (~> 2.2, >= 2.2.0)
|
connection_pool (~> 2.2, >= 2.2.0)
|
||||||
rack-protection (>= 1.5.0)
|
rack-protection (>= 1.5.0)
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import './breakpoints';
|
import './breakpoints';
|
||||||
import './flash';
|
import './flash';
|
||||||
|
import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
||||||
|
|
||||||
/* eslint-disable max-len */
|
/* eslint-disable max-len */
|
||||||
// MergeRequestTabs
|
// MergeRequestTabs
|
||||||
|
@ -266,6 +267,17 @@ import './flash';
|
||||||
|
|
||||||
new gl.Diff();
|
new gl.Diff();
|
||||||
this.scrollToElement('#diffs');
|
this.scrollToElement('#diffs');
|
||||||
|
|
||||||
|
$('.diff-file').each((i, el) => {
|
||||||
|
new BlobForkSuggestion({
|
||||||
|
openButtons: $(el).find('.js-edit-blob-link-fork-toggler'),
|
||||||
|
forkButtons: $(el).find('.js-fork-suggestion-button'),
|
||||||
|
cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'),
|
||||||
|
suggestionSections: $(el).find('.js-file-fork-suggestion-section'),
|
||||||
|
actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'),
|
||||||
|
})
|
||||||
|
.init();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ class PrometheusGraph {
|
||||||
const hasMetrics = $prometheusContainer.data('has-metrics');
|
const hasMetrics = $prometheusContainer.data('has-metrics');
|
||||||
this.docLink = $prometheusContainer.data('doc-link');
|
this.docLink = $prometheusContainer.data('doc-link');
|
||||||
this.integrationLink = $prometheusContainer.data('prometheus-integration');
|
this.integrationLink = $prometheusContainer.data('prometheus-integration');
|
||||||
|
this.state = '';
|
||||||
|
|
||||||
$(document).ajaxError(() => {});
|
$(document).ajaxError(() => {});
|
||||||
|
|
||||||
|
@ -38,8 +39,9 @@ class PrometheusGraph {
|
||||||
this.configureGraph();
|
this.configureGraph();
|
||||||
this.init();
|
this.init();
|
||||||
} else {
|
} else {
|
||||||
|
const prevState = this.state;
|
||||||
this.state = '.js-getting-started';
|
this.state = '.js-getting-started';
|
||||||
this.updateState();
|
this.updateState(prevState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,26 +55,26 @@ class PrometheusGraph {
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.getData().then((metricsResponse) => {
|
return this.getData().then((metricsResponse) => {
|
||||||
let enoughData = true;
|
let enoughData = true;
|
||||||
Object.keys(metricsResponse.metrics).forEach((key) => {
|
if (typeof metricsResponse === 'undefined') {
|
||||||
let currentKey;
|
enoughData = false;
|
||||||
if (key === 'cpu_values' || key === 'memory_values') {
|
|
||||||
currentKey = metricsResponse.metrics[key];
|
|
||||||
if (Object.keys(currentKey).length === 0) {
|
|
||||||
enoughData = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!enoughData) {
|
|
||||||
this.state = '.js-loading';
|
|
||||||
this.updateState();
|
|
||||||
} else {
|
} else {
|
||||||
|
Object.keys(metricsResponse.metrics).forEach((key) => {
|
||||||
|
if (key === 'cpu_values' || key === 'memory_values') {
|
||||||
|
const currentData = (metricsResponse.metrics[key])[0];
|
||||||
|
if (currentData.values.length <= 2) {
|
||||||
|
enoughData = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (enoughData) {
|
||||||
|
$(prometheusStatesContainer).hide();
|
||||||
|
$(prometheusParentGraphContainer).show();
|
||||||
this.transformData(metricsResponse);
|
this.transformData(metricsResponse);
|
||||||
this.createGraph();
|
this.createGraph();
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
|
||||||
new Flash('An error occurred when trying to load metrics. Please try again.');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,6 +344,8 @@ class PrometheusGraph {
|
||||||
|
|
||||||
getData() {
|
getData() {
|
||||||
const maxNumberOfRequests = 3;
|
const maxNumberOfRequests = 3;
|
||||||
|
this.state = '.js-loading';
|
||||||
|
this.updateState();
|
||||||
return gl.utils.backOff((next, stop) => {
|
return gl.utils.backOff((next, stop) => {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: metricsEndpoint,
|
url: metricsEndpoint,
|
||||||
|
@ -352,12 +356,11 @@ class PrometheusGraph {
|
||||||
this.backOffRequestCounter = this.backOffRequestCounter += 1;
|
this.backOffRequestCounter = this.backOffRequestCounter += 1;
|
||||||
if (this.backOffRequestCounter < maxNumberOfRequests) {
|
if (this.backOffRequestCounter < maxNumberOfRequests) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else if (this.backOffRequestCounter >= maxNumberOfRequests) {
|
||||||
stop({
|
stop(new Error('loading'));
|
||||||
status: resp.status,
|
|
||||||
metrics: data,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
} else if (!data.success) {
|
||||||
|
stop(new Error('loading'));
|
||||||
} else {
|
} else {
|
||||||
stop({
|
stop({
|
||||||
status: resp.status,
|
status: resp.status,
|
||||||
|
@ -373,8 +376,9 @@ class PrometheusGraph {
|
||||||
return resp.metrics;
|
return resp.metrics;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
const prevState = this.state;
|
||||||
this.state = '.js-unable-to-connect';
|
this.state = '.js-unable-to-connect';
|
||||||
this.updateState();
|
this.updateState(prevState);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,19 +386,20 @@ class PrometheusGraph {
|
||||||
Object.keys(metricsResponse.metrics).forEach((key) => {
|
Object.keys(metricsResponse.metrics).forEach((key) => {
|
||||||
if (key === 'cpu_values' || key === 'memory_values') {
|
if (key === 'cpu_values' || key === 'memory_values') {
|
||||||
const metricValues = (metricsResponse.metrics[key])[0];
|
const metricValues = (metricsResponse.metrics[key])[0];
|
||||||
if (metricValues !== undefined) {
|
this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
|
||||||
this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
|
time: new Date(metric[0] * 1000),
|
||||||
time: new Date(metric[0] * 1000),
|
value: metric[1],
|
||||||
value: metric[1],
|
}));
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateState() {
|
updateState(prevState) {
|
||||||
const $statesContainer = $(prometheusStatesContainer);
|
const $statesContainer = $(prometheusStatesContainer);
|
||||||
$(prometheusParentGraphContainer).hide();
|
$(prometheusParentGraphContainer).hide();
|
||||||
|
if (prevState) {
|
||||||
|
$(`${prevState}`, $statesContainer).addClass('hidden');
|
||||||
|
}
|
||||||
$(`${this.state}`, $statesContainer).removeClass('hidden');
|
$(`${this.state}`, $statesContainer).removeClass('hidden');
|
||||||
$(prometheusStatesContainer).show();
|
$(prometheusStatesContainer).show();
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,8 +108,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.award-control {
|
.award-control {
|
||||||
margin: 3px 5px 3px 0;
|
margin-right: 5px;
|
||||||
padding: .35em .4em;
|
|
||||||
outline: 0;
|
outline: 0;
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
|
|
|
@ -70,7 +70,7 @@ pre {
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
margin: $gl-padding 0;
|
margin: 24px 0;
|
||||||
border-top: 1px solid darken($gray-normal, 8%);
|
border-top: 1px solid darken($gray-normal, 8%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,14 +73,6 @@
|
||||||
|
|
||||||
&.wiki {
|
&.wiki {
|
||||||
padding: 30px $gl-padding;
|
padding: 30px $gl-padding;
|
||||||
|
|
||||||
.highlight {
|
|
||||||
margin-bottom: 9px;
|
|
||||||
|
|
||||||
> pre {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.blob-no-preview {
|
&.blob-no-preview {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
.timeline-entry {
|
.timeline-entry {
|
||||||
padding: $gl-padding $gl-btn-padding 14px;
|
padding: $gl-padding $gl-btn-padding 0;
|
||||||
border-color: $white-normal;
|
border-color: $white-normal;
|
||||||
color: $gl-text-color;
|
color: $gl-text-color;
|
||||||
border-bottom: 1px solid $border-white-light;
|
border-bottom: 1px solid $border-white-light;
|
||||||
|
|
|
@ -8,6 +8,13 @@
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p a:not(.no-attachment-icon) img {
|
||||||
|
// Remove bottom padding because
|
||||||
|
// <p> already has $gl-padding bottom
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
*:first-child:not(.katex-display) {
|
*:first-child:not(.katex-display) {
|
||||||
|
@ -47,44 +54,50 @@
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 1.75em;
|
font-size: 1.75em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 16px 0 10px;
|
margin: 24px 0 16px;
|
||||||
padding: 0 0 0.3em;
|
padding-bottom: 0.3em;
|
||||||
border-bottom: 1px solid $white-dark;
|
border-bottom: 1px solid $white-dark;
|
||||||
color: $gl-text-color;
|
color: $gl-text-color;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 16px 0 10px;
|
margin: 24px 0 16px;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
border-bottom: 1px solid $white-dark;
|
||||||
color: $gl-text-color;
|
color: $gl-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: 16px 0 10px;
|
margin: 24px 0 16px;
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
margin: 16px 0 10px;
|
margin: 24px 0 16px;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
margin: 16px 0 10px;
|
margin: 24px 0 16px;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h6 {
|
h6 {
|
||||||
margin: 16px 0 10px;
|
margin: 24px 0 16px;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
color: $gl-grayish-blue;
|
color: $gl-grayish-blue;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
padding: 8px 21px;
|
padding: 8px 24px;
|
||||||
margin: 12px 0;
|
margin: 16px 0;
|
||||||
border-left: 3px solid $white-dark;
|
border-left: 3px solid $white-dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,19 +108,20 @@
|
||||||
|
|
||||||
blockquote p {
|
blockquote p {
|
||||||
color: $gl-grayish-blue !important;
|
color: $gl-grayish-blue !important;
|
||||||
|
margin: 0;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
color: $gl-text-color;
|
color: $gl-text-color;
|
||||||
margin: 6px 0 0;
|
margin: 0 0 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
@extend .table;
|
@extend .table;
|
||||||
@extend .table-bordered;
|
@extend .table-bordered;
|
||||||
margin: 12px 0;
|
margin: 16px 0;
|
||||||
color: $gl-text-color;
|
color: $gl-text-color;
|
||||||
|
|
||||||
th {
|
th {
|
||||||
|
@ -120,7 +134,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
margin: 12px 0;
|
margin-bottom: 16px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.6em;
|
line-height: 1.6em;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
@ -134,7 +148,7 @@
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 3px 0 !important;
|
margin: 0 0 16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul:dir(rtl),
|
ul:dir(rtl),
|
||||||
|
|
|
@ -29,11 +29,5 @@
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
|
|
||||||
p {
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin-bottom: 16px;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -357,6 +357,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-page-description {
|
.detail-page-description {
|
||||||
|
padding: 16px 0 0;
|
||||||
|
|
||||||
small {
|
small {
|
||||||
color: $gray-darkest;
|
color: $gray-darkest;
|
||||||
}
|
}
|
||||||
|
@ -364,6 +366,8 @@
|
||||||
|
|
||||||
.edited-text {
|
.edited-text {
|
||||||
color: $gray-darkest;
|
color: $gray-darkest;
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
|
||||||
.author_link {
|
.author_link {
|
||||||
color: $gray-darkest;
|
color: $gray-darkest;
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
.note-edit-form {
|
.note-edit-form {
|
||||||
.note-form-actions {
|
.note-form-actions {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: $gl-padding;
|
margin: $gl-padding 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-preview-holder {
|
.note-preview-holder {
|
||||||
|
|
|
@ -102,13 +102,12 @@ ul.notes {
|
||||||
|
|
||||||
.note-awards {
|
.note-awards {
|
||||||
.js-awards-block {
|
.js-awards-block {
|
||||||
padding: 2px;
|
margin-bottom: 16px;
|
||||||
margin-top: 10px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-header {
|
.note-header {
|
||||||
padding-bottom: 3px;
|
padding-bottom: 8px;
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
|
|
||||||
@media (min-width: $screen-sm-min) {
|
@media (min-width: $screen-sm-min) {
|
||||||
|
@ -151,6 +150,10 @@ ul.notes {
|
||||||
margin-left: 65px;
|
margin-left: 65px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-header {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&.timeline-entry::after {
|
&.timeline-entry::after {
|
||||||
clear: none;
|
clear: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ class Admin::GroupsController < Admin::ApplicationController
|
||||||
|
|
||||||
if @group.save
|
if @group.save
|
||||||
@group.add_owner(current_user)
|
@group.add_owner(current_user)
|
||||||
redirect_to [:admin, @group], notice: 'Group was successfully created.'
|
redirect_to [:admin, @group], notice: "Group '#{@group.name}' was successfully created."
|
||||||
else
|
else
|
||||||
render "new"
|
render "new"
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,7 +29,7 @@ module BlobHelper
|
||||||
link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
|
link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
|
||||||
elsif current_user && can?(current_user, :fork_project, project)
|
elsif current_user && can?(current_user, :fork_project, project)
|
||||||
continue_params = {
|
continue_params = {
|
||||||
to: edit_path,
|
to: edit_path(project, ref, path, options),
|
||||||
notice: edit_in_new_fork_notice,
|
notice: edit_in_new_fork_notice,
|
||||||
notice_now: edit_in_new_fork_notice_now
|
notice_now: edit_in_new_fork_notice_now
|
||||||
}
|
}
|
||||||
|
|
|
@ -191,22 +191,23 @@ class MergeRequest < ActiveRecord::Base
|
||||||
merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
|
merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def diffs(diff_options = nil)
|
def diffs(diff_options = {})
|
||||||
if compare
|
if compare
|
||||||
compare.diffs(diff_options)
|
# When saving MR diffs, `no_collapse` is implicitly added (because we need
|
||||||
|
# to save the entire contents to the DB), so add that here for
|
||||||
|
# consistency.
|
||||||
|
compare.diffs(diff_options.merge(no_collapse: true))
|
||||||
else
|
else
|
||||||
merge_request_diff.diffs(diff_options)
|
merge_request_diff.diffs(diff_options)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def diff_size
|
def diff_size
|
||||||
# The `#diffs` method ends up at an instance of a class inheriting from
|
# Calling `merge_request_diff.diffs.real_size` will also perform
|
||||||
# `Gitlab::Diff::FileCollection::Base`, so use those options as defaults
|
# highlighting, which we don't need here.
|
||||||
# here too, to get the same diff size without performing highlighting.
|
return real_size if merge_request_diff
|
||||||
#
|
|
||||||
opts = Gitlab::Diff::FileCollection::Base.default_options.merge(diff_options || {})
|
|
||||||
|
|
||||||
raw_diffs(opts).size
|
diffs.real_size
|
||||||
end
|
end
|
||||||
|
|
||||||
def diff_base_commit
|
def diff_base_commit
|
||||||
|
|
|
@ -260,7 +260,7 @@ class MergeRequestDiff < ActiveRecord::Base
|
||||||
new_attributes[:state] = :empty
|
new_attributes[:state] = :empty
|
||||||
else
|
else
|
||||||
diff_collection = compare.diffs(Commit.max_diff_options)
|
diff_collection = compare.diffs(Commit.max_diff_options)
|
||||||
new_attributes[:real_size] = compare.diffs.real_size
|
new_attributes[:real_size] = diff_collection.real_size
|
||||||
|
|
||||||
if diff_collection.any?
|
if diff_collection.any?
|
||||||
new_diffs = dump_diffs(diff_collection)
|
new_diffs = dump_diffs(diff_collection)
|
||||||
|
|
|
@ -6,8 +6,8 @@ module Users
|
||||||
@params = params.dup
|
@params = params.dup
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute(skip_authorization: false)
|
||||||
raise Gitlab::Access::AccessDeniedError unless can_create_user?
|
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_create_user?
|
||||||
|
|
||||||
user = User.new(build_user_params)
|
user = User.new(build_user_params)
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,8 @@ module Users
|
||||||
@params = params.dup
|
@params = params.dup
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute(skip_authorization: false)
|
||||||
user = Users::BuildService.new(current_user, params).execute
|
user = Users::BuildService.new(current_user, params).execute(skip_authorization: skip_authorization)
|
||||||
|
|
||||||
@reset_token = user.generate_reset_token if user.recently_sent_password_reset?
|
@reset_token = user.generate_reset_token if user.recently_sent_password_reset?
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
.bs-callout.bs-callout-warning.clearfix
|
.bs-callout.bs-callout-warning.clearfix
|
||||||
%p
|
%p
|
||||||
User cohorts are only shown when the
|
User cohorts are only shown when the
|
||||||
= link_to 'usage ping', help_page_path('user/admin_area/usage_statistics'), target: '_blank'
|
= link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping'), target: '_blank'
|
||||||
is enabled. To enable it and see user cohorts,
|
is enabled. To enable it and see user cohorts,
|
||||||
visit
|
visit
|
||||||
= succeed '.' do
|
= succeed '.' do
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
Snippets
|
Snippets
|
||||||
|
|
||||||
- if project_nav_tab? :settings
|
- if project_nav_tab? :settings
|
||||||
= nav_link(path: %w[projects#edit members#show integrations#show repository#show ci_cd#show pages#show]) do
|
= nav_link(path: %w[projects#edit members#show integrations#show services#edit repository#show ci_cd#show pages#show]) do
|
||||||
= link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do
|
= link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do
|
||||||
%span
|
%span
|
||||||
Settings
|
Settings
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
- if current_user
|
||||||
|
.js-file-fork-suggestion-section.file-fork-suggestion.hidden
|
||||||
|
%span.file-fork-suggestion-note
|
||||||
|
You're not allowed to
|
||||||
|
%span.js-file-fork-suggestion-section-action
|
||||||
|
edit
|
||||||
|
files in this project directly. Please fork this project,
|
||||||
|
make your changes there, and submit a merge request.
|
||||||
|
= link_to 'Fork', nil, method: :post, class: 'js-fork-suggestion-button btn btn-grouped btn-inverted btn-new'
|
||||||
|
%button.js-cancel-fork-suggestion-button.btn.btn-grouped{ type: 'button' }
|
||||||
|
Cancel
|
|
@ -39,14 +39,4 @@
|
||||||
= replace_blob_link
|
= replace_blob_link
|
||||||
= delete_blob_link
|
= delete_blob_link
|
||||||
|
|
||||||
- if current_user
|
= render 'projects/fork_suggestion'
|
||||||
.js-file-fork-suggestion-section.file-fork-suggestion.hidden
|
|
||||||
%span.file-fork-suggestion-note
|
|
||||||
You're not allowed to
|
|
||||||
%span.js-file-fork-suggestion-section-action
|
|
||||||
edit
|
|
||||||
files in this project directly. Please fork this project,
|
|
||||||
make your changes there, and submit a merge request.
|
|
||||||
= link_to 'Fork', nil, method: :post, class: 'js-fork-suggestion-button btn btn-grouped btn-inverted btn-new'
|
|
||||||
%button.js-cancel-fork-suggestion-button.btn.btn-grouped{ type: 'button' }
|
|
||||||
Cancel
|
|
||||||
|
|
|
@ -18,4 +18,6 @@
|
||||||
= view_file_button(diff_commit.id, diff_file.new_path, project)
|
= view_file_button(diff_commit.id, diff_file.new_path, project)
|
||||||
= view_on_environment_button(diff_commit.id, diff_file.new_path, environment) if environment
|
= view_on_environment_button(diff_commit.id, diff_file.new_path, environment) if environment
|
||||||
|
|
||||||
|
= render 'projects/fork_suggestion'
|
||||||
|
|
||||||
= render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project
|
= render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
|
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
|
||||||
|
|
||||||
.issue-details.issuable-details
|
.issue-details.issuable-details
|
||||||
.detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) }
|
.detail-page-description.content-block
|
||||||
.issue-title-data.hidden{ "data" => { "initial-title" => markdown_field(@issue, :title),
|
.issue-title-data.hidden{ "data" => { "initial-title" => markdown_field(@issue, :title),
|
||||||
"endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue),
|
"endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue),
|
||||||
} }
|
} }
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
%a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
|
%a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
|
||||||
= icon('angle-double-left')
|
= icon('angle-double-left')
|
||||||
|
|
||||||
.detail-page-description.milestone-detail{ class: ('hide-bottom-border' unless @milestone.description.present? ) }
|
.detail-page-description.milestone-detail
|
||||||
%h2.title
|
%h2.title
|
||||||
= markdown_field(@milestone, :title)
|
= markdown_field(@milestone, :title)
|
||||||
%div
|
%div
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
- page_title @service.title, "Services"
|
- page_title @service.title, "Services"
|
||||||
|
= render "projects/settings/head"
|
||||||
= render 'form'
|
= render 'form'
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
%span
|
%span
|
||||||
Members
|
Members
|
||||||
- if can_edit
|
- if can_edit
|
||||||
= nav_link(controller: :integrations) do
|
= nav_link(controller: [:integrations, :services]) do
|
||||||
= link_to project_settings_integrations_path(@project), title: 'Integrations' do
|
= link_to project_settings_integrations_path(@project), title: 'Integrations' do
|
||||||
%span
|
%span
|
||||||
Integrations
|
Integrations
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Show group name on flash container when group is created from Admin area.
|
||||||
|
merge_request: 10905
|
||||||
|
author:
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Cleanup markdown spacing
|
||||||
|
merge_request:
|
||||||
|
author:
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Don't display the is_admin flag in most API responses
|
||||||
|
merge_request: 10846
|
||||||
|
author:
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Add sub-nav for Project Integration Services edit page
|
||||||
|
merge_request: 10813
|
||||||
|
author:
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Fix OAuth, LDAP and SAML SSO when regular sign-ups are disabled
|
||||||
|
merge_request:
|
||||||
|
author:
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Fix usage ping docs link from empty cohorts page
|
||||||
|
merge_request:
|
||||||
|
author:
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Add index to webhooks type column
|
||||||
|
merge_request:
|
||||||
|
author:
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Fixed Prometheus monitoring graphs not showing empty states in certain scenarios
|
||||||
|
merge_request:
|
||||||
|
author:
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Show sizes correctly in merge requests when diffs overflow
|
||||||
|
merge_request:
|
||||||
|
author:
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Upgrade Sidekiq to 4.2.10
|
||||||
|
merge_request:
|
||||||
|
author:
|
|
@ -0,0 +1,15 @@
|
||||||
|
class AddIndexToWebHooksType < ActiveRecord::Migration
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_concurrent_index :web_hooks, :type
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_concurrent_index :web_hooks, :type
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,7 +11,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20170423064036) do
|
ActiveRecord::Schema.define(version: 20170424142900) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -1371,6 +1371,7 @@ ActiveRecord::Schema.define(version: 20170423064036) do
|
||||||
end
|
end
|
||||||
|
|
||||||
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
|
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
|
||||||
|
add_index "web_hooks", ["type"], name: "index_web_hooks_on_type", using: :btree
|
||||||
|
|
||||||
add_foreign_key "boards", "projects"
|
add_foreign_key "boards", "projects"
|
||||||
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
|
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
|
||||||
|
|
|
@ -48,7 +48,6 @@ Example of response
|
||||||
"bio": null,
|
"bio": null,
|
||||||
"created_at": "2016-08-11T07:09:20.351Z",
|
"created_at": "2016-08-11T07:09:20.351Z",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"is_admin": true,
|
|
||||||
"linkedin": "",
|
"linkedin": "",
|
||||||
"location": null,
|
"location": null,
|
||||||
"name": "Administrator",
|
"name": "Administrator",
|
||||||
|
@ -106,7 +105,6 @@ Example of response
|
||||||
"bio": null,
|
"bio": null,
|
||||||
"created_at": "2016-08-11T07:09:20.351Z",
|
"created_at": "2016-08-11T07:09:20.351Z",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"is_admin": true,
|
|
||||||
"linkedin": "",
|
"linkedin": "",
|
||||||
"location": null,
|
"location": null,
|
||||||
"name": "Administrator",
|
"name": "Administrator",
|
||||||
|
@ -195,7 +193,6 @@ Example of response
|
||||||
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
|
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
|
||||||
"web_url": "http://localhost:3000/root",
|
"web_url": "http://localhost:3000/root",
|
||||||
"created_at": "2016-08-11T07:09:20.351Z",
|
"created_at": "2016-08-11T07:09:20.351Z",
|
||||||
"is_admin": true,
|
|
||||||
"bio": null,
|
"bio": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"skype": "",
|
"skype": "",
|
||||||
|
|
|
@ -57,7 +57,6 @@ Example of response
|
||||||
"bio": null,
|
"bio": null,
|
||||||
"created_at": "2015-12-21T13:14:24.077Z",
|
"created_at": "2015-12-21T13:14:24.077Z",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"is_admin": true,
|
|
||||||
"linkedin": "",
|
"linkedin": "",
|
||||||
"name": "Administrator",
|
"name": "Administrator",
|
||||||
"skype": "",
|
"skype": "",
|
||||||
|
@ -101,7 +100,6 @@ Example of response
|
||||||
"bio": null,
|
"bio": null,
|
||||||
"created_at": "2015-12-21T13:14:24.077Z",
|
"created_at": "2015-12-21T13:14:24.077Z",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"is_admin": true,
|
|
||||||
"linkedin": "",
|
"linkedin": "",
|
||||||
"name": "Administrator",
|
"name": "Administrator",
|
||||||
"skype": "",
|
"skype": "",
|
||||||
|
@ -173,7 +171,6 @@ Example of response
|
||||||
"bio": null,
|
"bio": null,
|
||||||
"created_at": "2015-12-21T13:14:24.077Z",
|
"created_at": "2015-12-21T13:14:24.077Z",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"is_admin": true,
|
|
||||||
"linkedin": "",
|
"linkedin": "",
|
||||||
"name": "Administrator",
|
"name": "Administrator",
|
||||||
"skype": "",
|
"skype": "",
|
||||||
|
@ -217,7 +214,6 @@ Example of response
|
||||||
"bio": null,
|
"bio": null,
|
||||||
"created_at": "2015-12-21T13:14:24.077Z",
|
"created_at": "2015-12-21T13:14:24.077Z",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"is_admin": true,
|
|
||||||
"linkedin": "",
|
"linkedin": "",
|
||||||
"name": "Administrator",
|
"name": "Administrator",
|
||||||
"skype": "",
|
"skype": "",
|
||||||
|
@ -284,7 +280,6 @@ Example of response
|
||||||
"bio": null,
|
"bio": null,
|
||||||
"created_at": "2015-12-21T13:14:24.077Z",
|
"created_at": "2015-12-21T13:14:24.077Z",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"is_admin": true,
|
|
||||||
"linkedin": "",
|
"linkedin": "",
|
||||||
"name": "Administrator",
|
"name": "Administrator",
|
||||||
"skype": "",
|
"skype": "",
|
||||||
|
|
|
@ -26,7 +26,6 @@ Parameters:
|
||||||
"avatar_url": "http://www.gravatar.com/avatar/cfa35b8cd2ec278026357769582fa563?s=40\u0026d=identicon",
|
"avatar_url": "http://www.gravatar.com/avatar/cfa35b8cd2ec278026357769582fa563?s=40\u0026d=identicon",
|
||||||
"web_url": "http://localhost:3000/john_smith",
|
"web_url": "http://localhost:3000/john_smith",
|
||||||
"created_at": "2015-09-03T07:24:01.670Z",
|
"created_at": "2015-09-03T07:24:01.670Z",
|
||||||
"is_admin": false,
|
|
||||||
"bio": null,
|
"bio": null,
|
||||||
"skype": "",
|
"skype": "",
|
||||||
"linkedin": "",
|
"linkedin": "",
|
||||||
|
|
|
@ -62,7 +62,6 @@ GET /users
|
||||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
|
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
|
||||||
"web_url": "http://localhost:3000/john_smith",
|
"web_url": "http://localhost:3000/john_smith",
|
||||||
"created_at": "2012-05-23T08:00:58Z",
|
"created_at": "2012-05-23T08:00:58Z",
|
||||||
"is_admin": false,
|
|
||||||
"bio": null,
|
"bio": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"skype": "",
|
"skype": "",
|
||||||
|
@ -95,7 +94,6 @@ GET /users
|
||||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg",
|
"avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg",
|
||||||
"web_url": "http://localhost:3000/jack_smith",
|
"web_url": "http://localhost:3000/jack_smith",
|
||||||
"created_at": "2012-05-23T08:01:01Z",
|
"created_at": "2012-05-23T08:01:01Z",
|
||||||
"is_admin": false,
|
|
||||||
"bio": null,
|
"bio": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"skype": "",
|
"skype": "",
|
||||||
|
@ -169,7 +167,6 @@ Parameters:
|
||||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
|
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
|
||||||
"web_url": "http://localhost:3000/john_smith",
|
"web_url": "http://localhost:3000/john_smith",
|
||||||
"created_at": "2012-05-23T08:00:58Z",
|
"created_at": "2012-05-23T08:00:58Z",
|
||||||
"is_admin": false,
|
|
||||||
"bio": null,
|
"bio": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"skype": "",
|
"skype": "",
|
||||||
|
@ -200,7 +197,6 @@ Parameters:
|
||||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
|
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
|
||||||
"web_url": "http://localhost:3000/john_smith",
|
"web_url": "http://localhost:3000/john_smith",
|
||||||
"created_at": "2012-05-23T08:00:58Z",
|
"created_at": "2012-05-23T08:00:58Z",
|
||||||
"is_admin": false,
|
|
||||||
"bio": null,
|
"bio": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"skype": "",
|
"skype": "",
|
||||||
|
@ -325,7 +321,6 @@ GET /user
|
||||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
|
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
|
||||||
"web_url": "http://localhost:3000/john_smith",
|
"web_url": "http://localhost:3000/john_smith",
|
||||||
"created_at": "2012-05-23T08:00:58Z",
|
"created_at": "2012-05-23T08:00:58Z",
|
||||||
"is_admin": false,
|
|
||||||
"bio": null,
|
"bio": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"skype": "",
|
"skype": "",
|
||||||
|
|
|
@ -33,7 +33,6 @@
|
||||||
## Backend howtos
|
## Backend howtos
|
||||||
|
|
||||||
- [Architecture](architecture.md) of GitLab
|
- [Architecture](architecture.md) of GitLab
|
||||||
- [CI setup](ci_setup.md) for testing GitLab
|
|
||||||
- [Gotchas](gotchas.md) to avoid
|
- [Gotchas](gotchas.md) to avoid
|
||||||
- [How to dump production data to staging](db_dump.md)
|
- [How to dump production data to staging](db_dump.md)
|
||||||
- [Instrumentation](instrumentation.md)
|
- [Instrumentation](instrumentation.md)
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
# CI setup
|
|
||||||
|
|
||||||
This document describes what services we use for testing GitLab and GitLab CI.
|
|
||||||
|
|
||||||
We currently use four CI services to test GitLab:
|
|
||||||
|
|
||||||
1. GitLab CI on [GitHost.io](https://gitlab-ce.githost.io/projects/4/) for the [GitLab.com repo](https://gitlab.com/gitlab-org/gitlab-ce)
|
|
||||||
2. GitLab CI at ci.gitlab.org to test the private GitLab B.V. repo at dev.gitlab.org
|
|
||||||
3. [Semephore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for [GitHub.com repo](https://github.com/gitlabhq/gitlabhq)
|
|
||||||
4. [Mock CI Service](../user/project/integrations/mock_ci.md) for local development
|
|
||||||
|
|
||||||
| Software @ configuration being tested | GitLab CI (ci.gitlab.org) | GitLab CI (GitHost.io) | Semaphore |
|
|
||||||
|---------------------------------------|---------------------------|---------------------------------------------------------------------------|-----------|
|
|
||||||
| GitLab CE @ MySQL | ✓ | ✓ [Core team can trigger builds](https://gitlab-ce.githost.io/projects/4) | |
|
|
||||||
| GitLab CE @ PostgreSQL | | | ✓ [Core team can trigger builds](https://semaphoreapp.com/gitlabhq/gitlabhq/branches/master) |
|
|
||||||
| GitLab EE @ MySQL | ✓ | | |
|
|
||||||
| GitLab CI @ MySQL | ✓ | | |
|
|
||||||
| GitLab CI @ PostgreSQL | | | ✓ |
|
|
||||||
| GitLab CI Runner | ✓ | | ✓ |
|
|
||||||
| GitLab Shell | ✓ | | ✓ |
|
|
||||||
| GitLab Shell | ✓ | | ✓ |
|
|
||||||
|
|
||||||
Core team has access to trigger builds if needed for GitLab CE.
|
|
||||||
|
|
||||||
We use [these build scripts](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) for testing with GitLab CI.
|
|
||||||
|
|
||||||
# Build configuration on [Semaphore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for testing the [GitHub.com repo](https://github.com/gitlabhq/gitlabhq)
|
|
||||||
|
|
||||||
- Language: Ruby
|
|
||||||
- Ruby version: 2.1.8
|
|
||||||
- database.yml: pg
|
|
||||||
|
|
||||||
Build commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt-get install cmake libicu-dev -y (Setup)
|
|
||||||
bundle install --deployment --path vendor/bundle (Setup)
|
|
||||||
cp config/gitlab.yml.example config/gitlab.yml (Setup)
|
|
||||||
bundle exec rake db:create (Setup)
|
|
||||||
bundle exec rake spinach (Thread #1)
|
|
||||||
bundle exec rake spec (thread #2)
|
|
||||||
bundle exec rake rubocop (thread #3)
|
|
||||||
bundle exec rake brakeman (thread #4)
|
|
||||||
bundle exec rake jasmine:ci (thread #5)
|
|
||||||
```
|
|
||||||
|
|
||||||
Use rubygems mirror.
|
|
|
@ -448,13 +448,22 @@ is used for Spinach tests as well.
|
||||||
|
|
||||||
### Monitoring
|
### Monitoring
|
||||||
|
|
||||||
The GitLab test suite is [monitored] and a [public dashboard] is available for
|
The GitLab test suite is [monitored] for the `master` branch, and any branch
|
||||||
everyone to see. Feel free to look at the slowest test files and try to improve
|
that includes `rspec-profile` in their name.
|
||||||
them.
|
|
||||||
|
A [public dashboard] is available for everyone to see. Feel free to look at the
|
||||||
|
slowest test files and try to improve them.
|
||||||
|
|
||||||
[monitored]: ./performance.md#rspec-profiling
|
[monitored]: ./performance.md#rspec-profiling
|
||||||
[public dashboard]: https://redash.gitlab.com/public/dashboards/l1WhHXaxrCWM5Ai9D7YDqHKehq6OU3bx5gssaiWe?org_slug=default
|
[public dashboard]: https://redash.gitlab.com/public/dashboards/l1WhHXaxrCWM5Ai9D7YDqHKehq6OU3bx5gssaiWe?org_slug=default
|
||||||
|
|
||||||
|
## CI setup
|
||||||
|
|
||||||
|
- On CE, the test suite only runs against PostgreSQL by default. We additionally
|
||||||
|
run the suite against MySQL for tags, `master`, and any branch that includes
|
||||||
|
`mysql` in the name.
|
||||||
|
- On EE, the test suite always runs both PostgreSQL and MySQL.
|
||||||
|
|
||||||
## Spinach (feature) tests
|
## Spinach (feature) tests
|
||||||
|
|
||||||
GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
|
GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
|
||||||
|
|
|
@ -12,7 +12,7 @@ You can leave a comment in the following places:
|
||||||
|
|
||||||
The comment area supports [Markdown] and [slash commands]. One can edit their
|
The comment area supports [Markdown] and [slash commands]. One can edit their
|
||||||
own comment at any time, and anyone with [Master access level][permissions] or
|
own comment at any time, and anyone with [Master access level][permissions] or
|
||||||
higher can also a comment made by someone else.
|
higher can also edit a comment made by someone else.
|
||||||
|
|
||||||
Apart from the standard comments, you also have the option to create a comment
|
Apart from the standard comments, you also have the option to create a comment
|
||||||
in the form of a resolvable or threaded discussion.
|
in the form of a resolvable or threaded discussion.
|
||||||
|
|
|
@ -431,7 +431,7 @@ Emphasis, aka italics, with *asterisks* or _underscores_.
|
||||||
|
|
||||||
Strong emphasis, aka bold, with **asterisks** or __underscores__.
|
Strong emphasis, aka bold, with **asterisks** or __underscores__.
|
||||||
|
|
||||||
Combined emphasis with **_asterisks and underscores_**.
|
Combined emphasis with **asterisks and _underscores_**.
|
||||||
|
|
||||||
Strikethrough uses two tildes. ~~Scratch this.~~
|
Strikethrough uses two tildes. ~~Scratch this.~~
|
||||||
```
|
```
|
||||||
|
@ -640,10 +640,11 @@ Here's a line for us to start with.
|
||||||
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
|
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
|
||||||
|
|
||||||
This line is also a separate paragraph, but...
|
This line is also a separate paragraph, but...
|
||||||
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
|
This line is only separated by a single newline, so it *does not break* and just follows the previous line in the *same paragraph*.
|
||||||
|
|
||||||
|
This line is also a separate paragraph, and...
|
||||||
|
This line is *on its own line*, because the previous line ends with two spaces. (but still in the *same paragraph*)
|
||||||
|
|
||||||
This line is also a separate paragraph, and...
|
|
||||||
This line is on its own line, because the previous line ends with two
|
|
||||||
spaces.
|
spaces.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -651,11 +652,12 @@ Here's a line for us to start with.
|
||||||
|
|
||||||
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
|
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
|
||||||
|
|
||||||
This line is also begins a separate paragraph, but...
|
This line is also a separate paragraph, but...
|
||||||
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
|
This line is only separated by a single newline, so it *does not break* and just follows the previous line in the *same paragraph*.
|
||||||
|
|
||||||
|
This line is also a separate paragraph, and...
|
||||||
|
This line is *on its own line*, because the previous line ends with two spaces. (but still in the *same paragraph*)
|
||||||
|
|
||||||
This line is also a separate paragraph, and...
|
|
||||||
This line is on its own line, because the previous line ends with two
|
|
||||||
spaces.
|
spaces.
|
||||||
|
|
||||||
### Tables
|
### Tables
|
||||||
|
|
|
@ -14,7 +14,6 @@ module API
|
||||||
|
|
||||||
class User < UserBasic
|
class User < UserBasic
|
||||||
expose :created_at
|
expose :created_at
|
||||||
expose :admin?, as: :is_admin
|
|
||||||
expose :bio, :location, :skype, :linkedin, :twitter, :website_url, :organization
|
expose :bio, :location, :skype, :linkedin, :twitter, :website_url, :organization
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -41,8 +40,9 @@ module API
|
||||||
expose :external
|
expose :external
|
||||||
end
|
end
|
||||||
|
|
||||||
class UserWithPrivateToken < UserPublic
|
class UserWithPrivateDetails < UserPublic
|
||||||
expose :private_token
|
expose :private_token
|
||||||
|
expose :admin?, as: :is_admin
|
||||||
end
|
end
|
||||||
|
|
||||||
class Email < Grape::Entity
|
class Email < Grape::Entity
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
module API
|
module API
|
||||||
class Session < Grape::API
|
class Session < Grape::API
|
||||||
desc 'Login to get token' do
|
desc 'Login to get token' do
|
||||||
success Entities::UserWithPrivateToken
|
success Entities::UserWithPrivateDetails
|
||||||
end
|
end
|
||||||
params do
|
params do
|
||||||
optional :login, type: String, desc: 'The username'
|
optional :login, type: String, desc: 'The username'
|
||||||
|
@ -14,7 +14,7 @@ module API
|
||||||
|
|
||||||
return unauthorized! unless user
|
return unauthorized! unless user
|
||||||
return render_api_error!('401 Unauthorized. You have 2FA enabled. Please use a personal access token to access the API', 401) if user.two_factor_enabled?
|
return render_api_error!('401 Unauthorized. You have 2FA enabled. Please use a personal access token to access the API', 401) if user.two_factor_enabled?
|
||||||
present user, with: Entities::UserWithPrivateToken
|
present user, with: Entities::UserWithPrivateDetails
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -433,7 +433,7 @@ module API
|
||||||
success Entities::UserPublic
|
success Entities::UserPublic
|
||||||
end
|
end
|
||||||
get do
|
get do
|
||||||
present current_user, with: sudo? ? Entities::UserWithPrivateToken : Entities::UserPublic
|
present current_user, with: sudo? ? Entities::UserWithPrivateDetails : Entities::UserPublic
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Get the currently authenticated user's SSH keys" do
|
desc "Get the currently authenticated user's SSH keys" do
|
||||||
|
|
|
@ -15,6 +15,10 @@ module Gitlab
|
||||||
super.tap { |_| store_highlight_cache }
|
super.tap { |_| store_highlight_cache }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def real_size
|
||||||
|
@merge_request_diff.real_size
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Extracted method to highlight in the same iteration to the diff_collection.
|
# Extracted method to highlight in the same iteration to the diff_collection.
|
||||||
|
|
|
@ -148,7 +148,7 @@ module Gitlab
|
||||||
|
|
||||||
def build_new_user
|
def build_new_user
|
||||||
user_params = user_attributes.merge(extern_uid: auth_hash.uid, provider: auth_hash.provider, skip_confirmation: true)
|
user_params = user_attributes.merge(extern_uid: auth_hash.uid, provider: auth_hash.provider, skip_confirmation: true)
|
||||||
Users::BuildService.new(nil, user_params).execute
|
Users::BuildService.new(nil, user_params).execute(skip_authorization: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_attributes
|
def user_attributes
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
feature 'Diffs URL', js: true, feature: true do
|
feature 'Diffs URL', js: true, feature: true do
|
||||||
before do
|
let(:project) { create(:project, :public) }
|
||||||
login_as :admin
|
let(:merge_request) { create(:merge_request, source_project: project) }
|
||||||
@merge_request = create(:merge_request)
|
|
||||||
@project = @merge_request.source_project
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when visit with */* as accept header' do
|
context 'when visit with */* as accept header' do
|
||||||
before(:each) do
|
before(:each) do
|
||||||
|
@ -13,9 +10,9 @@ feature 'Diffs URL', js: true, feature: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'renders the notes' do
|
it 'renders the notes' do
|
||||||
create :note_on_merge_request, project: @project, noteable: @merge_request, note: 'Rebasing with master'
|
create :note_on_merge_request, project: project, noteable: merge_request, note: 'Rebasing with master'
|
||||||
|
|
||||||
visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
|
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
|
||||||
|
|
||||||
# Load notes and diff through AJAX
|
# Load notes and diff through AJAX
|
||||||
expect(page).to have_css('.note-text', visible: false, text: 'Rebasing with master')
|
expect(page).to have_css('.note-text', visible: false, text: 'Rebasing with master')
|
||||||
|
@ -25,10 +22,9 @@ feature 'Diffs URL', js: true, feature: true do
|
||||||
|
|
||||||
context 'when merge request has overflow' do
|
context 'when merge request has overflow' do
|
||||||
it 'displays warning' do
|
it 'displays warning' do
|
||||||
allow_any_instance_of(MergeRequestDiff).to receive(:overflow?).and_return(true)
|
allow(Commit).to receive(:max_diff_options).and_return(max_files: 3)
|
||||||
allow(Commit).to receive(:max_diff_options).and_return(max_files: 20, max_lines: 20)
|
|
||||||
|
|
||||||
visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
|
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
|
||||||
|
|
||||||
page.within('.alert') do
|
page.within('.alert') do
|
||||||
expect(page).to have_text("Too many changes to show. Plain diff Email patch To preserve
|
expect(page).to have_text("Too many changes to show. Plain diff Email patch To preserve
|
||||||
|
@ -36,4 +32,35 @@ feature 'Diffs URL', js: true, feature: true do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when editing file' do
|
||||||
|
let(:author_user) { create(:user) }
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:forked_project) { Projects::ForkService.new(project, author_user).execute }
|
||||||
|
let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, target_project: project, author: author_user) }
|
||||||
|
let(:changelog_id) { Digest::SHA1.hexdigest("CHANGELOG") }
|
||||||
|
|
||||||
|
context 'as author' do
|
||||||
|
it 'shows direct edit link' do
|
||||||
|
login_as(author_user)
|
||||||
|
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
|
||||||
|
|
||||||
|
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
|
||||||
|
expect(page).to have_selector("[id=\"#{changelog_id}\"] a.js-edit-blob")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'as user who needs to fork' do
|
||||||
|
it 'shows fork/cancel confirmation' do
|
||||||
|
login_as(user)
|
||||||
|
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
|
||||||
|
|
||||||
|
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
|
||||||
|
find("[id=\"#{changelog_id}\"] .js-edit-blob").click
|
||||||
|
|
||||||
|
expect(page).to have_selector('.js-fork-suggestion-button', count: 1)
|
||||||
|
expect(page).to have_selector('.js-cancel-fork-suggestion-button', count: 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
"avatar_url",
|
"avatar_url",
|
||||||
"web_url",
|
"web_url",
|
||||||
"created_at",
|
"created_at",
|
||||||
"is_admin",
|
|
||||||
"bio",
|
"bio",
|
||||||
"location",
|
"location",
|
||||||
"skype",
|
"skype",
|
||||||
|
@ -43,7 +42,6 @@
|
||||||
"avatar_url": { "type": "string" },
|
"avatar_url": { "type": "string" },
|
||||||
"web_url": { "type": "string" },
|
"web_url": { "type": "string" },
|
||||||
"created_at": { "type": "date" },
|
"created_at": { "type": "date" },
|
||||||
"is_admin": { "type": "boolean" },
|
|
||||||
"bio": { "type": ["string", "null"] },
|
"bio": { "type": ["string", "null"] },
|
||||||
"location": { "type": ["string", "null"] },
|
"location": { "type": ["string", "null"] },
|
||||||
"skype": { "type": "string" },
|
"skype": { "type": "string" },
|
||||||
|
|
|
@ -108,6 +108,18 @@ describe Gitlab::LDAP::User, lib: true do
|
||||||
it "creates a new user if not found" do
|
it "creates a new user if not found" do
|
||||||
expect{ ldap_user.save }.to change{ User.count }.by(1)
|
expect{ ldap_user.save }.to change{ User.count }.by(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when signup is disabled' do
|
||||||
|
before do
|
||||||
|
stub_application_setting signup_enabled: false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates the user' do
|
||||||
|
ldap_user.save
|
||||||
|
|
||||||
|
expect(gl_user).to be_persisted
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'updating email' do
|
describe 'updating email' do
|
||||||
|
|
|
@ -40,6 +40,20 @@ describe Gitlab::OAuth::User, lib: true do
|
||||||
let(:provider) { 'twitter' }
|
let(:provider) { 'twitter' }
|
||||||
|
|
||||||
describe 'signup' do
|
describe 'signup' do
|
||||||
|
context 'when signup is disabled' do
|
||||||
|
before do
|
||||||
|
stub_application_setting signup_enabled: false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates the user' do
|
||||||
|
stub_omniauth_config(allow_single_sign_on: ['twitter'])
|
||||||
|
|
||||||
|
oauth_user.save
|
||||||
|
|
||||||
|
expect(gl_user).to be_persisted
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'marks user as having password_automatically_set' do
|
it 'marks user as having password_automatically_set' do
|
||||||
stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter'])
|
stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter'])
|
||||||
|
|
||||||
|
|
|
@ -211,6 +211,18 @@ describe Gitlab::Saml::User, lib: true do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when signup is disabled' do
|
||||||
|
before do
|
||||||
|
stub_application_setting signup_enabled: false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates the user' do
|
||||||
|
saml_user.save
|
||||||
|
|
||||||
|
expect(gl_user).to be_persisted
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'blocking' do
|
describe 'blocking' do
|
||||||
|
|
|
@ -199,10 +199,10 @@ describe MergeRequest, models: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when there are no MR diffs' do
|
context 'when there are no MR diffs' do
|
||||||
it 'delegates to the compare object' do
|
it 'delegates to the compare object, setting no_collapse: true' do
|
||||||
merge_request.compare = double(:compare)
|
merge_request.compare = double(:compare)
|
||||||
|
|
||||||
expect(merge_request.compare).to receive(:diffs).with(options)
|
expect(merge_request.compare).to receive(:diffs).with(options.merge(no_collapse: true))
|
||||||
|
|
||||||
merge_request.diffs(options)
|
merge_request.diffs(options)
|
||||||
end
|
end
|
||||||
|
@ -215,15 +215,22 @@ describe MergeRequest, models: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when there are MR diffs' do
|
context 'when there are MR diffs' do
|
||||||
before do
|
it 'returns the correct count' do
|
||||||
merge_request.save
|
merge_request.save
|
||||||
|
|
||||||
|
expect(merge_request.diff_size).to eq('105')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the correct count' do
|
it 'returns the correct overflow count' do
|
||||||
expect(merge_request.diff_size).to eq(105)
|
allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
|
||||||
|
merge_request.save
|
||||||
|
|
||||||
|
expect(merge_request.diff_size).to eq('2+')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not perform highlighting' do
|
it 'does not perform highlighting' do
|
||||||
|
merge_request.save
|
||||||
|
|
||||||
expect(Gitlab::Diff::Highlight).not_to receive(:new)
|
expect(Gitlab::Diff::Highlight).not_to receive(:new)
|
||||||
|
|
||||||
merge_request.diff_size
|
merge_request.diff_size
|
||||||
|
@ -231,7 +238,7 @@ describe MergeRequest, models: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when there are no MR diffs' do
|
context 'when there are no MR diffs' do
|
||||||
before do
|
def set_compare(merge_request)
|
||||||
merge_request.compare = CompareService.new(
|
merge_request.compare = CompareService.new(
|
||||||
merge_request.source_project,
|
merge_request.source_project,
|
||||||
merge_request.source_branch
|
merge_request.source_branch
|
||||||
|
@ -242,10 +249,21 @@ describe MergeRequest, models: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the correct count' do
|
it 'returns the correct count' do
|
||||||
expect(merge_request.diff_size).to eq(105)
|
set_compare(merge_request)
|
||||||
|
|
||||||
|
expect(merge_request.diff_size).to eq('105')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the correct overflow count' do
|
||||||
|
allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
|
||||||
|
set_compare(merge_request)
|
||||||
|
|
||||||
|
expect(merge_request.diff_size).to eq('2+')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not perform highlighting' do
|
it 'does not perform highlighting' do
|
||||||
|
set_compare(merge_request)
|
||||||
|
|
||||||
expect(Gitlab::Diff::Highlight).not_to receive(:new)
|
expect(Gitlab::Diff::Highlight).not_to receive(:new)
|
||||||
|
|
||||||
merge_request.diff_size
|
merge_request.diff_size
|
||||||
|
|
|
@ -32,6 +32,12 @@ describe API::Keys do
|
||||||
expect(json_response['user']['id']).to eq(user.id)
|
expect(json_response['user']['id']).to eq(user.id)
|
||||||
expect(json_response['user']['username']).to eq(user.username)
|
expect(json_response['user']['username']).to eq(user.username)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "does not include the user's `is_admin` flag" do
|
||||||
|
get api("/keys/#{key.id}", admin)
|
||||||
|
|
||||||
|
expect(json_response['user']['is_admin']).to be_nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -135,6 +135,12 @@ describe API::Users do
|
||||||
expect(json_response['username']).to eq(user.username)
|
expect(json_response['username']).to eq(user.username)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "does not return the user's `is_admin` flag" do
|
||||||
|
get api("/users/#{user.id}", user)
|
||||||
|
|
||||||
|
expect(json_response['is_admin']).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
it "returns a 401 if unauthenticated" do
|
it "returns a 401 if unauthenticated" do
|
||||||
get api("/users/9998")
|
get api("/users/9998")
|
||||||
expect(response).to have_http_status(401)
|
expect(response).to have_http_status(401)
|
||||||
|
@ -397,7 +403,6 @@ describe API::Users do
|
||||||
it "updates admin status" do
|
it "updates admin status" do
|
||||||
put api("/users/#{user.id}", admin), { admin: true }
|
put api("/users/#{user.id}", admin), { admin: true }
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
expect(json_response['is_admin']).to eq(true)
|
|
||||||
expect(user.reload.admin).to eq(true)
|
expect(user.reload.admin).to eq(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -411,7 +416,6 @@ describe API::Users do
|
||||||
it "does not update admin status" do
|
it "does not update admin status" do
|
||||||
put api("/users/#{admin_user.id}", admin), { can_create_group: false }
|
put api("/users/#{admin_user.id}", admin), { can_create_group: false }
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
expect(json_response['is_admin']).to eq(true)
|
|
||||||
expect(admin_user.reload.admin).to eq(true)
|
expect(admin_user.reload.admin).to eq(true)
|
||||||
expect(admin_user.can_create_group).to eq(false)
|
expect(admin_user.can_create_group).to eq(false)
|
||||||
end
|
end
|
||||||
|
|
|
@ -274,5 +274,11 @@ describe API::V3::Users do
|
||||||
|
|
||||||
expect(new_user).to be_confirmed
|
expect(new_user).to be_confirmed
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not reveal the `is_admin` flag of the user' do
|
||||||
|
post v3_api('/users', admin), attributes_for(:user)
|
||||||
|
|
||||||
|
expect(json_response['is_admin']).to be_nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue