Commit 9b9c6220 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into qa/gb/selenium-handle-domain-sessions

* master: (93 commits)
parents de7901ff 95daf62b
...@@ -2,6 +2,17 @@ ...@@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.2.4 (2017-12-08)
### Security (4 changes)
- Fix e-mail address disclosure through member search fields
- Prevent creating issues through API when user does not have permissions
- Prevent an information disclosure in the Groups API
- Fix user without access to private Wiki being able to see it on the project page
- Fix Cross-Site Scripting (XSS) vulnerability while editing a comment
## 10.2.3 (2017-11-30) ## 10.2.3 (2017-11-30)
### Fixed (7 changes) ### Fixed (7 changes)
......
...@@ -598,6 +598,7 @@ merge request: ...@@ -598,6 +598,7 @@ merge request:
present time and never use past tense (has been/was). For example instead present time and never use past tense (has been/was). For example instead
of _prohibited this user from being saved due to the following errors:_ the of _prohibited this user from being saved due to the following errors:_ the
text should be _sorry, we could not create your account because:_ text should be _sorry, we could not create your account because:_
1. Code should be written in [US English][us-english]
This is also the style used by linting tools such as This is also the style used by linting tools such as
[RuboCop](https://github.com/bbatsov/rubocop), [RuboCop](https://github.com/bbatsov/rubocop),
...@@ -663,6 +664,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor ...@@ -663,6 +664,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues [GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html [polling-etag]: https://docs.gitlab.com/ce/development/polling.html
[testing]: doc/development/testing_guide/index.md [testing]: doc/development/testing_guide/index.md
[us-english]: https://en.wikipedia.org/wiki/American_English
[^1]: Please note that specs other than JavaScript specs are considered backend [^1]: Please note that specs other than JavaScript specs are considered backend
code. code.
...@@ -411,3 +411,6 @@ gem 'flipper-active_record', '~> 0.10.2' ...@@ -411,3 +411,6 @@ gem 'flipper-active_record', '~> 0.10.2'
# Structured logging # Structured logging
gem 'lograge', '~> 0.5' gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.7' gem 'grape_logging', '~> 1.7'
# Asset synchronization
gem 'asset_sync', '~> 2.2.0'
...@@ -58,6 +58,11 @@ GEM ...@@ -58,6 +58,11 @@ GEM
asciidoctor (1.5.3) asciidoctor (1.5.3)
asciidoctor-plantuml (0.0.7) asciidoctor-plantuml (0.0.7)
asciidoctor (~> 1.5) asciidoctor (~> 1.5)
asset_sync (2.2.0)
activemodel (>= 4.1.0)
fog-core
mime-types (>= 2.99)
unf
ast (2.3.0) ast (2.3.0)
atomic (1.1.99) atomic (1.1.99)
attr_encrypted (3.0.3) attr_encrypted (3.0.3)
...@@ -486,7 +491,6 @@ GEM ...@@ -486,7 +491,6 @@ GEM
mini_mime (0.1.4) mini_mime (0.1.4)
mini_portile2 (2.3.0) mini_portile2 (2.3.0)
minitest (5.7.0) minitest (5.7.0)
mmap2 (2.2.9)
mousetrap-rails (1.4.6) mousetrap-rails (1.4.6)
multi_json (1.12.2) multi_json (1.12.2)
multi_xml (0.6.0) multi_xml (0.6.0)
...@@ -623,8 +627,7 @@ GEM ...@@ -623,8 +627,7 @@ GEM
parser parser
unparser unparser
procto (0.0.3) procto (0.0.3)
prometheus-client-mmap (0.7.0.beta39) prometheus-client-mmap (0.7.0.beta43)
mmap2 (~> 2.2, >= 2.2.9)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
...@@ -977,6 +980,7 @@ DEPENDENCIES ...@@ -977,6 +980,7 @@ DEPENDENCIES
asana (~> 0.6.0) asana (~> 0.6.0)
asciidoctor (~> 1.5.2) asciidoctor (~> 1.5.2)
asciidoctor-plantuml (= 0.0.7) asciidoctor-plantuml (= 0.0.7)
asset_sync (~> 2.2.0)
attr_encrypted (~> 3.0.0) attr_encrypted (~> 3.0.0)
awesome_print (~> 1.2.0) awesome_print (~> 1.2.0)
babosa (~> 1.0.2) babosa (~> 1.0.2)
......
...@@ -130,7 +130,8 @@ freeze date (the 7th) should have a corresponding Enterprise Edition merge ...@@ -130,7 +130,8 @@ freeze date (the 7th) should have a corresponding Enterprise Edition merge
request, even if there are no conflicts. This is to reduce the size of the request, even if there are no conflicts. This is to reduce the size of the
subsequent EE merge, as we often merge a lot to CE on the release date. For more subsequent EE merge, as we often merge a lot to CE on the release date. For more
information, see information, see
[limit conflicts with EE when developing on CE][limit_ee_conflicts]. [Automatic CE->EE merge][automatic_ce_ee_merge] and
[Guidelines for implementing Enterprise Edition features][ee_features].
### After the 7th ### After the 7th
...@@ -281,4 +282,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http ...@@ -281,4 +282,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http
["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements ["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements
[Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review [Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review
[done]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done [done]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done
[limit_ee_conflicts]: https://docs.gitlab.com/ce/development/limit_ee_conflicts.html [automatic_ce_ee_merge]: https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
[ee_features]: https://docs.gitlab.com/ce/development/ee_features.html
...@@ -121,7 +121,7 @@ export default class ImageFile { ...@@ -121,7 +121,7 @@ export default class ImageFile {
return $('.swipe.view', this.file).each((function(_this) { return $('.swipe.view', this.file).each((function(_this) {
return function(index, view) { return function(index, view) {
var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref; var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
ref = this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
$swipeFrame = $('.swipe-frame', view); $swipeFrame = $('.swipe-frame', view);
$swipeWrap = $('.swipe-wrap', view); $swipeWrap = $('.swipe-wrap', view);
$swipeBar = $('.swipe-bar', view); $swipeBar = $('.swipe-bar', view);
...@@ -158,7 +158,7 @@ export default class ImageFile { ...@@ -158,7 +158,7 @@ export default class ImageFile {
return $('.onion-skin.view', this.file).each((function(_this) { return $('.onion-skin.view', this.file).each((function(_this) {
return function(index, view) { return function(index, view) {
var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false; var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false;
ref = this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
$frame = $('.onion-skin-frame', view); $frame = $('.onion-skin-frame', view);
$frameAdded = $('.frame.added', view); $frameAdded = $('.frame.added', view);
$track = $('.drag-track', view); $track = $('.drag-track', view);
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
window.Compare = (function() { export default class Compare {
function Compare(opts) { constructor(opts) {
this.opts = opts; this.opts = opts;
this.source_loading = $(".js-source-loading"); this.source_loading = $(".js-source-loading");
this.target_loading = $(".js-target-loading"); this.target_loading = $(".js-target-loading");
...@@ -34,12 +34,12 @@ window.Compare = (function() { ...@@ -34,12 +34,12 @@ window.Compare = (function() {
this.initialState(); this.initialState();
} }
Compare.prototype.initialState = function() { initialState() {
this.getSourceHtml(); this.getSourceHtml();
return this.getTargetHtml(); this.getTargetHtml();
}; }
Compare.prototype.getTargetProject = function() { getTargetProject() {
return $.ajax({ return $.ajax({
url: this.opts.targetProjectUrl, url: this.opts.targetProjectUrl,
data: { data: {
...@@ -52,22 +52,22 @@ window.Compare = (function() { ...@@ -52,22 +52,22 @@ window.Compare = (function() {
return $('.js-target-branch-dropdown .dropdown-content').html(html); return $('.js-target-branch-dropdown .dropdown-content').html(html);
} }
}); });
}; }
Compare.prototype.getSourceHtml = function() { getSourceHtml() {
return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', { return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
ref: $("input[name='merge_request[source_branch]']").val() ref: $("input[name='merge_request[source_branch]']").val()
}); });
}; }
Compare.prototype.getTargetHtml = function() { getTargetHtml() {
return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', { return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
target_project_id: $("input[name='merge_request[target_project_id]']").val(), target_project_id: $("input[name='merge_request[target_project_id]']").val(),
ref: $("input[name='merge_request[target_branch]']").val() ref: $("input[name='merge_request[target_branch]']").val()
}); });
}; }
Compare.prototype.sendAjax = function(url, loading, target, data) { static sendAjax(url, loading, target, data) {
var $target; var $target;
$target = $(target); $target = $(target);
return $.ajax({ return $.ajax({
...@@ -84,7 +84,5 @@ window.Compare = (function() { ...@@ -84,7 +84,5 @@ window.Compare = (function() {
gl.utils.localTimeAgo($('.js-timeago', className)); gl.utils.localTimeAgo($('.js-timeago', className));
} }
}); });
}; }
}
return Compare;
})();
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */
window.CompareAutocomplete = (function() { export default function initCompareAutocomplete() {
function CompareAutocomplete() { $('.js-compare-dropdown').each(function() {
this.initDropdown(); var $dropdown, selected;
} $dropdown = $(this);
selected = $dropdown.data('selected');
CompareAutocomplete.prototype.initDropdown = function() { const $dropdownContainer = $dropdown.closest('.dropdown');
return $('.js-compare-dropdown').each(function() { const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer);
var $dropdown, selected; const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown = $(this); $dropdown.glDropdown({
selected = $dropdown.data('selected'); data: function(term, callback) {
const $dropdownContainer = $dropdown.closest('.dropdown'); return $.ajax({
const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer); url: $dropdown.data('refs-url'),
const $filterInput = $('input[type="search"]', $dropdownContainer); data: {
$dropdown.glDropdown({ ref: $dropdown.data('ref'),
data: function(term, callback) { search: term,
return $.ajax({
url: $dropdown.data('refs-url'),
data: {
ref: $dropdown.data('ref'),
search: term,
}
}).done(function(refs) {
return callback(refs);
});
},
selectable: true,
filterable: true,
filterRemote: true,
fieldName: $dropdown.data('field-name'),
filterInput: 'input[type="search"]',
renderRow: function(ref) {
var link;
if (ref.header != null) {
return $('<li />').addClass('dropdown-header').text(ref.header);
} else {
link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
return $('<li />').append(link);
} }
}, }).done(function(refs) {
id: function(obj, $el) { return callback(refs);
return $el.attr('data-ref'); });
}, },
toggleLabel: function(obj, $el) { selectable: true,
return $el.text().trim(); filterable: true,
} filterRemote: true,
}); fieldName: $dropdown.data('field-name'),
$filterInput.on('keyup', (e) => { filterInput: 'input[type="search"]',
const keyCode = e.keyCode || e.which; renderRow: function(ref) {
if (keyCode !== 13) return; var link;
const text = $filterInput.val(); if (ref.header != null) {
$fieldInput.val(text); return $('<li />').addClass('dropdown-header').text(ref.header);
$('.dropdown-toggle-text', $dropdown).text(text); } else {
$dropdownContainer.removeClass('open'); link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
}); return $('<li />').append(link);
$dropdownContainer.on('click', '.dropdown-content a', (e) => {
$dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) {
$dropdown.tooltip('fixTitle');
} }
}); },
id: function(obj, $el) {
return $el.attr('data-ref');
},
toggleLabel: function(obj, $el) {
return $el.text().trim();
}
});
$filterInput.on('keyup', (e) => {
const keyCode = e.keyCode || e.which;
if (keyCode !== 13) return;
const text = $filterInput.val();
$fieldInput.val(text);
$('.dropdown-toggle-text', $dropdown).text(text);
$dropdownContainer.removeClass('open');
}); });
};
return CompareAutocomplete; $dropdownContainer.on('click', '.dropdown-content a', (e) => {
})(); $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) {
$dropdown.tooltip('fixTitle');
}
});
});
}
...@@ -32,7 +32,9 @@ ...@@ -32,7 +32,9 @@
doAction() { doAction() {
this.isLoading = true; this.isLoading = true;
eventHub.$emit(`${this.type}.key`, this.deployKey); eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
this.isLoading = false;
});
}, },
}, },
computed: { computed: {
...@@ -50,6 +52,9 @@ ...@@ -50,6 +52,9 @@
:disabled="isLoading" :disabled="isLoading"
@click="doAction"> @click="doAction">
{{ text }} {{ text }}
<loading-icon v-if="isLoading" /> <loading-icon
v-if="isLoading"
:inline="true"
/>
</button> </button>
</template> </template>
...@@ -47,12 +47,15 @@ ...@@ -47,12 +47,15 @@
.then(() => this.fetchKeys()) .then(() => this.fetchKeys())
.catch(() => new Flash('Error enabling deploy key')); .catch(() => new Flash('Error enabling deploy key'));
}, },
disableKey(deployKey) { disableKey(deployKey, callback) {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (confirm('You are going to remove this deploy key. Are you sure?')) { if (confirm('You are going to remove this deploy key. Are you sure?')) {
this.service.disableKey(deployKey.id) this.service.disableKey(deployKey.id)
.then(() => this.fetchKeys()) .then(() => this.fetchKeys())
.then(callback)
.catch(() => new Flash('Error removing deploy key')); .catch(() => new Flash('Error removing deploy key'));
} else {
callback();
} }
}, },
}, },
......
...@@ -16,7 +16,8 @@ import './components/diff_note_avatars'; ...@@ -16,7 +16,8 @@ import './components/diff_note_avatars';
import './components/new_issue_for_discussion'; import './components/new_issue_for_discussion';
$(() => { $(() => {
const projectPath = document.querySelector('.merge-request').dataset.projectPath; const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box');
const projectPath = projectPathHolder.dataset.projectPath;
const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn'; const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
window.gl = window.gl || {}; window.gl = window.gl || {};
......
...@@ -43,7 +43,7 @@ class ResolveServiceClass { ...@@ -43,7 +43,7 @@ class ResolveServiceClass {
discussion.resolveAllNotes(resolvedBy); discussion.resolveAllNotes(resolvedBy);
} }
gl.mrWidget.checkStatus(); if (gl.mrWidget) gl.mrWidget.checkStatus();
discussion.updateHeadline(data); discussion.updateHeadline(data);
}) })
.catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.')); .catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.'));
......
...@@ -22,8 +22,8 @@ import NewCommitForm from './new_commit_form'; ...@@ -22,8 +22,8 @@ import NewCommitForm from './new_commit_form';
import Project from './project'; import Project from './project';
import projectAvatar from './project_avatar'; import projectAvatar from './project_avatar';
/* global MergeRequest */ /* global MergeRequest */
/* global Compare */ import Compare from './compare';
/* global CompareAutocomplete */ import initCompareAutocomplete from './compare_autocomplete';
/* global ProjectFindFile */ /* global ProjectFindFile */
import ProjectNew from './project_new'; import ProjectNew from './project_new';
import projectImport from './project_import'; import projectImport from './project_import';
...@@ -525,13 +525,6 @@ import ProjectVariables from './project_variables'; ...@@ -525,13 +525,6 @@ import ProjectVariables from './project_variables';
case 'projects:settings:ci_cd:show': case 'projects:settings:ci_cd:show':
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
import(/* webpackChunkName: "ci-cd-settings" */ './projects/ci_cd_settings_bundle')
.then(ciCdSettings => ciCdSettings.default())
.catch((err) => {
Flash(s__('ProjectSettings|Problem setting up the CI/CD settings JavaScript'));
throw err;
});
case 'groups:settings:ci_cd:show': case 'groups:settings:ci_cd:show':
new ProjectVariables(); new ProjectVariables();
break; break;
...@@ -629,7 +622,7 @@ import ProjectVariables from './project_variables'; ...@@ -629,7 +622,7 @@ import ProjectVariables from './project_variables';
projectAvatar(); projectAvatar();
switch (path[1]) { switch (path[1]) {
case 'compare': case 'compare':
new CompareAutocomplete(); initCompareAutocomplete();
break; break;
case 'edit': case 'edit':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
......
...@@ -8,18 +8,7 @@ import IssuablesHelper from './helpers/issuables_helper'; ...@@ -8,18 +8,7 @@ import IssuablesHelper from './helpers/issuables_helper';
export default class Issue { export default class Issue {
constructor() { constructor() {
if ($('a.btn-close').length) { if ($('a.btn-close').length) this.initIssueBtnEventListeners();
this.taskList = new TaskList({
dataType: 'issue',
fieldName: 'description',
selector: '.detail-page-description',
onSuccess: (result) => {
document.querySelector('#task_status').innerText = result.task_status;
document.querySelector('#task_status_short').innerText = result.task_status_short;
}
});
this.initIssueBtnEventListeners();
}
Issue.$btnNewBranch = $('#new-branch'); Issue.$btnNewBranch = $('#new-branch');
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap'); Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
......
...@@ -9,6 +9,7 @@ import descriptionComponent from './description.vue'; ...@@ -9,6 +9,7 @@ import descriptionComponent from './description.vue';
import editedComponent from './edited.vue'; import editedComponent from './edited.vue';
import formComponent from './form.vue'; import formComponent from './form.vue';
import '../../lib/utils/url_utility'; import '../../lib/utils/url_utility';
import RecaptchaDialogImplementor from '../../vue_shared/mixins/recaptcha_dialog_implementor';
export default { export default {
props: { props: {
...@@ -149,6 +150,11 @@ export default { ...@@ -149,6 +150,11 @@ export default {
editedComponent, editedComponent,
formComponent, formComponent,
}, },
mixins: [
RecaptchaDialogImplementor,
],
methods: { methods: {
openForm() { openForm() {
if (!this.showForm) { if (!this.showForm) {
...@@ -164,9 +170,11 @@ export default { ...@@ -164,9 +170,11 @@ export default {
closeForm() { closeForm() {
this.showForm = false; this.showForm = false;
}, },
updateIssuable() { updateIssuable() {
this.service.updateIssuable(this.store.formState) this.service.updateIssuable(this.store.formState)
.then(res => res.json()) .then(res => res.json())
.then(data => this.checkForSpam(data))
.then((data) => { .then((data) => {
if (location.pathname !== data.web_url) { if (location.pathname !== data.web_url) {
gl.utils.visitUrl(data.web_url); gl.utils.visitUrl(data.web_url);
...@@ -179,11 +187,24 @@ export default { ...@@ -179,11 +187,24 @@ export default {
this.store.updateState(data); this.store.updateState(data);
eventHub.$emit('close.form'); eventHub.$emit('close.form');
}) })
.catch(() => { .catch((error) => {
eventHub.$emit('close.form'); if (error && error.name === 'SpamError') {
window.Flash(`Error updating ${this.issuableType}`); this.openRecaptcha();
} else {
eventHub.$emit('close.form');
window.Flash(`Error updating ${this.issuableType}`);
}
}); });
}, },
closeRecaptchaDialog() {
this.store.setFormState({
updateLoading: false,
});
this.closeRecaptcha();
},
deleteIssuable() { deleteIssuable() {
this.service.deleteIssuable() this.service.deleteIssuable()
.then(res => res.json()) .then(res => res.json())
...@@ -237,9 +258,9 @@ export default { ...@@ -237,9 +258,9 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<div v-if="canUpdate && showForm">
<form-component <form-component
v-if="canUpdate && showForm"
:form-state="formState" :form-state="formState"
:can-destroy="canDestroy" :can-destroy="canDestroy"
:issuable-templates="issuableTemplates" :issuable-templates="issuableTemplates"
...@@ -251,30 +272,37 @@ export default { ...@@ -251,30 +272,37 @@ export default {
:can-attach-file="canAttachFile" :can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete" :enable-autocomplete="enableAutocomplete"
/> />
<div v-else>
<title-component <recaptcha-dialog
:issuable-ref="issuableRef" v-show="showRecaptcha"
:can-update="canUpdate" :html="recaptchaHTML"
:title-html="state.titleHtml" @close="closeRecaptchaDialog"
:title-text="state.titleText" />
:show-inline-edit-button="showInlineEditButton" </div>
/> <div v-else>
<description-component <title-component
v-if="state.descriptionHtml" :issuable-ref="issuableRef"
:can-update="canUpdate" :can-update="canUpdate"
:description-html="state.descriptionHtml" :title-html="state.titleHtml"
:description-text="state.descriptionText" :title-text="state.titleText"
:updated-at="state.updatedAt" :show-inline-edit-button="showInlineEditButton"
:task-status="state.taskStatus" />
:issuable-type="issuableType" <description-component
:update-url="updateEndpoint" v-if="state.descriptionHtml"
/> :can-update="canUpdate"
<edited-component :description-html="state.descriptionHtml"
v-if="hasUpdated" :description-text="state.descriptionText"
:updated-at="state.updatedAt" :updated-at="state.updatedAt"
:updated-by-name="state.updatedByName" :task-status="state.taskStatus"
:updated-by-path="state.updatedByPath" :issuable-type="issuableType"
/> :update-url="updateEndpoint"
</div> />
<edited-component
v-if="hasUpdated"
:updated-at="state.updatedAt"
:updated-by-name="state.updatedByName"
:updated-by-path="state.updatedByPath"
/>
</div> </div>
</div>
</template> </template>
<script> <script>
import animateMixin from '../mixins/animate'; import animateMixin from '../mixins/animate';
import TaskList from '../../task_list'; import TaskList from '../../task_list';
import RecaptchaDialogImplementor from '../../vue_shared/mixins/recaptcha_dialog_implementor';
export default { export default {
mixins: [animateMixin], mixins: [
animateMixin,
RecaptchaDialogImplementor,
],
props: { props: {
canUpdate: { canUpdate: {
type: Boolean, type: Boolean,
...@@ -51,6 +56,7 @@ ...@@ -51,6 +56,7 @@
this.updateTaskStatusText(); this.updateTaskStatusText();
}, },
}, },
methods: { methods: {
renderGFM() { renderGFM() {
$(this.$refs['gfm-content']).renderGFM(); $(this.$refs['gfm-content']).renderGFM();
...@@ -61,9 +67,19 @@ ...@@ -61,9 +67,19 @@
dataType: this.issuableType, dataType: this.issuableType,
fieldName: 'description', fieldName: 'description',
selector: '.detail-page-description', selector: '.detail-page-description',
onSuccess: this.taskListUpdateSuccess.bind(this),
}); });
} }
}, },
taskListUpdateSuccess(data) {
try {
this.checkForSpam(data);
} catch (error) {
if (error && error.name === 'SpamError') this.openRecaptcha();
}
},
updateTaskStatusText() { updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/); const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta'); const $issuableHeader = $('.issuable-meta');
...@@ -109,5 +125,11 @@ ...@@ -109,5 +125,11 @@
:data-update-url="updateUrl" :data-update-url="updateUrl"
> >
</textarea> </textarea>
<recaptcha-dialog
v-show="showRecaptcha"
:html="recaptchaHTML"
@close="closeRecaptcha"
/>
</div> </div>
</template> </template>
...@@ -5,7 +5,7 @@ import '../vue_shared/vue_resource_interceptor'; ...@@ -5,7 +5,7 @@ import '../vue_shared/vue_resource_interceptor';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const initialDataEl = document.getElementById('js-issuable-app-initial-data'); const initialDataEl = document.getElementById('js-issuable-app-initial-data');
const initialData = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"')); const props = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"'));
$('.issuable-edit').on('click', (e) => { $('.issuable-edit').on('click', (e) => {
e.preventDefault(); e.preventDefault();
...@@ -18,32 +18,9 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -18,32 +18,9 @@ document.addEventListener('DOMContentLoaded', () => {
components: { components: {
issuableApp, issuableApp,
}, },
data() {
return {
...initialData,
};
},
render(createElement) { render(createElement) {
return createElement('issuable-app', { return createElement('issuable-app', {
props: { props,
canUpdate: this.canUpdate,
canDestroy: this.canDestroy,
endpoint: this.endpoint,
issuableRef: this.issuableRef,
initialTitleHtml: this.initialTitleHtml,
initialTitleText: this.initialTitleText,
initialDescriptionHtml: this.initialDescriptionHtml,
initialDescriptionText: this.initialDescriptionText,
issuableTemplates: this.issuableTemplates,
markdownPreviewPath: this.markdownPreviewPath,
markdownDocsPath: this.markdownDocsPath,
projectPath: this.projectPath,
projectNamespace: this.projectNamespace,
updatedAt: this.updatedAt,
updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath,
initialTaskStatus: this.initialTaskStatus,
},
}); });
}, },
}); });
......
...@@ -9,7 +9,7 @@ export default class Job { ...@@ -9,7 +9,7 @@ export default class Job {
this.state = null; this.state = null;
this.options = options || $('.js-build-options').data(); this.options = options || $('.js-build-options').data();
this.pageUrl = this.options.pageUrl; this.pagePath = this.options.pagePath;
this.buildStatus = this.options.buildStatus; this.buildStatus = this.options.buildStatus;
this.state = this.options.logState; this.state = this.options.logState;
this.buildStage = this.options.buildStage; this.buildStage = this.options.buildStage;
...@@ -167,11 +167,11 @@ export default class Job { ...@@ -167,11 +167,11 @@ export default class Job {
getBuildTrace() { getBuildTrace() {
return $.ajax({ return $.ajax({
url: `${this.pageUrl}/trace.json`, url: `${this.pagePath}/trace.json`,
data: { state: this.state }, data: { state: this.state },
}) })
.done((log) => { .done((log) => {
setCiStatusFavicon(`${this.pageUrl}/status.json`); setCiStatusFavicon(`${this.pagePath}/status.json`);
if (log.state) { if (log.state) {
this.state = log.state; this.state = log.state;
...@@ -209,7 +209,7 @@ export default class Job { ...@@ -209,7 +209,7 @@ export default class Job {
} }
if (log.status !== this.buildStatus) { if (log.status !== this.buildStatus) {
gl.utils.visitUrl(this.pageUrl); gl.utils.visitUrl(this.pagePath);
} }
}) })
.fail(() => { .fail(() => {
......
...@@ -35,8 +35,6 @@ window.dateFormat = dateFormat; ...@@ -35,8 +35,6 @@ window.dateFormat = dateFormat;
w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) { w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) {
$timeagoEls.each((i, el) => { $timeagoEls.each((i, el) => {
el.setAttribute('title', el.getAttribute('title'));
if (setTimeago) { if (setTimeago) {
// Recreate with custom template // Recreate with custom template
$(el).tooltip({ $(el).tooltip({
......
...@@ -40,9 +40,6 @@ import './admin'; ...@@ -40,9 +40,6 @@ import './admin';
import './aside'; import './aside';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import bp from './breakpoints'; import bp from './breakpoints';
import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal'; import './confirm_danger_modal';
import Flash, { removeFlashClickListener } from './flash'; import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown'; import './gl_dropdown';
......
...@@ -21,6 +21,8 @@ ...@@ -21,6 +21,8 @@
hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics), hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath, documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath, settingsPath: metricsData.settingsPath,
tagsPath: metricsData.tagsPath,
projectPath: metricsData.projectPath,
metricsEndpoint: metricsData.additionalMetrics, metricsEndpoint: metricsData.additionalMetrics,
deploymentEndpoint: metricsData.deploymentEndpoint, deploymentEndpoint: metricsData.deploymentEndpoint,
emptyGettingStartedSvgPath: metricsData.emptyGettingStartedSvgPath, emptyGettingStartedSvgPath: metricsData.emptyGettingStartedSvgPath,
...@@ -112,6 +114,8 @@ ...@@ -112,6 +114,8 @@
:hover-data="hoverData" :hover-data="hoverData"
:update-aspect-ratio="updateAspectRatio" :update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData" :deployment-data="store.deploymentData"
:project-path="projectPath"
:tags-path="tagsPath"
/> />
</graph-group> </graph-group>
</div> </div>
......
...@@ -30,6 +30,14 @@ ...@@ -30,6 +30,14 @@
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
projectPath: {
type: String,
required: true,
},
tagsPath: {
type: String,
required: true,
},
}, },
mixins: [MonitoringMixin], mixins: [MonitoringMixin],
...@@ -251,6 +259,14 @@ ...@@ -251,6 +259,14 @@
:line-color="path.lineColor" :line-color="path.lineColor"
:area-color="path.areaColor" :area-color="path.areaColor"
/> />
<rect
class="prometheus-graph-overlay"
:width="(graphWidth - 70)"
:height="(graphHeight - 100)"
transform="translate(-5, 20)"
ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)">
</rect>
<graph-deployment <graph-deployment
:show-deploy-info="showDeployInfo" :show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData" :deployment-data="reducedDeploymentData"
...@@ -267,14 +283,6 @@ ...@@ -267,14 +283,6 @@
:graph-height-offset="graphHeightOffset" :graph-height-offset="graphHeightOffset"
:show-flag-content="showFlagContent" :show-flag-content="showFlagContent"
/> />
<rect
class="prometheus-graph-overlay"
:width="(graphWidth - 70)"
:height="(graphHeight - 100)"
transform="translate(-5, 20)"
ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)">
</rect>
</svg> </svg>
</svg> </svg>
</div> </div>
......
<script> <script>
import { dateFormat, timeFormat } from '../../utils/date_time_formatters'; import { dateFormatWithName, timeFormat } from '../../utils/date_time_formatters';
import Icon from '../../../vue_shared/components/icon.vue';
export default { export default {
props: { props: {
...@@ -25,6 +26,10 @@ ...@@ -25,6 +26,10 @@
}, },
}, },
components: {
Icon,
},
computed: { computed: {
calculatedHeight() { calculatedHeight() {
return this.graphHeight - this.graphHeightOffset; return this.graphHeight - this.graphHeightOffset;
...@@ -33,7 +38,7 @@ ...@@ -33,7 +38,7 @@
methods: { methods: {
refText(d) { refText(d) {
return d.tag ? d.ref : d.sha.slice(0, 6); return d.tag ? d.ref : d.sha.slice(0, 8);
}, },
formatTime(deploymentTime) { formatTime(deploymentTime) {
...@@ -41,7 +46,7 @@ ...@@ -41,7 +46,7 @@
}, },
formatDate(deploymentTime) { formatDate(deploymentTime) {
return dateFormat(deploymentTime); return dateFormatWithName(deploymentTime);
}, },
nameDeploymentClass(deployment) { nameDeploymentClass(deployment) {
...@@ -54,11 +59,19 @@ ...@@ -54,11 +59,19 @@
positionFlag(deployment) { positionFlag(deployment) {
let xPosition = 3; let xPosition = 3;
if (deployment.xPos > (this.graphWidth - 200)) { if (deployment.xPos > (this.graphWidth - 225)) {
xPosition = -97; xPosition = -142;
} }
return xPosition; return xPosition;
}, },
svgContainerHeight(tag) {
let svgHeight = 80;
if (!tag) {
svgHeight -= 20;
}
return svgHeight;
},
}, },
}; };
</script> </script>
...@@ -91,35 +104,75 @@ ...@@ -91,35 +104,75 @@
class="js-deploy-info-box" class="js-deploy-info-box"
:x="positionFlag(deployment)" :x="positionFlag(deployment)"
y="0" y="0"
width="92" width="134"
height="60"> :height="svgContainerHeight(deployment.tag)">
<rect <rect
class="rect-text-metric deploy-info-rect rect-metric" class="rect-text-metric deploy-info-rect rect-metric"
x="1" x="1"
y="1" y="1"
rx="2" rx="2"
width="90" width="132"
height="58"> :height="svgContainerHeight(deployment.tag) - 2">
</rect> </rect>
<g
transform="translate(5, 2)">
<text
class="deploy-info-text text-metric-bold">
{{refText(deployment)}}
</text>
</g>
<text
class="deploy-info-text"
y="18"
transform="translate(5, 2)">
{{formatDate(deployment.time)}}
</text>
<text <text
class="deploy-info-text text-metric-bold" class="deploy-info-text text-metric-bold"
y="38"
transform="translate(5, 2)"> transform="translate(5, 2)">
{{formatTime(deployment.time)}} Deployed
</text> </text>
<!--The date info-->
<g transform="translate(5, 20)">
<text class="deploy-info-text">
{{formatDate(deployment.time)}}
</text>
<text
class="deploy-info-text text-metric-bold"
x="62">
{{formatTime(deployment.time)}}
</text>
</g>
<line
class="divider-line"
x1="0"
y1="38"
x2="132"
:y2="38"
stroke="#000">
</line>
<!--Commit information-->
<g transform="translate(5, 40)">
<icon
name="commit"
:width="12"
:height="12"
:y="3">
</icon>
<a :xlink:href="deployment.commitUrl">
<text
class="deploy-info-text deploy-info-text-link"
transform="translate(20, 2)">
{{refText(deployment)}}
</text>
</a>
</g>
<!--Tag information-->
<g
transform="translate(5, 55)"
v-if="deployment.tag">
<icon
name="label"
:width="12"
:height="12"
:y="5">
</icon>
<a :xlink:href="deployment.tagUrl">
<text
class="deploy-info-text deploy-info-text-link"
transform="translate(20, 2)"
y="2">
{{deployment.tag}}
</text>
</a>
</g>
</svg> </svg>
</g> </g>
<svg <svg
......
...@@ -33,7 +33,9 @@ const mixins = { ...@@ -33,7 +33,9 @@ const mixins = {
id: deployment.id, id: deployment.id,
time, time,
sha: deployment.sha, sha: deployment.sha,
commitUrl: `${this.projectPath}/commit/${deployment.sha}`,
tag: deployment.tag, tag: deployment.tag,
tagUrl: `${this.tagsPath}/${deployment.tag}`,
ref: deployment.ref.name, ref: deployment.ref.name,
xPos, xPos,
showDeploymentFlag: false, showDeploymentFlag: false,
......
import d3 from 'd3'; import d3 from 'd3';
export const dateFormat = d3.time.format('%b %-d, %Y'); export const dateFormat = d3.time.format('%b %-d, %Y');
export const dateFormatWithName = d3.time.format('%a, %b %-d');
export const timeFormat = d3.time.format('%-I:%M%p'); export const timeFormat = d3.time.format('%-I:%M%p');
export const bisectDate = d3.bisector(d => d.time).left; export const bisectDate = d3.bisector(d => d.time).left;
......
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
import Flash from '../../flash'; import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue'; import noteHeader from './note_header.vue';
...@@ -85,7 +86,7 @@ ...@@ -85,7 +86,7 @@
}; };
this.isRequesting = true; this.isRequesting = true;
this.oldContent = this.note.note_html; this.oldContent = this.note.note_html;
this.note.note_html = noteText; this.note.note_html = escape(noteText);
this.updateNote(data) this.updateNote(data)
.then(() => { .then(() => {
......
...@@ -8,10 +8,18 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -8,10 +8,18 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
}, },
data() { data() {
const notesDataset = document.getElementById('js-vue-notes').dataset; const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
const currentUserData = parsedUserData ? {
id: parsedUserData.id,
name: parsedUserData.name,
username: parsedUserData.username,
avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
path: parsedUserData.path,
} : {};
return { return {
noteableData: JSON.parse(notesDataset.noteableData), noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData), currentUserData,
notesData: { notesData: {
lastFetchedAt: notesDataset.lastFetchedAt, lastFetchedAt: notesDataset.lastFetchedAt,
discussionsPath: notesDataset.discussionsPath, discussionsPath: notesDataset.discussionsPath,
......
function updateAutoDevopsRadios(radioWrappers) {
radioWrappers.forEach((radioWrapper) => {
const radio = radioWrapper.querySelector('.js-auto-devops-enable-radio');
const runPipelineCheckboxWrapper = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox-wrapper');
const runPipelineCheckbox = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox');
if (runPipelineCheckbox) {
runPipelineCheckbox.checked = radio.checked;
runPipelineCheckboxWrapper.classList.toggle('hide', !radio.checked);
}
});
}
export default function initCiCdSettings() {
const radioWrappers = document.querySelectorAll('.js-auto-devops-enable-radio-wrapper');
radioWrappers.forEach(radioWrapper =>
radioWrapper.addEventListener('change', () => updateAutoDevopsRadios(radioWrappers)),
);
}
import Project from '~/project';
import SmartInterval from '~/smart_interval'; import SmartInterval from '~/smart_interval';
import Flash from '../flash'; import Flash from '../flash';
import { import {
...@@ -140,6 +141,7 @@ export default { ...@@ -140,6 +141,7 @@ export default {
const el = document.createElement('div'); const el = document.createElement('div');
el.innerHTML = res.body; el.innerHTML = res.body;
document.body.appendChild(el); document.body.appendChild(el);
Project.initRefSwitcher();
} }
}) })
.catch(() => { .catch(() => {
......
...@@ -36,6 +36,30 @@ ...@@ -36,6 +36,30 @@
required: false, required: false,
default: '', default: '',
}, },
width: {
type: Number,
required: false,
default: null,
},
height: {
type: Number,
required: false,
default: null,
},
y: {
type: Number,
required: false,
default: null,
},
x: {
type: Number,
required: false,
default: null,
},
}, },
computed: { computed: {
...@@ -51,7 +75,11 @@ ...@@ -51,7 +75,11 @@
<template> <template>
<svg <svg
:class="[iconSizeClass, cssClasses]"> :class="[iconSizeClass, cssClasses]"
:width="width"
:height="height"
:x="x"
:y="y">
<use <use
v-bind="{'xlink:href':spriteHref}"/> v-bind="{'xlink:href':spriteHref}"/>
</svg> </svg>
......
...@@ -38,7 +38,8 @@ export default { ...@@ -38,7 +38,8 @@ export default {
}, },
primaryButtonLabel: { primaryButtonLabel: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
submitDisabled: { submitDisabled: {
type: Boolean, type: Boolean,
...@@ -113,8 +114,9 @@ export default { ...@@ -113,8 +114,9 @@ export default {
{{ closeButtonLabel }} {{ closeButtonLabel }}
</button> </button>
<button <button
v-if="primaryButtonLabel"
type="button" type="button"
class="btn pull-right" class="btn pull-right js-primary-button"
:disabled="submitDisabled" :disabled="submitDisabled"
:class="btnKindClass" :class="btnKindClass"
@click="emitSubmit(true)"> @click="emitSubmit(true)">
......
<script>
import PopupDialog from './popup_dialog.vue';
export default {
name: 'recaptcha-dialog',
props: {
html: {
type: String,
required: false,
default: '',
},
},
data() {
return {
script: {},
scriptSrc: 'https://www.google.com/recaptcha/api.js',
};
},
components: {
PopupDialog,
},
methods: {
appendRecaptchaScript() {
this.removeRecaptchaScript();
const script = document.createElement('script');
script.src = this.scriptSrc;
script.classList.add('js-recaptcha-script');
script.async = true;
script.defer = true;
this.script = script;
document.body.appendChild(script);
},
removeRecaptchaScript() {
if (this.script instanceof Element) this.script.remove();
},
close() {
this.removeRecaptchaScript();
this.$emit('close');
},
submit() {
this.$el.querySelector('form').submit();
},
},
watch: {
html() {
this.appendRecaptchaScript();
},
},
mounted() {
window.recaptchaDialogCallback = this.submit.bind(this);
},
};
</script>
<template>
<popup-dialog
kind="warning"
class="recaptcha-dialog js-recaptcha-dialog"
:hide-footer="true"
:title="__('Please solve the reCAPTCHA')"
@toggle="close"
>
<div slot="body">
<p>
{{__('We want to be sure it is you, please confirm you are not a robot.')}}
</p>
<div
ref="recaptcha"
v-html="html"
></div>
</div>
</popup-dialog>
</template>
import RecaptchaDialog from '../components/recaptcha_dialog.vue';
export default {
data() {
return {
showRecaptcha: false,
recaptchaHTML: '',
};
},
components: {
RecaptchaDialog,
},
methods: {
openRecaptcha() {
this.showRecaptcha = true;
},
closeRecaptcha() {
this.showRecaptcha = false;
},
checkForSpam(data) {
if (!data.recaptcha_html) return data;
this.recaptchaHTML = data.recaptcha_html;
const spamError = new Error(data.error_message);
spamError.name = 'SpamError';
spamError.message = 'SpamError';
throw spamError;
},
},
};
...@@ -48,3 +48,10 @@ body.modal-open { ...@@ -48,3 +48,10 @@ body.modal-open {
display: block; display: block;
} }
.recaptcha-dialog .recaptcha-form {
display: inline-block;
.recaptcha {
margin: 0;
}
}
...@@ -201,8 +201,9 @@ ...@@ -201,8 +201,9 @@
stroke-width: 1; stroke-width: 1;
} }
.deploy-info-text { .divider-line {
dominant-baseline: text-before-edge; stroke-width: 1;
stroke: $gray-darkest;
} }
.prometheus-state { .prometheus-state {
...@@ -312,6 +313,20 @@ ...@@ -312,6 +313,20 @@
stroke: $gray-darker; stroke: $gray-darker;
} }
.deploy-info-text {
dominant-baseline: text-before-edge;
font-size: 12px;
}
.deploy-info-text-link {
font-family: $monospace_font;
fill: $gl-link-color;
&:hover {
fill: $gl-link-hover-color;
}
}
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
.label-axis-text, .label-axis-text,
.text-metric-usage, .text-metric-usage,
......
...@@ -5,7 +5,7 @@ class Admin::HealthCheckController < Admin::ApplicationController ...@@ -5,7 +5,7 @@ class Admin::HealthCheckController < Admin::ApplicationController
end end
def reset_storage_health def reset_storage_health
Gitlab::Git::Storage::CircuitBreaker.reset_all! Gitlab::Git::Storage::FailureInfo.reset_all!
redirect_to admin_health_check_path, redirect_to admin_health_check_path,
notice: _('Git storage health information has been reset') notice: _('Git storage health information has been reset')
end end
......
...@@ -21,11 +21,11 @@ module IssuableActions ...@@ -21,11 +21,11 @@ module IssuableActions
respond_to do |format| respond_to do |format|
format.html do format.html do
recaptcha_check_with_fallback { render :edit } recaptcha_check_if_spammable { render :edit }
end end
format.json do format.json do
render_entity_json recaptcha_check_if_spammable(false) { render_entity_json }
end end
end end
...@@ -80,6 +80,12 @@ module IssuableActions ...@@ -80,6 +80,12 @@ module IssuableActions
private private
def recaptcha_check_if_spammable(should_redirect = true, &block)
return yield unless @issuable.is_a? Spammable
recaptcha_check_with_fallback(should_redirect, &block)
end
def render_conflict_response def render_conflict_response
respond_to do |format| respond_to do |format|
format.html do format.html do
......
...@@ -23,8 +23,8 @@ module SpammableActions ...@@ -23,8 +23,8 @@ module SpammableActions
@spam_config_loaded = Gitlab::Recaptcha.load_configurations! @spam_config_loaded = Gitlab::Recaptcha.load_configurations!
end end
def recaptcha_check_with_fallback(&fallback) def recaptcha_check_with_fallback(should_redirect = true, &fallback)
if spammable.valid? if should_redirect && spammable.valid?
redirect_to spammable_path redirect_to spammable_path
elsif render_recaptcha? elsif render_recaptcha?
ensure_spam_config_loaded! ensure_spam_config_loaded!
...@@ -33,7 +33,18 @@ module SpammableActions ...@@ -33,7 +33,18 @@ module SpammableActions
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end end
render :verify respond_to do |format|
format.html do
render :verify
end
format.json do
locals = { spammable: spammable, script: false, has_submit: false }
recaptcha_html = render_to_string(partial: 'shared/recaptcha_form', formats: :html, locals: locals)
render json: { recaptcha_html: recaptcha_html }
end
end
else else
yield yield
end end
......
module UploadsActions module UploadsActions
include Gitlab::Utils::StrongMemoize
def create def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute link_to_file = UploadService.new(model, params[:file], uploader_class).execute
...@@ -24,4 +26,25 @@ module UploadsActions ...@@ -24,4 +26,25 @@ module UploadsActions
send_file uploader.file.path, disposition: disposition send_file uploader.file.path, disposition: disposition
end end
private
def uploader
strong_memoize(:uploader) do
return if show_model.nil?
file_uploader = FileUploader.new(show_model, params[:secret])
file_uploader.retrieve_from_store!(params[:filename])
file_uploader
end
end
def image_or_video?
uploader && uploader.exists? && uploader.image_or_video?
end
def uploader_class
FileUploader
end
end end
class Groups::UploadsController < Groups::ApplicationController
include UploadsActions
skip_before_action :group, if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create]
private
def show_model
strong_memoize(:show_model) do
group_id = params[:group_id]
Group.find_by_full_path(group_id)
end
end
def authorize_upload_file!
render_404 unless can?(current_user, :upload_file, group)
end
def uploader
strong_memoize(:uploader) do
file_uploader = uploader_class.new(show_model, params[:secret])
file_uploader.retrieve_from_store!(params[:filename])
file_uploader
end
end
def uploader_class
NamespaceFileUploader
end
alias_method :model, :group
end
class HealthController < ActionController::Base class HealthController < ActionController::Base
protect_from_forgery with: :exception protect_from_forgery with: :exception, except: :storage_check
include RequiresWhitelistedMonitoringClient include RequiresWhitelistedMonitoringClient
CHECKS = [ CHECKS = [
...@@ -23,6 +23,15 @@ class HealthController < ActionController::Base ...@@ -23,6 +23,15 @@ class HealthController < ActionController::Base
render_check_results(results) render_check_results(results)
end end
def storage_check
results = Gitlab::Git::Storage::Checker.check_all
render json: {
check_interval: Gitlab::CurrentSettings.current_application_settings.circuitbreaker_check_interval,
results: results
}
end
private private
def render_check_results(results) def render_check_results(results)
......
...@@ -8,7 +8,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController ...@@ -8,7 +8,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
@personal_access_token = finder.build(personal_access_token_params) @personal_access_token = finder.build(personal_access_token_params)
if @personal_access_token.save if @personal_access_token.save
flash[:personal_access_token] = @personal_access_token.token PersonalAccessToken.redis_store!(current_user.id, @personal_access_token.token)
redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created." redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
else else
set_index_vars set_index_vars
...@@ -43,5 +43,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController ...@@ -43,5 +43,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
@inactive_personal_access_tokens = finder(state: 'inactive').execute @inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at) @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
@new_personal_access_token = PersonalAccessToken.redis_getdel(current_user.id)
end end
end end
...@@ -134,6 +134,23 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -134,6 +134,23 @@ class Projects::CommitController < Projects::ApplicationController
@grouped_diff_discussions = commit.grouped_diff_discussions @grouped_diff_discussions = commit.grouped_diff_discussions
@discussions = commit.discussions @discussions = commit.discussions
if merge_request_iid = params[:merge_request_iid]
@merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: merge_request_iid)
if @merge_request
@new_diff_note_attrs.merge!(
noteable_type: 'MergeRequest',
noteable_id: @merge_request.id
)
merge_request_commit_notes = @merge_request.notes.where(commit_id: @commit.id).inc_relations_for_view
merge_request_commit_diff_discussions = merge_request_commit_notes.grouped_diff_discussions(@commit.diff_refs)
@grouped_diff_discussions.merge!(merge_request_commit_diff_discussions) do |line_code, left, right|
left + right
end
end
end
@notes = (@grouped_diff_discussions.values.flatten + @discussions).flat_map(&:notes) @notes = (@grouped_diff_discussions.values.flatten + @discussions).flat_map(&:notes)
@notes = prepare_notes_for_rendering(@notes, @commit) @notes = prepare_notes_for_rendering(@notes, @commit)
end end
......
...@@ -28,7 +28,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont ...@@ -28,7 +28,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:task_num, :task_num,
:title, :title,
:discussion_locked, :discussion_locked,
label_ids: [] label_ids: []
] ]
end end
......
...@@ -4,6 +4,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic ...@@ -4,6 +4,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
include RendersNotes include RendersNotes
before_action :apply_diff_view_cookie! before_action :apply_diff_view_cookie!
before_action :commit
before_action :define_diff_vars before_action :define_diff_vars
before_action :define_diff_comment_vars before_action :define_diff_comment_vars
...@@ -20,18 +21,33 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic ...@@ -20,18 +21,33 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
private private
def define_diff_vars def define_diff_vars
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc
@compare = commit || find_merge_request_diff_compare
return render_404 unless @compare
@diffs = @compare.diffs(diff_options)
end
def commit
return nil unless commit_id = params[:commit_id].presence
return nil unless @merge_request.all_commits.exists?(sha: commit_id)
@commit ||= @project.commit(commit_id)
end
def find_merge_request_diff_compare
@merge_request_diff = @merge_request_diff =
if params[:diff_id] if diff_id = params[:diff_id].presence
@merge_request.merge_request_diffs.viewable.find(params[:diff_id]) @merge_request.merge_request_diffs.viewable.find_by(id: diff_id)
else else
@merge_request.merge_request_diff @merge_request.merge_request_diff
end end
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc return unless @merge_request_diff
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present? if @start_sha = params[:start_sha].presence
@start_sha = params[:start_sha]
@start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha } @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
unless @start_version unless @start_version
...@@ -40,20 +56,18 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic ...@@ -40,20 +56,18 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
end end
end end
@compare = if @start_sha
if @start_sha @merge_request_diff.compare_with(@start_sha)
@merge_request_diff.compare_with(@start_sha) else
else @merge_request_diff
@merge_request_diff end
end
@diffs = @compare.diffs(diff_options)
end end
def define_diff_comment_vars def define_diff_comment_vars
@new_diff_note_attrs = { @new_diff_note_attrs = {
noteable_type: 'MergeRequest', noteable_type: 'MergeRequest',
noteable_id: @merge_request.id noteable_id: @merge_request.id,
commit_id: @commit&.id
} }
@diff_notes_disabled = false @diff_notes_disabled = false
......
...@@ -7,11 +7,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -7,11 +7,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include IssuableCollections include IssuableCollections
skip_before_action :merge_request, only: [:index, :bulk_update] skip_before_action :merge_request, only: [:index, :bulk_update]
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort] before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :set_issuables_index, only: [:index] before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues] before_action :authenticate_user!, only: [:assign_related_issues]
def index def index
......
...@@ -29,7 +29,6 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -29,7 +29,6 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
:runners_token, :builds_enabled, :build_allow_git_fetch, :runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_in_minutes, :build_coverage_regex, :public_builds, :build_timeout_in_minutes, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path, :auto_cancel_pending_pipelines, :ci_config_path,
:run_auto_devops_pipeline_implicit, :run_auto_devops_pipeline_explicit,
auto_devops_attributes: [:id, :domain, :enabled] auto_devops_attributes: [:id, :domain, :enabled]
) )
end end
......
...@@ -8,31 +8,13 @@ class Projects::UploadsController < Projects::ApplicationController ...@@ -8,31 +8,13 @@ class Projects::UploadsController < Projects::ApplicationController
private private
def uploader def show_model
return @uploader if defined?(@uploader) strong_memoize(:show_model) do
namespace = params[:namespace_id]
id = params[:project_id]
namespace = params[:namespace_id] Project.find_by_full_path("#{namespace}/#{id}")
id = params[:project_id]
file_project = Project.find_by_full_path("#{namespace}/#{id}")
if file_project.nil?
@uploader = nil
return
end end
@uploader = FileUploader.new(file_project, params[:secret])
@uploader.retrieve_from_store!(params[:filename])
@uploader
end
def image_or_video?
uploader && uploader.exists? && uploader.image_or_video?
end
def uploader_class
FileUploader
end end
alias_method :model, :project alias_method :model, :project
......
...@@ -272,7 +272,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -272,7 +272,7 @@ class ProjectsController < Projects::ApplicationController
render 'projects/empty' if @project.empty_repo? render 'projects/empty' if @project.empty_repo?
else else
if @project.wiki_enabled? if can?(current_user, :read_wiki, @project)
@project_wiki = @project.wiki @project_wiki = @project.wiki
@wiki_home = @project_wiki.find_page('home', params[:version_id]) @wiki_home = @project_wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user) elsif @project.feature_available?(:issues, current_user)
......
...@@ -124,17 +124,6 @@ module ApplicationSettingsHelper ...@@ -124,17 +124,6 @@ module ApplicationSettingsHelper
_('The number of attempts GitLab will make to access a storage.') _('The number of attempts GitLab will make to access a storage.')
end end
def circuitbreaker_backoff_threshold_help_text
_("The number of failures after which GitLab will start temporarily "\
"disabling access to a storage shard on a host")
end
def circuitbreaker_failure_wait_time_help_text
_("When access to a storage fails. GitLab will prevent access to the "\
"storage for the time specified here. This allows the filesystem to "\
"recover. Repositories on failing shards are temporarly unavailable")
end
def circuitbreaker_failure_reset_time_help_text def circuitbreaker_failure_reset_time_help_text
_("The time in seconds GitLab will keep failure information. When no "\ _("The time in seconds GitLab will keep failure information. When no "\
"failures occur during this time, information about the mount is reset.") "failures occur during this time, information about the mount is reset.")
...@@ -145,6 +134,11 @@ module ApplicationSettingsHelper ...@@ -145,6 +134,11 @@ module ApplicationSettingsHelper
"timeout error will be raised.") "timeout error will be raised.")
end end
def circuitbreaker_check_interval_help_text
_("The time in seconds between storage checks. When a previous check did "\
"complete yet, GitLab will skip a check.")
end
def visible_attributes def visible_attributes
[ [
:admin_notification_email, :admin_notification_email,
...@@ -154,10 +148,9 @@ module ApplicationSettingsHelper ...@@ -154,10 +148,9 @@ module ApplicationSettingsHelper
:akismet_enabled, :akismet_enabled,
:auto_devops_enabled, :auto_devops_enabled,
:circuitbreaker_access_retries, :circuitbreaker_access_retries,
:circuitbreaker_backoff_threshold, :circuitbreaker_check_interval,
:circuitbreaker_failure_count_threshold, :circuitbreaker_failure_count_threshold,
:circuitbreaker_failure_reset_time, :circuitbreaker_failure_reset_time,
:circuitbreaker_failure_wait_time,
:circuitbreaker_storage_timeout, :circuitbreaker_storage_timeout,
:clientside_sentry_dsn, :clientside_sentry_dsn,
:clientside_sentry_enabled, :clientside_sentry_enabled,
......
...@@ -8,22 +8,6 @@ module AutoDevopsHelper ...@@ -8,22 +8,6 @@ module AutoDevopsHelper
!project.ci_service !project.ci_service
end end
def show_run_auto_devops_pipeline_checkbox_for_instance_setting?(project)
return false if project.repository.gitlab_ci_yml
if project&.auto_devops&.enabled.present?
!project.auto_devops.enabled && current_application_settings.auto_devops_enabled?
else
current_application_settings.auto_devops_enabled?
end
end
def show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(project)
return false if project.repository.gitlab_ci_yml
!project.auto_devops_enabled?
end
def auto_devops_warning_message(project) def auto_devops_warning_message(project)
missing_domain = !project.auto_devops&.has_domain? missing_domain = !project.auto_devops&.has_domain?
missing_service = !project.deployment_platform&.active? missing_service = !project.deployment_platform&.active?
......
...@@ -20,8 +20,7 @@ module BuildsHelper ...@@ -20,8 +20,7 @@ module BuildsHelper
def javascript_build_options def javascript_build_options
{ {
page_url: project_job_url(@project, @build), page_path: project_job_path(@project, @build),
build_url: project_job_url(@project, @build, :json),
build_status: @build.status, build_status: @build.status,
build_stage: @build.stage, build_stage: @build.stage,
log_state: '' log_state: ''
......
...@@ -228,4 +228,12 @@ module CommitsHelper ...@@ -228,4 +228,12 @@ module CommitsHelper
[commits, 0] [commits, 0]
end end
end end
def commit_path(project, commit, merge_request: nil)
if merge_request&.persisted?
diffs_project_merge_request_path(project, merge_request, commit_id: commit.id)
else
project_commit_path(project, commit)
end
end
end end
...@@ -86,6 +86,8 @@ module MarkupHelper ...@@ -86,6 +86,8 @@ module MarkupHelper
return '' unless text.present? return '' unless text.present?
context[:project] ||= @project context[:project] ||= @project
context[:group] ||= @group
html = markdown_unsafe(text, context) html = markdown_unsafe(text, context)
prepare_for_rendering(html, context) prepare_for_rendering(html, context)
end end
......
...@@ -101,6 +101,30 @@ module MergeRequestsHelper ...@@ -101,6 +101,30 @@ module MergeRequestsHelper
}.merge(merge_params_ee(merge_request)) }.merge(merge_params_ee(merge_request))
end end
def tab_link_for(merge_request, tab, options = {}, &block)
data_attrs = {
action: tab.to_s,
target: "##{tab}",
toggle: options.fetch(:force_link, false) ? '' : 'tab'
}
url = case tab
when :show
data_attrs[:target] = '#notes'
method(:project_merge_request_path)
when :commits
method(:commits_project_merge_request_path)
when :pipelines
method(:pipelines_project_merge_request_path)
when :diffs
method(:diffs_project_merge_request_path)
else
raise "Cannot create tab #{tab}."
end
link_to(url[merge_request.project, merge_request], data: data_attrs, &block)
end
def merge_params_ee(merge_request) def merge_params_ee(merge_request)
{} {}
end end
......
...@@ -58,7 +58,7 @@ module PreferencesHelper ...@@ -58,7 +58,7 @@ module PreferencesHelper
user_view user_view
elsif user_view == "activity" elsif user_view == "activity"
"activity" "activity"
elsif @project.wiki_enabled? elsif can?(current_user, :read_wiki, @project)
"wiki" "wiki"
elsif @project.feature_available?(:issues, current_user) elsif @project.feature_available?(:issues, current_user)
"projects/issues/issues" "projects/issues/issues"
......
...@@ -18,16 +18,12 @@ module StorageHealthHelper ...@@ -18,16 +18,12 @@ module StorageHealthHelper
current_failures = circuit_breaker.failure_count current_failures = circuit_breaker.failure_count
translation_params = { number_of_failures: current_failures, translation_params = { number_of_failures: current_failures,
maximum_failures: maximum_failures, maximum_failures: maximum_failures }
number_of_seconds: circuit_breaker.failure_wait_time }
if circuit_breaker.circuit_broken? if circuit_breaker.circuit_broken?
s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\ s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\
"retry automatically. Reset storage information when the problem is "\ "retry automatically. Reset storage information when the problem is "\
"resolved.") % translation_params "resolved.") % translation_params
elsif circuit_breaker.backing_off?
_("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\
"block access for %{number_of_seconds} seconds.") % translation_params
else else
_("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\ _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\
"allow access on the next attempt.") % translation_params "allow access on the next attempt.") % translation_params
......
...@@ -153,11 +153,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -153,11 +153,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true, presence: true,
numericality: { greater_than_or_equal_to: 0 } numericality: { greater_than_or_equal_to: 0 }
validates :circuitbreaker_backoff_threshold, validates :circuitbreaker_failure_count_threshold,
:circuitbreaker_failure_count_threshold,
:circuitbreaker_failure_wait_time,
:circuitbreaker_failure_reset_time, :circuitbreaker_failure_reset_time,
:circuitbreaker_storage_timeout, :circuitbreaker_storage_timeout,
:circuitbreaker_check_interval,
presence: true, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
...@@ -165,13 +164,6 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -165,13 +164,6 @@ class ApplicationSetting < ActiveRecord::Base
presence: true, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 1 } numericality: { only_integer: true, greater_than_or_equal_to: 1 }
validates_each :circuitbreaker_backoff_threshold do |record, attr, value|
if value.to_i >= record.circuitbreaker_failure_count_threshold
record.errors.add(attr, _("The circuitbreaker backoff threshold should be "\
"lower than the failure count threshold"))
end
end
validates :gitaly_timeout_default, validates :gitaly_timeout_default,
presence: true, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
......
...@@ -6,6 +6,8 @@ module Ci ...@@ -6,6 +6,8 @@ module Ci
include Presentable include Presentable
include Importable include Importable
MissingDependenciesError = Class.new(StandardError)
belongs_to :runner belongs_to :runner
belongs_to :trigger_request belongs_to :trigger_request
belongs_to :erased_by, class_name: 'User' belongs_to :erased_by, class_name: 'User'
...@@ -139,6 +141,10 @@ module Ci ...@@ -139,6 +141,10 @@ module Ci
Ci::Build.retry(build, build.user) Ci::Build.retry(build, build.user)
end end
end end
before_transition any => [:running] do |build|
build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
end
end end
def detailed_status(current_user) def detailed_status(current_user)
...@@ -478,6 +484,20 @@ module Ci ...@@ -478,6 +484,20 @@ module Ci
options[:dependencies]&.empty? options[:dependencies]&.empty?
end end
def validates_dependencies!
dependencies.each do |dependency|
raise MissingDependenciesError unless dependency.valid_dependency?
end
end
def valid_dependency?
return false unless complete?
return false if artifacts_expired?
return false if erased?
true
end
def hide_secrets(trace) def hide_secrets(trace)
return unless trace return unless trace
......
# coding: utf-8
class Commit class Commit
extend ActiveModel::Naming extend ActiveModel::Naming
extend Gitlab::Cache::RequestCache extend Gitlab::Cache::RequestCache
...@@ -25,7 +26,7 @@ class Commit ...@@ -25,7 +26,7 @@ class Commit
DIFF_HARD_LIMIT_FILES = 1000 DIFF_HARD_LIMIT_FILES = 1000
DIFF_HARD_LIMIT_LINES = 50000 DIFF_HARD_LIMIT_LINES = 50000
MIN_SHA_LENGTH = 7 MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH
COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze
def banzai_render_context(field) def banzai_render_context(field)
......
...@@ -43,7 +43,8 @@ class CommitStatus < ActiveRecord::Base ...@@ -43,7 +43,8 @@ class CommitStatus < ActiveRecord::Base
script_failure: 1, script_failure: 1,
api_failure: 2, api_failure: 2,
stuck_or_timeout_failure: 3, stuck_or_timeout_failure: 3,
runner_system_failure: 4 runner_system_failure: 4,
missing_dependency_failure: 5
} }
## ##
......
...@@ -32,6 +32,10 @@ module DiscussionOnDiff ...@@ -32,6 +32,10 @@ module DiscussionOnDiff
first_note.position.new_path first_note.position.new_path
end end
def on_merge_request_commit?
for_merge_request? && commit_id.present?
end
# Returns an array of at most 16 highlighted lines above a diff note # Returns an array of at most 16 highlighted lines above a diff note
def truncated_diff_lines(highlight: true) def truncated_diff_lines(highlight: true)
lines = highlight ? highlighted_diff_lines : diff_lines lines = highlight ? highlighted_diff_lines : diff_lines
......
...@@ -24,7 +24,11 @@ class DiffDiscussion < Discussion ...@@ -24,7 +24,11 @@ class DiffDiscussion < Discussion
return unless for_merge_request? return unless for_merge_request?
return {} if active? return {} if active?
noteable.version_params_for(position.diff_refs) if on_merge_request_commit?
{ commit_id: commit_id }
else
noteable.version_params_for(position.diff_refs)
end
end end
def reply_attributes def reply_attributes
......
...@@ -17,6 +17,7 @@ class DiffNote < Note ...@@ -17,6 +17,7 @@ class DiffNote < Note
validates :noteable_type, inclusion: { in: NOTEABLE_TYPES } validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
validate :positions_complete validate :positions_complete
validate :verify_supported validate :verify_supported
validate :diff_refs_match_commit, if: :for_commit?
before_validation :set_original_position, on: :create before_validation :set_original_position, on: :create
before_validation :update_position, on: :create, if: :on_text? before_validation :update_position, on: :create, if: :on_text?
...@@ -135,6 +136,12 @@ class DiffNote < Note ...@@ -135,6 +136,12 @@ class DiffNote < Note
errors.add(:position, "is invalid") errors.add(:position, "is invalid")
end end
def diff_refs_match_commit
return if self.original_position.diff_refs == self.commit.diff_refs
errors.add(:commit_id, 'does not match the diff refs')
end
def keep_around_commits def keep_around_commits
project.repository.keep_around(self.original_position.base_sha) project.repository.keep_around(self.original_position.base_sha)
project.repository.keep_around(self.original_position.start_sha) project.repository.keep_around(self.original_position.start_sha)
......
...@@ -11,6 +11,7 @@ class Discussion ...@@ -11,6 +11,7 @@ class Discussion
:author, :author,
:noteable, :noteable,
:commit_id,
:for_commit?, :for_commit?,
:for_merge_request?, :for_merge_request?,
......
# Placeholder class for model that is implemented in EE # Placeholder class for model that is implemented in EE
# It will reserve (ee#3853) '&' as a reference prefix, but the table does not exists in CE # It reserves '&' as a reference prefix, but the table does not exists in CE
class Epic < ActiveRecord::Base class Epic < ActiveRecord::Base
# TODO: this will be implemented as part of #3853 def self.reference_prefix
def to_reference '&'
end
def self.reference_prefix_escaped
'&amp;'
end end
end end
...@@ -72,7 +72,7 @@ class Event < ActiveRecord::Base ...@@ -72,7 +72,7 @@ class Event < ActiveRecord::Base
# We're using preload for "push_event_payload" as otherwise the association # We're using preload for "push_event_payload" as otherwise the association
# is not always available (depending on the query being built). # is not always available (depending on the query being built).
includes(:author, :project, project: :namespace) includes(:author, :project, project: :namespace)
.preload(:target, :push_event_payload) .preload(:push_event_payload, target: :author)
end end
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) } scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
......
...@@ -298,6 +298,10 @@ class Group < Namespace ...@@ -298,6 +298,10 @@ class Group < Namespace
end end
end end
def hashed_storage?(_feature)
false
end
private private
def update_two_factor_requirement def update_two_factor_requirement
......
...@@ -649,6 +649,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -649,6 +649,7 @@ class MergeRequest < ActiveRecord::Base
.to_sql .to_sql
Note.from("(#{union}) #{Note.table_name}") Note.from("(#{union}) #{Note.table_name}")
.includes(:noteable)
end end
alias_method :discussion_notes, :related_notes alias_method :discussion_notes, :related_notes
...@@ -925,21 +926,27 @@ class MergeRequest < ActiveRecord::Base ...@@ -925,21 +926,27 @@ class MergeRequest < ActiveRecord::Base
.order(id: :desc) .order(id: :desc)
end end
# Note that this could also return SHA from now dangling commits def all_commits
#
def all_commit_shas
return commit_shas unless persisted?
diffs_relation = merge_request_diffs
# MySQL doesn't support LIMIT in a subquery. # MySQL doesn't support LIMIT in a subquery.
diffs_relation = diffs_relation.recent if Gitlab::Database.postgresql? diffs_relation = if Gitlab::Database.postgresql?
merge_request_diffs.recent
else
merge_request_diffs
end
MergeRequestDiffCommit MergeRequestDiffCommit
.where(merge_request_diff: diffs_relation) .where(merge_request_diff: diffs_relation)
.limit(10_000) .limit(10_000)
.pluck('sha') end
.uniq
# Note that this could also return SHA from now dangling commits
#
def all_commit_shas
@all_commit_shas ||= begin
return commit_shas unless persisted?
all_commits.pluck(:sha).uniq
end
end end
def merge_commit def merge_commit
......
...@@ -40,6 +40,7 @@ class Namespace < ActiveRecord::Base ...@@ -40,6 +40,7 @@ class Namespace < ActiveRecord::Base
namespace_path: true namespace_path: true
validate :nesting_level_allowed validate :nesting_level_allowed
validate :allowed_path_by_redirects
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
...@@ -257,4 +258,14 @@ class Namespace < ActiveRecord::Base ...@@ -257,4 +258,14 @@ class Namespace < ActiveRecord::Base
Namespace.where(id: descendants.select(:id)) Namespace.where(id: descendants.select(:id))
.update_all(share_with_group_lock: true) .update_all(share_with_group_lock: true)
end end
def allowed_path_by_redirects
return if path.nil?
errors.add(:path, "#{path} has been taken before. Please use another one") if namespace_previously_created_with_same_path?
end
def namespace_previously_created_with_same_path?
RedirectRoute.permanent.exists?(path: path)
end
end end
...@@ -230,16 +230,18 @@ class Note < ActiveRecord::Base ...@@ -230,16 +230,18 @@ class Note < ActiveRecord::Base
for_personal_snippet? for_personal_snippet?
end end
def commit
@commit ||= project.commit(commit_id) if commit_id.present?
end
# override to return commits, which are not active record # override to return commits, which are not active record
def noteable def noteable
if for_commit? return commit if for_commit?
@commit ||= project.commit(commit_id)
else super
super
end
# Temp fix to prevent app crash
# if note commit id doesn't exist
rescue rescue
# Temp fix to prevent app crash
# if note commit id doesn't exist
nil nil
end end
...@@ -401,6 +403,10 @@ class Note < ActiveRecord::Base ...@@ -401,6 +403,10 @@ class Note < ActiveRecord::Base
noteable_object&.touch noteable_object&.touch
end end
def banzai_render_context(field)
super.merge(noteable: noteable)
end
private private
def keep_around_commit def keep_around_commit
......
...@@ -3,6 +3,8 @@ class PersonalAccessToken < ActiveRecord::Base ...@@ -3,6 +3,8 @@ class PersonalAccessToken < ActiveRecord::Base
include TokenAuthenticatable include TokenAuthenticatable
add_authentication_token_field :token add_authentication_token_field :token
REDIS_EXPIRY_TIME = 3.minutes
serialize :scopes, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :scopes, Array # rubocop:disable Cop/ActiveRecordSerialize
belongs_to :user belongs_to :user
...@@ -27,6 +29,21 @@ class PersonalAccessToken < ActiveRecord::Base ...@@ -27,6 +29,21 @@ class PersonalAccessToken < ActiveRecord::Base
!revoked? && !expired? !revoked? && !expired?
end end
def self.redis_getdel(user_id)
Gitlab::Redis::SharedState.with do |redis|
token = redis.get(redis_shared_state_key(user_id))
redis.del(redis_shared_state_key(user_id))
token
end
end
def self.redis_store!(user_id, token)
Gitlab::Redis::SharedState.with do |redis|
redis.set(redis_shared_state_key(user_id), token, ex: REDIS_EXPIRY_TIME)
token
end
end
protected protected
def validate_scopes def validate_scopes
...@@ -38,4 +55,8 @@ class PersonalAccessToken < ActiveRecord::Base ...@@ -38,4 +55,8 @@ class PersonalAccessToken < ActiveRecord::Base
def set_default_scopes def set_default_scopes
self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty? self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty?
end end
def self.redis_shared_state_key(user_id)
"gitlab:personal_access_token:#{user_id}"
end
end end
...@@ -227,7 +227,6 @@ class Project < ActiveRecord::Base ...@@ -227,7 +227,6 @@ class Project < ActiveRecord::Base
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
delegate :empty_repo?, to: :repository
# Validations # Validations
validates :creator, presence: true, on: :create validates :creator, presence: true, on: :create
...@@ -499,6 +498,10 @@ class Project < ActiveRecord::Base ...@@ -499,6 +498,10 @@ class Project < ActiveRecord::Base
auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled? auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled?
end end
def empty_repo?
repository.empty?
end
def repository_storage_path def repository_storage_path
Gitlab.config.repositories.storages[repository_storage].try(:[], 'path') Gitlab.config.repositories.storages[repository_storage].try(:[], 'path')
end end
......
...@@ -17,4 +17,32 @@ class RedirectRoute < ActiveRecord::Base ...@@ -17,4 +17,32 @@ class RedirectRoute < ActiveRecord::Base
where(wheres, path, "#{sanitize_sql_like(path)}/%") where(wheres, path, "#{sanitize_sql_like(path)}/%")
end end
scope :permanent, -> do
if column_permanent_exists?
where(permanent: true)
else
none
end
end
scope :temporary, -> do
if column_permanent_exists?
where(permanent: [false, nil])
else
all
end
end
default_value_for :permanent, false
def permanent=(value)
if self.class.column_permanent_exists?
super
end
end
def self.column_permanent_exists?
ActiveRecord::Base.connection.column_exists?(:redirect_routes, :permanent)
end
end end
...@@ -37,7 +37,7 @@ class Repository ...@@ -37,7 +37,7 @@ class Repository
issue_template_names merge_request_template_names).freeze issue_template_names merge_request_template_names).freeze
# Methods that use cache_method but only memoize the value # Methods that use cache_method but only memoize the value
MEMOIZED_CACHED_METHODS = %i(license empty_repo?).freeze MEMOIZED_CACHED_METHODS = %i(license).freeze
# Certain method caches should be refreshed when certain types of files are # Certain method caches should be refreshed when certain types of files are
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
...@@ -497,7 +497,11 @@ class Repository ...@@ -497,7 +497,11 @@ class Repository
end end
cache_method :exists? cache_method :exists?
delegate :empty?, to: :raw_repository def empty?
return true unless exists?
!has_visible_content?
end
cache_method :empty? cache_method :empty?
# The size of this repository in megabytes. # The size of this repository in megabytes.
...@@ -944,13 +948,8 @@ class Repository ...@@ -944,13 +948,8 @@ class Repository
end end
end end
def empty_repo?
!exists? || !has_visible_content?
end
cache_method :empty_repo?, memoize_only: true
def search_files_by_content(query, ref) def search_files_by_content(query, ref)
return [] if empty_repo? || query.blank? return [] if empty? || query.blank?
offset = 2 offset = 2
args = %W(grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) args = %W(grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
...@@ -959,7 +958,7 @@ class Repository ...@@ -959,7 +958,7 @@ class Repository
end end
def search_files_by_name(query, ref) def search_files_by_name(query, ref)
return [] if empty_repo? || query.blank? return [] if empty? || query.blank?
args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)}) args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)})
......
...@@ -8,6 +8,8 @@ class Route < ActiveRecord::Base ...@@ -8,6 +8,8 @@ class Route < ActiveRecord::Base
presence: true, presence: true,
uniqueness: { case_sensitive: false } uniqueness: { case_sensitive: false }
validate :ensure_permanent_paths
after_create :delete_conflicting_redirects after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :path_changed? after_update :delete_conflicting_redirects, if: :path_changed?
after_update :create_redirect_for_old_path after_update :create_redirect_for_old_path
...@@ -40,7 +42,7 @@ class Route < ActiveRecord::Base ...@@ -40,7 +42,7 @@ class Route < ActiveRecord::Base
# We are not calling route.delete_conflicting_redirects here, in hopes # We are not calling route.delete_conflicting_redirects here, in hopes
# of avoiding deadlocks. The parent (self, in this method) already # of avoiding deadlocks. The parent (self, in this method) already
# called it, which deletes conflicts for all descendants. # called it, which deletes conflicts for all descendants.
route.create_redirect(old_path) if attributes[:path] route.create_redirect(old_path, permanent: permanent_redirect?) if attributes[:path]
end end
end end
end end
...@@ -50,16 +52,30 @@ class Route < ActiveRecord::Base ...@@ -50,16 +52,30 @@ class Route < ActiveRecord::Base
end end
def conflicting_redirects def conflicting_redirects
RedirectRoute.matching_path_and_descendants(path) RedirectRoute.temporary.matching_path_and_descendants(path)
end end
def create_redirect(path) def create_redirect(path, permanent: false)
RedirectRoute.create(source: source, path: path) RedirectRoute.create(source: source, path: path, permanent: permanent)
end end
private private
def create_redirect_for_old_path def create_redirect_for_old_path
create_redirect(path_was) if path_changed? create_redirect(path_was, permanent: permanent_redirect?) if path_changed?
end
def permanent_redirect?
source_type != "Project"
end
def ensure_permanent_paths
return if path.nil?
errors.add(:path, "#{path} has been taken before. Please use another one") if conflicting_redirect_exists?
end
def conflicting_redirect_exists?
RedirectRoute.permanent.matching_path_and_descendants(path).exists?
end end
end end
...@@ -315,6 +315,8 @@ class User < ActiveRecord::Base ...@@ -315,6 +315,8 @@ class User < ActiveRecord::Base
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def search(query) def search(query)
query = query.downcase
order = <<~SQL order = <<~SQL
CASE CASE
WHEN users.name = %{query} THEN 0 WHEN users.name = %{query} THEN 0
...@@ -324,8 +326,11 @@ class User < ActiveRecord::Base ...@@ -324,8 +326,11 @@ class User < ActiveRecord::Base
END END
SQL SQL
fuzzy_search(query, [:name, :email, :username]) where(
.reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name) fuzzy_arel_match(:name, query)
.or(fuzzy_arel_match(:username, query))
.or(arel_table[:email].eq(query))
).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
end end
# searches user by given pattern # searches user by given pattern
...@@ -333,15 +338,17 @@ class User < ActiveRecord::Base ...@@ -333,15 +338,17 @@ class User < ActiveRecord::Base
# This method uses ILIKE on PostgreSQL and LIKE on MySQL. # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
def search_with_secondary_emails(query) def search_with_secondary_emails(query)
query = query.downcase
email_table = Email.arel_table email_table = Email.arel_table
matched_by_emails_user_ids = email_table matched_by_emails_user_ids = email_table
.project(email_table[:user_id]) .project(email_table[:user_id])
.where(Email.fuzzy_arel_match(:email, query)) .where(email_table[:email].eq(query))
where( where(
fuzzy_arel_match(:name, query) fuzzy_arel_match(:name, query)
.or(fuzzy_arel_match(:email, query))
.or(fuzzy_arel_match(:username, query)) .or(fuzzy_arel_match(:username, query))
.or(arel_table[:email].eq(query))
.or(arel_table[:id].in(matched_by_emails_user_ids)) .or(arel_table[:id].in(matched_by_emails_user_ids))
) )
end end
...@@ -1054,13 +1061,13 @@ class User < ActiveRecord::Base ...@@ -1054,13 +1061,13 @@ class User < ActiveRecord::Base
end end
def todos_done_count(force: false) def todos_done_count(force: false)
Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: 20.minutes) do
TodosFinder.new(self, state: :done).execute.count TodosFinder.new(self, state: :done).execute.count
end end
end end
def todos_pending_count(force: false) def todos_pending_count(force: false)
Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: 20.minutes) do
TodosFinder.new(self, state: :pending).execute.count TodosFinder.new(self, state: :pending).execute.count
end end
end end
......
...@@ -30,7 +30,12 @@ class GroupPolicy < BasePolicy ...@@ -30,7 +30,12 @@ class GroupPolicy < BasePolicy
rule { public_group } .enable :read_group rule { public_group } .enable :read_group
rule { logged_in_viewable }.enable :read_group rule { logged_in_viewable }.enable :read_group
rule { guest } .enable :read_group
rule { guest }.policy do
enable :read_group
enable :upload_file
end
rule { admin } .enable :read_group rule { admin } .enable :read_group
rule { has_projects } .enable :read_group rule { has_projects } .enable :read_group
......
...@@ -12,18 +12,19 @@ module Ci ...@@ -12,18 +12,19 @@ module Ci
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block) def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block)
@pipeline = Ci::Pipeline.new @pipeline = Ci::Pipeline.new
command = OpenStruct.new(source: source, command = Gitlab::Ci::Pipeline::Chain::Command.new(
origin_ref: params[:ref], source: source,
checkout_sha: params[:checkout_sha], origin_ref: params[:ref],
after_sha: params[:after], checkout_sha: params[:checkout_sha],
before_sha: params[:before], after_sha: params[:after],
trigger_request: trigger_request, before_sha: params[:before],
schedule: schedule, trigger_request: trigger_request,
ignore_skip_ci: ignore_skip_ci, schedule: schedule,
save_incompleted: save_on_errors, ignore_skip_ci: ignore_skip_ci,
seeds_block: block, save_incompleted: save_on_errors,
project: project, seeds_block: block,
current_user: current_user) project: project,
current_user: current_user)
sequence = Gitlab::Ci::Pipeline::Chain::Sequence sequence = Gitlab::Ci::Pipeline::Chain::Sequence
.new(pipeline, command, SEQUENCE) .new(pipeline, command, SEQUENCE)
......
...@@ -54,6 +54,9 @@ module Ci ...@@ -54,6 +54,9 @@ module Ci
# we still have to return 409 in the end, # we still have to return 409 in the end,
# to make sure that this is properly handled by runner. # to make sure that this is properly handled by runner.
valid = false valid = false
rescue Ci::Build::MissingDependenciesError
build.drop!(:missing_dependency_failure)
valid = false
end end
end end
......
...@@ -6,7 +6,7 @@ module MergeRequests ...@@ -6,7 +6,7 @@ module MergeRequests
@oldrev, @newrev = oldrev, newrev @oldrev, @newrev = oldrev, newrev
@branch_name = Gitlab::Git.ref_name(ref) @branch_name = Gitlab::Git.ref_name(ref)
find_new_commits Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits))
# Be sure to close outstanding MRs before reloading them to avoid generating an # Be sure to close outstanding MRs before reloading them to avoid generating an
# empty diff during a manual merge # empty diff during a manual merge
close_merge_requests close_merge_requests
......
...@@ -20,7 +20,7 @@ class MetricsService ...@@ -20,7 +20,7 @@ class MetricsService
end end
def metrics_text def metrics_text
"#{health_metrics_text}#{prometheus_metrics_text}" prometheus_metrics_text.concat(health_metrics_text)
end end
private private
......
...@@ -15,7 +15,7 @@ module Projects ...@@ -15,7 +15,7 @@ module Projects
return error("Could not set the default branch") unless project.change_head(params[:default_branch]) return error("Could not set the default branch") unless project.change_head(params[:default_branch])
end end
if project.update_attributes(update_params) if project.update_attributes(params.except(:default_branch))
if project.previous_changes.include?('path') if project.previous_changes.include?('path')
project.rename_repo project.rename_repo
else else
...@@ -32,15 +32,13 @@ module Projects ...@@ -32,15 +32,13 @@ module Projects
end end
def run_auto_devops_pipeline? def run_auto_devops_pipeline?
params.dig(:run_auto_devops_pipeline_explicit) == 'true' || params.dig(:run_auto_devops_pipeline_implicit) == 'true' return false if project.repository.gitlab_ci_yml || !project.auto_devops.previous_changes.include?('enabled')
project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && current_application_settings.auto_devops_enabled?)
end end
private private
def update_params
params.except(:default_branch, :run_auto_devops_pipeline_explicit, :run_auto_devops_pipeline_implicit)
end
def renaming_project_with_container_registry_tags? def renaming_project_with_container_registry_tags?
new_path = params[:path] new_path = params[:path]
......
module ProtectedBranches
class AccessLevelParams
attr_reader :type, :params
def initialize(type, params)
@type = type
@params = params_with_default(params)
end
def access_levels
ce_style_access_level
end
private
def params_with_default(params)
params[:"#{type}_access_level"] ||= Gitlab::Access::MASTER if use_default_access_level?(params)
params
end
def use_default_access_level?(params)
true
end
def ce_style_access_level
access_level = params[:"#{type}_access_level"]
return [] unless access_level
[{ access_level: access_level }]
end
end
end
module ProtectedBranches
class ApiService < BaseService
def create
@push_params = AccessLevelParams.new(:push, params)
@merge_params = AccessLevelParams.new(:merge, params)
verify_params!
protected_branch_params = {
name: params[:name],
push_access_levels_attributes: @push_params.access_levels,
merge_access_levels_attributes: @merge_params.access_levels
}
::ProtectedBranches::CreateService.new(@project, @current_user, protected_branch_params).execute
end
private
def verify_params!
# EE-only
end
end
end
...@@ -29,11 +29,11 @@ class FileUploader < GitlabUploader ...@@ -29,11 +29,11 @@ class FileUploader < GitlabUploader
# model - Object that responds to `full_path` and `disk_path` # model - Object that responds to `full_path` and `disk_path`
# #
# Returns a String without a trailing slash # Returns a String without a trailing slash
def self.dynamic_path_segment(project) def self.dynamic_path_segment(model)
if project.hashed_storage?(:attachments) if model.hashed_storage?(:attachments)
dynamic_path_builder(project.disk_path) dynamic_path_builder(model.disk_path)
else else
dynamic_path_builder(project.full_path) dynamic_path_builder(model.full_path)
end end
end end
......
class NamespaceFileUploader < FileUploader
def self.base_dir
File.join(root_dir, '-', 'system', 'namespace')
end
def self.dynamic_path_segment(model)
dynamic_path_builder(model.id.to_s)
end
private
def secure_url
File.join('/uploads', @secret, file.filename)
end
end
...@@ -545,6 +545,12 @@ ...@@ -545,6 +545,12 @@
%fieldset %fieldset
%legend Git Storage Circuitbreaker settings %legend Git Storage Circuitbreaker settings
.form-group
= f.label :circuitbreaker_check_interval, _('Check interval'), class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_check_interval, class: 'form-control'
.help-block
= circuitbreaker_check_interval_help_text
.form-group .form-group
= f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2' = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
...@@ -557,18 +563,6 @@ ...@@ -557,18 +563,6 @@
= f.number_field :circuitbreaker_storage_timeout, class: 'form-control' = f.number_field :circuitbreaker_storage_timeout, class: 'form-control'
.help-block .help-block
= circuitbreaker_storage_timeout_help_text = circuitbreaker_storage_timeout_help_text
.form-group
= f.label :circuitbreaker_backoff_threshold, _('Number of failures before backing off'), class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_backoff_threshold, class: 'form-control'
.help-block
= circuitbreaker_backoff_threshold_help_text
.form-group
= f.label :circuitbreaker_failure_wait_time, _('Seconds to wait after a storage failure'), class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_failure_wait_time, class: 'form-control'
.help-block
= circuitbreaker_failure_wait_time_help_text
.form-group .form-group
= f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2' = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
......
...@@ -32,9 +32,17 @@ ...@@ -32,9 +32,17 @@
- elsif discussion.diff_discussion? - elsif discussion.diff_discussion?
on on
= conditional_link_to url.present?, url do = conditional_link_to url.present?, url do
- unless discussion.active? - if discussion.on_merge_request_commit?
an old version of - unless discussion.active?
the diff an outdated change in
commit
%span.commit-sha= Commit.truncate_sha(discussion.commit_id)
- else
- unless discussion.active?
an old version of
the diff
= time_ago_with_tooltip(discussion.created_at, placement: "bottom", html_class: "note-created-ago") = time_ago_with_tooltip(discussion.created_at, placement: "bottom", html_class: "note-created-ago")
= render "discussions/headline", discussion: discussion = render "discussions/headline", discussion: discussion
......
- humanized_resource_name = spammable.class.model_name.human.downcase - humanized_resource_name = spammable.class.model_name.human.downcase
- resource_name = spammable.class.model_name.singular
%h3.page-title %h3.page-title
Anti-spam verification Anti-spam verification
...@@ -8,16 +7,4 @@ ...@@ -8,16 +7,4 @@
%p %p
#{"We detected potential spam in the #{humanized_resource_name}. Please solve the reCAPTCHA to proceed."} #{"We detected potential spam in the #{humanized_resource_name}. Please solve the reCAPTCHA to proceed."}
= form_for form do |f| = render 'shared/recaptcha_form', spammable: spammable
.recaptcha
- params[resource_name].each do |field, value|
= hidden_field(resource_name, field, value: value)
= hidden_field_tag(:spam_log_id, spammable.spam_log.id)
= hidden_field_tag(:recaptcha_verification, true)
= recaptcha_tags
-# Yields a block with given extra params.
= yield
.row-content-block.footer-block
= f.submit "Submit #{humanized_resource_name}", class: 'btn btn-create'
...@@ -13,7 +13,14 @@ ...@@ -13,7 +13,14 @@
.location-badge= label .location-badge= label
.search-input-wrap .search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } } .dropdown{ data: { url: search_autocomplete_path } }
= search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }, aria: { label: 'Search' } = search_field_tag 'search', nil, placeholder: 'Search',
class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options',
spellcheck: false,
tabindex: '1',
autocomplete: 'off',
data: { issues_path: issues_dashboard_path,
mr_path: merge_requests_dashboard_path },
aria: { label: 'Search' }
%button.hidden.js-dropdown-search-toggle{ type: 'button', data: { toggle: 'dropdown' } } %button.hidden.js-dropdown-search-toggle{ type: 'button', data: { toggle: 'dropdown' } }
.dropdown-menu.dropdown-select .dropdown-menu.dropdown-select
= dropdown_content do = dropdown_content do
......
...@@ -4,4 +4,10 @@ ...@@ -4,4 +4,10 @@
- nav "group" - nav "group"
- @left_sidebar = true - @left_sidebar = true
- content_for :page_specific_javascripts do
- if current_user
-# haml-lint:disable InlineJavaScript
:javascript
window.uploads_path = "#{group_uploads_path(@group)}";
= render template: "layouts/application" = render template: "layouts/application"
...@@ -15,14 +15,13 @@ ...@@ -15,14 +15,13 @@
They are the only accepted password when you have Two-Factor Authentication (2FA) enabled. They are the only accepted password when you have Two-Factor Authentication (2FA) enabled.
.col-lg-8 .col-lg-8
- if @new_personal_access_token
- if flash[:personal_access_token]
.created-personal-access-token-container .created-personal-access-token-container
%h5.prepend-top-0 %h5.prepend-top-0
Your New Personal Access Token Your New Personal Access Token
.form-group .form-group
= text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-personal-access-token-help-block" = text_field_tag 'created-personal-access-token', @new_personal_access_token, readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-personal-access-token-help-block"
= clipboard_button(text: flash[:personal_access_token], title: "Copy personal access token to clipboard", placement: "left") = clipboard_button(text: @new_personal_access_token, title: "Copy personal access token to clipboard", placement: "left")
%span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again. %span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again.
%hr %hr
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- if defined?(@merge_request) && @merge_request.discussion_locked? - if defined?(@merge_request) && @merge_request.discussion_locked?
.issuable-note-warning .issuable-note-warning
= icon('lock', class: 'icon') = sprite_icon('lock', size: 16, css_class: 'icon')
%span %span
= _('This merge request is locked.') = _('This merge request is locked.')
= _('Only project members can comment.') = _('Only project members can comment.')
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
%td.branch-commit %td.branch-commit
- if can?(current_user, :read_build, job) - if can?(current_user, :read_build, job)
= link_to project_job_url(job.project, job) do = link_to project_job_path(job.project, job) do
%span.build-link ##{job.id} %span.build-link ##{job.id}
- else - else
%span.build-link ##{job.id} %span.build-link ##{job.id}
......
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
%li= link_to s_("DownloadCommit|Email Patches"), project_commit_path(@project, @commit, format: :patch) %li= link_to s_("DownloadCommit|Email Patches"), project_commit_path(@project, @commit, format: :patch)
%li= link_to s_("DownloadCommit|Plain Diff"), project_commit_path(@project, @commit, format: :diff) %li= link_to s_("DownloadCommit|Plain Diff"), project_commit_path(@project, @commit, format: :diff)
.commit-box .commit-box{ data: { project_path: project_path(@project) } }
%h3.commit-title %h3.commit-title
= markdown(@commit.title, pipeline: :single_line, author: @commit.author) = markdown(@commit.title, pipeline: :single_line, author: @commit.author)
- if @commit.description.present? - if @commit.description.present?
...@@ -80,3 +80,13 @@ ...@@ -80,3 +80,13 @@
- if last_pipeline.duration - if last_pipeline.duration
in in
= time_interval_in_words last_pipeline.duration = time_interval_in_words last_pipeline.duration
- if @merge_request
.well-segment
= icon('info-circle fw')
This commit is part of merge request
= succeed '.' do
= link_to @merge_request.to_reference, diffs_project_merge_request_path(@project, @merge_request, commit_id: @commit.id)
Comments created here will be created in the context of that merge request.
...@@ -6,6 +6,9 @@ ...@@ -6,6 +6,9 @@
- @content_class = limited_container_width - @content_class = limited_container_width
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits" - page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
- page_description @commit.description - page_description @commit.description
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('diff_notes')
.container-fluid{ class: [limited_container_width, container_class] } .container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box" = render "commit_box"
......
- ref = local_assigns.fetch(:ref) - view_details = local_assigns.fetch(:view_details, false)
- merge_request = local_assigns.fetch(:merge_request, nil)
- cache_key = [project.full_path, commit.id, current_application_settings, @path.presence, current_controller?(:commits), I18n.locale] - project = local_assigns.fetch(:project) { merge_request&.project }
- cache_key.push(commit.status(ref)) if commit.status(ref) - ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
- link = commit_path(project, commit, merge_request: merge_request)
- cache_key = [project.full_path,
commit.id,
current_application_settings,
@path.presence,
current_controller?(:commits),
merge_request&.iid,
view_details,
commit.status(ref),
I18n.locale].compact
= cache(cache_key, expires_in: 1.day) do = cache(cache_key, expires_in: 1.day) do
%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" } %li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
...@@ -11,7 +22,7 @@ ...@@ -11,7 +22,7 @@
.commit-detail .commit-detail
.commit-content .commit-content
= link_to_markdown_field(commit, :title, project_commit_path(project, commit.id), class: "commit-row-message item-title") = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title")
%span.commit-row-message.visible-xs-inline %span.commit-row-message.visible-xs-inline
&middot; &middot;
= commit.short_id = commit.short_id
...@@ -31,8 +42,7 @@ ...@@ -31,8 +42,7 @@
- commit_text = _('%{commit_author_link} committed %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago } - commit_text = _('%{commit_author_link} committed %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago }
#{ commit_text.html_safe } #{ commit_text.html_safe }
.commit-actions.flex-row.hidden-xs
.commit-actions.hidden-xs
- if request.xhr? - if request.xhr?
= render partial: 'projects/commit/signature', object: commit.signature = render partial: 'projects/commit/signature', object: commit.signature
- else - else
...@@ -41,6 +51,9 @@ ...@@ -41,6 +51,9 @@
- if commit.status(ref) - if commit.status(ref)
= render_commit_status(commit, ref: ref) = render_commit_status(commit, ref: ref)
= link_to commit.short_id, project_commit_path(project, commit), class: "commit-sha btn btn-transparent btn-link" = link_to commit.short_id, link, class: "commit-sha btn btn-transparent btn-link"
= clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard")) = clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"))
= link_to_browse_code(project, commit) = link_to_browse_code(project, commit)
- if view_details && merge_request
= link_to "View details", project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "btn btn-default"
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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