Commit 12109f67 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into 'feature/gb/variables-expressions-in-only-except-ee'

 Conflicts:
   app/models/ci/build.rb
parents 1f75a144 69cfb90b
......@@ -15,6 +15,7 @@ app/models/project_services/packagist_service.rb
lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
lib/gitlab/background_migration/*
app/models/project_services/kubernetes_service.rb
lib/gitlab/workhorse.rb
ee/db/**/*
ee/app/serializers/ee/merge_request_widget_entity.rb
......
......@@ -51,6 +51,7 @@ eslint-report.html
/db/data.yml
/doc/code/*
/dump.rdb
/jsconfig.json
/log/*.log*
/node_modules/
/nohup.out
......
See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html
<!--See the general Documentation guidelines https://docs.gitlab.com/ce/development/writing_documentation.html -->
## What does this MR do?
(briefly describe what this MR is about)
<!-- Briefly describe what this MR is about -->
## Related issues
<!-- Mention the issue(s) this MR closes or is related to -->
Closes
## Moving docs to a new location?
See the guidelines: http://docs.gitlab.com/ce/development/doc_styleguide.html#changing-document-location
Read the guidelines:
https://docs.gitlab.com/ce/development/writing_documentation.html#changing-document-location
- [ ] Make sure the old link is not removed and has its contents replaced with a link to the new location.
- [ ] Make sure the old link is not removed and has its contents replaced with
a link to the new location.
- [ ] Make sure internal links pointing to the document in question are not broken.
- [ ] Search and replace any links referring to old docs in GitLab Rails app, specifically under the `app/views/` directory.
- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ee/development/doc_styleguide.html#redirections-for-pages-with-disqus-comments) to the new document if there are any Disqus comments on the old document thread.
- [ ] If working on CE, submit an MR to EE with the changes as well.
- [ ] Search and replace any links referring to old docs in GitLab Rails app,
specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories.
- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/writing_documentation.html#redirections-for-pages-with-disqus-comments)
to the new document if there are any Disqus comments on the old document thread.
- [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE
with the changes as well (https://docs.gitlab.com/ce/development/writing_documentation.html#cherry-picking-from-ce-to-ee).
- [ ] Ping one of the technical writers for review.
/label ~Documentation
......@@ -156,8 +156,8 @@ gem 'rdoc', '~> 4.2'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'asciidoctor-plantuml', '0.0.7'
gem 'asciidoctor', '~> 1.5.6'
gem 'asciidoctor-plantuml', '0.0.8'
gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0'
......@@ -436,7 +436,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.88.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.91.0', require: 'gitaly'
gem 'grpc', '~> 1.10.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
......
......@@ -56,8 +56,8 @@ GEM
faraday_middleware (~> 0.9)
faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0)
asciidoctor (1.5.3)
asciidoctor-plantuml (0.0.7)
asciidoctor (1.5.6.2)
asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5)
asset_sync (2.2.0)
activemodel (>= 4.1.0)
......@@ -314,7 +314,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.88.0)
gitaly-proto (0.91.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (5.3.3)
......@@ -1028,8 +1028,8 @@ DEPENDENCIES
akismet (~> 2.0)
allocations (~> 1.0)
asana (~> 0.6.0)
asciidoctor (~> 1.5.2)
asciidoctor-plantuml (= 0.0.7)
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.2.0)
attr_encrypted (~> 3.0.0)
awesome_print (~> 1.2.0)
......@@ -1096,7 +1096,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.88.0)
gitaly-proto (~> 0.91.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
......
......@@ -757,7 +757,7 @@ GitLabDropdown = (function() {
}
if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
return;
return [selectedObject];
}
if (el.hasClass(ACTIVE_CLASS) && value !== 0) {
......
......@@ -6,6 +6,7 @@ export const defaultEditorOptions = {
minimap: {
enabled: false,
},
wordWrap: 'bounded',
};
export default [
......
......@@ -20,6 +20,11 @@
type: String,
required: true,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
......@@ -42,6 +47,7 @@
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
>
<textarea
......
......@@ -48,6 +48,11 @@
required: false,
default: true,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
......@@ -92,6 +97,7 @@
:form-state="formState"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
/>
<edit-actions
......
......@@ -11,11 +11,19 @@
type: String,
required: true,
},
helpUrl: {
type: String,
required: false,
default: '',
},
},
computed: {
hasTitle() {
return this.title.length > 0;
},
hasHelpURL() {
return this.helpUrl.length > 0;
},
},
};
</script>
......@@ -28,5 +36,21 @@
{{ title }}:
</span>
{{ value }}
<span
v-if="hasHelpURL"
class="help-button pull-right"
>
<a
:href="helpUrl"
target="_blank"
rel="noopener noreferrer nofollow"
>
<i
class="fa fa-question-circle"
aria-hidden="true"
></i>
</a>
</span>
</p>
</template>
......@@ -22,6 +22,11 @@
type: Boolean,
required: true,
},
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
},
computed: {
shouldRenderContent() {
......@@ -39,6 +44,21 @@
runnerId() {
return `#${this.job.runner.id}`;
},
hasTimeout() {
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== '';
},
timeout() {
if (this.job.metadata == null) {
return '';
}
let t = this.job.metadata.timeout_human_readable;
if (this.job.metadata.timeout_source !== '') {
t += ` (from ${this.job.metadata.timeout_source})`;
}
return t;
},
renderBlock() {
return this.job.merge_request ||
this.job.duration ||
......@@ -114,6 +134,13 @@
title="Queued"
:value="queued"
/>
<detail-row
class="js-job-timeout"
v-if="hasTimeout"
title="Timeout"
:help-url="runnerHelpUrl"
:value="timeout"
/>
<detail-row
class="js-job-runner"
v-if="job.runner"
......
......@@ -51,6 +51,7 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
job: this.mediator.store.state.job,
runnerHelpUrl: dataset.runnerHelpUrl,
},
});
},
......
<script>
import _ from 'underscore';
import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue';
import Graph from './graph.vue';
import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store';
import eventHub from '../event_hub';
import _ from 'underscore';
import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue';
import Graph from './graph.vue';
import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store';
import eventHub from '../event_hub';
export default {
components: {
Graph,
GraphGroup,
EmptyState,
export default {
components: {
Graph,
GraphGroup,
EmptyState,
},
props: {
hasMetrics: {
type: Boolean,
required: false,
default: true,
},
props: {
hasMetrics: {
type: Boolean,
required: false,
default: true,
},
showLegend: {
type: Boolean,
required: false,
default: true,
},
showPanels: {
type: Boolean,
required: false,
default: true,
},
forceSmallGraph: {
type: Boolean,
required: false,
default: false,
},
documentationPath: {
type: String,
required: true,
},
settingsPath: {
type: String,
required: true,
},
clustersPath: {
type: String,
required: true,
},
tagsPath: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
metricsEndpoint: {
type: String,
required: true,
},
deploymentEndpoint: {
type: String,
required: false,
default: null,
},
emptyGettingStartedSvgPath: {
type: String,
required: true,
},
emptyLoadingSvgPath: {
type: String,
required: true,
},
emptyNoDataSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: {
type: String,
required: true,
},
showLegend: {
type: Boolean,
required: false,
default: true,
},
data() {
return {
store: new MonitoringStore(),
state: 'gettingStarted',
showEmptyState: true,
updateAspectRatio: false,
updatedAspectRatios: 0,
hoverData: {},
resizeThrottled: {},
};
showPanels: {
type: Boolean,
required: false,
default: true,
},
created() {
this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint,
deploymentEndpoint: this.deploymentEndpoint,
});
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$on('hoverChanged', this.hoverChanged);
forceSmallGraph: {
type: Boolean,
required: false,
default: false,
},
beforeDestroy() {
eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$off('hoverChanged', this.hoverChanged);
window.removeEventListener('resize', this.resizeThrottled, false);
documentationPath: {
type: String,
required: true,
},
mounted() {
this.resizeThrottled = _.throttle(this.resize, 600);
if (!this.hasMetrics) {
this.state = 'gettingStarted';
} else {
this.getGraphsData();
window.addEventListener('resize', this.resizeThrottled, false);
settingsPath: {
type: String,
required: true,
},
clustersPath: {
type: String,
required: true,
},
tagsPath: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
metricsEndpoint: {
type: String,
required: true,
},
deploymentEndpoint: {
type: String,
required: false,
default: null,
},
emptyGettingStartedSvgPath: {
type: String,
required: true,
},
emptyLoadingSvgPath: {
type: String,
required: true,
},
emptyNoDataSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: {
type: String,
required: true,
},
},
data() {
return {
store: new MonitoringStore(),
state: 'gettingStarted',
showEmptyState: true,
updateAspectRatio: false,
updatedAspectRatios: 0,
hoverData: {},
resizeThrottled: {},
};
},
created() {
this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint,
deploymentEndpoint: this.deploymentEndpoint,
});
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$on('hoverChanged', this.hoverChanged);
},
beforeDestroy() {
eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$off('hoverChanged', this.hoverChanged);
window.removeEventListener('resize', this.resizeThrottled, false);
},
mounted() {
this.resizeThrottled = _.throttle(this.resize, 600);
if (!this.hasMetrics) {
this.state = 'gettingStarted';
} else {
this.getGraphsData();
window.addEventListener('resize', this.resizeThrottled, false);
}
},
methods: {
getGraphsData() {
this.state = 'loading';
Promise.all([
this.service.getGraphsData().then(data => this.store.storeMetrics(data)),
this.service
.getDeploymentData()
.then(data => this.store.storeDeploymentData(data))
.catch(() => new Flash('Error getting deployment information.')),
])
.then(() => {
if (this.store.groups.length < 1) {
this.state = 'noData';
return;
}
this.showEmptyState = false;
})
.catch(() => {
this.state = 'unableToConnect';
});
},
resize() {
this.updateAspectRatio = true;
},
toggleAspectRatio() {
this.updatedAspectRatios = this.updatedAspectRatios += 1;
if (this.store.getMetricsCount() === this.updatedAspectRatios) {
this.updateAspectRatio = !this.updateAspectRatio;
this.updatedAspectRatios = 0;
}
},
methods: {
getGraphsData() {
this.state = 'loading';
Promise.all([
this.service.getGraphsData()
.then(data => this.store.storeMetrics(data)),
this.service.getDeploymentData()
.then(data => this.store.storeDeploymentData(data))
.catch(() => new Flash('Error getting deployment information.')),
])
.then(() => {
if (this.store.groups.length < 1) {
this.state = 'noData';
return;
}
this.showEmptyState = false;
})
.catch(() => { this.state = 'unableToConnect'; });
},
resize() {
this.updateAspectRatio = true;
},
toggleAspectRatio() {
this.updatedAspectRatios = this.updatedAspectRatios += 1;
if (this.store.getMetricsCount() === this.updatedAspectRatios) {
this.updateAspectRatio = !this.updateAspectRatio;
this.updatedAspectRatios = 0;
}
},
hoverChanged(data) {
this.hoverData = data;
},
hoverChanged(data) {
this.hoverData = data;
},
};
},
};
</script>
<template>
......
<script>
export default {
props: {
documentationPath: {
type: String,
required: true,
},
settingsPath: {
type: String,
required: false,
default: '',
},
clustersPath: {
type: String,
required: false,
default: '',
},
selectedState: {
type: String,
required: true,
},
emptyGettingStartedSvgPath: {
type: String,
required: true,
},
emptyLoadingSvgPath: {
type: String,
required: true,
},
emptyNoDataSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: {
type: String,
required: true,
},
export default {
props: {
documentationPath: {
type: String,
required: true,
},
settingsPath: {
type: String,
required: false,
default: '',
},
clustersPath: {
type: String,
required: false,
default: '',
},
selectedState: {
type: String,
required: true,
},
data() {
return {
states: {
gettingStarted: {
svgUrl: this.emptyGettingStartedSvgPath,
title: 'Get started with performance monitoring',
description: `Stay updated about the performance and health
emptyGettingStartedSvgPath: {
type: String,
required: true,
},
emptyLoadingSvgPath: {
type: String,
required: true,
},
emptyNoDataSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: {
type: String,
required: true,
},
},
data() {
return {
states: {
gettingStarted: {
svgUrl: this.emptyGettingStartedSvgPath,
title: 'Get started with performance monitoring',
description: `Stay updated about the performance and health
of your environment by configuring Prometheus to monitor your deployments.`,
buttonText: 'Install Prometheus on clusters',
buttonPath: this.clustersPath,
secondaryButtonText: 'Configure existing Prometheus',
secondaryButtonPath: this.settingsPath,
},
loading: {
svgUrl: this.emptyLoadingSvgPath,
title: 'Waiting for performance data',
description: `Creating graphs uses the data from the Prometheus server.
buttonText: 'Install Prometheus on clusters',
buttonPath: this.clustersPath,
secondaryButtonText: 'Configure existing Prometheus',
secondaryButtonPath: this.settingsPath,
},
loading: {
svgUrl: this.emptyLoadingSvgPath,
title: 'Waiting for performance data',
description: `Creating graphs uses the data from the Prometheus server.
If this takes a long time, ensure that data is available.`,
buttonText: 'View documentation',
buttonPath: this.documentationPath,
},
noData: {
svgUrl: this.emptyNoDataSvgPath,
title: 'No data found',
description: `You are connected to the Prometheus server, but there is currently
buttonText: 'View documentation',
buttonPath: this.documentationPath,
},
noData: {
svgUrl: this.emptyNoDataSvgPath,
title: 'No data found',
description: `You are connected to the Prometheus server, but there is currently
no data to display.`,
buttonText: 'Configure Prometheus',
buttonPath: this.settingsPath,
},
unableToConnect: {
svgUrl: this.emptyUnableToConnectSvgPath,
title: 'Unable to connect to Prometheus server',
description: 'Ensure connectivity is available from the GitLab server to the ',
buttonText: 'View documentation',
buttonPath: this.documentationPath,
},
buttonText: 'Configure Prometheus',
buttonPath: this.settingsPath,
},
unableToConnect: {
svgUrl: this.emptyUnableToConnectSvgPath,
title: 'Unable to connect to Prometheus server',
description: 'Ensure connectivity is available from the GitLab server to the ',
buttonText: 'View documentation',
buttonPath: this.documentationPath,
},
};
},
computed: {
currentState() {
return this.states[this.selectedState];
},
showButtonDescription() {
if (this.selectedState === 'unableToConnect') return true;
return false;
},
};
},
computed: {
currentState() {
return this.states[this.selectedState];
},
showButtonDescription() {
if (this.selectedState === 'unableToConnect') return true;
return false;
},
};
},
};
</script>
<template>
......
<script>
export default {
props: {
deploymentData: {
type: Array,
required: true,
},
graphHeight: {
type: Number,
required: true,
},
graphHeightOffset: {
type: Number,
required: true,
},
export default {
props: {
deploymentData: {
type: Array,
required: true,
},
computed: {
calculatedHeight() {
return this.graphHeight - this.graphHeightOffset;
},
graphHeight: {
type: Number,
required: true,
},
methods: {
transformDeploymentGroup(deployment) {
return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
},
graphHeightOffset: {
type: Number,
required: true,
},
};
},
computed: {
calculatedHeight() {
return this.graphHeight - this.graphHeightOffset;
},
},
methods: {
transformDeploymentGroup(deployment) {
return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
},
},
};
</script>
<template>
<g class="deploy-info">
......
<script>
import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
import icon from '../../../vue_shared/components/icon.vue';
import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
import icon from '../../../vue_shared/components/icon.vue';
export default {
components: {
icon,
},
props: {
currentXCoordinate: {
type: Number,
required: true,
},
currentData: {
type: Object,
required: true,
},
deploymentFlagData: {
type: Object,
required: false,
default: null,
},
graphHeight: {
type: Number,
required: true,
},
graphHeightOffset: {
type: Number,
required: true,
},
realPixelRatio: {
type: Number,
required: true,
},
showFlagContent: {
type: Boolean,
required: true,
},
timeSeries: {
type: Array,
required: true,
},
unitOfDisplay: {
type: String,
required: true,
},
currentDataIndex: {
type: Number,
required: true,
},
legendTitle: {
type: String,
required: true,
},
export default {
components: {
icon,
},
props: {
currentXCoordinate: {
type: Number,
required: true,
},
computed: {
formatTime() {
return this.deploymentFlagData ?
timeFormat(this.deploymentFlagData.time) :
timeFormat(this.currentData.time);
},
formatDate() {
return this.deploymentFlagData ?
dateFormat(this.deploymentFlagData.time) :
dateFormat(this.currentData.time);
},
cursorStyle() {
const xCoordinate = this.deploymentFlagData ?
this.deploymentFlagData.xPos :
this.currentXCoordinate;
const offsetTop = 20 * this.realPixelRatio;
const offsetLeft = (70 + xCoordinate) * this.realPixelRatio;
const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio;
return {
top: `${offsetTop}px`,
left: `${offsetLeft}px`,
height: `${height}px`,
};
},
flagOrientation() {
if (this.currentXCoordinate * this.realPixelRatio > 120) {
return 'left';
}
return 'right';
},
currentData: {
type: Object,
required: true,
},
deploymentFlagData: {
type: Object,
required: false,
default: null,
},
graphHeight: {
type: Number,
required: true,
},
graphHeightOffset: {
type: Number,
required: true,
},
realPixelRatio: {
type: Number,
required: true,
},
showFlagContent: {
type: Boolean,
required: true,
},
timeSeries: {
type: Array,
required: true,
},
unitOfDisplay: {
type: String,
required: true,
},
currentDataIndex: {
type: Number,
required: true,
},
legendTitle: {
type: String,
required: true,
},
},
computed: {
formatTime() {
return this.deploymentFlagData
? timeFormat(this.deploymentFlagData.time)
: timeFormat(this.currentData.time);
},
formatDate() {
return this.deploymentFlagData
? dateFormat(this.deploymentFlagData.time)
: dateFormat(this.currentData.time);
},
cursorStyle() {
const xCoordinate = this.deploymentFlagData
? this.deploymentFlagData.xPos
: this.currentXCoordinate;
methods: {
seriesMetricValue(series) {
const index = this.deploymentFlagData ?
this.deploymentFlagData.seriesIndex :
this.currentDataIndex;
const value = series.values[index] &&
series.values[index].value;
if (isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
},
seriesMetricLabel(index, series) {
if (this.timeSeries.length < 2) {
return this.legendTitle;
}
if (series.metricTag) {
return series.metricTag;
}
return `series ${index + 1}`;
},
const offsetTop = 20 * this.realPixelRatio;
const offsetLeft = (70 + xCoordinate) * this.realPixelRatio;
const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio;
strokeDashArray(type) {
if (type === 'dashed') return '6, 3';
if (type === 'dotted') return '3, 3';
return null;
},
return {
top: `${offsetTop}px`,
left: `${offsetLeft}px`,
height: `${height}px`,
};
},
flagOrientation() {
if (this.currentXCoordinate * this.realPixelRatio > 120) {
return 'left';
}
return 'right';
},
},
methods: {
seriesMetricValue(series) {
const index = this.deploymentFlagData
? this.deploymentFlagData.seriesIndex
: this.currentDataIndex;
const value = series.values[index] && series.values[index].value;
if (isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
},
seriesMetricLabel(index, series) {
if (this.timeSeries.length < 2) {
return this.legendTitle;
}
if (series.metricTag) {
return series.metricTag;
}
return `series ${index + 1}`;
},
strokeDashArray(type) {
if (type === 'dashed') return '6, 3';
if (type === 'dotted') return '3, 3';
return null;
},
};
},
};
</script>
<template>
......
<script>
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
export default {
props: {
graphWidth: {
type: Number,
required: true,
},
graphHeight: {
type: Number,
required: true,
},
margin: {
type: Object,
required: true,
},
measurements: {
type: Object,
required: true,
},
legendTitle: {
type: String,
required: true,
},
yAxisLabel: {
type: String,
required: true,
},
timeSeries: {
type: Array,
required: true,
},
unitOfDisplay: {
type: String,
required: true,
},
currentDataIndex: {
type: Number,
required: true,
},
showLegendGroup: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
yLabelWidth: 0,
yLabelHeight: 0,
seriesXPosition: 0,
metricUsageXPosition: 0,
};
},
computed: {
textTransform() {
const yCoordinate = (((this.graphHeight - this.margin.top)
+ this.measurements.axisLabelLineOffset) / 2) || 0;
return `translate(15, ${yCoordinate}) rotate(-90)`;
},
rectTransform() {
const yCoordinate = (((this.graphHeight - this.margin.top)
+ this.measurements.axisLabelLineOffset) / 2)
+ (this.yLabelWidth / 2) || 0;
return `translate(0, ${yCoordinate}) rotate(-90)`;
},
xPosition() {
return (((this.graphWidth + this.measurements.axisLabelLineOffset) / 2)
- this.margin.right) || 0;
},
yPosition() {
return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0;
},
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
export default {
props: {
graphWidth: {
type: Number,
required: true,
},
mounted() {
this.$nextTick(() => {
const bbox = this.$refs.ylabel.getBBox();
this.metricUsageXPosition = 0;
this.seriesXPosition = 0;
if (this.$refs.legendTitleSvg != null) {
this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width;
}
if (this.$refs.seriesTitleSvg != null) {
this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width;
}
this.yLabelWidth = bbox.width + 10; // Added some padding
this.yLabelHeight = bbox.height + 5;
});
},
methods: {
translateLegendGroup(index) {
return `translate(0, ${12 * (index)})`;
},
formatMetricUsage(series) {
const value = series.values[this.currentDataIndex] &&
series.values[this.currentDataIndex].value;
if (isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`;
},
graphHeight: {
type: Number,
required: true,
},
margin: {
type: Object,
required: true,
},
measurements: {
type: Object,
required: true,
},
legendTitle: {
type: String,
required: true,
},
yAxisLabel: {
type: String,
required: true,
},
timeSeries: {
type: Array,
required: true,
},
unitOfDisplay: {
type: String,
required: true,
},
currentDataIndex: {
type: Number,
required: true,
},
showLegendGroup: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
yLabelWidth: 0,
yLabelHeight: 0,
seriesXPosition: 0,
metricUsageXPosition: 0,
};
},
computed: {
textTransform() {
const yCoordinate =
(this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 || 0;
createSeriesString(index, series) {
if (series.metricTag) {
return `${series.metricTag} ${this.formatMetricUsage(series)}`;
}
return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
},
return `translate(15, ${yCoordinate}) rotate(-90)`;
},
rectTransform() {
const yCoordinate =
(this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 +
this.yLabelWidth / 2 || 0;
strokeDashArray(type) {
if (type === 'dashed') return '6, 3';
if (type === 'dotted') return '3, 3';
return null;
},
return `translate(0, ${yCoordinate}) rotate(-90)`;
},
xPosition() {
return (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - this.margin.right || 0;
},
yPosition() {
return this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset || 0;
},
},
mounted() {
this.$nextTick(() => {
const bbox = this.$refs.ylabel.getBBox();
this.metricUsageXPosition = 0;
this.seriesXPosition = 0;
if (this.$refs.legendTitleSvg != null) {
this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width;
}
if (this.$refs.seriesTitleSvg != null) {
this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width;
}
this.yLabelWidth = bbox.width + 10; // Added some padding
this.yLabelHeight = bbox.height + 5;
});
},
methods: {
translateLegendGroup(index) {
return `translate(0, ${12 * index})`;
},
formatMetricUsage(series) {
const value =
series.values[this.currentDataIndex] && series.values[this.currentDataIndex].value;
if (isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`;
},
createSeriesString(index, series) {
if (series.metricTag) {
return `${series.metricTag} ${this.formatMetricUsage(series)}`;
}
return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
},
strokeDashArray(type) {
if (type === 'dashed') return '6, 3';
if (type === 'dotted') return '3, 3';
return null;
},
};
},
};
</script>
<template>
<g class="axis-label-container">
......
<script>
export default {
props: {
generatedLinePath: {
type: String,
required: true,
},
generatedAreaPath: {
type: String,
required: true,
},
lineStyle: {
type: String,
required: false,
default: '',
},
lineColor: {
type: String,
required: true,
},
areaColor: {
type: String,
required: true,
},
export default {
props: {
generatedLinePath: {
type: String,
required: true,
},
computed: {
strokeDashArray() {
if (this.lineStyle === 'dashed') return '3, 1';
if (this.lineStyle === 'dotted') return '1, 1';
return null;
},
generatedAreaPath: {
type: String,
required: true,
},
};
lineStyle: {
type: String,
required: false,
default: '',
},
lineColor: {
type: String,
required: true,
},
areaColor: {
type: String,
required: true,
},
},
computed: {
strokeDashArray() {
if (this.lineStyle === 'dashed') return '3, 1';
if (this.lineStyle === 'dotted') return '1, 1';
return null;
},
},
};
</script>
<template>
<g>
......
<script>
export default {
props: {
name: {
type: String,
required: true,
},
showPanels: {
type: Boolean,
required: false,
default: true,
},
export default {
props: {
name: {
type: String,
required: true,
},
};
showPanels: {
type: Boolean,
required: false,
default: true,
},
},
};
</script>
<template>
......
<script>
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
export default {
components: {
GlModal,
export default {
components: {
GlModal,
},
props: {
milestoneTitle: {
type: String,
required: true,
},
props: {
milestoneTitle: {
type: String,
required: true,
},
url: {
type: String,
required: true,
},
url: {
type: String,
required: true,
},
computed: {
title() {
return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle });
},
text() {
return s__(`Milestones|Promoting this milestone will make it available for all projects inside the group.
Existing project milestones with the same title will be merged.
This action cannot be reversed.`);
},
groupName: {
type: String,
required: true,
},
methods: {
onSubmit() {
eventHub.$emit('promoteMilestoneModal.requestStarted', this.url);
return axios.post(this.url, { params: { format: 'json' } })
.then((response) => {
eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: true });
visitUrl(response.data.url);
})
.catch((error) => {
eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: false });
createFlash(error);
},
computed: {
title() {
return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), {
milestoneTitle: this.milestoneTitle,
});
},
text() {
const milestonePromotion = sprintf(
s__(`Milestones|Promoting %{milestone} will make it available for all projects inside %{groupName}.
Existing project milestones with the same name will be merged. `),
{
milestone: this.milestoneTitle,
groupName: this.groupName,
},
);
const missingFeatureWarn = sprintf(
s__(`Milestones|Group milestones are currently %{linkStart} missing features such as burndown charts. %{linkEnd}
You will not have these features once you've promoted a project milestone.
They will be available in future releases.`),
{
linkStart: `<a href="https://docs.gitlab.com/ee/user/project/milestones/"
target="_blank" rel="noopener noreferrer">`,
linkEnd: '</a>',
},
false,
);
const finalWarning = s__('Milestones|This action cannot be reversed.');
return sprintf(
s__(
`Milestones|<p>%{milestonePromotion}</p>
<p>%{missingFeatureWarn}</p>%{finalWarning}`,
),
{
milestonePromotion,
missingFeatureWarn,
finalWarning,
},
false,
);
},
},
methods: {
onSubmit() {
eventHub.$emit('promoteMilestoneModal.requestStarted', this.url);
return axios
.post(this.url, { params: { format: 'json' } })
.then(response => {
eventHub.$emit('promoteMilestoneModal.requestFinished', {
milestoneUrl: this.url,
successful: true,
});
},
visitUrl(response.data.url);
})
.catch(error => {
eventHub.$emit('promoteMilestoneModal.requestFinished', {
milestoneUrl: this.url,
successful: false,
});
createFlash(error);
});
},
};
},
};
</script>
<template>
<gl-modal
......@@ -58,7 +101,10 @@
>
{{ title }}
</template>
{{ text }}
<div
v-html="text"
>
</div>
</gl-modal>
</template>
......@@ -25,6 +25,7 @@ export default () => {
const modalProps = {
milestoneTitle: button.dataset.milestoneTitle,
url: button.dataset.url,
groupName: button.dataset.groupName,
};
eventHub.$once('promoteMilestoneModal.requestStarted', onRequestStarted);
eventHub.$emit('promoteMilestoneModal.props', modalProps);
......@@ -54,6 +55,7 @@ export default () => {
return {
modalProps: {
milestoneTitle: '',
groupName: '',
url: '',
},
};
......
<script>
import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import GlModal from '~/vue_shared/components/gl_modal.vue';
......@@ -27,19 +28,26 @@
type: String,
required: true,
},
groupName: {
type: String,
required: true,
},
},
computed: {
text() {
return s__(`Milestones|Promoting this label will make it available for all projects inside the group.
Existing project labels with the same title will be merged. This action cannot be reversed.`);
return sprintf(s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}.
Existing project labels with the same title will be merged. This action cannot be reversed.`), {
labelTitle: this.labelTitle,
groupName: this.groupName,
});
},
title() {
const label = `<span
class="label color-label"
style="background-color: ${this.labelColor}; color: ${this.labelTextColor};"
>${this.labelTitle}</span>`;
>${_.escape(this.labelTitle)}</span>`;
return sprintf(s__('Labels|Promote label %{labelTitle} to Group Label?'), {
return sprintf(s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'), {
labelTitle: label,
}, false);
},
......@@ -69,6 +77,7 @@
>
<div
slot="title"
class="modal-title-with-label"
v-html="title"
>
{{ title }}
......
......@@ -30,6 +30,7 @@ const initLabelIndex = () => {
labelColor: button.dataset.labelColor,
labelTextColor: button.dataset.labelTextColor,
url: button.dataset.url,
groupName: button.dataset.groupName,
};
eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
eventHub.$emit('promoteLabelModal.props', modalProps);
......@@ -62,6 +63,7 @@ const initLabelIndex = () => {
labelColor: '',
labelTextColor: '',
url: '',
groupName: '',
},
};
},
......
<script>
import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time';
export default {
name: 'time-tracking-comparison-pane',
name: 'TimeTrackingComparisonPane',
props: {
timeSpent: {
type: Number,
......@@ -43,47 +44,50 @@ export default {
return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
},
},
template: `
<div class="time-tracking-comparison-pane">
};
</script>
<template>
<div class="time-tracking-comparison-pane">
<div
class="compare-meter"
data-toggle="tooltip"
data-placement="top"
role="timeRemainingDisplay"
:aria-valuenow="timeRemainingTooltip"
:title="timeRemainingTooltip"
:data-original-title="timeRemainingTooltip"
:class="timeRemainingStatusClass"
>
<div
class="compare-meter"
data-toggle="tooltip"
data-placement="top"
role="timeRemainingDisplay"
:aria-valuenow="timeRemainingTooltip"
:title="timeRemainingTooltip"
:data-original-title="timeRemainingTooltip"
:class="timeRemainingStatusClass"
class="meter-container"
role="timeSpentPercent"
:aria-valuenow="timeRemainingPercent"
>
<div
class="meter-container"
role="timeSpentPercent"
:aria-valuenow="timeRemainingPercent"
:style="{ width: timeRemainingPercent }"
class="meter-fill"
>
<div
:style="{ width: timeRemainingPercent }"
class="meter-fill"
/>
</div>
<div class="compare-display-container">
<div class="compare-display pull-left">
<span class="compare-label">
</div>
<div class="compare-display-container">
<div class="compare-display pull-left">
<span class="compare-label">
{{ s__('TimeTracking|Spent') }}
</span>
<span class="compare-value spent">
{{ timeSpentHumanReadable }}
</span>
</div>
<div class="compare-display estimated pull-right">
<span class="compare-label">
{{ s__('TimeTrackingEstimated|Est') }}
</span>
<span class="compare-value">
{{ timeEstimateHumanReadable }}
</span>
</div>
</span>
<span class="compare-value spent">
{{ timeSpentHumanReadable }}
</span>
</div>
<div class="compare-display estimated pull-right">
<span class="compare-label">
{{ s__('TimeTrackingEstimated|Est') }}
</span>
<span class="compare-value">
{{ timeEstimateHumanReadable }}
</span>
</div>
</div>
</div>
`,
};
</div>
</template>
......@@ -4,7 +4,7 @@ import TimeTrackingCollapsedState from './collapsed_state.vue';
import timeTrackingSpentOnlyPane from './spent_only_pane';
import timeTrackingNoTrackingPane from './no_tracking_pane';
import timeTrackingEstimateOnlyPane from './estimate_only_pane';
import timeTrackingComparisonPane from './comparison_pane';
import TimeTrackingComparisonPane from './comparison_pane.vue';
import eventHub from '../../event_hub';
......@@ -15,7 +15,7 @@ export default {
'time-tracking-estimate-only-pane': timeTrackingEstimateOnlyPane,
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane,
'time-tracking-comparison-pane': timeTrackingComparisonPane,
TimeTrackingComparisonPane,
'time-tracking-help-state': timeTrackingHelpState,
},
props: {
......
......@@ -17,8 +17,8 @@ export default {
/>
<div class="media-body space-children">
<span class="bold">
The source branch HEAD has recently changed.
Please reload the page and review the changes before merging.
{{ s__(`mrWidget|The source branch HEAD has recently changed.
Please reload the page and review the changes before merging`) }}
</span>
</div>
</div>
......
......@@ -32,6 +32,11 @@
required: false,
default: '',
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
......@@ -139,6 +144,7 @@
<markdown-toolbar
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
:can-attach-file="canAttachFile"
/>
</div>
</div>
......
......@@ -10,6 +10,11 @@
required: false,
default: '',
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
hasQuickActionsDocsPath() {
......@@ -50,7 +55,10 @@
are supported
</template>
</div>
<span class="uploading-container">
<span
v-if="canAttachFile"
class="uploading-container"
>
<span class="uploading-progress-container hide">
<i
class="fa fa-file-image-o toolbar-button-icon"
......
......@@ -2,7 +2,15 @@
* Styles the GitLab application with a specific color theme
*/
@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) {
@mixin gitlab-theme(
$color-100,
$color-200,
$color-500,
$color-700,
$color-800,
$color-900,
$color-alternate
) {
// Header
.navbar-gitlab {
......@@ -23,7 +31,7 @@
> li {
> a:hover,
> a:focus {
background-color: rgba($color-200, .2);
background-color: rgba($color-200, 0.2);
}
&.active > a,
......@@ -33,7 +41,7 @@
}
&.line-separator {
border-left: 1px solid rgba($color-200, .2);
border-left: 1px solid rgba($color-200, 0.2);
}
}
}
......@@ -56,7 +64,7 @@
&:hover,
&:focus {
@media (min-width: $screen-sm-min) {
background-color: rgba($color-200, .2);
background-color: rgba($color-200, 0.2);
}
svg {
......@@ -91,34 +99,34 @@
> a {
&:hover,
&:focus {
background-color: rgba($color-200, .2);
background-color: rgba($color-200, 0.2);
}
}
}
.search {
form {
background-color: rgba($color-200, .2);
background-color: rgba($color-200, 0.2);
&:hover {
background-color: rgba($color-200, .3);
background-color: rgba($color-200, 0.3);
}
}
.location-badge {
color: $color-100;
background-color: rgba($color-200, .1);
background-color: rgba($color-200, 0.1);
border-right: 1px solid $color-800;
}
.search-input::placeholder {
color: rgba($color-200, .8);
color: rgba($color-200, 0.8);
}
.search-input-wrap {
.search-icon,
.clear-icon {
fill: rgba($color-200, .8);
fill: rgba($color-200, 0.8);
}
}
......@@ -133,7 +141,7 @@
.search-input-wrap {
.search-icon {
fill: rgba($color-200, .8);
fill: rgba($color-200, 0.8);
}
}
}
......@@ -144,7 +152,6 @@
color: $color-900;
}
// Sidebar
.nav-sidebar li.active {
box-shadow: inset 4px 0 0 $color-700;
......@@ -169,28 +176,90 @@
font-weight: $gl-font-weight-bold;
}
}
}
// Web IDE
.ide-sidebar-link {
color: $color-200;
background-color: $color-700;
&:hover,
&:focus {
background-color: $color-500;
}
&:active {
background: $color-800;
}
}
.branch-container {
border-left-color: $color-700;
}
.branch-header-title {
color: $color-700;
}
}
body {
&.ui_indigo {
@include gitlab-theme($indigo-100, $indigo-200, $indigo-500, $indigo-700, $indigo-800, $indigo-900, $white-light);
@include gitlab-theme(
$indigo-100,
$indigo-200,
$indigo-500,
$indigo-700,
$indigo-800,
$indigo-900,
$white-light
);
}
&.ui_dark {
@include gitlab-theme($theme-gray-100, $theme-gray-200, $theme-gray-500, $theme-gray-700, $theme-gray-800, $theme-gray-900, $white-light);
@include gitlab-theme(
$theme-gray-100,
$theme-gray-200,
$theme-gray-500,
$theme-gray-700,
$theme-gray-800,
$theme-gray-900,
$white-light
);
}
&.ui_blue {
@include gitlab-theme($theme-blue-100, $theme-blue-200, $theme-blue-500, $theme-blue-700, $theme-blue-800, $theme-blue-900, $white-light);
@include gitlab-theme(
$theme-blue-100,
$theme-blue-200,
$theme-blue-500,
$theme-blue-700,
$theme-blue-800,
$theme-blue-900,
$white-light
);
}
&.ui_green {
@include gitlab-theme($theme-green-100, $theme-green-200, $theme-green-500, $theme-green-700, $theme-green-800, $theme-green-900, $white-light);
@include gitlab-theme(
$theme-green-100,
$theme-green-200,
$theme-green-500,
$theme-green-700,
$theme-green-800,
$theme-green-900,
$white-light
);
}
&.ui_light {
@include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700);
@include gitlab-theme(
$theme-gray-900,
$theme-gray-700,
$theme-gray-800,
$theme-gray-700,
$theme-gray-700,
$theme-gray-100,
$theme-gray-700
);
.navbar-gitlab {
background-color: $theme-gray-100;
......@@ -270,5 +339,9 @@ body {
.sidebar-top-level-items > li.active .badge {
color: $theme-gray-900;
}
.ide-sidebar-link {
color: $white-light;
}
}
}
......@@ -4,9 +4,15 @@
.page-title,
.modal-title {
.modal-title-with-label span {
vertical-align: middle;
display: inline-block;
}
.color-label {
font-size: $gl-font-size;
padding: $gl-vert-padding $label-padding-modal;
vertical-align: middle;
}
}
......
......@@ -402,7 +402,7 @@
}
.branch-container {
border-left: 4px solid $indigo-700;
border-left: 4px solid;
margin-bottom: $gl-bar-padding;
}
......@@ -414,7 +414,6 @@
.branch-header-title {
flex: 1;
padding: $grid-size $gl-padding;
color: $indigo-700;
font-weight: $gl-font-weight-bold;
svg {
......@@ -767,20 +766,7 @@
.ide-sidebar-link {
padding: $gl-padding-8 $gl-padding;
background: $indigo-700;
color: $white-light;
text-decoration: none;
display: flex;
align-items: center;
&:focus,
&:hover {
color: $white-light;
text-decoration: underline;
background: $indigo-500;
}
&:active {
background: $indigo-800;
}
font-weight: $gl-font-weight-bold;
}
......@@ -112,7 +112,7 @@ class Projects::LabelsController < Projects::ApplicationController
begin
return render_404 unless promote_service.execute(@label)
flash[:notice] = "#{@label.title} promoted to group label."
flash[:notice] = "#{@label.title} promoted to <a href=\"#{group_labels_path(@project.group)}\">group label</a>.".html_safe
respond_to do |format|
format.html do
redirect_to(project_labels_path(@project), status: 303)
......
......@@ -75,9 +75,9 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def promote
Milestones::PromoteService.new(project, current_user).execute(milestone)
promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
flash[:notice] = "#{milestone.title} promoted to group milestone"
flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\">group milestone</a>.".html_safe
respond_to do |format|
format.html do
redirect_to project_milestones_path(project)
......
......@@ -5,12 +5,8 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
@project.repository.branches
end
def create_service_class
::ProtectedBranches::CreateService
end
def update_service_class
::ProtectedBranches::UpdateService
def service_namespace
::ProtectedBranches
end
def load_protected_ref
......
......@@ -37,7 +37,7 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
end
def destroy
@protected_ref.destroy
destroy_service_class.new(@project, current_user).execute(@protected_ref)
respond_to do |format|
format.html { redirect_to_repository_settings(@project) }
......@@ -47,6 +47,18 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
protected
def create_service_class
service_namespace::CreateService
end
def update_service_class
service_namespace::UpdateService
end
def destroy_service_class
service_namespace::DestroyService
end
def access_level_attributes
%i(access_level id user_id _destroy group_id)
end
......
......@@ -5,12 +5,8 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController
@project.repository.tags
end
def create_service_class
::ProtectedTags::CreateService
end
def update_service_class
::ProtectedTags::UpdateService
def service_namespace
::ProtectedTags
end
def load_protected_ref
......
......@@ -39,7 +39,10 @@ module PageLayoutHelper
end
def favicon
Rails.env.development? ? 'favicon-green.ico' : 'favicon.ico'
return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY'])
return 'favicon-green.ico' if Rails.env.development?
'favicon.ico'
end
def page_image
......
......@@ -27,6 +27,9 @@ module Ci
has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :metadata, class_name: 'Ci::BuildMetadata'
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
##
# The "environment" field for builds is a String, and is the unexpanded name!
#
......@@ -158,6 +161,14 @@ module Ci
before_transition any => [:running] do |build|
build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
end
before_transition pending: :running do |build|
build.ensure_metadata.update_timeout_state
end
end
def ensure_metadata
metadata || build_metadata(project: project)
end
def detailed_status(current_user)
......@@ -238,10 +249,6 @@ module Ci
latest_builds.where('stage_idx < ?', stage_idx)
end
def timeout
project.build_timeout
end
def triggered_by?(current_user)
user == current_user
end
......
module Ci
# The purpose of this class is to store Build related data that can be disposed.
# Data that should be persisted forever, should be stored with Ci::Build model.
class BuildMetadata < ActiveRecord::Base
extend Gitlab::Ci::Model
include Presentable
include ChronicDurationAttribute
self.table_name = 'ci_builds_metadata'
belongs_to :build, class_name: 'Ci::Build'
belongs_to :project
validates :build, presence: true
validates :project, presence: true
chronic_duration_attr_reader :timeout_human_readable, :timeout
enum timeout_source: {
unknown_timeout_source: 1,
project_timeout_source: 2,
runner_timeout_source: 3
}
def update_timeout_state
return unless build.runner.present?
project_timeout = project&.build_timeout
timeout = [project_timeout, build.runner.maximum_timeout].compact.min
timeout_source = timeout < project_timeout ? :runner_timeout_source : :project_timeout_source
update(timeout: timeout, timeout_source: timeout_source)
end
end
end
......@@ -3,13 +3,14 @@ module Ci
extend Gitlab::Ci::Model
include Gitlab::SQL::Pattern
include RedisCacheable
include ChronicDurationAttribute
prepend EE::Ci::Runner
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour
UPDATE_DB_RUNNER_INFO_EVERY = 40.minutes
AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level].freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......@@ -52,6 +53,12 @@ module Ci
cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout
validates :maximum_timeout, allow_nil: true,
numericality: { greater_than_or_equal_to: 600,
message: 'needs to be at least 10 minutes' }
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
......
module ChronicDurationAttribute
extend ActiveSupport::Concern
class_methods do
def chronic_duration_attr_reader(virtual_attribute, source_attribute)
define_method(virtual_attribute) do
chronic_duration_attributes[virtual_attribute] || output_chronic_duration_attribute(source_attribute)
end
end
def chronic_duration_attr_writer(virtual_attribute, source_attribute)
chronic_duration_attr_reader(virtual_attribute, source_attribute)
define_method("#{virtual_attribute}=") do |value|
chronic_duration_attributes[virtual_attribute] = value.presence || ''
begin
new_value = ChronicDuration.parse(value).to_i if value.present?
assign_attributes(source_attribute => new_value)
rescue ChronicDuration::DurationParseError
# ignore error as it will be caught by validation
end
end
validates virtual_attribute, allow_nil: true, duration: true
end
alias_method :chronic_duration_attr, :chronic_duration_attr_writer
end
def chronic_duration_attributes
@chronic_duration_attributes ||= {}
end
def output_chronic_duration_attribute(source_attribute)
value = attributes[source_attribute.to_s]
ChronicDuration.output(value, format: :short) if value
end
end
......@@ -562,18 +562,25 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff(true)
end
def viewable_diffs
@viewable_diffs ||= merge_request_diffs.viewable.to_a
end
def merge_request_diff_for(diff_refs_or_sha)
@merge_request_diffs_by_diff_refs_or_sha ||= Hash.new do |h, diff_refs_or_sha|
diffs = merge_request_diffs.viewable
h[diff_refs_or_sha] =
if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
diffs.find_by_diff_refs(diff_refs_or_sha)
else
diffs.find_by(head_commit_sha: diff_refs_or_sha)
end
end
matcher =
if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
{
'start_commit_sha' => diff_refs_or_sha.start_sha,
'head_commit_sha' => diff_refs_or_sha.head_sha,
'base_commit_sha' => diff_refs_or_sha.base_sha
}
else
{ 'head_commit_sha' => diff_refs_or_sha }
end
@merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha]
viewable_diffs.find do |diff|
diff.attributes.slice(*matcher.keys) == matcher
end
end
def version_params_for(diff_refs)
......
......@@ -36,7 +36,7 @@ class GemnasiumService < Service
after: data[:after],
token: token,
api_key: api_key,
repo: project.repository.path_to_repo
repo: project.repository.path_to_repo # Gitaly: fixed by https://gitlab.com/gitlab-org/security-products/gemnasium-migration/issues/9
)
end
end
......@@ -100,10 +100,6 @@ class Repository
"#<#{self.class.name}:#{@disk_path}>"
end
def create_hooks
Gitlab::Git::Repository.create_hooks(path_to_repo, Gitlab.config.gitlab_shell.hooks_path)
end
def commit(ref = 'HEAD')
return nil unless exists?
return ref if ref.is_a?(::Commit)
......
class ProtectedBranchPolicy < BasePolicy
delegate { @subject.project }
rule { can?(:admin_project) }.policy do
enable :create_protected_branch
enable :update_protected_branch
enable :destroy_protected_branch
end
end
module Ci
class BuildMetadataPresenter < Gitlab::View::Presenter::Delegated
TIMEOUT_SOURCES = {
unknown_timeout_source: nil,
project_timeout_source: 'project',
runner_timeout_source: 'runner'
}.freeze
presents :metadata
def timeout_source
return unless metadata.timeout_source?
TIMEOUT_SOURCES[metadata.timeout_source.to_sym] ||
metadata.timeout_source
end
end
end
......@@ -5,6 +5,8 @@ class BuildDetailsEntity < JobEntity
expose :runner, using: RunnerEntity
expose :pipeline, using: PipelineEntity
expose :metadata, using: BuildMetadataEntity
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
erase_project_job_path(project, build)
......
class BuildMetadataEntity < Grape::Entity
expose :timeout_human_readable do |metadata|
metadata.timeout_human_readable unless metadata.timeout.nil?
end
expose :timeout_source do |metadata|
metadata.present.timeout_source
end
end
......@@ -7,8 +7,14 @@ class StatusEntity < Grape::Entity
expose :details_path
expose :favicon do |status|
dir = 'ci_favicons'
dir = File.join(dir, 'dev') if Rails.env.development?
dir =
if Gitlab::Utils.to_boolean(ENV['CANARY'])
File.join('ci_favicons', 'canary')
elsif Rails.env.development?
File.join('ci_favicons', 'dev')
else
'ci_favicons'
end
ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
end
......
module ProtectedBranches
class CreateService < BaseService
attr_reader :protected_branch
def execute(skip_authorization: false)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can?(current_user, :admin_project, project)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || authorized?
protected_branch.save
protected_branch
end
def authorized?
can?(current_user, :create_protected_branch, protected_branch)
end
private
project.protected_branches.create(params)
def protected_branch
@protected_branch ||= project.protected_branches.new(params)
end
end
end
module ProtectedBranches
class DestroyService < BaseService
def execute(protected_branch)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :destroy_protected_branch, protected_branch)
protected_branch.destroy
end
end
end
module ProtectedBranches
class UpdateService < BaseService
def execute(protected_branch)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :update_protected_branch, protected_branch)
protected_branch.update(params)
protected_branch
......
module ProtectedTags
class DestroyService < BaseService
def execute(protected_tag)
protected_tag.destroy
end
end
end
......@@ -34,7 +34,8 @@ class VerifyPagesDomainService < BaseService
# Prevent any pre-existing grace period from being truncated
reverify = [domain.enabled_until, VERIFICATION_PERIOD.from_now].compact.max
domain.update!(verified_at: Time.now, enabled_until: reverify)
domain.assign_attributes(verified_at: Time.now, enabled_until: reverify)
domain.save!(validate: false)
if was_disabled
notify(:enabled)
......@@ -47,7 +48,9 @@ class VerifyPagesDomainService < BaseService
def unverify_domain!
if domain.verified?
domain.update!(verified_at: nil)
domain.assign_attributes(verified_at: nil)
domain.save!(validate: false)
notify(:verification_failed)
end
......@@ -55,7 +58,8 @@ class VerifyPagesDomainService < BaseService
end
def disable_domain!
domain.update!(verified_at: nil, enabled_until: nil)
domain.assign_attributes(verified_at: nil, enabled_until: nil)
domain.save!(validate: false)
notify(:disabled)
......
......@@ -228,17 +228,9 @@ module ObjectStorage
raise 'Failed to update object store' unless updated
end
def use_file
if file_storage?
return yield path
end
begin
cache_stored_file!
yield cache_path
ensure
FileUtils.rm_f(cache_path)
cache_storage.delete_dir!(cache_path(nil))
def use_file(&blk)
with_exclusive_lease do
unsafe_use_file(&blk)
end
end
......@@ -248,12 +240,9 @@ module ObjectStorage
# new_store: Enum (Store::LOCAL, Store::REMOTE)
#
def migrate!(new_store)
uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
raise 'Already running' unless uuid
unsafe_migrate!(new_store)
ensure
Gitlab::ExclusiveLease.cancel(exclusive_lease_key, uuid)
with_exclusive_lease do
unsafe_migrate!(new_store)
end
end
def schedule_background_upload(*args)
......@@ -385,6 +374,15 @@ module ObjectStorage
"object_storage_migrate:#{model.class}:#{model.id}"
end
def with_exclusive_lease
uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
raise 'exclusive lease already taken' unless uuid
yield uuid
ensure
Gitlab::ExclusiveLease.cancel(exclusive_lease_key, uuid)
end
#
# Move the file to another store
#
......@@ -419,4 +417,18 @@ module ObjectStorage
raise e
end
end
def unsafe_use_file
if file_storage?
return yield path
end
begin
cache_stored_file!
yield cache_path
ensure
FileUtils.rm_f(cache_path)
cache_storage.delete_dir!(cache_path(nil))
end
end
end
......@@ -62,12 +62,16 @@
= link_to @project.ssh_url_to_repo, project_path(@project)
- if @project.repository.exists?
%li
%span.light fs:
%span.light Gitaly storage name:
%strong
= @project.repository.path_to_repo
= @project.repository.storage
%li
%span.light Gitaly relative path:
%strong
= @project.repository.relative_path
%li
%span.light Storage:
%span.light Storage used:
%strong= storage_counter(@project.statistics.storage_size)
(
= storage_counter(@project.statistics.repository_size)
......
......@@ -85,19 +85,6 @@
= link_to project_path_locks_path(@project) do
#{ _('Locked Files') }
- if project_nav_tab? :container_registry
= nav_link(controller: %w[projects/registry/repositories]) do
= link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
.nav-icon-container
= sprite_icon('disk')
%span.nav-item-name
Registry
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: %w[projects/registry/repositories], html_options: { class: "fly-out-top-item" } ) do
= link_to project_container_registry_index_path(@project) do
%strong.fly-out-top-item-name
#{ _('Registry') }
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
= link_to project_issues_path(@project), class: 'shortcuts-issues' do
......@@ -245,6 +232,19 @@
%span
Charts
- if project_nav_tab? :container_registry
= nav_link(controller: %w[projects/registry/repositories]) do
= link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
.nav-icon-container
= sprite_icon('disk')
%span.nav-item-name
Registry
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: %w[projects/registry/repositories], html_options: { class: "fly-out-top-item" } ) do
= link_to project_container_registry_index_path(@project) do
%strong.fly-out-top-item-name
#{ _('Registry') }
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
= link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do
......
%h4.prepend-top-20
= s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p
- link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
- link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index', anchor: 'adding-an-existing-kubernetes-cluster'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes').html_safe % { link_to_help_page: link_to_help_page }
......@@ -113,4 +113,4 @@
.js-build-options{ data: javascript_build_options }
#js-job-details-vue{ data: { endpoint: project_job_path(@project, @build, format: :json) } }
#js-job-details-vue{ data: { endpoint: project_job_path(@project, @build, format: :json), runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner') } }
......@@ -30,6 +30,7 @@
%button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
target: '#promote-milestone-modal',
milestone_title: @milestone.title,
group_name: @project.group.name,
url: promote_project_milestone_path(@milestone.project, @milestone),
container: 'body' },
disabled: true,
......
......@@ -8,3 +8,5 @@
= render 'form', { f: f }
.form-actions
= f.submit 'Create New Domain', class: "btn btn-save"
.pull-right
= link_to _('Cancel'), project_pages_path(@project), class: 'btn btn-cancel'
......@@ -39,6 +39,12 @@
Description
.col-sm-10
= f.text_field :description, class: 'form-control'
.form-group
= label_tag :maximum_timeout_human_readable, class: 'control-label' do
Maximum job timeout
.col-sm-10
= f.text_field :maximum_timeout_human_readable, class: 'form-control'
.help-block This timeout will take precedence when lower than Project-defined timeout
.form-group
= label_tag :tag_list, class: 'control-label' do
Tags
......
......@@ -55,6 +55,9 @@
%tr
%td Description
%td= @runner.description
%tr
%td Maximum job timeout
%td= @runner.maximum_timeout_human_readable
%tr
%td Last contact
%td
......
......@@ -60,6 +60,7 @@
label_title: label.title,
label_color: label.color,
label_text_color: label.text_color,
group_name: label.project.group.name,
target: '#promote-label-modal',
container: 'body',
toggle: 'modal' } }
......
......@@ -56,6 +56,7 @@
type: 'button',
data: { url: promote_project_milestone_path(milestone.project, milestone),
milestone_title: milestone.title,
group_name: @project.group.name,
target: '#promote-milestone-modal',
container: 'body',
toggle: 'modal' } }
......
......@@ -28,16 +28,17 @@ class GitGarbageCollectWorker
task = task.to_sym
cmd = command(task)
repo_path = project.repository.path_to_repo
description = "'#{cmd.join(' ')}' in #{repo_path}"
Gitlab::GitLogger.info(description)
gitaly_migrate(GITALY_MIGRATED_TASKS[task]) do |is_enabled|
if is_enabled
gitaly_call(task, project.repository.raw_repository)
else
repo_path = project.repository.path_to_repo
description = "'#{cmd.join(' ')}' in #{repo_path}"
Gitlab::GitLogger.info(description)
output, status = Gitlab::Popen.popen(cmd, repo_path)
Gitlab::GitLogger.error("#{description} failed:\n#{output}") unless status.zero?
end
end
......
# Gitaly issue: https://gitlab.com/gitlab-org/gitaly/issues/1110
class RepositoryForkWorker
include ApplicationWorker
include Gitlab::ShellAdapter
......
---
title: Adds cancel btn to new pages domain page
merge_request: 18026
author: Jacopo Beschi @jacopo-beschi
type: added
---
title: Fixed bug in dropdown selector when selecting the same selection again
merge_request: 14631
author: bitsapien
type: fixed
---
title: Avoid validation errors when running the Pages domain verification service
merge_request: 17992
author:
type: fixed
---
title: Update asciidoctor-plantuml to 0.0.8
merge_request: 18022
author: Takuya Noguchi
type: performance
---
title: Remove unused index from events table.
merge_request: 18014
author:
type: other
---
title: Fix data race between ObjectStorage background_upload and Pages publishing
merge_request:
author:
type: fixed
---
title: Add yellow favicon when `CANARY=true` to differientate canary environment
merge_request: 12477
author:
type: changed
---
title: Add per-runner configured job timeout
merge_request: 17221
author:
type: added
---
title: Fix listing commit branch/tags that contain special characters
merge_request:
author:
type: fixed
---
title: Correct copy text for the promote milestone and label modals
merge_request: 17726
author:
type: fixed
---
title: Move 'Registry' after 'CI/CD' in project navigation sidebar
merge_request: 18018
author: Elias Werberich
type: changed
---
title: Reduce number of queries when viewing a merge request
merge_request:
author:
type: performance
---
title: Move TimeTrackingComparisonPane vue component
merge_request: 17931
author: George Tsiolis
type: performance
---
title: Add i18n and update specs for ShaMismatch vue component
merge_request: 17870
author: George Tsiolis
type: performance
---
title: Make all workhorse gitaly calls opt-out
merge_request: 18002
title: Make all workhorse gitaly calls opt-out, take 2
merge_request: 18043
author:
type: other
---
title: Test if remote repository exists when importing wikis
merge_request:
author:
type: fixed
class AddMaximumTimeoutToCiRunners < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :ci_runners, :maximum_timeout, :integer
end
end
class CreateCiBuildsMetadataTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :ci_builds_metadata do |t|
t.integer :build_id, null: false
t.integer :project_id, null: false
t.integer :timeout
t.integer :timeout_source, null: false, default: 1
t.foreign_key :ci_builds, column: :build_id, on_delete: :cascade
t.foreign_key :projects, column: :project_id, on_delete: :cascade
t.index :build_id, unique: true
t.index :project_id
end
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveIndexFromEventsTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
remove_concurrent_index :events, :author_id
end
def down
add_concurrent_index :events, :author_id
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180323150945) do
ActiveRecord::Schema.define(version: 20180327101207) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -411,6 +411,16 @@ ActiveRecord::Schema.define(version: 20180323150945) do
add_index "ci_builds", ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree
add_index "ci_builds", ["user_id"], name: "index_ci_builds_on_user_id", using: :btree
create_table "ci_builds_metadata", force: :cascade do |t|
t.integer "build_id", null: false
t.integer "project_id", null: false
t.integer "timeout"
t.integer "timeout_source", default: 1, null: false
end
add_index "ci_builds_metadata", ["build_id"], name: "index_ci_builds_metadata_on_build_id", unique: true, using: :btree
add_index "ci_builds_metadata", ["project_id"], name: "index_ci_builds_metadata_on_project_id", using: :btree
create_table "ci_group_variables", force: :cascade do |t|
t.string "key", null: false
t.text "value"
......@@ -549,6 +559,7 @@ ActiveRecord::Schema.define(version: 20180323150945) do
t.boolean "locked", default: false, null: false
t.integer "access_level", default: 0, null: false
t.string "ip_address"
t.integer "maximum_timeout"
end
add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree
......@@ -882,7 +893,6 @@ ActiveRecord::Schema.define(version: 20180323150945) do
add_index "events", ["action"], name: "index_events_on_action", using: :btree
add_index "events", ["author_id", "project_id"], name: "index_events_on_author_id_and_project_id", using: :btree
add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree
add_index "events", ["project_id", "id"], name: "index_events_on_project_id_and_id", using: :btree
add_index "events", ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id", using: :btree
......@@ -2606,6 +2616,8 @@ ActiveRecord::Schema.define(version: 20180323150945) do
add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify
add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade
add_foreign_key "ci_builds", "projects", name: "fk_befce0568a", on_delete: :cascade
add_foreign_key "ci_builds_metadata", "ci_builds", column: "build_id", on_delete: :cascade
add_foreign_key "ci_builds_metadata", "projects", on_delete: :cascade
add_foreign_key "ci_group_variables", "namespaces", column: "group_id", name: "fk_33ae4d58d8", on_delete: :cascade
add_foreign_key "ci_job_artifacts", "ci_builds", column: "job_id", on_delete: :cascade
add_foreign_key "ci_job_artifacts", "projects", on_delete: :cascade
......
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