summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/blob/blob_fork_suggestion.js53
-rw-r--r--app/assets/javascripts/blob/notebook/index.js6
-rw-r--r--app/assets/javascripts/dispatcher.js3
-rw-r--r--app/assets/javascripts/merge_request_tabs.js11
-rw-r--r--app/assets/javascripts/monitoring/prometheus_graph.js63
-rw-r--r--app/assets/javascripts/notebook/cells/code.vue58
-rw-r--r--app/assets/javascripts/notebook/cells/code/index.vue57
-rw-r--r--app/assets/javascripts/notebook/cells/index.js2
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue98
-rw-r--r--app/assets/javascripts/notebook/cells/output/html.vue22
-rw-r--r--app/assets/javascripts/notebook/cells/output/image.vue27
-rw-r--r--app/assets/javascripts/notebook/cells/output/index.vue83
-rw-r--r--app/assets/javascripts/notebook/cells/prompt.vue30
-rw-r--r--app/assets/javascripts/notebook/index.vue75
-rw-r--r--app/assets/javascripts/notebook/lib/highlight.js22
-rw-r--r--app/assets/javascripts/shortcuts.js7
-rw-r--r--app/assets/stylesheets/framework/awards.scss3
-rw-r--r--app/assets/stylesheets/framework/blocks.scss1
-rw-r--r--app/assets/stylesheets/framework/common.scss2
-rw-r--r--app/assets/stylesheets/framework/files.scss8
-rw-r--r--app/assets/stylesheets/framework/nav.scss2
-rw-r--r--app/assets/stylesheets/framework/timeline.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss69
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss6
-rw-r--r--app/assets/stylesheets/pages/issuable.scss6
-rw-r--r--app/assets/stylesheets/pages/note_form.scss3
-rw-r--r--app/assets/stylesheets/pages/notes.scss13
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/projects/milestones_controller.rb1
-rw-r--r--app/helpers/blob_helper.rb2
-rw-r--r--app/helpers/ci_status_helper.rb25
-rw-r--r--app/helpers/icons_helper.rb5
-rw-r--r--app/helpers/submodule_helper.rb12
-rw-r--r--app/models/ci/pipeline.rb20
-rw-r--r--app/models/concerns/cache_markdown_field.rb126
-rw-r--r--app/models/concerns/routable.rb15
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/individual_note_discussion.rb4
-rw-r--r--app/models/member.rb28
-rw-r--r--app/models/members/group_member.rb12
-rw-r--r--app/models/members/project_member.rb4
-rw-r--r--app/models/merge_request.rb17
-rw-r--r--app/models/merge_request_diff.rb2
-rw-r--r--app/models/out_of_context_discussion.rb6
-rw-r--r--app/models/project_team.rb4
-rw-r--r--app/services/ci/expire_pipeline_cache_service.rb51
-rw-r--r--app/services/users/build_service.rb4
-rw-r--r--app/services/users/create_service.rb4
-rw-r--r--app/services/users/migrate_to_ghost_user_service.rb34
-rw-r--r--app/views/admin/cohorts/index.html.haml2
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/projects/_fork_suggestion.html.haml11
-rw-r--r--app/views/projects/_last_commit.html.haml3
-rw-r--r--app/views/projects/blob/_header.html.haml12
-rw-r--r--app/views/projects/diffs/_file.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/new.html.haml5
-rw-r--r--app/views/projects/services/edit.html.haml1
-rw-r--r--app/views/projects/settings/_head.html.haml2
-rw-r--r--app/views/shared/milestones/_issuable.html.haml13
-rw-r--r--app/workers/clear_database_cache_worker.rb24
-rw-r--r--app/workers/expire_pipeline_cache_worker.rb57
64 files changed, 923 insertions, 328 deletions
diff --git a/app/assets/javascripts/blob/blob_fork_suggestion.js b/app/assets/javascripts/blob/blob_fork_suggestion.js
index 3baf81905fe..47c431fb809 100644
--- a/app/assets/javascripts/blob/blob_fork_suggestion.js
+++ b/app/assets/javascripts/blob/blob_fork_suggestion.js
@@ -16,47 +16,44 @@ const defaults = {
class BlobForkSuggestion {
constructor(options) {
this.elementMap = Object.assign({}, defaults, options);
- this.onClickWrapper = this.onClick.bind(this);
-
- document.addEventListener('click', this.onClickWrapper);
+ this.onOpenButtonClick = this.onOpenButtonClick.bind(this);
+ this.onCancelButtonClick = this.onCancelButtonClick.bind(this);
}
- showSuggestionSection(forkPath, action = 'edit') {
- [].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => {
- suggestionSection.classList.remove('hidden');
- });
+ init() {
+ this.bindEvents();
- [].forEach.call(this.elementMap.forkButtons, (forkButton) => {
- forkButton.setAttribute('href', forkPath);
- });
+ return this;
+ }
- [].forEach.call(this.elementMap.actionTextPieces, (actionTextPiece) => {
- // eslint-disable-next-line no-param-reassign
- actionTextPiece.textContent = action;
- });
+ bindEvents() {
+ $(this.elementMap.openButtons).on('click', this.onOpenButtonClick);
+ $(this.elementMap.cancelButtons).on('click', this.onCancelButtonClick);
}
- hideSuggestionSection() {
- [].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => {
- suggestionSection.classList.add('hidden');
- });
+ showSuggestionSection(forkPath, action = 'edit') {
+ $(this.elementMap.suggestionSections).removeClass('hidden');
+ $(this.elementMap.forkButtons).attr('href', forkPath);
+ $(this.elementMap.actionTextPieces).text(action);
}
- onClick(e) {
- const el = e.target;
+ hideSuggestionSection() {
+ $(this.elementMap.suggestionSections).addClass('hidden');
+ }
- if ([].includes.call(this.elementMap.openButtons, el)) {
- const { forkPath, action } = el.dataset;
- this.showSuggestionSection(forkPath, action);
- }
+ onOpenButtonClick(e) {
+ const forkPath = $(e.currentTarget).attr('data-fork-path');
+ const action = $(e.currentTarget).attr('data-action');
+ this.showSuggestionSection(forkPath, action);
+ }
- if ([].includes.call(this.elementMap.cancelButtons, el)) {
- this.hideSuggestionSection();
- }
+ onCancelButtonClick() {
+ this.hideSuggestionSection();
}
destroy() {
- document.removeEventListener('click', this.onClickWrapper);
+ $(this.elementMap.openButtons).off('click', this.onOpenButtonClick);
+ $(this.elementMap.cancelButtons).off('click', this.onCancelButtonClick);
}
}
diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js
index 9b8bfbfc8c0..36fe8a7184f 100644
--- a/app/assets/javascripts/blob/notebook/index.js
+++ b/app/assets/javascripts/blob/notebook/index.js
@@ -1,10 +1,9 @@
/* eslint-disable no-new */
import Vue from 'vue';
import VueResource from 'vue-resource';
-import NotebookLab from 'vendor/notebooklab';
+import notebookLab from '../../notebook/index.vue';
Vue.use(VueResource);
-Vue.use(NotebookLab);
export default () => {
const el = document.getElementById('js-notebook-viewer');
@@ -19,6 +18,9 @@ export default () => {
json: {},
};
},
+ components: {
+ notebookLab,
+ },
template: `
<div class="container-fluid md prepend-top-default append-bottom-default">
<div
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 20db2698ba8..b20673cd03c 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -97,7 +97,8 @@ const ShortcutsBlob = require('./shortcuts_blob');
cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'),
suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'),
actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'),
- });
+ })
+ .init();
}
switch (page) {
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index f7f6a773036..6075157ec31 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -5,6 +5,7 @@
import Cookies from 'js-cookie';
import './breakpoints';
import './flash';
+import BlobForkSuggestion from './blob/blob_fork_suggestion';
/* eslint-disable max-len */
// MergeRequestTabs
@@ -266,6 +267,16 @@ import './flash';
new gl.Diff();
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'),
+ });
+ });
},
});
}
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
index aff507abb91..78bb0e6fb47 100644
--- a/app/assets/javascripts/monitoring/prometheus_graph.js
+++ b/app/assets/javascripts/monitoring/prometheus_graph.js
@@ -22,6 +22,7 @@ class PrometheusGraph {
const hasMetrics = $prometheusContainer.data('has-metrics');
this.docLink = $prometheusContainer.data('doc-link');
this.integrationLink = $prometheusContainer.data('prometheus-integration');
+ this.state = '';
$(document).ajaxError(() => {});
@@ -38,8 +39,9 @@ class PrometheusGraph {
this.configureGraph();
this.init();
} else {
+ const prevState = this.state;
this.state = '.js-getting-started';
- this.updateState();
+ this.updateState(prevState);
}
}
@@ -53,26 +55,26 @@ class PrometheusGraph {
}
init() {
- this.getData().then((metricsResponse) => {
+ return this.getData().then((metricsResponse) => {
let enoughData = true;
- Object.keys(metricsResponse.metrics).forEach((key) => {
- let currentKey;
- if (key === 'cpu_values' || key === 'memory_values') {
- currentKey = metricsResponse.metrics[key];
- if (Object.keys(currentKey).length === 0) {
- enoughData = false;
- }
- }
- });
- if (!enoughData) {
- this.state = '.js-loading';
- this.updateState();
+ if (typeof metricsResponse === 'undefined') {
+ enoughData = false;
} else {
+ Object.keys(metricsResponse.metrics).forEach((key) => {
+ if (key === 'cpu_values' || key === 'memory_values') {
+ const currentData = (metricsResponse.metrics[key])[0];
+ if (currentData.values.length <= 2) {
+ enoughData = false;
+ }
+ }
+ });
+ }
+ if (enoughData) {
+ $(prometheusStatesContainer).hide();
+ $(prometheusParentGraphContainer).show();
this.transformData(metricsResponse);
this.createGraph();
}
- }).catch(() => {
- new Flash('An error occurred when trying to load metrics. Please try again.');
});
}
@@ -342,6 +344,8 @@ class PrometheusGraph {
getData() {
const maxNumberOfRequests = 3;
+ this.state = '.js-loading';
+ this.updateState();
return gl.utils.backOff((next, stop) => {
$.ajax({
url: metricsEndpoint,
@@ -352,12 +356,11 @@ class PrometheusGraph {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < maxNumberOfRequests) {
next();
- } else {
- stop({
- status: resp.status,
- metrics: data,
- });
+ } else if (this.backOffRequestCounter >= maxNumberOfRequests) {
+ stop(new Error('loading'));
}
+ } else if (!data.success) {
+ stop(new Error('loading'));
} else {
stop({
status: resp.status,
@@ -373,8 +376,9 @@ class PrometheusGraph {
return resp.metrics;
})
.catch(() => {
+ const prevState = this.state;
this.state = '.js-unable-to-connect';
- this.updateState();
+ this.updateState(prevState);
});
}
@@ -382,19 +386,20 @@ class PrometheusGraph {
Object.keys(metricsResponse.metrics).forEach((key) => {
if (key === 'cpu_values' || key === 'memory_values') {
const metricValues = (metricsResponse.metrics[key])[0];
- if (metricValues !== undefined) {
- this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
- time: new Date(metric[0] * 1000),
- value: metric[1],
- }));
- }
+ this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
+ time: new Date(metric[0] * 1000),
+ value: metric[1],
+ }));
}
});
}
- updateState() {
+ updateState(prevState) {
const $statesContainer = $(prometheusStatesContainer);
$(prometheusParentGraphContainer).hide();
+ if (prevState) {
+ $(`${prevState}`, $statesContainer).addClass('hidden');
+ }
$(`${this.state}`, $statesContainer).removeClass('hidden');
$(prometheusStatesContainer).show();
}
diff --git a/app/assets/javascripts/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue
new file mode 100644
index 00000000000..b8a16356576
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/code.vue
@@ -0,0 +1,58 @@
+<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>
diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue
new file mode 100644
index 00000000000..31b30f601e2
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/code/index.vue
@@ -0,0 +1,57 @@
+<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>
diff --git a/app/assets/javascripts/notebook/cells/index.js b/app/assets/javascripts/notebook/cells/index.js
new file mode 100644
index 00000000000..e4c255609fe
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/index.js
@@ -0,0 +1,2 @@
+export { default as MarkdownCell } from './markdown.vue';
+export { default as CodeCell } from './code.vue';
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
new file mode 100644
index 00000000000..3e8240d10ec
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -0,0 +1,98 @@
+<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>
diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue
new file mode 100644
index 00000000000..0f39cd138df
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/output/html.vue
@@ -0,0 +1,22 @@
+<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>
diff --git a/app/assets/javascripts/notebook/cells/output/image.vue b/app/assets/javascripts/notebook/cells/output/image.vue
new file mode 100644
index 00000000000..f3b873bbc0f
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/output/image.vue
@@ -0,0 +1,27 @@
+<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>
diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue
new file mode 100644
index 00000000000..23c9ea78939
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/output/index.vue
@@ -0,0 +1,83 @@
+<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>
diff --git a/app/assets/javascripts/notebook/cells/prompt.vue b/app/assets/javascripts/notebook/cells/prompt.vue
new file mode 100644
index 00000000000..4540e4248d8
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/prompt.vue
@@ -0,0 +1,30 @@
+<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>
diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue
new file mode 100644
index 00000000000..fd62c1231ef
--- /dev/null
+++ b/app/assets/javascripts/notebook/index.vue
@@ -0,0 +1,75 @@
+<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>
diff --git a/app/assets/javascripts/notebook/lib/highlight.js b/app/assets/javascripts/notebook/lib/highlight.js
new file mode 100644
index 00000000000..74ade6d2edf
--- /dev/null
+++ b/app/assets/javascripts/notebook/lib/highlight.js
@@ -0,0 +1,22 @@
+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;
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 5b6bb2bf3f5..85659d7fa39 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -57,8 +57,11 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
Shortcuts.prototype.toggleMarkdownPreview = function(e) {
// Check if short-cut was triggered while in Write Mode
- if ($(e.target).hasClass('js-note-text')) {
- $('.js-md-preview-button').focus();
+ const $target = $(e.target);
+ const $form = $target.closest('form');
+
+ if ($target.hasClass('js-note-text')) {
+ $('.js-md-preview-button', $form).focus();
}
return $(document).triggerHandler('markdown-preview:toggle', [e]);
};
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index f614f262316..b2102d2fbc5 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -108,8 +108,7 @@
}
.award-control {
- margin: 3px 5px 3px 0;
- padding: .35em .4em;
+ margin-right: 5px;
outline: 0;
&.disabled {
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 52425262925..f3e2a5db0a6 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -230,7 +230,6 @@
float: right;
margin-top: 8px;
padding-bottom: 8px;
- border-bottom: 1px solid $border-color;
}
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 0fd7203e72b..638c1403b25 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -70,7 +70,7 @@ pre {
}
hr {
- margin: $gl-padding 0;
+ margin: 24px 0;
border-top: 1px solid darken($gray-normal, 8%);
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index a5a8522739e..df819ffe4bc 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -73,14 +73,6 @@
&.wiki {
padding: 30px $gl-padding;
-
- .highlight {
- margin-bottom: 9px;
-
- > pre {
- margin: 0;
- }
- }
}
&.blob-no-preview {
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index e6d808717f3..b6cf5101d60 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -110,7 +110,7 @@
.top-area {
@include clearfix;
- border-bottom: 1px solid $white-normal;
+ border-bottom: 1px solid $border-color;
.nav-text {
padding-top: 16px;
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index cd23deb6d75..d2164a1d333 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -4,7 +4,7 @@
padding: 0;
.timeline-entry {
- padding: $gl-padding $gl-btn-padding 14px;
+ padding: $gl-padding $gl-btn-padding 0;
border-color: $white-normal;
color: $gl-text-color;
border-bottom: 1px solid $border-white-light;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 664539e93e1..96d8a812723 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -8,6 +8,13 @@
img {
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) {
@@ -47,44 +54,50 @@
h1 {
font-size: 1.75em;
font-weight: 600;
- margin: 16px 0 10px;
- padding: 0 0 0.3em;
+ margin: 24px 0 16px;
+ padding-bottom: 0.3em;
border-bottom: 1px solid $white-dark;
color: $gl-text-color;
+
+ &:first-child {
+ margin-top: 0;
+ }
}
h2 {
font-size: 1.5em;
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;
}
h3 {
- margin: 16px 0 10px;
+ margin: 24px 0 16px;
font-size: 1.3em;
}
h4 {
- margin: 16px 0 10px;
+ margin: 24px 0 16px;
font-size: 1.2em;
}
h5 {
- margin: 16px 0 10px;
+ margin: 24px 0 16px;
font-size: 1em;
}
h6 {
- margin: 16px 0 10px;
+ margin: 24px 0 16px;
font-size: 0.95em;
}
blockquote {
color: $gl-grayish-blue;
font-size: inherit;
- padding: 8px 21px;
- margin: 12px 0;
+ padding: 8px 24px;
+ margin: 16px 0;
border-left: 3px solid $white-dark;
}
@@ -95,19 +108,20 @@
blockquote p {
color: $gl-grayish-blue !important;
+ margin: 0;
font-size: inherit;
line-height: 1.5;
}
p {
color: $gl-text-color;
- margin: 6px 0 0;
+ margin: 0 0 16px;
}
table {
@extend .table;
@extend .table-bordered;
- margin: 12px 0;
+ margin: 16px 0;
color: $gl-text-color;
th {
@@ -120,7 +134,7 @@
}
pre {
- margin: 12px 0;
+ margin-bottom: 16px;
font-size: 13px;
line-height: 1.6em;
overflow-x: auto;
@@ -134,7 +148,7 @@
ul,
ol {
padding: 0;
- margin: 3px 0 !important;
+ margin: 0 0 16px !important;
}
ul:dir(rtl),
@@ -338,3 +352,32 @@ h4 {
.idiff.addition {
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;
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index ad592a45941..49741c963df 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -111,6 +111,7 @@ $gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
$gl-header-color: #4c4e54;
$gl-header-nav-hover-color: #434343;
+$placeholder-text-color: rgba(0, 0, 0, .42);
/*
* Lists
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 46fd19c93f9..f3de05aa5f6 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -29,11 +29,5 @@
.description {
margin-top: 6px;
-
- p {
- &:last-child {
- margin-bottom: 0;
- }
- }
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 8d3d1a72b9b..97fab513b01 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -52,7 +52,7 @@
.title {
padding: 0;
- margin: 0;
+ margin-bottom: 16px;
border-bottom: none;
}
@@ -357,6 +357,8 @@
}
.detail-page-description {
+ padding: 16px 0 0;
+
small {
color: $gray-darkest;
}
@@ -364,6 +366,8 @@
.edited-text {
color: $gray-darkest;
+ display: block;
+ margin: 0 0 16px;
.author_link {
color: $gray-darkest;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index b637994adf8..62f654ed343 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -28,7 +28,7 @@
.note-edit-form {
.note-form-actions {
position: relative;
- margin-top: $gl-padding;
+ margin: $gl-padding 0;
}
.note-preview-holder {
@@ -387,6 +387,7 @@
@media (max-width: $screen-xs-max) {
display: flex;
width: 100%;
+ margin-bottom: 10px;
.comment-btn {
flex-grow: 1;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 2ea2ff8362b..69a95db6920 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -102,13 +102,12 @@ ul.notes {
.note-awards {
.js-awards-block {
- padding: 2px;
- margin-top: 10px;
+ margin-bottom: 16px;
}
}
.note-header {
- padding-bottom: 3px;
+ padding-bottom: 8px;
padding-right: 20px;
@media (min-width: $screen-sm-min) {
@@ -151,6 +150,10 @@ ul.notes {
margin-left: 65px;
}
+ .note-header {
+ padding-bottom: 0;
+ }
+
&.timeline-entry::after {
clear: none;
}
@@ -386,6 +389,10 @@ ul.notes {
.note-headline-meta {
display: inline-block;
white-space: nowrap;
+
+ .system-note-message {
+ white-space: normal;
+ }
}
/**
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index fc8d4d02ddf..5885b3543bb 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -28,7 +28,7 @@ class Admin::GroupsController < Admin::ApplicationController
if @group.save
@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
render "new"
end
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 408c0c60cb0..d0dd524c484 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -23,6 +23,7 @@ class Projects::MilestonesController < Projects::ApplicationController
respond_to do |format|
format.html do
+ @project_namespace = @project.namespace.becomes(Namespace)
@milestones = @milestones.includes(:project)
@milestones = @milestones.page(params[:page])
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 3736e1ffcbb..36b16421e8f 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -29,7 +29,7 @@ module BlobHelper
link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
elsif current_user && can?(current_user, :fork_project, project)
continue_params = {
- to: edit_path,
+ to: edit_path(project, ref, path, options),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 2de9e0de310..32b1e7822af 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -1,10 +1,16 @@
+##
+# DEPRECATED
+#
+# These helpers are deprecated in favor of detailed CI/CD statuses.
+#
+# See 'detailed_status?` method and `Gitlab::Ci::Status` module.
+#
module CiStatusHelper
def ci_status_path(pipeline)
project = pipeline.project
namespace_project_pipeline_path(project.namespace, project, pipeline)
end
- # Is used by Commit and Merge Request Widget
def ci_label_for_status(status)
if detailed_status?(status)
return status.label
@@ -22,6 +28,23 @@ module CiStatusHelper
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)
status = subject.try(:status) || 'not found'
status.humanize
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index ab3ef454e1c..55fa81e95ef 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -7,6 +7,11 @@ module IconsHelper
# 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.
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)
end
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index fb95f2b565e..a762b320d56 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -5,7 +5,7 @@ module SubmoduleHelper
def submodule_links(submodule_item, ref = nil, repository = @repository)
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
project = $2
@@ -37,14 +37,16 @@ module SubmoduleHelper
end
def self_url?(url, namespace, project)
- return true if url == [Gitlab.config.gitlab.url, '/', namespace, '/',
- project, '.git'].join('')
- url == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
+ url_no_dotgit = url.chomp('.git')
+ return true if url_no_dotgit == [Gitlab.config.gitlab.url, '/', namespace, '/',
+ project].join('')
+ url_with_dotgit = url_no_dotgit + '.git'
+ url_with_dotgit == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
end
def relative_self_url?(url)
# (./)?(../repo.git) || (./)?(../../project/repo.git) )
- url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\z/
+ url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*(\.git)?\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*(\.git)?\z/
end
def standard_links(host, namespace, project, commit)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 445247f1b41..4be4aa9ffe2 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -75,29 +75,32 @@ module Ci
pipeline.update_duration
end
+ before_transition any => [:manual] do |pipeline|
+ pipeline.update_duration
+ end
+
before_transition canceled: any - [:canceled] do |pipeline|
pipeline.auto_canceled_by = nil
end
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
after_transition any => [:success] do |pipeline|
- pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
+ pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end
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
after_transition do |pipeline, transition|
next if transition.loopback?
pipeline.run_after_commit do
- PipelineHooksWorker.perform_async(id)
- Ci::ExpirePipelineCacheService.new(project, nil)
- .execute(pipeline)
+ PipelineHooksWorker.perform_async(pipeline.id)
+ ExpirePipelineCacheWorker.perform_async(pipeline.id)
end
end
@@ -385,6 +388,11 @@ module Ci
.select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
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)
Gitlab::Ci::Status::Pipeline::Factory
.new(self, current_user)
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 8ea95beed79..2eedc143968 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -8,6 +8,14 @@
#
# Corresponding foo_html, bar_html and baz_html fields should exist.
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
# stores the rendering contexts for the latter
class FieldData
@@ -30,60 +38,71 @@ module CacheMarkdownField
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?
false
end
- extend ActiveSupport::Concern
+ # Returns the default Banzai render context for the cached markdown field.
+ def banzai_render_context(field)
+ raise ArgumentError.new("Unknown field: #{field.inspect}") unless
+ cached_markdown_fields.markdown_fields.include?(field)
- included do
- cattr_reader :cached_markdown_fields do
- FieldData.new
- end
+ # Always include a project key, or Banzai complains
+ project = self.project if self.respond_to?(:project)
+ context = cached_markdown_fields[field].merge(project: project)
- # Returns the default Banzai render context for the cached markdown field.
- def banzai_render_context(field)
- raise ArgumentError.new("Unknown field: #{field.inspect}") unless
- cached_markdown_fields.markdown_fields.include?(field)
+ # Banzai is less strict about authors, so don't always have an author key
+ context[:author] = self.author if self.respond_to?(:author)
- # Always include a project key, or Banzai complains
- project = self.project if self.respond_to?(:project)
- context = cached_markdown_fields[field].merge(project: project)
+ context
+ end
- # Banzai is less strict about authors, so don't always have an author key
- context[:author] = self.author if self.respond_to?(:author)
+ # Update every column in a row if any one is invalidated, as we only store
+ # one version per row
+ def refresh_markdown_cache!(do_update: false)
+ options = { skip_project_check: skip_project_check? }
- context
- end
+ 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
- # Allow callers to look up the cache field name, rather than hardcoding it
- def markdown_cache_field_for(field)
- raise ArgumentError.new("Unknown field: #{field}") unless
- cached_markdown_fields.markdown_fields.include?(field)
+ updates.each {|html_field, data| write_attribute(html_field, data) }
- cached_markdown_fields.html_field(field)
+ 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
+ cached_markdown_fields.markdown_fields.include?(markdown_field)
+
+ __send__(cached_markdown_fields.html_field(markdown_field))
+ end
+
+ included do
+ cattr_reader :cached_markdown_fields do
+ FieldData.new
end
# Always exclude _html fields from attributes (including serialization).
@@ -92,12 +111,16 @@ module CacheMarkdownField
def attributes
attrs = attributes_before_markdown_cache
+ attrs.delete('cached_markdown_version')
+
cached_markdown_fields.html_fields.each do |field|
attrs.delete(field)
end
attrs
end
+
+ before_save :refresh_markdown_cache!, if: :invalidated_markdown_cache?
end
class_methods do
@@ -107,31 +130,18 @@ module CacheMarkdownField
# a corresponding _html field. Any custom rendering options may be provided
# as a 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
html_field = cached_markdown_fields.html_field(markdown_field)
- cache_method = "#{markdown_field}_cache_refresh".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
# author and project invalidate the cache in all circumstances.
define_method(invalidation_method) do
changed_fields = changed_attributes.keys
- invalidations = changed_fields & [markdown_field.to_s, "author", "project"]
- !invalidations.empty?
+ invalidations = changed_fields & [markdown_field.to_s, *INVALIDATED_BY]
+ !invalidations.empty? || !cached_html_up_to_date?(markdown_field)
end
-
- before_save cache_method, if: invalidation_method
end
end
end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index aca99feee53..b28e05d0c28 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -163,7 +163,20 @@ module Routable
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
+ 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?
@full_path ||= route.path
else
@@ -173,8 +186,6 @@ module Routable
end
end
- private
-
def full_name_changed?
name_changed? || parent_changed?
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 106084175ff..cbc10b00cf5 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -125,7 +125,7 @@ class Group < Namespace
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
- GroupMember.add_users_to_group(
+ GroupMember.add_users(
self,
users,
access_level,
diff --git a/app/models/individual_note_discussion.rb b/app/models/individual_note_discussion.rb
index c3f21c55240..6be8ca45739 100644
--- a/app/models/individual_note_discussion.rb
+++ b/app/models/individual_note_discussion.rb
@@ -10,4 +10,8 @@ class IndividualNoteDiscussion < Discussion
def individual_note?
true
end
+
+ def reply_attributes
+ super.tap { |attrs| attrs.delete(:discussion_id) }
+ end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 0545bd4eedf..97fba501759 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -151,6 +151,22 @@ class Member < ActiveRecord::Base
member
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
Gitlab::Access.sym_options
end
@@ -173,18 +189,6 @@ class Member < ActiveRecord::Base
# There is no current user for bulk actions, in which case anything is allowed
!current_user || current_user.can?(:"update_#{member.type.underscore}", member)
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
def real_source_type
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 483425cd30f..28e10bc6172 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -21,18 +21,6 @@ class GroupMember < Member
Gitlab::Access.sym_options_with_owner
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
source
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 912820b51ac..b3a91feb091 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -16,7 +16,7 @@ class ProjectMember < Member
before_destroy :delete_member_todos
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
# or symbol like :master representing role
@@ -39,7 +39,7 @@ class ProjectMember < Member
project_ids.each do |project_id|
project = Project.find(project_id)
- add_users_to_source(
+ add_users(
project,
users,
access_level,
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 1d4827375d7..9d2288c311e 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -191,22 +191,23 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
end
- def diffs(diff_options = nil)
+ def diffs(diff_options = {})
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
merge_request_diff.diffs(diff_options)
end
end
def diff_size
- # The `#diffs` method ends up at an instance of a class inheriting from
- # `Gitlab::Diff::FileCollection::Base`, so use those options as defaults
- # here too, to get the same diff size without performing highlighting.
- #
- opts = Gitlab::Diff::FileCollection::Base.default_options.merge(diff_options || {})
+ # Calling `merge_request_diff.diffs.real_size` will also perform
+ # highlighting, which we don't need here.
+ return real_size if merge_request_diff
- raw_diffs(opts).size
+ diffs.real_size
end
def diff_base_commit
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 6604af2b47e..f0a3c30ea74 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -260,7 +260,7 @@ class MergeRequestDiff < ActiveRecord::Base
new_attributes[:state] = :empty
else
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?
new_diffs = dump_diffs(diff_collection)
diff --git a/app/models/out_of_context_discussion.rb b/app/models/out_of_context_discussion.rb
index 85794630f70..4227c40b69a 100644
--- a/app/models/out_of_context_discussion.rb
+++ b/app/models/out_of_context_discussion.rb
@@ -15,8 +15,12 @@ class OutOfContextDiscussion < Discussion
def self.override_discussion_id(note)
discussion_id(note)
end
-
+
def self.note_class
Note
end
+
+ def reply_attributes
+ super.tap { |attrs| attrs.delete(:discussion_id) }
+ end
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 6d6644053f8..543b9b293e0 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -50,8 +50,8 @@ class ProjectTeam
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
- ProjectMember.add_users_to_projects(
- [project.id],
+ ProjectMember.add_users(
+ project,
users,
access_level,
current_user: current_user,
diff --git a/app/services/ci/expire_pipeline_cache_service.rb b/app/services/ci/expire_pipeline_cache_service.rb
deleted file mode 100644
index 91d9c1d2ba1..00000000000
--- a/app/services/ci/expire_pipeline_cache_service.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-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
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
index 9a0a5a12f91..d2a1c161026 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -6,8 +6,8 @@ module Users
@params = params.dup
end
- def execute
- raise Gitlab::Access::AccessDeniedError unless can_create_user?
+ def execute(skip_authorization: false)
+ raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_create_user?
user = User.new(build_user_params)
diff --git a/app/services/users/create_service.rb b/app/services/users/create_service.rb
index a2105d31f71..e22f7225ae2 100644
--- a/app/services/users/create_service.rb
+++ b/app/services/users/create_service.rb
@@ -6,8 +6,8 @@ module Users
@params = params.dup
end
- def execute
- user = Users::BuildService.new(current_user, params).execute
+ def execute(skip_authorization: false)
+ user = Users::BuildService.new(current_user, params).execute(skip_authorization: skip_authorization)
@reset_token = user.generate_reset_token if user.recently_sent_password_reset?
diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb
index 1e1ed1791ec..4628c4c6f6e 100644
--- a/app/services/users/migrate_to_ghost_user_service.rb
+++ b/app/services/users/migrate_to_ghost_user_service.rb
@@ -15,27 +15,39 @@ module Users
end
def execute
- # Block the user before moving records to prevent a data race.
- # For example, if the user creates an issue after `migrate_issues`
- # runs and before the user is destroyed, the destroy will fail with
- # an exception.
- user.block
+ transition = user.block_transition
user.transaction do
+ # Block the user before moving records to prevent a data race.
+ # For example, if the user creates an issue after `migrate_issues`
+ # runs and before the user is destroyed, the destroy will fail with
+ # an exception.
+ user.block
+
+ # 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
migrate_issues
migrate_merge_requests
migrate_notes
migrate_abuse_reports
- migrate_award_emoji
+ migrate_award_emojis
end
-
- user.reload
end
- private
-
def migrate_issues
user.issues.update_all(author_id: ghost_user.id)
end
@@ -52,7 +64,7 @@ module Users
user.reported_abuse_reports.update_all(reporter_id: ghost_user.id)
end
- def migrate_award_emoji
+ def migrate_award_emojis
user.award_emoji.update_all(user_id: ghost_user.id)
end
end
diff --git a/app/views/admin/cohorts/index.html.haml b/app/views/admin/cohorts/index.html.haml
index 46fe12a5a99..be8644c0ca6 100644
--- a/app/views/admin/cohorts/index.html.haml
+++ b/app/views/admin/cohorts/index.html.haml
@@ -9,7 +9,7 @@
.bs-callout.bs-callout-warning.clearfix
%p
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,
visit
= succeed '.' do
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 37429c7cfc0..8ab9747efc5 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -56,7 +56,7 @@
Snippets
- 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
%span
Settings
diff --git a/app/views/projects/_fork_suggestion.html.haml b/app/views/projects/_fork_suggestion.html.haml
new file mode 100644
index 00000000000..c855bfaf067
--- /dev/null
+++ b/app/views/projects/_fork_suggestion.html.haml
@@ -0,0 +1,11 @@
+- if current_user
+ .js-file-fork-suggestion-section.file-fork-suggestion.hidden
+ %span.file-fork-suggestion-note
+ You're not allowed to
+ %span.js-file-fork-suggestion-section-action
+ edit
+ files in this project directly. Please fork this project,
+ make your changes there, and submit a merge request.
+ = link_to 'Fork', nil, method: :post, class: 'js-fork-suggestion-button btn btn-grouped btn-inverted btn-new'
+ %button.js-cancel-fork-suggestion-button.btn.btn-grouped{ type: 'button' }
+ Cancel
diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml
index e1fea8ccf3d..df3b1c75508 100644
--- a/app/views/projects/_last_commit.html.haml
+++ b/app/views/projects/_last_commit.html.haml
@@ -1,10 +1,9 @@
-
- ref = local_assigns.fetch(:ref)
- status = commit.status(ref)
- if status
= 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_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_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index d46e4534497..c553db84ee0 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -39,14 +39,4 @@
= replace_blob_link
= delete_blob_link
-- 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
+= render 'projects/fork_suggestion'
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 0232a09b4a8..4622b980754 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -18,4 +18,6 @@
= 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
+ = render 'projects/fork_suggestion'
+
= render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index bf98efbdfdf..3971ea44ef3 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -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'
.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),
"canDescription" => can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '',
} }
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index e8c9d7f8429..33bbbd9a3f8 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -36,7 +36,7 @@
%a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
- .detail-page-description.milestone-detail{ class: ('hide-bottom-border' unless @milestone.description.present? ) }
+ .detail-page-description.milestone-detail
%h2.title
= markdown_field(@milestone, :title)
%div
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 0c7b53e5a9a..9e292729425 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -145,7 +145,8 @@
}
});
+ $('#project_import_url').disable();
$('.import_git').click(function( event ) {
- $projectImportUrl = $('#project_import_url')
- $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'))
+ $projectImportUrl = $('#project_import_url');
+ $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'));
});
diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml
index 50ed78286d2..0f1a76a104a 100644
--- a/app/views/projects/services/edit.html.haml
+++ b/app/views/projects/services/edit.html.haml
@@ -1,2 +1,3 @@
- page_title @service.title, "Services"
+= render "projects/settings/head"
= render 'form'
diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml
index 88bcb541dac..e50a543ffa8 100644
--- a/app/views/projects/settings/_head.html.haml
+++ b/app/views/projects/settings/_head.html.haml
@@ -14,7 +14,7 @@
%span
Members
- if can_edit
- = nav_link(controller: :integrations) do
+ = nav_link(controller: [:integrations, :services]) do
= link_to project_settings_integrations_path(@project), title: 'Integrations' do
%span
Integrations
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 4c7d69d40d5..5247d6a51e6 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -1,11 +1,14 @@
-# @project is present when viewing Project's milestone
- project = @project || issuable.project
+- namespace = @project_namespace || project.namespace.becomes(Namespace)
- assignee = issuable.assignee
- 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)
-%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
- if show_project_name
%strong #{project.name} &middot;
@@ -13,17 +16,17 @@
%strong #{project.name_with_namespace} &middot;
- if issuable.is_a?(Issue)
= 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
= link_to [project.namespace.becomes(Namespace), project, issuable] do
%span.issuable-number= issuable.to_reference
- 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)
%span.assignee-icon
- 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
- image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '')
diff --git a/app/workers/clear_database_cache_worker.rb b/app/workers/clear_database_cache_worker.rb
deleted file mode 100644
index c4cb4733482..00000000000
--- a/app/workers/clear_database_cache_worker.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# 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
diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb
new file mode 100644
index 00000000000..603e2f1aaea
--- /dev/null
+++ b/app/workers/expire_pipeline_cache_worker.rb
@@ -0,0 +1,57 @@
+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