Commit fee0c932 authored by Regis's avatar Regis

fix merge conflict with issue_show haml

parents ab7e5912 cc52dfab
...@@ -49,7 +49,7 @@ eslint-report.html ...@@ -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/
This diff is collapsed.
This diff is collapsed.
...@@ -716,7 +716,7 @@ GEM ...@@ -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)
......
...@@ -73,7 +73,7 @@ One small thing you also have to do when installing it yourself is to copy the e ...@@ -73,7 +73,7 @@ One small thing you also have to do when installing it yourself is to copy the e
cp config/unicorn.rb.example.development config/unicorn.rb cp config/unicorn.rb.example.development config/unicorn.rb
Instructions on how to start GitLab and how to run the tests can be found in the [development section of the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit#development). Instructions on how to start GitLab and how to run the tests can be found in the [getting started section of the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit#getting-started).
## Software stack ## Software stack
......
9.1.0-pre 9.2.0-pre
...@@ -16,47 +16,44 @@ const defaults = { ...@@ -16,47 +16,44 @@ const defaults = {
class BlobForkSuggestion { class BlobForkSuggestion {
constructor(options) { constructor(options) {
this.elementMap = Object.assign({}, defaults, options); this.elementMap = Object.assign({}, defaults, options);
this.onClickWrapper = this.onClick.bind(this); this.onOpenButtonClick = this.onOpenButtonClick.bind(this);
this.onCancelButtonClick = this.onCancelButtonClick.bind(this);
document.addEventListener('click', this.onClickWrapper);
} }
showSuggestionSection(forkPath, action = 'edit') { init() {
[].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => { this.bindEvents();
suggestionSection.classList.remove('hidden');
});
[].forEach.call(this.elementMap.forkButtons, (forkButton) => { return this;
forkButton.setAttribute('href', forkPath); }
});
[].forEach.call(this.elementMap.actionTextPieces, (actionTextPiece) => { bindEvents() {
// eslint-disable-next-line no-param-reassign $(this.elementMap.openButtons).on('click', this.onOpenButtonClick);
actionTextPiece.textContent = action; $(this.elementMap.cancelButtons).on('click', this.onCancelButtonClick);
});
} }
hideSuggestionSection() { showSuggestionSection(forkPath, action = 'edit') {
[].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => { $(this.elementMap.suggestionSections).removeClass('hidden');
suggestionSection.classList.add('hidden'); $(this.elementMap.forkButtons).attr('href', forkPath);
}); $(this.elementMap.actionTextPieces).text(action);
} }
onClick(e) { hideSuggestionSection() {
const el = e.target; $(this.elementMap.suggestionSections).addClass('hidden');
}
if ([].includes.call(this.elementMap.openButtons, el)) { onOpenButtonClick(e) {
const { forkPath, action } = el.dataset; const forkPath = $(e.currentTarget).attr('data-fork-path');
const action = $(e.currentTarget).attr('data-action');
this.showSuggestionSection(forkPath, action); this.showSuggestionSection(forkPath, action);
} }
if ([].includes.call(this.elementMap.cancelButtons, el)) { onCancelButtonClick() {
this.hideSuggestionSection(); this.hideSuggestionSection();
} }
}
destroy() { destroy() {
document.removeEventListener('click', this.onClickWrapper); $(this.elementMap.openButtons).off('click', this.onOpenButtonClick);
$(this.elementMap.cancelButtons).off('click', this.onCancelButtonClick);
} }
} }
......
/* eslint-disable no-new */ /* eslint-disable no-new */
import Vue from 'vue'; import Vue from 'vue';
import VueResource from 'vue-resource'; import VueResource from 'vue-resource';
import NotebookLab from 'vendor/notebooklab'; import notebookLab from '../../notebook/index.vue';
Vue.use(VueResource); Vue.use(VueResource);
Vue.use(NotebookLab);
export default () => { export default () => {
const el = document.getElementById('js-notebook-viewer'); const el = document.getElementById('js-notebook-viewer');
...@@ -19,6 +18,9 @@ export default () => { ...@@ -19,6 +18,9 @@ export default () => {
json: {}, json: {},
}; };
}, },
components: {
notebookLab,
},
template: ` template: `
<div class="container-fluid md prepend-top-default append-bottom-default"> <div class="container-fluid md prepend-top-default append-bottom-default">
<div <div
......
...@@ -97,7 +97,8 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -97,7 +97,8 @@ const ShortcutsBlob = require('./shortcuts_blob');
cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'), cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'),
suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'), suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'),
actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'), actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'),
}); })
.init();
} }
switch (page) { switch (page) {
......
...@@ -5,6 +5,7 @@ ...@@ -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,16 @@ import './flash'; ...@@ -266,6 +267,16 @@ 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'),
});
});
}, },
}); });
} }
......
...@@ -22,6 +22,7 @@ class PrometheusGraph { ...@@ -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 { ...@@ -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 { ...@@ -53,26 +55,26 @@ class PrometheusGraph {
} }
init() { init() {
this.getData().then((metricsResponse) => { return this.getData().then((metricsResponse) => {
let enoughData = true; let enoughData = true;
if (typeof metricsResponse === 'undefined') {
enoughData = false;
} else {
Object.keys(metricsResponse.metrics).forEach((key) => { Object.keys(metricsResponse.metrics).forEach((key) => {
let currentKey;
if (key === 'cpu_values' || key === 'memory_values') { if (key === 'cpu_values' || key === 'memory_values') {
currentKey = metricsResponse.metrics[key]; const currentData = (metricsResponse.metrics[key])[0];
if (Object.keys(currentKey).length === 0) { if (currentData.values.length <= 2) {
enoughData = false; enoughData = false;
} }
} }
}); });
if (!enoughData) { }
this.state = '.js-loading'; if (enoughData) {
this.updateState(); $(prometheusStatesContainer).hide();
} else { $(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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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();
} }
......
<template>
<div class="cell">
<code-cell
type="input"
:raw-code="rawInputCode"
:count="cell.execution_count"
:code-css-class="codeCssClass" />
<output-cell
v-if="hasOutput"
:count="cell.execution_count"
:output="output"
:code-css-class="codeCssClass" />
</div>
</template>
<script>
import CodeCell from './code/index.vue';
import OutputCell from './output/index.vue';
export default {
components: {
'code-cell': CodeCell,
'output-cell': OutputCell,
},
props: {
cell: {
type: Object,
required: true,
},
codeCssClass: {
type: String,
required: false,
default: '',
},
},
computed: {
rawInputCode() {
if (this.cell.source) {
return this.cell.source.join('');
}
return '';
},
hasOutput() {
return this.cell.outputs.length;
},
output() {
return this.cell.outputs[0];
},
},
};
</script>
<style scoped>
.cell {
flex-direction: column;
}
</style>
<template>
<div :class="type">
<prompt
:type="promptType"
:count="count" />
<pre
class="language-python"
:class="codeCssClass"
ref="code"
v-text="code">
</pre>
</div>
</template>
<script>
import Prism from '../../lib/highlight';
import Prompt from '../prompt.vue';
export default {
components: {
prompt: Prompt,
},
props: {
count: {
type: Number,
required: false,
default: 0,
},
codeCssClass: {
type: String,
required: false,
default: '',
},
type: {
type: String,
required: true,
},
rawCode: {
type: String,
required: true,
},
},
computed: {
code() {
return this.rawCode;
},
promptType() {
const type = this.type.split('put')[0];
return type.charAt(0).toUpperCase() + type.slice(1);
},
},
mounted() {
Prism.highlightElement(this.$refs.code);
},
};
</script>
export { default as MarkdownCell } from './markdown.vue';
export { default as CodeCell } from './code.vue';
<template>
<div class="cell text-cell">
<prompt />
<div class="markdown" v-html="markdown"></div>
</div>
</template>
<script>
/* global katex */
import marked from 'marked';
import Prompt from './prompt.vue';
const renderer = new marked.Renderer();
/*
Regex to match KaTex blocks.
Supports the following:
\begin{equation}<math>\end{equation}
$$<math>$$
inline $<math>$
The matched text then goes through the KaTex renderer & then outputs the HTML
*/
const katexRegexString = `(
^\\\\begin{[a-zA-Z]+}\\s
|
^\\$\\$
|
\\s\\$(?!\\$)
)
(.+?)
(
\\s\\\\end{[a-zA-Z]+}$
|
\\$\\$$
|
\\$
)
`.replace(/\s/g, '').trim();
renderer.paragraph = (t) => {
let text = t;
let inline = false;
if (typeof katex !== 'undefined') {
const katexString = text.replace(/\\/g, '\\');
const matches = new RegExp(katexRegexString, 'gi').exec(katexString);
if (matches && matches.length > 0) {
if (matches[1].trim() === '$' && matches[3].trim() === '$') {
inline = true;
text = `${katexString.replace(matches[0], '')} ${katex.renderToString(matches[2])}`;
} else {
text = katex.renderToString(matches[2]);
}
}
}
return `<p class="${inline ? 'inline-katex' : ''}">${text}</p>`;
};
marked.setOptions({
sanitize: true,
renderer,
});
export default {
components: {
prompt: Prompt,
},
props: {
cell: {
type: Object,
required: true,
},
},
computed: {
markdown() {
return marked(this.cell.source.join(''));
},
},
};
</script>
<style>
.markdown .katex {
display: block;
text-align: center;
}
.markdown .inline-katex .katex {
display: inline;
text-align: initial;
}
</style>
<template>
<div class="output">
<prompt />
<div v-html="rawCode"></div>
</div>
</template>
<script>
import Prompt from '../prompt.vue';
export default {
props: {
rawCode: {
type: String,
required: true,
},
},
components: {
prompt: Prompt,
},
};
</script>
<template>
<div class="output">
<prompt />
<img
:src="'data:' + outputType + ';base64,' + rawCode" />
</div>
</template>
<script>
import Prompt from '../prompt.vue';
export default {
props: {
outputType: {
type: String,
required: true,
},
rawCode: {
type: String,
required: true,
},
},
components: {
prompt: Prompt,
},
};
</script>
<template>
<component :is="componentName"
type="output"
:outputType="outputType"
:count="count"
:raw-code="rawCode"
:code-css-class="codeCssClass" />
</template>
<script>
import CodeCell from '../code/index.vue';
import Html from './html.vue';
import Image from './image.vue';
export default {
props: {
codeCssClass: {
type: String,
required: false,
default: '',
},
count: {
type: Number,
required: false,
default: 0,
},
output: {
type: Object,
requred: true,
},
},
components: {
'code-cell': CodeCell,
'html-output': Html,
'image-output': Image,
},
data() {
return {
outputType: '',
};
},
computed: {
componentName() {
if (this.output.text) {
return 'code-cell';
} else if (this.output.data['image/png']) {
this.outputType = 'image/png';
return 'image-output';
} else if (this.output.data['text/html']) {
this.outputType = 'text/html';
return 'html-output';
} else if (this.output.data['image/svg+xml']) {
this.outputType = 'image/svg+xml';
return 'html-output';
}
this.outputType = 'text/plain';
return 'code-cell';
},
rawCode() {
if (this.output.text) {
return this.output.text.join('');
}
return this.dataForType(this.outputType);
},
},
methods: {
dataForType(type) {
let data = this.output.data[type];
if (typeof data === 'object') {
data = data.join('');
}
return data;
},
},
};
</script>
<template>
<div class="prompt">
<span v-if="type && count">
{{ type }} [{{ count }}]:
</span>
</div>
</template>
<script>
export default {
props: {
type: {
type: String,
required: false,
},
count: {
type: Number,
required: false,
},
},
};
</script>
<style scoped>
.prompt {
padding: 0 10px;
min-width: 7em;
font-family: monospace;
}
</style>
<template>
<div v-if="hasNotebook">
<component
v-for="(cell, index) in cells"
:is="cellType(cell.cell_type)"
:cell="cell"
:key="index"
:code-css-class="codeCssClass" />
</div>
</template>
<script>
import {
MarkdownCell,
CodeCell,
} from './cells';
export default {
components: {
'code-cell': CodeCell,
'markdown-cell': MarkdownCell,
},
props: {
notebook: {
type: Object,
required: true,
},
codeCssClass: {
type: String,
required: false,
default: '',
},
},
methods: {
cellType(type) {
return `${type}-cell`;
},
},
computed: {
cells() {
if (this.notebook.worksheets) {
const data = {
cells: [],
};
return this.notebook.worksheets.reduce((cellData, sheet) => {
const cellDataCopy = cellData;
cellDataCopy.cells = cellDataCopy.cells.concat(sheet.cells);
return cellDataCopy;
}, data).cells;
}
return this.notebook.cells;
},
hasNotebook() {
return Object.keys(this.notebook).length;
},
},
};
</script>
<style>
.cell,
.input,
.output {
display: flex;
width: 100%;
margin-bottom: 10px;
}
.cell pre {
margin: 0;
width: 100%;
}
</style>
import Prism from 'prismjs';
import 'prismjs/components/prism-python';
import 'prismjs/plugins/custom-class/prism-custom-class';
Prism.plugins.customClass.map({
comment: 'c',
error: 'err',
operator: 'o',
constant: 'kc',
namespace: 'kn',
keyword: 'k',
string: 's',
number: 'm',
'attr-name': 'na',
builtin: 'nb',
entity: 'ni',
function: 'nf',
tag: 'nt',
variable: 'nv',
});
export default Prism;
...@@ -57,8 +57,11 @@ import findAndFollowLink from './shortcuts_dashboard_navigation'; ...@@ -57,8 +57,11 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
Shortcuts.prototype.toggleMarkdownPreview = function(e) { Shortcuts.prototype.toggleMarkdownPreview = function(e) {
// Check if short-cut was triggered while in Write Mode // Check if short-cut was triggered while in Write Mode
if ($(e.target).hasClass('js-note-text')) { const $target = $(e.target);
$('.js-md-preview-button').focus(); const $form = $target.closest('form');
if ($target.hasClass('js-note-text')) {
$('.js-md-preview-button', $form).focus();
} }
return $(document).triggerHandler('markdown-preview:toggle', [e]); return $(document).triggerHandler('markdown-preview:toggle', [e]);
}; };
......
...@@ -108,8 +108,7 @@ ...@@ -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 {
......
...@@ -230,7 +230,6 @@ ...@@ -230,7 +230,6 @@
float: right; float: right;
margin-top: 8px; margin-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
border-bottom: 1px solid $border-color;
} }
} }
......
...@@ -70,7 +70,7 @@ pre { ...@@ -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 @@ ...@@ -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 {
......
...@@ -110,7 +110,7 @@ ...@@ -110,7 +110,7 @@
.top-area { .top-area {
@include clearfix; @include clearfix;
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $border-color;
.nav-text { .nav-text {
padding-top: 16px; padding-top: 16px;
......
...@@ -4,7 +4,7 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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),
...@@ -338,3 +352,32 @@ h4 { ...@@ -338,3 +352,32 @@ h4 {
.idiff.addition { .idiff.addition {
background: $line-added-dark; background: $line-added-dark;
} }
/**
* form text input i.e. search bar, comments, forms, etc.
*/
input,
textarea {
&::-webkit-input-placeholder {
color: $placeholder-text-color;
}
// support firefox 19+ vendor prefix
&::-moz-placeholder {
color: $placeholder-text-color;
opacity: 1; // FF defaults to 0.54
}
// scss-lint:disable PseudoElement
// support Edge vendor prefix
&::-ms-input-placeholder {
color: $placeholder-text-color;
}
// scss-lint:disable PseudoElement
// support IE vendor prefix
&:-ms-input-placeholder {
color: $placeholder-text-color;
}
}
...@@ -111,6 +111,7 @@ $gl-gray: $gl-text-color; ...@@ -111,6 +111,7 @@ $gl-gray: $gl-text-color;
$gl-gray-dark: #313236; $gl-gray-dark: #313236;
$gl-header-color: #4c4e54; $gl-header-color: #4c4e54;
$gl-header-nav-hover-color: #434343; $gl-header-nav-hover-color: #434343;
$placeholder-text-color: rgba(0, 0, 0, .42);
/* /*
* Lists * Lists
......
...@@ -29,11 +29,5 @@ ...@@ -29,11 +29,5 @@
.description { .description {
margin-top: 6px; margin-top: 6px;
p {
&:last-child {
margin-bottom: 0;
}
}
} }
} }
...@@ -52,7 +52,7 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 {
...@@ -387,6 +387,7 @@ ...@@ -387,6 +387,7 @@
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
display: flex; display: flex;
width: 100%; width: 100%;
margin-bottom: 10px;
.comment-btn { .comment-btn {
flex-grow: 1; flex-grow: 1;
......
...@@ -102,13 +102,12 @@ ul.notes { ...@@ -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 { ...@@ -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;
} }
...@@ -386,6 +389,10 @@ ul.notes { ...@@ -386,6 +389,10 @@ ul.notes {
.note-headline-meta { .note-headline-meta {
display: inline-block; display: inline-block;
white-space: nowrap; white-space: nowrap;
.system-note-message {
white-space: normal;
}
} }
/** /**
......
...@@ -28,7 +28,7 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -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
......
...@@ -23,6 +23,7 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -23,6 +23,7 @@ class Projects::MilestonesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
@project_namespace = @project.namespace.becomes(Namespace)
@milestones = @milestones.includes(:project) @milestones = @milestones.includes(:project)
@milestones = @milestones.page(params[:page]) @milestones = @milestones.page(params[:page])
end end
......
...@@ -29,7 +29,7 @@ module BlobHelper ...@@ -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
} }
......
##
# DEPRECATED
#
# These helpers are deprecated in favor of detailed CI/CD statuses.
#
# See 'detailed_status?` method and `Gitlab::Ci::Status` module.
#
module CiStatusHelper module CiStatusHelper
def ci_status_path(pipeline) def ci_status_path(pipeline)
project = pipeline.project project = pipeline.project
namespace_project_pipeline_path(project.namespace, project, pipeline) namespace_project_pipeline_path(project.namespace, project, pipeline)
end end
# Is used by Commit and Merge Request Widget
def ci_label_for_status(status) def ci_label_for_status(status)
if detailed_status?(status) if detailed_status?(status)
return status.label return status.label
...@@ -22,6 +28,23 @@ module CiStatusHelper ...@@ -22,6 +28,23 @@ module CiStatusHelper
end end
end end
def ci_text_for_status(status)
if detailed_status?(status)
return status.text
end
case status
when 'success'
'passed'
when 'success_with_warnings'
'passed'
when 'manual'
'blocked'
else
status
end
end
def ci_status_for_statuseable(subject) def ci_status_for_statuseable(subject)
status = subject.try(:status) || 'not found' status = subject.try(:status) || 'not found'
status.humanize status.humanize
......
...@@ -7,6 +7,11 @@ module IconsHelper ...@@ -7,6 +7,11 @@ module IconsHelper
# font-awesome-rails gem, but should we ever use a different icon pack in the # font-awesome-rails gem, but should we ever use a different icon pack in the
# future we won't have to change hundreds of method calls. # future we won't have to change hundreds of method calls.
def icon(names, options = {}) def icon(names, options = {})
if (options.keys & %w[aria-hidden aria-label]).empty?
# Add `aria-hidden` if there are no aria's set
options['aria-hidden'] = true
end
options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options) options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
end end
......
...@@ -5,7 +5,7 @@ module SubmoduleHelper ...@@ -5,7 +5,7 @@ module SubmoduleHelper
def submodule_links(submodule_item, ref = nil, repository = @repository) def submodule_links(submodule_item, ref = nil, repository = @repository)
url = repository.submodule_url_for(ref, submodule_item.path) url = repository.submodule_url_for(ref, submodule_item.path)
return url, nil unless url =~ /([^\/:]+)\/([^\/]+\.git)\Z/ return url, nil unless url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
namespace = $1 namespace = $1
project = $2 project = $2
...@@ -37,14 +37,16 @@ module SubmoduleHelper ...@@ -37,14 +37,16 @@ module SubmoduleHelper
end end
def self_url?(url, namespace, project) def self_url?(url, namespace, project)
return true if url == [Gitlab.config.gitlab.url, '/', namespace, '/', url_no_dotgit = url.chomp('.git')
project, '.git'].join('') return true if url_no_dotgit == [Gitlab.config.gitlab.url, '/', namespace, '/',
url == gitlab_shell.url_to_repo([namespace, '/', project].join('')) project].join('')
url_with_dotgit = url_no_dotgit + '.git'
url_with_dotgit == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
end end
def relative_self_url?(url) def relative_self_url?(url)
# (./)?(../repo.git) || (./)?(../../project/repo.git) ) # (./)?(../repo.git) || (./)?(../../project/repo.git) )
url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\z/ url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*(\.git)?\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*(\.git)?\z/
end end
def standard_links(host, namespace, project, commit) def standard_links(host, namespace, project, commit)
......
...@@ -75,29 +75,32 @@ module Ci ...@@ -75,29 +75,32 @@ module Ci
pipeline.update_duration pipeline.update_duration
end end
before_transition any => [:manual] do |pipeline|
pipeline.update_duration
end
before_transition canceled: any - [:canceled] do |pipeline| before_transition canceled: any - [:canceled] do |pipeline|
pipeline.auto_canceled_by = nil pipeline.auto_canceled_by = nil
end end
after_transition [:created, :pending] => :running do |pipeline| after_transition [:created, :pending] => :running do |pipeline|
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) } pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end end
after_transition any => [:success] do |pipeline| after_transition any => [:success] do |pipeline|
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) } pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end end
after_transition [:created, :pending, :running] => :success do |pipeline| after_transition [:created, :pending, :running] => :success do |pipeline|
pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) } pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) }
end end
after_transition do |pipeline, transition| after_transition do |pipeline, transition|
next if transition.loopback? next if transition.loopback?
pipeline.run_after_commit do pipeline.run_after_commit do
PipelineHooksWorker.perform_async(id) PipelineHooksWorker.perform_async(pipeline.id)
Ci::ExpirePipelineCacheService.new(project, nil) ExpirePipelineCacheWorker.perform_async(pipeline.id)
.execute(pipeline)
end end
end end
...@@ -385,6 +388,11 @@ module Ci ...@@ -385,6 +388,11 @@ module Ci
.select { |merge_request| merge_request.head_pipeline.try(:id) == self.id } .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
end end
# All the merge requests for which the current pipeline runs/ran against
def all_merge_requests
@all_merge_requests ||= project.merge_requests.where(source_branch: ref)
end
def detailed_status(current_user) def detailed_status(current_user)
Gitlab::Ci::Status::Pipeline::Factory Gitlab::Ci::Status::Pipeline::Factory
.new(self, current_user) .new(self, current_user)
......
...@@ -8,6 +8,14 @@ ...@@ -8,6 +8,14 @@
# #
# Corresponding foo_html, bar_html and baz_html fields should exist. # Corresponding foo_html, bar_html and baz_html fields should exist.
module CacheMarkdownField module CacheMarkdownField
extend ActiveSupport::Concern
# Increment this number every time the renderer changes its output
CACHE_VERSION = 1
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
# Knows about the relationship between markdown and html field names, and # Knows about the relationship between markdown and html field names, and
# stores the rendering contexts for the latter # stores the rendering contexts for the latter
class FieldData class FieldData
...@@ -30,39 +38,10 @@ module CacheMarkdownField ...@@ -30,39 +38,10 @@ module CacheMarkdownField
end end
end end
# Dynamic registries don't really work in Rails as it's not guaranteed that
# every class will be loaded, so hardcode the list.
CACHING_CLASSES = %w[
AbuseReport
Appearance
ApplicationSetting
BroadcastMessage
Issue
Label
MergeRequest
Milestone
Namespace
Note
Project
Release
Snippet
].freeze
def self.caching_classes
CACHING_CLASSES.map(&:constantize)
end
def skip_project_check? def skip_project_check?
false false
end end
extend ActiveSupport::Concern
included do
cattr_reader :cached_markdown_fields do
FieldData.new
end
# Returns the default Banzai render context for the cached markdown field. # Returns the default Banzai render context for the cached markdown field.
def banzai_render_context(field) def banzai_render_context(field)
raise ArgumentError.new("Unknown field: #{field.inspect}") unless raise ArgumentError.new("Unknown field: #{field.inspect}") unless
...@@ -78,12 +57,52 @@ module CacheMarkdownField ...@@ -78,12 +57,52 @@ module CacheMarkdownField
context context
end end
# Allow callers to look up the cache field name, rather than hardcoding it # Update every column in a row if any one is invalidated, as we only store
def markdown_cache_field_for(field) # one version per row
def refresh_markdown_cache!(do_update: false)
options = { skip_project_check: skip_project_check? }
updates = cached_markdown_fields.markdown_fields.map do |markdown_field|
[
cached_markdown_fields.html_field(markdown_field),
Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
]
end.to_h
updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION
updates.each {|html_field, data| write_attribute(html_field, data) }
update_columns(updates) if persisted? && do_update
end
def cached_html_up_to_date?(markdown_field)
html_field = cached_markdown_fields.html_field(markdown_field)
markdown_changed = attribute_changed?(markdown_field) || false
html_changed = attribute_changed?(html_field) || false
CacheMarkdownField::CACHE_VERSION == cached_markdown_version &&
(html_changed || markdown_changed == html_changed)
end
def invalidated_markdown_cache?
cached_markdown_fields.html_fields.any? {|html_field| attribute_invalidated?(html_field) }
end
def attribute_invalidated?(attr)
__send__("#{attr}_invalidated?")
end
def cached_html_for(markdown_field)
raise ArgumentError.new("Unknown field: #{field}") unless raise ArgumentError.new("Unknown field: #{field}") unless
cached_markdown_fields.markdown_fields.include?(field) cached_markdown_fields.markdown_fields.include?(markdown_field)
cached_markdown_fields.html_field(field) __send__(cached_markdown_fields.html_field(markdown_field))
end
included do
cattr_reader :cached_markdown_fields do
FieldData.new
end end
# Always exclude _html fields from attributes (including serialization). # Always exclude _html fields from attributes (including serialization).
...@@ -92,12 +111,16 @@ module CacheMarkdownField ...@@ -92,12 +111,16 @@ module CacheMarkdownField
def attributes def attributes
attrs = attributes_before_markdown_cache attrs = attributes_before_markdown_cache
attrs.delete('cached_markdown_version')
cached_markdown_fields.html_fields.each do |field| cached_markdown_fields.html_fields.each do |field|
attrs.delete(field) attrs.delete(field)
end end
attrs attrs
end end
before_save :refresh_markdown_cache!, if: :invalidated_markdown_cache?
end end
class_methods do class_methods do
...@@ -107,31 +130,18 @@ module CacheMarkdownField ...@@ -107,31 +130,18 @@ module CacheMarkdownField
# a corresponding _html field. Any custom rendering options may be provided # a corresponding _html field. Any custom rendering options may be provided
# as a context. # as a context.
def cache_markdown_field(markdown_field, context = {}) def cache_markdown_field(markdown_field, context = {})
raise "Add #{self} to CacheMarkdownField::CACHING_CLASSES" unless
CacheMarkdownField::CACHING_CLASSES.include?(self.to_s)
cached_markdown_fields[markdown_field] = context cached_markdown_fields[markdown_field] = context
html_field = cached_markdown_fields.html_field(markdown_field) html_field = cached_markdown_fields.html_field(markdown_field)
cache_method = "#{markdown_field}_cache_refresh".to_sym
invalidation_method = "#{html_field}_invalidated?".to_sym invalidation_method = "#{html_field}_invalidated?".to_sym
define_method(cache_method) do
options = { skip_project_check: skip_project_check? }
html = Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
__send__("#{html_field}=", html)
true
end
# The HTML becomes invalid if any dependent fields change. For now, assume # The HTML becomes invalid if any dependent fields change. For now, assume
# author and project invalidate the cache in all circumstances. # author and project invalidate the cache in all circumstances.
define_method(invalidation_method) do define_method(invalidation_method) do
changed_fields = changed_attributes.keys changed_fields = changed_attributes.keys
invalidations = changed_fields & [markdown_field.to_s, "author", "project"] invalidations = changed_fields & [markdown_field.to_s, *INVALIDATED_BY]
!invalidations.empty? !invalidations.empty? || !cached_html_up_to_date?(markdown_field)
end end
before_save cache_method, if: invalidation_method
end end
end end
end end
...@@ -163,7 +163,20 @@ module Routable ...@@ -163,7 +163,20 @@ module Routable
end end
end end
# Every time `project.namespace.becomes(Namespace)` is called for polymorphic_path,
# a new instance is instantiated, and we end up duplicating the same query to retrieve
# the route. Caching this per request ensures that even if we have multiple instances,
# we will not have to duplicate work, avoiding N+1 queries in some cases.
def full_path def full_path
return uncached_full_path unless RequestStore.active?
key = "routable/full_path/#{self.class.name}/#{self.id}"
RequestStore[key] ||= uncached_full_path
end
private
def uncached_full_path
if route && route.path.present? if route && route.path.present?
@full_path ||= route.path @full_path ||= route.path
else else
...@@ -173,8 +186,6 @@ module Routable ...@@ -173,8 +186,6 @@ module Routable
end end
end end
private
def full_name_changed? def full_name_changed?
name_changed? || parent_changed? name_changed? || parent_changed?
end end
......
...@@ -125,7 +125,7 @@ class Group < Namespace ...@@ -125,7 +125,7 @@ class Group < Namespace
end end
def add_users(users, access_level, current_user: nil, expires_at: nil) def add_users(users, access_level, current_user: nil, expires_at: nil)
GroupMember.add_users_to_group( GroupMember.add_users(
self, self,
users, users,
access_level, access_level,
......
...@@ -10,4 +10,8 @@ class IndividualNoteDiscussion < Discussion ...@@ -10,4 +10,8 @@ class IndividualNoteDiscussion < Discussion
def individual_note? def individual_note?
true true
end end
def reply_attributes
super.tap { |attrs| attrs.delete(:discussion_id) }
end
end end
...@@ -151,6 +151,22 @@ class Member < ActiveRecord::Base ...@@ -151,6 +151,22 @@ class Member < ActiveRecord::Base
member member
end end
def add_users(source, users, access_level, current_user: nil, expires_at: nil)
return [] unless users.present?
self.transaction do
users.map do |user|
add_user(
source,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
end
def access_levels def access_levels
Gitlab::Access.sym_options Gitlab::Access.sym_options
end end
...@@ -173,18 +189,6 @@ class Member < ActiveRecord::Base ...@@ -173,18 +189,6 @@ class Member < ActiveRecord::Base
# There is no current user for bulk actions, in which case anything is allowed # There is no current user for bulk actions, in which case anything is allowed
!current_user || current_user.can?(:"update_#{member.type.underscore}", member) !current_user || current_user.can?(:"update_#{member.type.underscore}", member)
end end
def add_users_to_source(source, users, access_level, current_user: nil, expires_at: nil)
users.each do |user|
add_user(
source,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
end end
def real_source_type def real_source_type
......
...@@ -21,18 +21,6 @@ class GroupMember < Member ...@@ -21,18 +21,6 @@ class GroupMember < Member
Gitlab::Access.sym_options_with_owner Gitlab::Access.sym_options_with_owner
end end
def self.add_users_to_group(group, users, access_level, current_user: nil, expires_at: nil)
self.transaction do
add_users_to_source(
group,
users,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
def group def group
source source
end end
......
...@@ -16,7 +16,7 @@ class ProjectMember < Member ...@@ -16,7 +16,7 @@ class ProjectMember < Member
before_destroy :delete_member_todos before_destroy :delete_member_todos
class << self class << self
# Add users to project teams with passed access option # Add users to projects with passed access option
# #
# access can be an integer representing a access code # access can be an integer representing a access code
# or symbol like :master representing role # or symbol like :master representing role
...@@ -39,7 +39,7 @@ class ProjectMember < Member ...@@ -39,7 +39,7 @@ class ProjectMember < Member
project_ids.each do |project_id| project_ids.each do |project_id|
project = Project.find(project_id) project = Project.find(project_id)
add_users_to_source( add_users(
project, project,
users, users,
access_level, access_level,
......
...@@ -191,22 +191,23 @@ class MergeRequest < ActiveRecord::Base ...@@ -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 ...@@ -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)
......
...@@ -19,4 +19,8 @@ class OutOfContextDiscussion < Discussion ...@@ -19,4 +19,8 @@ class OutOfContextDiscussion < Discussion
def self.note_class def self.note_class
Note Note
end end
def reply_attributes
super.tap { |attrs| attrs.delete(:discussion_id) }
end
end end
...@@ -50,8 +50,8 @@ class ProjectTeam ...@@ -50,8 +50,8 @@ class ProjectTeam
end end
def add_users(users, access_level, current_user: nil, expires_at: nil) def add_users(users, access_level, current_user: nil, expires_at: nil)
ProjectMember.add_users_to_projects( ProjectMember.add_users(
[project.id], project,
users, users,
access_level, access_level,
current_user: current_user, current_user: current_user,
......
module Ci
class ExpirePipelineCacheService < BaseService
attr_reader :pipeline
def execute(pipeline)
@pipeline = pipeline
store = Gitlab::EtagCaching::Store.new
store.touch(project_pipelines_path)
store.touch(commit_pipelines_path) if pipeline.commit
store.touch(new_merge_request_pipelines_path)
merge_requests_pipelines_paths.each { |path| store.touch(path) }
Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(@pipeline)
end
private
def project_pipelines_path
Gitlab::Routing.url_helpers.namespace_project_pipelines_path(
project.namespace,
project,
format: :json)
end
def commit_pipelines_path
Gitlab::Routing.url_helpers.pipelines_namespace_project_commit_path(
project.namespace,
project,
pipeline.commit.id,
format: :json)
end
def new_merge_request_pipelines_path
Gitlab::Routing.url_helpers.new_namespace_project_merge_request_path(
project.namespace,
project,
format: :json)
end
def merge_requests_pipelines_paths
pipeline.merge_requests.collect do |merge_request|
Gitlab::Routing.url_helpers.pipelines_namespace_project_merge_request_path(
project.namespace,
project,
merge_request,
format: :json)
end
end
end
end
...@@ -6,8 +6,8 @@ module Users ...@@ -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 ...@@ -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?
......
...@@ -15,27 +15,39 @@ module Users ...@@ -15,27 +15,39 @@ module Users
end end
def execute def execute
transition = user.block_transition
user.transaction do
# Block the user before moving records to prevent a data race. # Block the user before moving records to prevent a data race.
# For example, if the user creates an issue after `migrate_issues` # For example, if the user creates an issue after `migrate_issues`
# runs and before the user is destroyed, the destroy will fail with # runs and before the user is destroyed, the destroy will fail with
# an exception. # an exception.
user.block user.block
user.transaction do # Reverse the user block if record migration fails
if !migrate_records && transition
transition.rollback
user.save!
end
end
user.reload
end
private
def migrate_records
user.transaction(requires_new: true) do
@ghost_user = User.ghost @ghost_user = User.ghost
migrate_issues migrate_issues
migrate_merge_requests migrate_merge_requests
migrate_notes migrate_notes
migrate_abuse_reports migrate_abuse_reports
migrate_award_emoji migrate_award_emojis
end end
user.reload
end end
private
def migrate_issues def migrate_issues
user.issues.update_all(author_id: ghost_user.id) user.issues.update_all(author_id: ghost_user.id)
end end
...@@ -52,7 +64,7 @@ module Users ...@@ -52,7 +64,7 @@ module Users
user.reported_abuse_reports.update_all(reporter_id: ghost_user.id) user.reported_abuse_reports.update_all(reporter_id: ghost_user.id)
end end
def migrate_award_emoji def migrate_award_emojis
user.award_emoji.update_all(user_id: ghost_user.id) user.award_emoji.update_all(user_id: ghost_user.id)
end end
end end
......
...@@ -9,7 +9,7 @@ ...@@ -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 @@ ...@@ -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
......
- 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
- ref = local_assigns.fetch(:ref) - ref = local_assigns.fetch(:ref)
- status = commit.status(ref) - status = commit.status(ref)
- if status - if status
= link_to pipelines_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do = link_to pipelines_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do
= ci_icon_for_status(status) = ci_icon_for_status(status)
= ci_label_for_status(status) = ci_text_for_status(status)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message" = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
......
...@@ -39,14 +39,4 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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" => { "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue), .issue-title-data.hidden{ "data" => { "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue),
"canDescription" => can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '', "canDescription" => can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '',
} } } }
......
...@@ -36,7 +36,7 @@ ...@@ -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
......
...@@ -145,7 +145,8 @@ ...@@ -145,7 +145,8 @@
} }
}); });
$('#project_import_url').disable();
$('.import_git').click(function( event ) { $('.import_git').click(function( event ) {
$projectImportUrl = $('#project_import_url') $projectImportUrl = $('#project_import_url');
$projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled')) $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'));
}); });
- page_title @service.title, "Services" - page_title @service.title, "Services"
= render "projects/settings/head"
= render 'form' = render 'form'
...@@ -14,7 +14,7 @@ ...@@ -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
......
-# @project is present when viewing Project's milestone -# @project is present when viewing Project's milestone
- project = @project || issuable.project - project = @project || issuable.project
- namespace = @project_namespace || project.namespace.becomes(Namespace)
- assignee = issuable.assignee - assignee = issuable.assignee
- issuable_type = issuable.class.table_name - issuable_type = issuable.class.table_name
- base_url_args = [project.namespace.becomes(Namespace), project, issuable_type] - base_url_args = [namespace, project]
- issuable_type_args = base_url_args + [issuable_type]
- issuable_url_args = base_url_args + [issuable]
- can_update = can?(current_user, :"update_#{issuable.to_ability_name}", issuable) - can_update = can?(current_user, :"update_#{issuable.to_ability_name}", issuable)
%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'is-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-id' => issuable.id, 'data-url' => polymorphic_path(issuable) } %li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'is-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-id' => issuable.id, 'data-url' => polymorphic_path(issuable_url_args) }
%span %span
- if show_project_name - if show_project_name
%strong #{project.name} &middot; %strong #{project.name} &middot;
...@@ -13,17 +16,17 @@ ...@@ -13,17 +16,17 @@
%strong #{project.name_with_namespace} &middot; %strong #{project.name_with_namespace} &middot;
- if issuable.is_a?(Issue) - if issuable.is_a?(Issue)
= confidential_icon(issuable) = confidential_icon(issuable)
= link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title = link_to_gfm issuable.title, issuable_url_args, title: issuable.title
.issuable-detail .issuable-detail
= link_to [project.namespace.becomes(Namespace), project, issuable] do = link_to [project.namespace.becomes(Namespace), project, issuable] do
%span.issuable-number= issuable.to_reference %span.issuable-number= issuable.to_reference
- issuable.labels.each do |label| - issuable.labels.each do |label|
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do = link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do
- render_colored_label(label) - render_colored_label(label)
%span.assignee-icon %span.assignee-icon
- if assignee - if assignee
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }), = link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }),
class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
- image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '') - image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '')
# This worker clears all cache fields in the database, working in batches.
class ClearDatabaseCacheWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
BATCH_SIZE = 1000
def perform
CacheMarkdownField.caching_classes.each do |kls|
fields = kls.cached_markdown_fields.html_fields
clear_cache_fields = fields.each_with_object({}) do |field, memo|
memo[field] = nil
end
Rails.logger.debug("Clearing Markdown cache for #{kls}: #{fields.inspect}")
kls.unscoped.in_batches(of: BATCH_SIZE) do |relation|
relation.update_all(clear_cache_fields)
end
end
nil
end
end
class ExpirePipelineCacheWorker
include Sidekiq::Worker
include PipelineQueue
def perform(pipeline_id)
pipeline = Ci::Pipeline.find_by(id: pipeline_id)
return unless pipeline
project = pipeline.project
store = Gitlab::EtagCaching::Store.new
store.touch(project_pipelines_path(project))
store.touch(commit_pipelines_path(project, pipeline.commit)) if pipeline.commit
store.touch(new_merge_request_pipelines_path(project))
each_pipelines_merge_request_path(project, pipeline) do |path|
store.touch(path)
end
Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(pipeline)
end
private
def project_pipelines_path(project)
Gitlab::Routing.url_helpers.namespace_project_pipelines_path(
project.namespace,
project,
format: :json)
end
def commit_pipelines_path(project, commit)
Gitlab::Routing.url_helpers.pipelines_namespace_project_commit_path(
project.namespace,
project,
commit.id,
format: :json)
end
def new_merge_request_pipelines_path(project)
Gitlab::Routing.url_helpers.new_namespace_project_merge_request_path(
project.namespace,
project,
format: :json)
end
def each_pipelines_merge_request_path(project, pipeline)
pipeline.all_merge_requests.each do |merge_request|
path = Gitlab::Routing.url_helpers.pipelines_namespace_project_merge_request_path(
project.namespace,
project,
merge_request,
format: :json)
yield(path)
end
end
end
---
title: Show CI status as Favicon on Pipelines, Job and MR pages
merge_request: 10144
author:
---
title: Expose CI/CD status API endpoints with Gitlab::Ci::Status facility on pipeline,
job and merge request for favicon
merge_request: 9561
author: dosuken123
---
title: Database SSL support for backup script.
merge_request: 9715
author: Guillaume Simon
---
title: Update rugged to 0.25.1.1
merge_request: 10286
author: Elan Ruusamäe
---
title: Tags can be protected, restricting creation of matching tags by user role
merge_request: 10356
author:
---
title: Update permalink/blame buttons with line number fragment hash
merge_request:
author:
---
title: Added merge requests empty state
merge_request: 7342
author:
---
title: Add ability to disable Merge Request URL on push
merge_request: 9663
author: Alex Sanford
---
title: Show group name on flash container when group is created from Admin area.
merge_request: 10905
author:
---
title: Fix symlink icon in project tree
merge_request: 9780
author: mhasbini
---
title: Fix API group/issues default state filter
merge_request:
author: Alexander Randa
---
title: Move milestone summary content into the sidebar
merge_request: 10096
author:
---
title: Adding non_archived scope for counting projects
merge_request: 8305
author: Naveen Kumar
---
title: Link issuable reference to itself in meta-header
merge_request: 9641
author: mhasbini
---
title: Prevent builds dropdown to close when the user clicks in a build
merge_request:
author:
---
title: Set GIT_TERMINAL_PROMPT env variable in initializer
merge_request: 10372
author:
---
title: Add /-/readiness /-/liveness and /-/metrics endpoints to track application health
merge_request: 10416
author:
---
title: Add dashboard and group milestones count badges
merge_request: 9836
author: Alex Braha Stoll
---
title: New file from interface on existing branch
merge_request: 8427
author: Jacopo Beschi @jacopo-beschi
---
title: Add metadata to system notes
merge_request: 9964
author:
---
title: Hide form inputs for group member without editing rights
merge_request: 7816
author:
---
title: Add ECMAScript polyfills for Symbol and Array.find
merge_request: 10120
author:
---
title: Remove no-new annotation from file_template_mediator.js.
merge_request: !9782
author:
---
title: Create a new issue for a single discussion in a Merge Request
merge_request: 8266
author: Bob Van Landuyt
---
title: Prevent users from disconnecting GitLab account from CAS
merge_request: 10282
author:
---
title: Don't show links to tag a commit for users that are not permitted
merge_request: 8407
author:
---
title: Changed dropdown style slightly
merge_request:
author:
---
title: Change gfm textarea to use monospace font
merge_request:
author:
---
title: Strip reference prefixes on branch creation
merge_request: 8498
author: Matthieu Tardy
---
title: Add Undo mark all as done to Todos
merge_request: 9890
author: Jacopo Beschi @jacopo-beschi
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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