Commit 8556d701 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 4249-show-results-from-docker-image-scan-in-the-merge-request-widget

* master: (45 commits)
  Containing epic
  Docs: add index for doc/administration/operations/
  Resolve merge conflicts with dev.gitlab.org/master after security release (EE port)
  fixed import
  updated imports
  Resolve conflicts in app/models/user.rb
  Manually add 10.2.4 changelog entries
  Merge branch 'note-preview' into 'security-10-2'
  Merge branch 'issue_30663' into 'security-10-2'
  Merge branch 'rs-security-group-api' into 'security-10-2'
  Merge branch 'bvl-10-2-email-disclosure' into 'security-10-2'
  Merge branch '36679-non-authorized-user-may-see-wikis-or-pipeline-page' into 'security-10-2'
  Port from CE: Allow git pull/push on project redirects
  Clean up config docs for Geo
  Merge branch 'sh-bump-redis-actionpack' into 'master'
  Add recaptcha_check_if_spammable for issualbes than arent spammables
  Add `allow_mirror_update` to `Command`
  Fixed karma
  Standardise on self-signed certificates for PostgreSQL TLS
  Change postgresql -> PostgreSQL
  ...
parents 1ab96d89 65cbc16b
Please view this file on the master branch, on stable branches it's out of date.
## 10.2.4 (2017-12-07)
- No changes.
## 10.2.3 (2017-11-30)
### Fixed (5 changes)
......@@ -93,6 +97,10 @@ Please view this file on the master branch, on stable branches it's out of date.
- Enhance the documentation for gitlab-ctl replicate-geo-database. !3268
## 10.1.5 (2017-12-07)
- No changes.
## 10.1.4 (2017-11-14)
- No changes.
......@@ -147,6 +155,10 @@ Please view this file on the master branch, on stable branches it's out of date.
- [OTHER] Add partial index on push_rules.is_sample.
- Add new push rule to reject unsigned commits. !2913
## 10.0.7 (2017-12-07)
- No changes.
## 10.0.5 (2017-11-03)
- [FIXED] Find stuck scheduled import jobs and also mark them as failed. !3055
......
......@@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.2.4 (2017-12-07)
### Security (5 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)
### Fixed (7 changes)
......@@ -237,6 +248,17 @@ entry.
- Add Gitaly metrics to the performance bar.
## 10.1.5 (2017-12-07)
### Security (5 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.1.4 (2017-11-14)
### Fixed (4 changes)
......@@ -485,6 +507,17 @@ entry.
- creation of keys moved to services. !13331 (haseebeqx)
- Add username as GL_USERNAME in hooks.
## 10.0.7 (2017-12-07)
### Security (5 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.0.5 (2017-11-03)
- [FIXED] Fix incorrect X-axis labels in Prometheus graphs. !14258
......
......@@ -181,7 +181,7 @@ gem 're2', '~> 1.1.1'
gem 'version_sorter', '~> 2.1.0'
# Cache
gem 'redis-rails', '~> 5.0.1'
gem 'redis-rails', '~> 5.0.2'
# Redis
gem 'redis', '~> 3.2'
......
......@@ -728,24 +728,24 @@ GEM
recursive-open-struct (1.0.0)
redcarpet (3.4.0)
redis (3.3.3)
redis-actionpack (5.0.1)
redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 1.4.0)
redis-activesupport (5.0.1)
redis-store (>= 1.1.0, < 2)
redis-activesupport (5.0.4)
activesupport (>= 3, < 6)
redis-store (~> 1.2.0)
redis-store (>= 1.3, < 2)
redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4)
redis-rack (1.6.0)
rack (~> 1.5)
redis-store (~> 1.2.0)
redis-rails (5.0.1)
redis-actionpack (~> 5.0.0)
redis-activesupport (~> 5.0.0)
redis-store (~> 1.2.0)
redis-store (1.2.0)
redis (>= 2.2)
redis-rack (2.0.3)
rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2)
redis-rails (5.0.2)
redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2)
redis-store (1.4.1)
redis (>= 2.2, < 5)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
......@@ -1168,7 +1168,7 @@ DEPENDENCIES
redcarpet (~> 3.4)
redis (~> 3.2)
redis-namespace (~> 1.5.2)
redis-rails (~> 5.0.1)
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
rouge (~> 2.0)
......
<script>
import Flash from '~/flash';
import GitlabSlackService from '../services/gitlab_slack_service';
import * as UrlUtility from '../../lib/utils/url_utility';
import { redirectTo } from '../../lib/utils/url_utility';
export default {
props: {
......@@ -75,7 +75,7 @@
addToSlack() {
GitlabSlackService.addToSlack(this.slackLinkPath, this.selectedProjectId)
.then(response => UrlUtility.redirectTo(response.data.add_to_slack_link))
.then(response => redirectTo(response.data.add_to_slack_link))
.catch(() => Flash('Unable to build Slack link.'));
},
},
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */
import { refreshCurrentPage } from './lib/utils/url_utility';
window.Admin = (function() {
function Admin() {
......@@ -40,10 +41,10 @@ window.Admin = (function() {
return $('.change-owner-link').show();
});
$('li.project_member').bind('ajax:success', function() {
return gl.utils.refreshCurrentPage();
return refreshCurrentPage();
});
$('li.group_member').bind('ajax:success', function() {
return gl.utils.refreshCurrentPage();
return refreshCurrentPage();
});
showBlacklistType = function() {
if ($("input[name='blacklist_type']:checked").val() === 'file') {
......
......@@ -5,7 +5,7 @@
// %button.js-toggle-button
// %div.js-toggle-content
//
import '~/lib/utils/url_utility';
import { getLocationHash } from '../lib/utils/url_utility';
$(() => {
function toggleContainer(container, toggleState) {
......@@ -33,7 +33,7 @@ $(() => {
// If we're accessing a permalink, ensure it is not inside a
// closed js-toggle-container!
const hash = window.gl.utils.getLocationHash();
const hash = getLocationHash();
const anchor = hash && document.getElementById(hash);
const container = anchor && $(anchor).closest('.js-toggle-container');
......
/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */
import Dropzone from 'dropzone';
import '../lib/utils/url_utility';
import { visitUrl } from '../lib/utils/url_utility';
import { HIDDEN_CLASS } from '../lib/utils/constants';
import csrf from '../lib/utils/csrf';
......@@ -49,7 +49,7 @@ export default class BlobFileDropzone {
});
this.on('success', function (header, response) {
$('#modal-upload-blob').modal('hide');
window.gl.utils.visitUrl(response.filePath);
visitUrl(response.filePath);
});
this.on('maxfilesexceeded', function (file) {
dropzoneMessage.addClass(HIDDEN_CLASS);
......
import { getLocationHash } from '../lib/utils/url_utility';
const lineNumberRe = /^L[0-9]+/;
const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
const hash = gl.utils.getLocationHash();
const hash = getLocationHash();
if (hash && lineNumberRe.test(hash)) {
const hashUrlString = `#${hash}`;
......
......@@ -3,6 +3,7 @@
import Flash from '~/flash';
import PopupDialog from '~/vue_shared/components/popup_dialog.vue';
import { visitUrl } from '../../lib/utils/url_utility';
import BoardMilestoneSelect from './milestone_select.vue';
import BoardWeightSelect from './weight_select.vue';
import BoardLabelsSelect from './labels_select.vue';
......@@ -138,7 +139,7 @@ export default {
if (this.isDeleteForm) {
gl.boardService.deleteBoard(this.currentBoard)
.then(() => {
gl.utils.visitUrl(Store.rootPath);
visitUrl(Store.rootPath);
})
.catch(() => {
Flash('Failed to delete board. Please try again.');
......@@ -148,7 +149,7 @@ export default {
gl.boardService.createBoard(this.board)
.then(resp => resp.json())
.then((data) => {
gl.utils.visitUrl(data.board_path);
visitUrl(data.board_path);
})
.catch(() => {
Flash('Unable to save your changes. Please try again.');
......
/* 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() {
function Compare(opts) {
export default class Compare {
constructor(opts) {
this.opts = opts;
this.source_loading = $(".js-source-loading");
this.target_loading = $(".js-target-loading");
......@@ -34,12 +34,12 @@ window.Compare = (function() {
this.initialState();
}
Compare.prototype.initialState = function() {
initialState() {
this.getSourceHtml();
return this.getTargetHtml();
};
this.getTargetHtml();
}
Compare.prototype.getTargetProject = function() {
getTargetProject() {
return $.ajax({
url: this.opts.targetProjectUrl,
data: {
......@@ -52,22 +52,22 @@ window.Compare = (function() {
return $('.js-target-branch-dropdown .dropdown-content').html(html);
}
});
};
}
Compare.prototype.getSourceHtml = function() {
return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
getSourceHtml() {
return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
ref: $("input[name='merge_request[source_branch]']").val()
});
};
}
Compare.prototype.getTargetHtml = function() {
return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
getTargetHtml() {
return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
target_project_id: $("input[name='merge_request[target_project_id]']").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;
$target = $(target);
return $.ajax({
......@@ -84,7 +84,5 @@ window.Compare = (function() {
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 */
window.CompareAutocomplete = (function() {
function CompareAutocomplete() {
this.initDropdown();
}
CompareAutocomplete.prototype.initDropdown = function() {
return $('.js-compare-dropdown').each(function() {
var $dropdown, selected;
$dropdown = $(this);
selected = $dropdown.data('selected');
const $dropdownContainer = $dropdown.closest('.dropdown');
const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer);
const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown.glDropdown({
data: function(term, callback) {
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);
export default function initCompareAutocomplete() {
$('.js-compare-dropdown').each(function() {
var $dropdown, selected;
$dropdown = $(this);
selected = $dropdown.data('selected');
const $dropdownContainer = $dropdown.closest('.dropdown');
const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer);
const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown.glDropdown({
data: function(term, callback) {
return $.ajax({
url: $dropdown.data('refs-url'),
data: {
ref: $dropdown.data('ref'),
search: term,
}
},
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');
});
$dropdownContainer.on('click', '.dropdown-content a', (e) => {
$dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) {
$dropdown.tooltip('fixTitle');
}).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);
}
});
},
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');
}
});
});
}
......@@ -28,7 +28,7 @@ export default class ContextualSidebar {
this.$closeSidebar.on('click', () => this.toggleSidebarNav(false));
this.$overlay.on('click', () => this.toggleSidebarNav(false));
this.$sidebarToggle.on('click', () => {
const value = !this.$sidebar.hasClass('sidebar-icons-only');
const value = !this.$sidebar.hasClass('sidebar-collapsed-desktop');
this.toggleCollapsedSidebar(value);
});
......@@ -43,16 +43,16 @@ export default class ContextualSidebar {
}
toggleSidebarNav(show) {
this.$sidebar.toggleClass('nav-sidebar-expanded', show);
this.$sidebar.toggleClass('sidebar-expanded-mobile', show);
this.$overlay.toggleClass('mobile-nav-open', show);
this.$sidebar.removeClass('sidebar-icons-only');
this.$sidebar.removeClass('sidebar-collapsed-desktop');
}
toggleCollapsedSidebar(collapsed) {
const breakpoint = bp.getBreakpointSize();
if (this.$sidebar.length) {
this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
this.$sidebar.toggleClass('sidebar-collapsed-desktop', collapsed);
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
}
ContextualSidebar.setCollapsedCookie(collapsed);
......
import './lib/utils/url_utility';
import { getLocationHash } from './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff';
import imageDiffHelper from './image_diff/helpers/index';
......@@ -31,7 +31,7 @@ export default class Diff {
isBound = true;
}
if (gl.utils.getLocationHash()) {
if (getLocationHash()) {
this.highlightSelectedLine();
}
......@@ -73,7 +73,7 @@ export default class Diff {
}
openAnchoredDiff(cb) {
const locationHash = gl.utils.getLocationHash();
const locationHash = getLocationHash();
const anchoredDiff = locationHash && locationHash.split('_')[0];
if (!anchoredDiff) return;
......@@ -128,7 +128,7 @@ export default class Diff {
}
// eslint-disable-next-line class-methods-use-this
highlightSelectedLine() {
const hash = gl.utils.getLocationHash();
const hash = getLocationHash();
const $diffFiles = $('.diff-file');
$diffFiles.find('.hll').removeClass('hll');
......
......@@ -17,7 +17,8 @@ import './components/diff_note_avatars';
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';
window.gl = window.gl || {};
......
......@@ -43,7 +43,7 @@ class ResolveServiceClass {
discussion.resolveAllNotes(resolvedBy);
}
gl.mrWidget.checkStatus();
if (gl.mrWidget) gl.mrWidget.checkStatus();
discussion.updateHeadline(data);
})
.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';
import Project from './project';
import projectAvatar from './project_avatar';
/* global MergeRequest */
/* global Compare */
/* global CompareAutocomplete */
import Compare from './compare';
import initCompareAutocomplete from './compare_autocomplete';
/* global PathLocks */
/* global ProjectFindFile */
import ProjectNew from './project_new';
......@@ -711,7 +711,7 @@ import initGroupAnalytics from './init_group_analytics';
projectAvatar();
switch (path[1]) {
case 'compare':
new CompareAutocomplete();
initCompareAutocomplete();
break;
case 'edit':
shortcut_handler = new ShortcutsNavigation();
......
import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
import FilteredSearchContainer from './container';
import RecentSearchesRoot from './recent_searches_root';
......@@ -579,7 +580,7 @@ class FilteredSearchManager {
if (this.updateObject) {
this.updateObject(parameterizedUrl);
} else {
gl.utils.visitUrl(parameterizedUrl);
visitUrl(parameterizedUrl);
}
}
......
......@@ -21,7 +21,7 @@ let headerHeight = 50;
export const getHeaderHeight = () => headerHeight;
export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-icons-only');
export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-collapsed-desktop');
export const canShowActiveSubItems = (el) => {
if (el.classList.contains('active') && !isSidebarCollapsed()) {
......
......@@ -2,6 +2,7 @@
/* global fuzzaldrinPlus */
import _ from 'underscore';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { visitUrl } from './lib/utils/url_utility';
import { isObject } from './lib/utils/type_utility';
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput;
......@@ -868,7 +869,7 @@ GitLabDropdown = (function() {
if ($el.length) {
var href = $el.attr('href');
if (href && href !== '#') {
gl.utils.visitUrl(href);
visitUrl(href);
} else {
$el.trigger('click');
}
......
......@@ -5,7 +5,7 @@ import eventHub from '../event_hub';
import { getParameterByName } from '../../lib/utils/common_utils';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import { COMMON_STR } from '../constants';
import { mergeUrlParams } from '../../lib/utils/url_utility';
import groupsComponent from './groups.vue';
export default {
......@@ -93,7 +93,7 @@ export default {
this.isLoading = false;
$.scrollTo(0);
const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href);
const currentPath = mergeUrlParams({ page }, window.location.href);
window.history.replaceState({
page: currentPath,
}, document.title, currentPath);
......
<script>
import { visitUrl } from '../../lib/utils/url_utility';
import tooltip from '../../vue_shared/directives/tooltip';
import identicon from '../../vue_shared/components/identicon.vue';
import eventHub from '../event_hub';
......@@ -60,7 +61,7 @@ export default {
if (this.hasChildren) {
eventHub.$emit('toggleChildren', this.group);
} else {
gl.utils.visitUrl(this.group.relativePath);
visitUrl(this.group.relativePath);
}
}
},
......
import { visitUrl } from '../lib/utils/url_utility';
import DropLab from '../droplab/drop_lab';
import ISetter from '../droplab/plugins/input_setter';
......@@ -54,9 +55,9 @@ export default class NewGroupChild {
onClickNewGroupChildButton(e) {
if (e.target.dataset.action === NEW_PROJECT) {
gl.utils.visitUrl(this.newGroupPath);
visitUrl(this.newGroupPath);
} else if (e.target.dataset.action === NEW_SUBGROUP) {
gl.utils.visitUrl(this.subgroupPath);
visitUrl(this.subgroupPath);
}
}
}
......@@ -8,18 +8,7 @@ import IssuablesHelper from './helpers/issuables_helper';
export default class Issue {
constructor() {
if ($('a.btn-close').length) {
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();
}
if ($('a.btn-close').length) this.initIssueBtnEventListeners();
Issue.$btnNewBranch = $('#new-branch');
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
......
<script>
import Visibility from 'visibilityjs';
import { visitUrl } from '../../lib/utils/url_utility';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
import Service from '../services/index';
......@@ -8,7 +9,7 @@ import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
import '../../lib/utils/url_utility';
import RecaptchaDialogImplementor from '../../vue_shared/mixins/recaptcha_dialog_implementor';
export default {
props: {
......@@ -149,6 +150,11 @@ export default {
editedComponent,
formComponent,
},
mixins: [
RecaptchaDialogImplementor,
],
methods: {
openForm() {
if (!this.showForm) {
......@@ -164,12 +170,14 @@ export default {
closeForm() {
this.showForm = false;
},
updateIssuable() {
this.service.updateIssuable(this.store.formState)
.then(res => res.json())
.then(data => this.checkForSpam(data))
.then((data) => {
if (location.pathname !== data.web_url) {
gl.utils.visitUrl(data.web_url);
visitUrl(data.web_url);
}
return this.service.getData();
......@@ -179,11 +187,24 @@ export default {
this.store.updateState(data);
eventHub.$emit('close.form');
})
.catch(() => {
eventHub.$emit('close.form');
window.Flash(`Error updating ${this.issuableType}`);
.catch((error) => {
if (error && error.name === 'SpamError') {
this.openRecaptcha();
} else {
eventHub.$emit('close.form');
window.Flash(`Error updating ${this.issuableType}`);
}
});
},
closeRecaptchaDialog() {
this.store.setFormState({
updateLoading: false,
});
this.closeRecaptcha();
},
deleteIssuable() {
this.service.deleteIssuable()
.then(res => res.json())
......@@ -191,7 +212,7 @@ export default {
// Stop the poll so we don't get 404's with the issuable not existing
this.poll.stop();
gl.utils.visitUrl(data.web_url);
visitUrl(data.web_url);
})
.catch(() => {
eventHub.$emit('close.form');
......@@ -237,9 +258,9 @@ export default {
</script>
<template>
<div>
<div>
<div v-if="canUpdate && showForm">
<form-component
v-if="canUpdate && showForm"
:form-state="formState"
:can-destroy="canDestroy"
:issuable-templates="issuableTemplates"
......@@ -250,30 +271,37 @@ export default {
:show-delete-button="showDeleteButton"
:enable-autocomplete="enableAutocomplete"
/>
<div v-else>
<title-component
:issuable-ref="issuableRef"
:can-update="canUpdate"
:title-html="state.titleHtml"
:title-text="state.titleText"
:show-inline-edit-button="showInlineEditButton"
/>
<description-component
v-if="state.descriptionHtml"
:can-update="canUpdate"
:description-html="state.descriptionHtml"
:description-text="state.descriptionText"
:updated-at="state.updatedAt"
:task-status="state.taskStatus"
:issuable-type="issuableType"
:update-url="updateEndpoint"
/>
<edited-component
v-if="hasUpdated"
:updated-at="state.updatedAt"
:updated-by-name="state.updatedByName"
:updated-by-path="state.updatedByPath"
/>
</div>
<recaptcha-dialog
v-show="showRecaptcha"
:html="recaptchaHTML"
@close="closeRecaptchaDialog"
/>
</div>
<div v-else>
<title-component
:issuable-ref="issuableRef"
:can-update="canUpdate"
:title-html="state.titleHtml"
:title-text="state.titleText"
:show-inline-edit-button="showInlineEditButton"
/>
<description-component
v-if="state.descriptionHtml"
:can-update="canUpdate"
:description-html="state.descriptionHtml"
:description-text="state.descriptionText"
:updated-at="state.updatedAt"
:task-status="state.taskStatus"
:issuable-type="issuableType"
:update-url="updateEndpoint"
/>
<edited-component
v-if="hasUpdated"
:updated-at="state.updatedAt"
:updated-by-name="state.updatedByName"
:updated-by-path="state.updatedByPath"
/>
</div>
</div>
</template>
<script>
import animateMixin from '../mixins/animate';
import TaskList from '../../task_list';
import RecaptchaDialogImplementor from '../../vue_shared/mixins/recaptcha_dialog_implementor';
export default {
mixins: [animateMixin],
mixins: [
animateMixin,
RecaptchaDialogImplementor,
],
props: {
canUpdate: {
type: Boolean,
......@@ -51,6 +56,7 @@
this.updateTaskStatusText();
},
},
methods: {
renderGFM() {
$(this.$refs['gfm-content']).renderGFM();
......@@ -61,9 +67,19 @@
dataType: this.issuableType,
fieldName: '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() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta');
......@@ -109,5 +125,11 @@
:data-update-url="updateUrl"
>
</textarea>
<recaptcha-dialog
v-show="showRecaptcha"
:html="recaptchaHTML"
@close="closeRecaptcha"
/>
</div>
</template>
import _ from 'underscore';
import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
import { bytesToKiB } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils';
......@@ -209,7 +210,7 @@ export default class Job {
}
if (log.status !== this.buildStatus) {
gl.utils.visitUrl(this.pagePath);
visitUrl(this.pagePath);
}
})
.fail(() => {
......
import { getLocationHash } from './url_utility';
export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
......@@ -65,7 +66,7 @@ export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventNa
// automatically adjust scroll position for hash urls taking the height of the navbar into account
// https://github.com/twitter/bootstrap/issues/1768
export const handleLocationHash = () => {
let hash = window.gl.utils.getLocationHash();
let hash = getLocationHash();
if (!hash) return;
// This is required to handle non-unicode characters in hash
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
var base;
var w = window;
if (w.gl == null) {
w.gl = {};
}
if ((base = w.gl).utils == null) {
base.utils = {};
}
// Returns an array containing the value(s) of the
// of the key passed as an argument
w.gl.utils.getParameterValues = function(sParam) {
var i, sPageURL, sParameterName, sURLVariables, values;
sPageURL = decodeURIComponent(window.location.search.substring(1));
sURLVariables = sPageURL.split('&');
sParameterName = void 0;
values = [];
i = 0;
while (i < sURLVariables.length) {
sParameterName = sURLVariables[i].split('=');
export function getParameterValues(sParam) {
const sPageURL = decodeURIComponent(window.location.search.substring(1));
return sPageURL.split('&').reduce((acc, urlParam) => {
const sParameterName = urlParam.split('=');
if (sParameterName[0] === sParam) {
values.push(sParameterName[1].replace(/\+/g, ' '));
acc.push(sParameterName[1].replace(/\+/g, ' '));
}
i += 1;
}
return values;
};
return acc;
}, []);
}
// @param {Object} params - url keys and value to merge
// @param {String} url
w.gl.utils.mergeUrlParams = function(params, url) {
var lastChar, newUrl, paramName, paramValue, pattern;
newUrl = decodeURIComponent(url);
for (paramName in params) {
paramValue = params[paramName];
pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)");
if (paramValue == null) {
newUrl = newUrl.replace(pattern, '');
export function mergeUrlParams(params, url) {
let newUrl = Object.keys(params).reduce((acc, paramName) => {
const paramValue = params[paramName];
const pattern = new RegExp(`\\b(${paramName}=).*?(&|$)`);
if (paramValue === null) {
return acc.replace(pattern, '');
} else if (url.search(pattern) !== -1) {
newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2");
} else {
newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
return acc.replace(pattern, `$1${paramValue}$2`);
}
}
return `${acc}${acc.indexOf('?') > 0 ? '&' : '?'}${paramName}=${paramValue}`;
}, decodeURIComponent(url));
// Remove a trailing ampersand
lastChar = newUrl[newUrl.length - 1];
const lastChar = newUrl[newUrl.length - 1];
if (lastChar === '&') {
newUrl = newUrl.slice(0, -1);
}
return newUrl;
};
// removes parameter query string from url. returns the modified url
w.gl.utils.removeParamQueryString = function(url, param) {
var urlVariables, variables;
url = decodeURIComponent(url);
urlVariables = url.split('&');
return ((function() {
var j, len, results;
results = [];
for (j = 0, len = urlVariables.length; j < len; j += 1) {
variables = urlVariables[j];
if (variables.indexOf(param) === -1) {
results.push(variables);
}
}
return results;
})()).join('&');
};
w.gl.utils.removeParams = (params) => {
}
export function removeParamQueryString(url, param) {
const decodedUrl = decodeURIComponent(url);
const urlVariables = decodedUrl.split('&');
return urlVariables.filter(variable => variable.indexOf(param) === -1).join('&');
}
export function removeParams(params) {
const url = document.createElement('a');
url.href = window.location.href;
params.forEach((param) => {
url.search = w.gl.utils.removeParamQueryString(url.search, param);
url.search = removeParamQueryString(url.search, param);
});
return url.href;
};
w.gl.utils.getLocationHash = function(url) {
var hashIndex;
if (typeof url === 'undefined') {
// Note: We can't use window.location.hash here because it's
// not consistent across browsers - Firefox will pre-decode it
url = window.location.href;
}
hashIndex = url.indexOf('#');
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
};
}
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(window.location.href);
export function getLocationHash(url = window.location.href) {
const hashIndex = url.indexOf('#');
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
}
// eslint-disable-next-line import/prefer-default-export
export function visitUrl(url, external = false) {
if (external) {
// Simulate `target="blank" rel="noopener noreferrer"`
......@@ -100,12 +76,10 @@ export function visitUrl(url, external = false) {
}
}
export function refreshCurrentPage() {
visitUrl(window.location.href);
}
export function redirectTo(url) {
return window.location.assign(url);
}
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
visitUrl,
};
......@@ -29,7 +29,7 @@ import './commit/image_file';
// lib/utils
import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility';
import './lib/utils/url_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility';
// behaviors
import './behaviors/';
......@@ -40,9 +40,6 @@ import './admin';
import './aside';
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal';
import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
......@@ -131,7 +128,7 @@ $(function () {
// `hashchange` is not triggered when link target is already in window.location
$body.on('click', 'a[href^="#"]', function() {
var href = this.getAttribute('href');
if (href.substr(1) === gl.utils.getLocationHash()) {
if (href.substr(1) === getLocationHash()) {
setTimeout(handleLocationHash, 1);
}
});
......@@ -306,7 +303,7 @@ $(function () {
const action = `${this.action}${link.search === '' ? '?' : '&'}`;
event.preventDefault();
gl.utils.visitUrl(`${action}${$(this).serialize()}`);
visitUrl(`${action}${$(this).serialize()}`);
});
/**
......
......@@ -11,6 +11,7 @@ import {
handleLocationHash,
isMetaClick,
} from './lib/utils/common_utils';
import { getLocationHash } from './lib/utils/url_utility';
import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff';
......@@ -317,7 +318,7 @@ import Diff from './diff';
// Scroll any linked note into view
// Similar to `toggler_behavior` in the discussion tab
const hash = window.gl.utils.getLocationHash();
const hash = getLocationHash();
const anchor = hash && $container.find(`.note[id="${hash}"]`);
if (anchor && anchor.length > 0) {
const notesContent = anchor.closest('.notes_content');
......
/* eslint-disable func-names, space-before-function-paren, no-var, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */
import Api from './api';
import './lib/utils/url_utility';
import { mergeUrlParams } from './lib/utils/url_utility';
export default class NamespaceSelect {
constructor(opts) {
......@@ -50,7 +50,7 @@ export default class NamespaceSelect {
}
},
url(namespace) {
return gl.utils.mergeUrlParams({ [fieldName]: namespace.id }, window.location.href);
return mergeUrlParams({ [fieldName]: namespace.id }, window.location.href);
},
});
}
......
......@@ -16,6 +16,7 @@ import Autosize from 'autosize';
import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form';
......@@ -330,7 +331,7 @@ export default class Notes {
}
static updateNoteTargetSelector($note) {
const hash = gl.utils.getLocationHash();
const hash = getLocationHash();
// Needs to be an explicit true/false for the jQuery `toggleClass(force)`
const addTargetClass = Boolean(hash && $note.filter(`#${hash}`).length > 0);
$note.toggleClass('target', addTargetClass);
......
<script>
import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
......@@ -85,7 +86,7 @@
};
this.isRequesting = true;
this.oldContent = this.note.note_html;
this.note.note_html = noteText;
this.note.note_html = escape(noteText);
this.updateNote(data)
.then(() => {
......
<script>
import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility';
import Flash from '../../flash';
import store from '../stores/';
import * as constants from '../constants';
......@@ -95,7 +96,7 @@
this.poll();
},
checkLocationHash() {
const hash = gl.utils.getLocationHash();
const hash = getLocationHash();
const element = document.getElementById(hash);
if (hash && element) {
......
import { getParameterByName } from '~/lib/utils/common_utils';
import '~/lib/utils/url_utility';
import { removeParams } from './lib/utils/url_utility';
(() => {
const ENDLESS_SCROLL_BOTTOM_PX = 400;
......@@ -7,7 +7,7 @@ import '~/lib/utils/url_utility';
const Pager = {
init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) {
this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']);
this.url = $('.content_list').data('href') || removeParams(['limit', 'offset']);
this.limit = limit;
this.offset = parseInt(getParameterByName('offset'), 10) || this.limit;
this.disable = disable;
......
import 'vendor/peek';
import 'vendor/peek.performance_bar';
import { getParameterValues } from './lib/utils/url_utility';
export default class PerformanceBar {
constructor(opts) {
......@@ -39,7 +40,7 @@ export default class PerformanceBar {
}
handleLineProfileLink(e) {
const lineProfilerParameter = gl.utils.getParameterValues('lineprofiler');
const lineProfilerParameter = getParameterValues('lineprofiler');
const lineProfilerParameterRegex = new RegExp(`lineprofiler=${lineProfilerParameter[0]}`);
const shouldToggleModal = lineProfilerParameter.length > 0 &&
lineProfilerParameterRegex.test(e.currentTarget.href);
......
/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
import Cookies from 'js-cookie';
import { visitUrl } from './lib/utils/url_utility';
import projectSelect from './project_select';
export default class Project {
......@@ -134,7 +135,7 @@ export default class Project {
var action = $form.attr('action');
var divider = action.indexOf('?') === -1 ? '?' : '&';
if (shouldVisit) {
gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
visitUrl(`${action}${divider}${$form.serialize()}`);
}
}
},
......
import '../lib/utils/url_utility';
import { getParameterValues } from '../lib/utils/url_utility';
const bindEvents = () => {
const path = gl.utils.getParameterValues('path')[0];
const path = getParameterValues('path')[0];
// get the path url and append it in the inputS
$('.js-path-name').val(path);
......
import Vue from 'vue';
import { visitUrl } from '../../lib/utils/url_utility';
import flash from '../../flash';
import service from '../services';
import * as types from './mutation_types';
export const redirectToUrl = (_, url) => gl.utils.visitUrl(url);
export const redirectToUrl = (_, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
......
import { visitUrl } from '../../../lib/utils/url_utility';
import { normalizeHeaders } from '../../../lib/utils/common_utils';
import flash from '../../../flash';
import service from '../../services';
......@@ -73,7 +74,7 @@ export const clickedTreeRow = ({ commit, dispatch }, row) => {
} else if (row.type === 'submodule') {
commit(types.TOGGLE_LOADING, row);
gl.utils.visitUrl(row.url);
visitUrl(row.url);
} else if (row.type === 'blob' && row.opened) {
dispatch('setFileActive', row);
} else {
......
import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap';
import { refreshCurrentPage, visitUrl } from './lib/utils/url_utility';
import findAndFollowLink from './shortcuts_dashboard_navigation';
const defaultStopCallback = Mousetrap.stopCallback;
......@@ -38,7 +39,7 @@ export default class Shortcuts {
if (typeof findFileURL !== 'undefined' && findFileURL !== null) {
Mousetrap.bind('t', () => {
gl.utils.visitUrl(findFileURL);
visitUrl(findFileURL);
});
}
......@@ -62,7 +63,7 @@ export default class Shortcuts {
} else {
Cookies.set(performanceBarCookieName, 'true', { path: '/' });
}
gl.utils.refreshCurrentPage();
refreshCurrentPage();
}
static toggleMarkdownPreview(e) {
......
/* global Mousetrap */
import { getLocationHash, visitUrl } from './lib/utils/url_utility';
import Shortcuts from './shortcuts';
const defaults = {
......@@ -18,9 +18,9 @@ export default class ShortcutsBlob extends Shortcuts {
moveToFilePermalink() {
if (this.options.fileBlobPermalinkUrl) {
const hash = gl.utils.getLocationHash();
const hash = getLocationHash();
const hashUrlString = hash ? `#${hash}` : '';
gl.utils.visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`);
visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`);
}
}
}
import Store from 'ee/sidebar/stores/sidebar_store';
import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
import Service from './services/sidebar_service';
......@@ -80,7 +81,7 @@ export default class SidebarMediator {
.then(response => response.json())
.then((data) => {
if (location.pathname !== data.web_url) {
gl.utils.visitUrl(data.web_url);
visitUrl(data.web_url);
}
});
}
......
/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */
import { visitUrl } from './lib/utils/url_utility';
import UsersSelect from './users_select';
import { isMetaClick } from './lib/utils/common_utils';
......@@ -150,7 +150,7 @@ export default class Todos {
window.open(todoLink, windowTarget);
} else {
gl.utils.visitUrl(todoLink);
visitUrl(todoLink);
}
}
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, class-methods-use-this */
import { visitUrl } from './lib/utils/url_utility';
export default class TreeView {
constructor() {
......@@ -14,7 +15,7 @@ export default class TreeView {
e.preventDefault();
return window.open(path, '_blank');
} else {
return gl.utils.visitUrl(path);
return visitUrl(path);
}
}
});
......@@ -56,7 +57,7 @@ export default class TreeView {
} else if (e.which === 13) {
path = $('.tree-item.selected .tree-item-file-name a').attr('href');
if (path) {
return gl.utils.visitUrl(path);
return visitUrl(path);
}
}
});
......
import '~/lib/utils/datetime_utility';
import { visitUrl } from '../../lib/utils/url_utility';
import Flash from '../../flash';
import MemoryUsage from './mr_widget_memory_usage';
import StatusIcon from './mr_widget_status_icon';
......@@ -36,7 +37,7 @@ export default {
.then(res => res.json())
.then((res) => {
if (res.redirect_url) {
gl.utils.visitUrl(res.redirect_url);
visitUrl(res.redirect_url);
}
})
.catch(() => {
......
......@@ -38,7 +38,8 @@ export default {
},
primaryButtonLabel: {
type: String,
required: true,
required: false,
default: '',
},
submitDisabled: {
type: Boolean,
......@@ -113,8 +114,9 @@ export default {
{{ closeButtonLabel }}
</button>
<button
v-if="primaryButtonLabel"
type="button"
class="btn pull-right"
class="btn pull-right js-primary-button"
:disabled="submitDisabled"
:class="btnKindClass"
@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;
},
},
};
.page-with-contextual-sidebar {
transition: padding-left $sidebar-transition-duration;
@media (min-width: $screen-md-min) {
padding-left: $contextual-sidebar-collapsed-width;
}
......@@ -27,8 +29,10 @@
.context-header {
position: relative;
margin-right: 2px;
width: $contextual-sidebar-width;
a {
transition: padding $sidebar-transition-duration;
font-weight: $gl-font-weight-bold;
display: flex;
align-items: center;
......@@ -63,10 +67,10 @@
}
.nav-sidebar {
transition: width $sidebar-transition-duration, left $sidebar-transition-duration;
position: fixed;
z-index: 400;
width: $contextual-sidebar-width;
transition: left $sidebar-transition-duration;
top: $header-height;
bottom: 0;
left: 0;
......@@ -74,16 +78,15 @@
box-shadow: inset -2px 0 0 $border-color;
transform: translate3d(0, 0, 0);
&:not(.sidebar-icons-only) {
&:not(.sidebar-collapsed-desktop) {
@media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
box-shadow: inset -2px 0 0 $border-color,
2px 1px 3px $dropdown-shadow-color;
}
}
&.sidebar-icons-only {
width: auto;
min-width: $contextual-sidebar-collapsed-width;
&.sidebar-collapsed-desktop {
width: $contextual-sidebar-collapsed-width;
.nav-sidebar-inner-scroll {
overflow-x: hidden;
......@@ -108,12 +111,11 @@
}
}
&.nav-sidebar-expanded {
&.sidebar-expanded-mobile {
left: 0;
}
a {
transition: none;
text-decoration: none;
}
......@@ -126,9 +128,10 @@
white-space: nowrap;
a {
transition: padding $sidebar-transition-duration;
display: flex;
align-items: center;
padding: 12px 16px;
padding: 12px 15px;
color: $gl-text-color-secondary;
}
......@@ -288,7 +291,8 @@
> a {
margin-left: 4px;
padding-left: 12px;
// Subtract width of left border on active element
padding-left: 11px;
}
.badge {
......@@ -313,6 +317,7 @@
.toggle-sidebar-button,
.close-nav-button {
width: $contextual-sidebar-width - 2px;
transition: width $sidebar-transition-duration;
position: fixed;
bottom: 0;
padding: 16px;
......@@ -343,20 +348,21 @@
}
}
.collapse-text {
white-space: nowrap;
overflow: hidden;
}
.sidebar-icons-only {
.sidebar-collapsed-desktop {
.context-header {
height: 61px;
height: 60px;
width: $contextual-sidebar-collapsed-width;
a {
padding: 10px 4px;
}
}
li a {
padding: 12px 15px;
}
.sidebar-top-level-items > li {
&.active a {
padding-left: 12px;
......@@ -374,8 +380,8 @@
}
.toggle-sidebar-button {
width: $contextual-sidebar-collapsed-width - 2px;
padding: 16px;
width: $contextual-sidebar-collapsed-width - 2px;
.collapse-text,
.icon-angle-double-left {
......
......@@ -47,3 +47,11 @@ body.modal-open {
.modal.popup-dialog {
display: block;
}
.recaptcha-dialog .recaptcha-form {
display: inline-block;
.recaptcha {
margin: 0;
}
}
......@@ -5,10 +5,9 @@ $grid-size: 8px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 250px;
$sidebar-transition-duration: .15s;
$sidebar-transition-duration: .3s;
$sidebar-breakpoint: 1024px;
$default-transition-duration: .15s;
$right-sidebar-transition-duration: .3s;
$contextual-sidebar-width: 220px;
$contextual-sidebar-collapsed-width: 50px;
......
......@@ -92,7 +92,7 @@
position: relative;
@media (min-width: $screen-sm-min) {
transition: width $right-sidebar-transition-duration;
transition: width $sidebar-transition-duration;
width: 100%;
&.is-compact {
......@@ -520,8 +520,8 @@
.right-sidebar.right-sidebar-expanded {
&.boards-sidebar-slide-enter-active,
&.boards-sidebar-slide-leave-active {
transition: width $right-sidebar-transition-duration,
padding $right-sidebar-transition-duration;
transition: width $sidebar-transition-duration,
padding $sidebar-transition-duration;
}
&.boards-sidebar-slide-enter,
......
......@@ -126,7 +126,7 @@
top: $header-height;
bottom: 0;
right: 0;
transition: width $right-sidebar-transition-duration;
transition: width $sidebar-transition-duration;
background: $gray-light;
z-index: 200;
overflow: hidden;
......
......@@ -21,11 +21,11 @@ module IssuableActions
respond_to do |format|
format.html do
recaptcha_check_with_fallback { render :edit }
recaptcha_check_if_spammable { render :edit }
end
format.json do
render_entity_json
recaptcha_check_if_spammable(false) { render_entity_json }
end
end
......@@ -80,6 +80,12 @@ module IssuableActions
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
respond_to do |format|
format.html do
......
......@@ -23,8 +23,8 @@ module SpammableActions
@spam_config_loaded = Gitlab::Recaptcha.load_configurations!
end
def recaptcha_check_with_fallback(&fallback)
if spammable.valid?
def recaptcha_check_with_fallback(should_redirect = true, &fallback)
if should_redirect && spammable.valid?
redirect_to spammable_path
elsif render_recaptcha?
ensure_spam_config_loaded!
......@@ -33,7 +33,18 @@ module SpammableActions
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
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
yield
end
......
......@@ -134,6 +134,23 @@ class Projects::CommitController < Projects::ApplicationController
@grouped_diff_discussions = commit.grouped_diff_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 = prepare_notes_for_rendering(@notes, @commit)
end
......
......@@ -30,7 +30,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:task_num,
:title,
:discussion_locked,
label_ids: []
]
end
......
......@@ -4,6 +4,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
include RendersNotes
before_action :apply_diff_view_cookie!
before_action :commit
before_action :define_diff_vars
before_action :define_diff_comment_vars
......@@ -20,18 +21,33 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
private
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 =
if params[:diff_id]
@merge_request.merge_request_diffs.viewable.find(params[:diff_id])
if diff_id = params[:diff_id].presence
@merge_request.merge_request_diffs.viewable.find_by(id: diff_id)
else
@merge_request.merge_request_diff
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 }
if params[:start_sha].present?
@start_sha = params[:start_sha]
if @start_sha = params[:start_sha].presence
@start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
unless @start_version
......@@ -40,20 +56,18 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
end
end
@compare =
if @start_sha
@merge_request_diff.compare_with(@start_sha)
else
@merge_request_diff
end
@diffs = @compare.diffs(diff_options)
if @start_sha
@merge_request_diff.compare_with(@start_sha)
else
@merge_request_diff
end
end
def define_diff_comment_vars
@new_diff_note_attrs = {
noteable_type: 'MergeRequest',
noteable_id: @merge_request.id
noteable_id: @merge_request.id,
commit_id: @commit&.id
}
@diff_notes_disabled = false
......
......@@ -9,11 +9,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
prepend ::EE::Projects::MergeRequestsController
skip_before_action :merge_request, only: [:index, :bulk_update]
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues]
def index
......
......@@ -275,7 +275,7 @@ class ProjectsController < Projects::ApplicationController
render 'projects/empty' if @project.empty_repo?
else
if @project.wiki_enabled?
if can?(current_user, :read_wiki, @project)
@project_wiki = @project.wiki
@wiki_home = @project_wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user)
......
......@@ -228,4 +228,12 @@ module CommitsHelper
[commits, 0]
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
......@@ -140,6 +140,30 @@ module MergeRequestsHelper
}.merge(merge_params_ee(merge_request))
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)
{ squash: merge_request.squash }
end
......
......@@ -58,7 +58,7 @@ module PreferencesHelper
user_view
elsif user_view == "activity"
"activity"
elsif @project.wiki_enabled?
elsif can?(current_user, :read_wiki, @project)
"wiki"
elsif @project.feature_available?(:issues, current_user)
"projects/issues/issues"
......
# coding: utf-8
class Commit
extend ActiveModel::Naming
extend Gitlab::Cache::RequestCache
......@@ -25,7 +26,7 @@ class Commit
DIFF_HARD_LIMIT_FILES = 1000
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
def banzai_render_context(field)
......
......@@ -32,6 +32,10 @@ module DiscussionOnDiff
first_note.position.new_path
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
def truncated_diff_lines(highlight: true)
lines = highlight ? highlighted_diff_lines : diff_lines
......
......@@ -24,7 +24,11 @@ class DiffDiscussion < Discussion
return unless for_merge_request?
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
def reply_attributes
......
......@@ -22,6 +22,7 @@ class DiffNote < Note
validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
validate :positions_complete
validate :verify_supported
validate :diff_refs_match_commit, if: :for_commit?
before_validation :set_original_position, on: :create
before_validation :update_position, on: :create, if: :on_text?
......@@ -140,6 +141,12 @@ class DiffNote < Note
errors.add(:position, "is invalid")
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
project.repository.keep_around(self.original_position.base_sha)
project.repository.keep_around(self.original_position.start_sha)
......
......@@ -11,6 +11,7 @@ class Discussion
:author,
:noteable,
:commit_id,
:for_commit?,
:for_merge_request?,
......
......@@ -667,6 +667,7 @@ class MergeRequest < ActiveRecord::Base
.to_sql
Note.from("(#{union}) #{Note.table_name}")
.includes(:noteable)
end
alias_method :discussion_notes, :related_notes
......@@ -942,21 +943,27 @@ class MergeRequest < ActiveRecord::Base
.order(id: :desc)
end
# Note that this could also return SHA from now dangling commits
#
def all_commit_shas
return commit_shas unless persisted?
diffs_relation = merge_request_diffs
def all_commits
# 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
.where(merge_request_diff: diffs_relation)
.limit(10_000)
.pluck('sha')
.uniq
end
# 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
def merge_commit
......
......@@ -41,6 +41,7 @@ class Namespace < ActiveRecord::Base
namespace_path: true
validate :nesting_level_allowed
validate :allowed_path_by_redirects
delegate :name, to: :owner, allow_nil: true, prefix: true
......@@ -270,4 +271,14 @@ class Namespace < ActiveRecord::Base
Namespace.where(id: descendants.select(:id))
.update_all(share_with_group_lock: true)
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
......@@ -236,16 +236,18 @@ class Note < ActiveRecord::Base
for_personal_snippet?
end
def commit
@commit ||= project.commit(commit_id) if commit_id.present?
end
# override to return commits, which are not active record
def noteable
if for_commit?
@commit ||= project.commit(commit_id)
else
super
end
# Temp fix to prevent app crash
# if note commit id doesn't exist
return commit if for_commit?
super
rescue
# Temp fix to prevent app crash
# if note commit id doesn't exist
nil
end
......@@ -375,6 +377,42 @@ class Note < ActiveRecord::Base
Gitlab::EtagCaching::Store.new.touch(key)
end
def touch(*args)
# We're not using an explicit transaction here because this would in all
# cases result in all future queries going to the primary, even if no writes
# are performed.
#
# We touch the noteable first so its SELECT query can run before our writes,
# ensuring it runs on a secondary (if no prior write took place).
touch_noteable
super
end
# By default Rails will issue an "SELECT *" for the relation, which is
# overkill for just updating the timestamps. To work around this we manually
# touch the data so we can SELECT only the columns we need.
def touch_noteable
# Commits are not stored in the DB so we can't touch them.
return if for_commit?
assoc = association(:noteable)
noteable_object =
if assoc.loaded?
noteable
else
# If the object is not loaded (e.g. when notes are loaded async) we
# _only_ want the data we actually need.
assoc.scope.select(:id, :updated_at).take
end
noteable_object&.touch
end
def banzai_render_context(field)
super.merge(noteable: noteable)
end
private
def keep_around_commit
......
......@@ -17,4 +17,32 @@ class RedirectRoute < ActiveRecord::Base
where(wheres, path, "#{sanitize_sql_like(path)}/%")
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
......@@ -8,6 +8,8 @@ class Route < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
validate :ensure_permanent_paths
after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :path_changed?
after_update :create_redirect_for_old_path
......@@ -40,7 +42,7 @@ class Route < ActiveRecord::Base
# We are not calling route.delete_conflicting_redirects here, in hopes
# of avoiding deadlocks. The parent (self, in this method) already
# 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
......@@ -50,16 +52,30 @@ class Route < ActiveRecord::Base
end
def conflicting_redirects
RedirectRoute.matching_path_and_descendants(path)
RedirectRoute.temporary.matching_path_and_descendants(path)
end
def create_redirect(path)
RedirectRoute.create(source: source, path: path)
def create_redirect(path, permanent: false)
RedirectRoute.create(source: source, path: path, permanent: permanent)
end
private
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
......@@ -329,6 +329,8 @@ class User < ActiveRecord::Base
#
# Returns an ActiveRecord::Relation.
def search(query)
query = query.downcase
order = <<~SQL
CASE
WHEN users.name = %{query} THEN 0
......@@ -338,8 +340,11 @@ class User < ActiveRecord::Base
END
SQL
fuzzy_search(query, [:name, :email, :username])
.reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
where(
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
# searches user by given pattern
......@@ -347,15 +352,17 @@ class User < ActiveRecord::Base
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
def search_with_secondary_emails(query)
query = query.downcase
email_table = Email.arel_table
matched_by_emails_user_ids = email_table
.project(email_table[:user_id])
.where(Email.fuzzy_arel_match(:email, query))
.where(email_table[:email].eq(query))
where(
fuzzy_arel_match(:name, query)
.or(fuzzy_arel_match(:email, query))
.or(fuzzy_arel_match(:username, query))
.or(arel_table[:email].eq(query))
.or(arel_table[:id].in(matched_by_emails_user_ids))
)
end
......
......@@ -14,19 +14,23 @@ module Ci
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, mirror_update: false, &block)
@pipeline = Ci::Pipeline.new
command = OpenStruct.new(source: source,
origin_ref: params[:ref],
checkout_sha: params[:checkout_sha],
after_sha: params[:after],
before_sha: params[:before],
trigger_request: trigger_request,
schedule: schedule,
ignore_skip_ci: ignore_skip_ci,
save_incompleted: save_on_errors,
allow_mirror_update: mirror_update,
seeds_block: block,
project: project,
current_user: current_user)
command = Gitlab::Ci::Pipeline::Chain::Command.new(
source: source,
origin_ref: params[:ref],
checkout_sha: params[:checkout_sha],
after_sha: params[:after],
before_sha: params[:before],
trigger_request: trigger_request,
schedule: schedule,
ignore_skip_ci: ignore_skip_ci,
save_incompleted: save_on_errors,
seeds_block: block,
project: project,
current_user: current_user,
# EE specific
allow_mirror_update: mirror_update
)
sequence = Gitlab::Ci::Pipeline::Chain::Sequence
.new(pipeline, command, SEQUENCE)
......
......@@ -6,7 +6,7 @@ module MergeRequests
@oldrev, @newrev = oldrev, newrev
@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
# empty diff during a manual merge
close_merge_requests
......
......@@ -32,9 +32,17 @@
- elsif discussion.diff_discussion?
on
= conditional_link_to url.present?, url do
- unless discussion.active?
an old version of
the diff
- if discussion.on_merge_request_commit?
- unless discussion.active?
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")
= render "discussions/headline", discussion: discussion
......
- humanized_resource_name = spammable.class.model_name.human.downcase
- resource_name = spammable.class.model_name.singular
%h3.page-title
Anti-spam verification
......@@ -8,16 +7,4 @@
%p
#{"We detected potential spam in the #{humanized_resource_name}. Please solve the reCAPTCHA to proceed."}
= form_for form do |f|
.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'
= render 'shared/recaptcha_form', spammable: spammable
.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
.context-header
= link_to admin_root_path, title: 'Admin Overview' do
......
......@@ -5,7 +5,7 @@
- if @group.feature_available?(:group_issue_boards)
- issues_sub_menu_items.push('boards#index', 'boards#show')
.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
.context-header
= link_to group_path(@group), title: @group.name do
......
.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
.context-header
= link_to profile_path, title: 'Profile Settings' do
......
.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
- can_edit = can?(current_user, :admin_project, @project)
.context-header
......
......@@ -47,7 +47,7 @@
%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)
.commit-box
.commit-box{ data: { project_path: project_path(@project) } }
%h3.commit-title
= markdown(@commit.title, pipeline: :single_line, author: @commit.author)
- if @commit.description.present?
......@@ -80,3 +80,13 @@
- if last_pipeline.duration
in
= 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 @@
- @content_class = limited_container_width
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
- 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] }
= render "commit_box"
......
- ref = local_assigns.fetch(:ref)
- view_details = local_assigns.fetch(:view_details, false)
- merge_request = local_assigns.fetch(:merge_request, nil)
- project = local_assigns.fetch(:project) { merge_request&.project }
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
- cache_key = [project.full_path, commit.id, current_application_settings, @path.presence, current_controller?(:commits), I18n.locale]
- cache_key.push(commit.status(ref)) if commit.status(ref)
- 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
-# EE-only
- show_project_name = local_assigns.fetch(:show_project_name, false)
......@@ -15,7 +26,7 @@
.commit-detail
.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
&middot;
= commit.short_id
......@@ -38,8 +49,7 @@
%span.project_namespace
= project.name_with_namespace
.commit-actions.hidden-xs
.commit-actions.flex-row.hidden-xs
- if request.xhr?
= render partial: 'projects/commit/signature', object: commit.signature
- else
......@@ -48,6 +58,9 @@
- if commit.status(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"))
= 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"
- ref = local_assigns.fetch(:ref)
- merge_request = local_assigns.fetch(:merge_request, nil)
- project = local_assigns.fetch(:project) { merge_request&.project }
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
- commits, hidden = limited_commits(@commits)
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits|
......@@ -8,7 +11,7 @@
%li.commits-row{ data: { day: day } }
%ul.content-list.commit-list.flex-list
= render partial: 'projects/commits/commit', collection: commits, locals: { project: project, ref: ref }
= render partial: 'projects/commits/commit', collection: commits, locals: { project: project, ref: ref, merge_request: merge_request }
- if hidden > 0
%li.alert.alert-warning
......
......@@ -5,4 +5,4 @@
= custom_icon ('illustration_no_commits')
- else
%ol#commits-list.list-unstyled
= render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch
= render "projects/commits/commits", merge_request: @merge_request
- if @commit
.info-well.hidden-xs.prepend-top-default
.well-segment
%ul.blob-commit-info
= render 'projects/commits/commit', commit: @commit, merge_request: @merge_request, view_details: true
- if @merge_request_diff && different_base?(@start_version, @merge_request_diff)
.mr-version-controls
.content-block
= icon('info-circle')
Selected versions have different base commits.
Changes will include
= link_to project_compare_path(@project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do
new commits
from
= succeed '.' do
%code.ref-name= @merge_request.target_branch
- if @merge_request_diff.collected? || @merge_request_diff.overflow?
= render 'projects/merge_requests/diffs/versions'
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true
- elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
= render 'projects/merge_requests/diffs/version_controls'
= render 'projects/merge_requests/diffs/different_base'
= render 'projects/merge_requests/diffs/not_all_comments_displayed'
= render 'projects/merge_requests/diffs/commit_widget'
- if @merge_request_diff&.empty?
.nothing-here-block
= image_tag 'illustrations/merge_request_changes_empty.svg'
= succeed '.' do
No changes between
%span.ref-name= @merge_request.source_branch
and
%span.ref-name= @merge_request.target_branch
%p= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-save'
- else
- diff_viewable = @merge_request_diff ? @merge_request_diff.collected? || @merge_request_diff.overflow? : true
- if diff_viewable
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true
- if @commit || @start_version || (@merge_request_diff && !@merge_request_diff.latest?)
.mr-version-controls
.content-block.comments-disabled-notif.clearfix
= icon('info-circle')
= succeed '.' do
- if @commit
Only comments from the following commit are shown below
- else
Not all comments are displayed because you're
- if @start_version
comparing two versions of the diff
- else
viewing an old version of the diff
.pull-right
= link_to diffs_project_merge_request_path(@merge_request.project, @merge_request), class: 'btn btn-sm' do
Show latest version
= "of the diff" if @commit
- if @merge_request_diffs.size > 1
- if @merge_request_diff && @merge_request_diffs.size > 1
.mr-version-controls
.mr-version-menus-container.content-block
Changes between
......@@ -71,27 +71,3 @@
(base)
%div
%strong.commit-sha= short_sha(@merge_request_diff.base_commit_sha)
- if different_base?(@start_version, @merge_request_diff)
.content-block
= icon('info-circle')
Selected versions have different base commits.
Changes will include
= link_to project_compare_path(@project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do
new commits
from
= succeed '.' do
%code= @merge_request.target_branch
- if @start_version || !@merge_request_diff.latest?
.comments-disabled-notif.content-block
= icon('info-circle')
Not all comments are displayed because you're
- if @start_version
comparing two versions
- else
viewing an old version
of the diff.
.pull-right
= link_to 'Show latest version', diffs_project_merge_request_path(@project, @merge_request), class: 'btn btn-sm'
......@@ -9,7 +9,7 @@
= webpack_bundle_tag('diff_notes')
= webpack_bundle_tag('issuable')
.merge-request{ 'data-mr-action': "#{j params[:tab].presence || 'show'}", 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) }
.merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project) } }
= render "projects/merge_requests/mr_title"
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
......@@ -46,21 +46,21 @@
.nav-links.scrolling-tabs
%ul.merge-request-tabs
%li.notes-tab
= link_to project_merge_request_path(@project, @merge_request), data: { target: 'div#notes', action: 'show', toggle: 'tab' } do
= tab_link_for @merge_request, :show, force_link: @commit.present? do
Discussion
%span.badge= @merge_request.related_notes.user.count
- if @merge_request.source_project
%li.commits-tab
= link_to commits_project_merge_request_path(@project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
= tab_link_for @merge_request, :commits do
Commits
%span.badge= @commits_count
- if @pipelines.any?
%li.pipelines-tab
= link_to pipelines_project_merge_request_path(@project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
= tab_link_for @merge_request, :pipelines do
Pipelines
%span.badge.js-pipelines-mr-count= @pipelines.size
%li.diffs-tab
= link_to diffs_project_merge_request_path(@project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
= tab_link_for @merge_request, :diffs do
Changes
%span.badge= @merge_request.diff_size
#resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true }
......
- resource_name = spammable.class.model_name.singular
- humanized_resource_name = spammable.class.model_name.human.downcase
- script = local_assigns.fetch(:script, true)
- has_submit = local_assigns.fetch(:has_submit, true)
= form_for resource_name, method: :post, html: { class: 'recaptcha-form js-recaptcha-form' } do |f|
.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 script: script, callback: 'recaptchaDialogCallback'
-# Yields a block with given extra params.
= yield
- if has_submit
.row-content-block.footer-block
= f.submit "Submit #{humanized_resource_name}", class: 'btn btn-create'
---
title: Add recaptcha modal to issue updates detected as spam
merge_request: 15408
author:
type: fixed
---
title: Allow git pull/push on group/user/project redirects
merge_request: 15670
author:
type: added
---
title: Animate contextual sidebar on collapse/expand
merge_request:
author:
type: changed
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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