Commit 21755ac9 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into copy-as-md

# Conflicts:
#	app/assets/javascripts/lib/utils/common_utils.js.es6
parents 43dc6263 b55c1bc4
......@@ -15,6 +15,7 @@
"filenames"
],
"rules": {
"filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"]
"filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"],
"no-multiple-empty-lines": ["error", { "max": 1 }]
}
}
......@@ -46,7 +46,7 @@ linters:
max: 80
MultilinePipe:
enabled: false
enabled: true
MultilineScript:
enabled: true
......@@ -77,7 +77,7 @@ linters:
- Style/WhileUntilModifier
RubyComments:
enabled: false
enabled: true
SpaceBeforeScript:
enabled: true
......@@ -97,7 +97,7 @@ linters:
enabled: true
UnnecessaryInterpolation:
enabled: false
enabled: true
UnnecessaryStringOutput:
enabled: false
enabled: true
......@@ -2,6 +2,14 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 8.16.1 (2017-01-23)
- Ensure export files are removed after a namespace is deleted.
- Don't allow project guests to subscribe to merge requests through the API. (Robert Schilling)
- Prevent users from creating notes on resources they can't access.
- Prevent users from deleting system deploy keys via the project deploy key API.
- Upgrade omniauth gem to 1.3.2.
## 8.16.0 (2017-02-22)
- Add LDAP Rake task to rename a provider. !2181
......
......@@ -261,6 +261,9 @@
case 'projects:artifacts:browse':
new BuildArtifacts();
break;
case 'help:index':
gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
break;
case 'search:show':
new Search();
break;
......
......@@ -58,6 +58,7 @@ var CustomEvent = require('./custom_event_polyfill');
var utils = require('./utils');
var DropDown = function(list) {
this.currentIndex = 0;
this.hidden = true;
this.list = list;
this.items = [];
......@@ -164,15 +165,21 @@ Object.assign(DropDown.prototype, {
},
show: function() {
// debugger
this.list.style.display = 'block';
this.hidden = false;
if (this.hidden) {
// debugger
this.list.style.display = 'block';
this.currentIndex = 0;
this.hidden = false;
}
},
hide: function() {
// debugger
this.list.style.display = 'none';
this.hidden = true;
if (!this.hidden) {
// debugger
this.list.style.display = 'none';
this.currentIndex = 0;
this.hidden = true;
}
},
destroy: function() {
......@@ -478,6 +485,8 @@ Object.assign(HookInput.prototype, {
this.input = function input(e) {
if(self.hasRemovedEvents) return;
self.list.show();
var inputEvent = new CustomEvent('input.dl', {
detail: {
hook: self,
......@@ -487,7 +496,6 @@ Object.assign(HookInput.prototype, {
cancelable: true
});
e.target.dispatchEvent(inputEvent);
self.list.show();
}
this.keyup = function keyup(e) {
......@@ -503,6 +511,8 @@ Object.assign(HookInput.prototype, {
}
function keyEvent(e, keyEventName){
self.list.show();
var keyEvent = new CustomEvent(keyEventName, {
detail: {
hook: self,
......@@ -514,7 +524,6 @@ Object.assign(HookInput.prototype, {
cancelable: true
});
e.target.dispatchEvent(keyEvent);
self.list.show();
}
this.events = this.events || {};
......@@ -572,24 +581,43 @@ require('./window')(function(w){
module.exports = function(){
var currentKey;
var currentFocus;
var currentIndex = 0;
var isUpArrow = false;
var isDownArrow = false;
var removeHighlight = function removeHighlight(list) {
var listItems = list.list.querySelectorAll('li');
var listItems = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider)'), 0);
var listItemsTmp = [];
for(var i = 0; i < listItems.length; i++) {
listItems[i].classList.remove('dropdown-active');
var listItem = listItems[i];
listItem.classList.remove('dropdown-active');
if (listItem.style.display !== 'none') {
listItemsTmp.push(listItem);
}
}
return listItems;
return listItemsTmp;
};
var setMenuForArrows = function setMenuForArrows(list) {
var listItems = removeHighlight(list);
if(currentIndex>0){
if(!listItems[currentIndex-1]){
currentIndex = currentIndex-1;
if(list.currentIndex>0){
if(!listItems[list.currentIndex-1]){
list.currentIndex = list.currentIndex-1;
}
if (listItems[list.currentIndex-1]) {
var el = listItems[list.currentIndex-1];
var filterDropdownEl = el.closest('.filter-dropdown');
el.classList.add('dropdown-active');
if (filterDropdownEl) {
var filterDropdownBottom = filterDropdownEl.offsetHeight;
var elOffsetTop = el.offsetTop - 30;
if (elOffsetTop > filterDropdownBottom) {
filterDropdownEl.scrollTop = elOffsetTop - filterDropdownBottom;
}
}
}
listItems[currentIndex-1].classList.add('dropdown-active');
}
};
......@@ -597,13 +625,13 @@ require('./window')(function(w){
var list = e.detail.hook.list;
removeHighlight(list);
list.show();
currentIndex = 0;
list.currentIndex = 0;
isUpArrow = false;
isDownArrow = false;
};
var selectItem = function selectItem(list) {
var listItems = removeHighlight(list);
var currentItem = listItems[currentIndex-1];
var currentItem = listItems[list.currentIndex-1];
var listEvent = new CustomEvent('click.dl', {
detail: {
list: list,
......@@ -617,6 +645,8 @@ require('./window')(function(w){
var keydown = function keydown(e){
var typedOn = e.target;
var list = e.detail.hook.list;
var currentIndex = list.currentIndex;
isUpArrow = false;
isDownArrow = false;
......@@ -648,6 +678,7 @@ require('./window')(function(w){
if(isUpArrow){ currentIndex--; }
if(isDownArrow){ currentIndex++; }
if(currentIndex < 0){ currentIndex = 0; }
list.currentIndex = currentIndex;
setMenuForArrows(e.detail.hook.list);
};
......
......@@ -29,6 +29,7 @@ require('../window')(function(w){
init: function init(hook) {
var self = this;
var config = hook.config.droplabAjax;
this.hook = hook;
if (!config || !config.endpoint || !config.method) {
return;
......@@ -52,19 +53,26 @@ require('../window')(function(w){
this._loadUrlData(config.endpoint)
.then(function(d) {
if (config.loadingTemplate) {
var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]');
var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
if (dataLoadingTemplate) {
dataLoadingTemplate.outerHTML = self.listTemplate;
}
}
hook.list[config.method].call(hook.list, d);
if (!self.hook.list.hidden) {
self.hook.list[config.method].call(self.hook.list, d);
}
}).catch(function(e) {
throw new droplabAjaxException(e.message || e);
});
},
destroy: function() {
if (this.listTemplate) {
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
dynamicList.outerHTML = this.listTemplate;
}
}
};
});
......@@ -76,4 +84,4 @@ module.exports = function(callback) {
};
},{}]},{},[1])(1)
});
\ No newline at end of file
});
......@@ -93,6 +93,7 @@ require('../window')(function(w){
self.hook.list.setData.call(self.hook.list, data);
}
self.notLoading();
self.hook.list.currentIndex = 0;
});
},
......@@ -142,4 +143,4 @@ module.exports = function(callback) {
};
},{}]},{},[1])(1)
});
\ No newline at end of file
});
......@@ -6,6 +6,8 @@ require('../window')(function(w){
w.droplabFilter = {
keydownWrapper: function(e){
var hiddenCount = 0;
var dataHiddenCount = 0;
var list = e.detail.hook.list;
var data = list.data;
var value = e.detail.hook.trigger.value.toLowerCase();
......@@ -27,10 +29,22 @@ require('../window')(function(w){
};
}
dataHiddenCount = data.filter(function(o) {
return !o.droplab_hidden;
}).length;
matches = data.map(function(o) {
return filterFunction(o, value);
});
list.render(matches);
hiddenCount = matches.filter(function(o) {
return !o.droplab_hidden;
}).length;
if (dataHiddenCount !== hiddenCount) {
list.render(matches);
list.currentIndex = 0;
}
},
init: function init(hookInput) {
......@@ -57,4 +71,4 @@ module.exports = function(callback) {
};
},{}]},{},[1])(1)
});
\ No newline at end of file
});
......@@ -3,7 +3,6 @@
//= require ./components/environment
//= require ./vue_resource_interceptor
$(() => {
window.gl = window.gl || {};
......
......@@ -84,7 +84,7 @@
let inputValue = input.value;
// Replace all spaces inside quote marks with underscores
// This helps with matching the beginning & end of a token:key
inputValue = inputValue.replace(/"(.*?)"/g, str => str.replace(/\s/g, '_'));
inputValue = inputValue.replace(/("(.*?)"|:\s+)/g, str => str.replace(/\s/g, '_'));
// Get the right position for the word selected
// Regex matches first space
......
......@@ -64,13 +64,26 @@
}
checkForEnter(e) {
if (e.keyCode === 38 || e.keyCode === 40) {
const selectionStart = this.filteredSearchInput.selectionStart;
e.preventDefault();
this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
}
if (e.keyCode === 13) {
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
const dropdownEl = dropdown.element;
const activeElements = dropdownEl.querySelectorAll('.dropdown-active');
e.preventDefault();
// Prevent droplab from opening dropdown
this.dropdownManager.destroyDroplab();
if (!activeElements.length) {
// Prevent droplab from opening dropdown
this.dropdownManager.destroyDroplab();
this.search();
this.search();
}
}
}
......
......@@ -61,7 +61,6 @@
return labels;
}
/**
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
......@@ -80,7 +79,6 @@
return result;
}
/**
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
......
......@@ -215,5 +215,19 @@
const matchingNodes = parentNode.querySelectorAll(selector);
return Array.prototype.indexOf.call(matchingNodes, node) !== -1;
};
/**
this will take in the headers from an API response and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
*/
w.gl.utils.normalizeHeaders = (headers) => {
const upperCaseHeaders = {};
Object.keys(headers).forEach((e) => {
upperCaseHeaders[e.toUpperCase()] = headers[e];
});
return upperCaseHeaders;
};
})(window);
}).call(this);
......@@ -220,7 +220,6 @@
})(this));
};
/*
Increase @pollingInterval up to 120 seconds on every function call,
if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
......@@ -244,7 +243,6 @@
return this.initRefresh();
};
Notes.prototype.handleCreateChanges = function(note) {
if (typeof note === 'undefined') {
return;
......@@ -294,7 +292,6 @@
}
};
/*
Check if note does not exists on page
*/
......@@ -307,7 +304,6 @@
return this.view === 'parallel';
};
/*
Render note in discussion area.
......@@ -358,7 +354,6 @@
return this.updateNotesCount(1);
};
/*
Called in response the main target form has been successfully submitted.
......@@ -390,7 +385,6 @@
return form.find(".js-note-text").trigger("input");
};
/*
Shows the main form and does some setup on it.
......@@ -415,7 +409,6 @@
return this.parentTimeline = form.parents('.timeline');
};
/*
General note form setup.
......@@ -432,7 +425,6 @@
return new Autosave(textarea, ["Note", form.find("#note_noteable_type").val(), form.find("#note_noteable_id").val(), form.find("#note_commit_id").val(), form.find("#note_type").val(), form.find("#note_line_code").val(), form.find("#note_position").val()]);
};
/*
Called in response to the new note form being submitted
......@@ -448,7 +440,6 @@
return new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', this.parentTimeline);
};
/*
Called in response to the new note form being submitted
......@@ -473,7 +464,6 @@
this.removeDiscussionNoteForm($form);
};
/*
Called in response to the edit note form being submitted
......@@ -498,7 +488,6 @@
}
};
Notes.prototype.checkContentToAllowEditing = function($el) {
var initialContent = $el.find('.original-note-content').text().trim();
var currentContent = $el.find('.note-textarea').val();
......@@ -522,7 +511,6 @@
return isAllowed;
};
/*
Called in response to clicking the edit note link
......@@ -551,7 +539,6 @@
this.putEditFormInPlace($target);
};
/*
Called in response to clicking the edit note link
......@@ -596,7 +583,6 @@
return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note'));
};
/*
Called in response to deleting a note of any kind.
......@@ -636,7 +622,6 @@
return this.updateNotesCount(-1);
};
/*
Called in response to clicking the delete attachment link
......@@ -653,7 +638,6 @@
return note.find(".current-note-edit-form").remove();
};
/*
Called when clicking on the "reply" button for a diff line.
......@@ -673,7 +657,6 @@
return this.setupDiscussionNoteForm(replyLink, form);
};
/*
Shows the diff or discussion form and does some setup on it.
......@@ -715,7 +698,6 @@
.addClass("discussion-form js-discussion-note-form");
};
/*
Called when clicking on the "add a comment" button on the side of a diff line.
......@@ -772,7 +754,6 @@
}
};
/*
Called in response to "cancel" on a diff note form.
......@@ -806,7 +787,6 @@
return this.removeDiscussionNoteForm(form);
};
/*
Called after an attachment file has been selected.
......@@ -821,7 +801,6 @@
return form.find(".js-attachment-filename").text(filename);
};
/*
Called when the tab visibility changes
*/
......
(() => {
class VersionCheckImage {
static bindErrorEvent(imageElement) {
imageElement.off('error').on('error', () => imageElement.hide());
}
}
window.gl = window.gl || {};
gl.VersionCheckImage = VersionCheckImage;
})();
......@@ -22,47 +22,51 @@
<div class="controls pull-right">
<div class="btn-group inline">
<div class="btn-group">
<a
<button
v-if='actions'
class="dropdown-toggle btn btn-default js-pipeline-dropdown-manual-actions"
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
data-toggle="dropdown"
title="Manual build"
alt="Manual Build"
data-placement="top"
data-toggle="dropdown"
aria-label="Manual build"
>
<span v-html='svgs.iconPlay'></span>
<i class="fa fa-caret-down"></i>
</a>
<span v-html='svgs.iconPlay' aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='action in pipeline.details.manual_actions'>
<a
rel="nofollow"
data-method="post"
:href='action.path'
title="Manual build"
>
<span v-html='svgs.iconPlay'></span>
<span title="Manual build">{{action.name}}</span>
<span v-html='svgs.iconPlay' aria-hidden="true"></span>
<span>{{action.name}}</span>
</a>
</li>
</ul>
</div>
<div class="btn-group">
<a
<button
v-if='artifacts'
class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
data-toggle="dropdown"
title="Artifacts"
data-placement="top"
data-toggle="dropdown"
type="button"
aria-label="Artifacts"
>
<i class="fa fa-download"></i>
<i class="fa fa-caret-down"></i>
</a>
<i class="fa fa-download" aria-hidden="true"></i>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='artifact in pipeline.details.artifacts'>
<a
rel="nofollow"
:href='artifact.path'
>
<i class="fa fa-download"></i>
<i class="fa fa-download" aria-hidden="true"></i>
<span>{{download(artifact.name)}}</span>
</a>
</li>
......@@ -76,9 +80,12 @@
title="Retry"
rel="nofollow"
data-method="post"
data-placement="top"
data-toggle="dropdown"
:href='pipeline.retry_path'
aria-label="Retry"
>
<i class="fa fa-repeat"></i>
<i class="fa fa-repeat" aria-hidden="true"></i>
</a>
<a
v-if='pipeline.flags.cancelable'
......@@ -86,10 +93,12 @@
title="Cancel"
rel="nofollow"
data-method="post"
data-placement="top"
data-toggle="dropdown"
:href='pipeline.cancel_path'
data-original-title="Cancel"
aria-label="Cancel"
>
<i class="fa fa-remove"></i>
<i class="fa fa-remove" aria-hidden="true"></i>
</a>
</div>
</div>
......
......@@ -82,12 +82,13 @@
data-placement="top"
data-toggle="dropdown"
type="button"
:aria-label='stage.title'
>
<span v-html="svg"></span>
<i class="fa fa-caret-down "></i>
<span v-html="svg" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up"></div>
<div class="arrow-up" aria-hidden="true"></div>
<div
@click='keepGraph($event)'
:class="dropdownClass"
......
......@@ -4,19 +4,15 @@
((gl) => {
const pageValues = (headers) => {
const normalizedHeaders = {};
Object.keys(headers).forEach((e) => {
normalizedHeaders[e.toUpperCase()] = headers[e];
});
const normalized = gl.utils.normalizeHeaders(headers);
const paginationInfo = {
perPage: +normalizedHeaders['X-PER-PAGE'],
page: +normalizedHeaders['X-PAGE'],
total: +normalizedHeaders['X-TOTAL'],
totalPages: +normalizedHeaders['X-TOTAL-PAGES'],
nextPage: +normalizedHeaders['X-NEXT-PAGE'],
previousPage: +normalizedHeaders['X-PREV-PAGE'],
perPage: +normalized['X-PER-PAGE'],
page: +normalized['X-PAGE'],
total: +normalized['X-TOTAL'],
totalPages: +normalized['X-TOTAL-PAGES'],
nextPage: +normalized['X-NEXT-PAGE'],
previousPage: +normalized['X-PREV-PAGE'],
};
return paginationInfo;
......
......@@ -82,7 +82,12 @@
}
.block-controls {
float: right;
display: -webkit-flex;
display: flex;
-webkit-justify-content: flex-end;
justify-content: flex-end;
-webkit-flex: 1;
flex: 1;
.control {
float: left;
......@@ -282,3 +287,8 @@
}
}
}
.flex-container-block {
display: -webkit-flex;
display: flex;
}
......@@ -79,6 +79,16 @@
overflow: auto;
}
%filter-dropdown-item-btn-hover {
background-color: $dropdown-hover-color;
color: $white-light;
text-decoration: none;
.avatar {
border-color: $white-light;
}
}
.filter-dropdown-item {
.btn {
border: none;
......@@ -103,13 +113,7 @@
&:hover,
&:focus {
background-color: $dropdown-hover-color;
color: $white-light;
text-decoration: none;
.avatar {
border-color: $white-light;
}
@extend %filter-dropdown-item-btn-hover;
}
}
......@@ -131,6 +135,12 @@
}
}
.filter-dropdown-item.dropdown-active {
.btn {
@extend %filter-dropdown-item-btn-hover;
}
}
.hint-dropdown {
width: 250px;
}
......
......@@ -203,6 +203,10 @@
position: relative;
margin-right: 6px;
.tooltip {
white-space: nowrap;
}
.tooltip-inner {
padding: 3px 4px;
}
......@@ -288,6 +292,10 @@
}
}
}
.tooltip {
white-space: nowrap;
}
}
.build-link {
......
......@@ -929,8 +929,32 @@ pre.light-well {
.variables-table {
table-layout: fixed;
&.table-responsive {
border: none;
}
.variable-key {
width: 30%;
width: 300px;
max-width: 300px;
overflow: hidden;
word-wrap: break-word;
// override bootstrap
white-space: normal!important;
@media (max-width: $screen-sm-max) {
width: 150px;
max-width: 150px;
}
}
.variable-value {
@media(max-width: $screen-xs-max) {
width: 150px;
max-width: 150px;
overflow: hidden;
word-wrap: break-word;
}
}
}
......
......@@ -5,7 +5,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def update
if @application_setting.update_attributes(application_setting_params)
successful = ApplicationSettings::UpdateService
.new(@application_setting, current_user, application_setting_params)
.execute
if successful
redirect_to admin_application_settings_path,
notice: 'Application settings saved successfully'
else
......
......@@ -3,7 +3,7 @@ module CompareHelper
from.present? &&
to.present? &&
from != to &&
project.feature_available?(:merge_requests, current_user) &&
can?(current_user, :create_merge_request, project) &&
project.repository.branch_names.include?(from) &&
project.repository.branch_names.include?(to)
end
......
......@@ -14,7 +14,7 @@ module GroupsHelper
def group_title(group, name = nil, url = nil)
full_title = ''
group.parents.each do |parent|
group.ancestors.each do |parent|
full_title += link_to(simple_sanitize(parent.name), group_path(parent))
full_title += ' / '.html_safe
end
......
module VersionCheckHelper
def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled
image_tag VersionCheck.new.url
image_url = VersionCheck.new.url
image_tag image_url, class: 'js-version-status-badge'
end
end
end
......@@ -38,6 +38,14 @@ module Emails
mail_answer_thread(@snippet, note_thread_options(recipient_id))
end
def note_personal_snippet_email(recipient_id, note_id)
setup_note_mail(note_id, recipient_id)
@snippet = @note.noteable
@target_url = snippet_url(@note.noteable)
mail_answer_thread(@snippet, note_thread_options(recipient_id))
end
private
def note_target_url_options
......
......@@ -22,6 +22,17 @@ class Ability
end
end
# Given a list of users and a snippet this method returns the users that can
# read the given snippet.
def users_that_can_read_personal_snippet(users, snippet)
case snippet.visibility_level
when Snippet::INTERNAL, Snippet::PUBLIC
users
when Snippet::PRIVATE
users.include?(snippet.author) ? [snippet.author] : []
end
end
# Returns an Array of Issues that can be read by the given user.
#
# issues - The issues to reduce down to those readable by the user.
......
......@@ -13,6 +13,49 @@ class ApplicationSetting < ActiveRecord::Base
[\r\n] # any number of newline characters
}x
DEFAULTS_CE = {
after_sign_up_text: nil,
akismet_enabled: false,
container_registry_token_expire_delay: 5,
default_branch_protection: Settings.gitlab['default_branch_protection'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_projects_limit: Settings.gitlab['default_projects_limit'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
disabled_oauth_sign_in_sources: [],
domain_whitelist: Settings.gitlab['domain_whitelist'],
gravatar_enabled: Settings.gravatar['enabled'],
help_page_text: nil,
housekeeping_bitmaps_enabled: true,
housekeeping_enabled: true,
housekeeping_full_repack_period: 50,
housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10,
import_sources: Gitlab::ImportSources.values,
koding_enabled: false,
koding_url: nil,
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
plantuml_enabled: false,
plantuml_url: nil,
recaptcha_enabled: false,
repository_checks_enabled: true,
repository_storages: ['default'],
require_two_factor_authentication: false,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
session_expire_delay: Settings.gitlab['session_expire_delay'],
send_user_confirmation_email: false,
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
shared_runners_text: nil,
sidekiq_throttling_enabled: false,
sign_in_text: nil,
signin_enabled: Settings.gitlab['signin_enabled'],
signup_enabled: Settings.gitlab['signup_enabled'],
two_factor_grace_period: 48,
user_default_external: false
}
DEFAULTS = DEFAULTS_CE
serialize :restricted_visibility_levels
serialize :import_sources
serialize :disabled_oauth_sign_in_sources, Array
......@@ -163,46 +206,7 @@ class ApplicationSetting < ActiveRecord::Base
end
def self.create_from_defaults
create(
default_projects_limit: Settings.gitlab['default_projects_limit'],
default_branch_protection: Settings.gitlab['default_branch_protection'],
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
sign_in_text: nil,
after_sign_up_text: nil,
help_page_text: nil,
shared_runners_text: nil,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
session_expire_delay: Settings.gitlab['session_expire_delay'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
domain_whitelist: Settings.gitlab['domain_whitelist'],
import_sources: Gitlab::ImportSources.values,
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
two_factor_grace_period: 48,
recaptcha_enabled: false,
akismet_enabled: false,
koding_enabled: false,
koding_url: nil,
plantuml_enabled: false,
plantuml_url: nil,
repository_checks_enabled: true,
disabled_oauth_sign_in_sources: [],
send_user_confirmation_email: false,
container_registry_token_expire_delay: 5,
repository_storages: ['default'],
user_default_external: false,
sidekiq_throttling_enabled: false,
housekeeping_enabled: true,
housekeeping_bitmaps_enabled: true,
housekeeping_incremental_repack_period: 10,
housekeeping_full_repack_period: 50,
housekeeping_gc_period: 200,
)
create(DEFAULTS)
end
def home_page_url_column_exist
......
......@@ -51,6 +51,10 @@ module CacheMarkdownField
CACHING_CLASSES.map(&:constantize)
end
def skip_project_check?
false
end
extend ActiveSupport::Concern
included do
......@@ -112,7 +116,8 @@ module CacheMarkdownField
invalidation_method = "#{html_field}_invalidated?".to_sym
define_method(cache_method) do
html = Banzai::Renderer.cacheless_render_field(self, markdown_field)
options = { skip_project_check: skip_project_check? }
html = Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
__send__("#{html_field}=", html)
true
end
......
......@@ -49,7 +49,11 @@ module Mentionable
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr)
options = options.merge(cache_key: [self, attr], author: author)
options = options.merge(
cache_key: [self, attr],
author: author,
skip_project_check: skip_project_check?
)
extractor.analyze(text, options)
end
......@@ -121,4 +125,8 @@ module Mentionable
def cross_reference_exists?(target)
SystemNoteService.cross_reference_exists?(target, local_reference)
end
def skip_project_check?
false
end
end
......@@ -96,6 +96,11 @@ module Participable
participants.merge(ext.users)
Ability.users_that_can_read_project(participants.to_a, project)
case self
when PersonalSnippet
Ability.users_that_can_read_personal_snippet(participants.to_a, self)
else
Ability.users_that_can_read_project(participants.to_a, project)
end
end
end
......@@ -60,6 +60,21 @@ module Routable
joins(:route).where(wheres.join(' OR '))
end
end
# Builds a relation to find multiple objects that are nested under user membership
#
# Usage:
#
# Klass.member_descendants(1)
#
# Returns an ActiveRecord::Relation.
def member_descendants(user_id)
joins(:route).
joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%')
INNER JOIN members ON members.source_id = r2.source_id
AND members.source_type = r2.source_type").
where('members.user_id = ?', user_id)
end
end
private
......
......@@ -201,7 +201,7 @@ class Group < Namespace
end
def members_with_parents
GroupMember.where(requested_at: nil, source_id: parents.map(&:id).push(id))
GroupMember.where(requested_at: nil, source_id: ancestors.map(&:id).push(id))
end
def users_with_parents
......
......@@ -865,9 +865,11 @@ class MergeRequest < ActiveRecord::Base
paths: paths
)
active_diff_notes.each do |note|
service.execute(note)
Gitlab::Timeless.timeless(note, &:save)
transaction do
active_diff_notes.each do |note|
service.execute(note)
Gitlab::Timeless.timeless(note, &:save)
end
end
end
......
......@@ -185,8 +185,26 @@ class Namespace < ActiveRecord::Base
end
end
def parents
@parents ||= parent ? parent.parents + [parent] : []
# Scopes the model on ancestors of the record
def ancestors
if parent_id
path = route.path
paths = []
until path.blank?
path = path.rpartition('/').first
paths << path
end
self.class.joins(:route).where('routes.path IN (?)', paths).reorder('routes.path ASC')
else
self.class.none
end
end
# Scopes the model on direct and indirect children of the record
def descendants
self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC')
end
private
......
......@@ -43,7 +43,8 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true
delegate :title, to: :noteable, allow_nil: true
validates :note, :project, presence: true
validates :note, presence: true
validates :project, presence: true, unless: :for_personal_snippet?
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
......@@ -53,7 +54,7 @@ class Note < ActiveRecord::Base
validates :commit_id, presence: true, if: :for_commit?
validates :author, presence: true
validate unless: [:for_commit?, :importing?] do |note|
validate unless: [:for_commit?, :importing?, :for_personal_snippet?] do |note|
unless note.noteable.try(:project) == note.project
errors.add(:invalid_project, 'Note and noteable project mismatch')
end
......@@ -83,7 +84,7 @@ class Note < ActiveRecord::Base
after_initialize :ensure_discussion_id
before_validation :nullify_blank_type, :nullify_blank_line_code
before_validation :set_discussion_id
after_save :keep_around_commit
after_save :keep_around_commit, unless: :for_personal_snippet?
class << self
def model_name
......@@ -165,6 +166,14 @@ class Note < ActiveRecord::Base
noteable_type == "Snippet"
end
def for_personal_snippet?
noteable.is_a?(PersonalSnippet)
end
def skip_project_check?
for_personal_snippet?
end
# override to return commits, which are not active record
def noteable
if for_commit?
......@@ -220,6 +229,10 @@ class Note < ActiveRecord::Base
note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
end
def to_ability_name
for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore
end
private
def keep_around_commit
......
......@@ -8,15 +8,16 @@ class Route < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
after_update :rename_children, if: :path_changed?
after_update :rename_descendants, if: :path_changed?
def rename_children
def rename_descendants
# We update each row separately because MySQL does not have regexp_replace.
# rubocop:disable Rails/FindEach
Route.where('path LIKE ?', "#{path_was}/%").each do |route|
# Note that update column skips validation and callbacks.
# We need this to avoid recursive call of rename_children method
# We need this to avoid recursive call of rename_descendants method
route.update_column(:path, route.path.sub(path_was, path))
end
# rubocop:enable Rails/FindEach
end
end
......@@ -179,8 +179,8 @@ class User < ActiveRecord::Base
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) }
scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'ASC')) }
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
......@@ -439,6 +439,15 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})")
end
def nested_groups
Group.member_descendants(id)
end
def nested_projects
Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL').
member_descendants(id)
end
def refresh_authorized_projects
Users::RefreshAuthorizedProjectsService.new(self).execute
end
......
module ApplicationSettings
class BaseService < ::BaseService
def initialize(application_setting, user, params = {})
@application_setting, @current_user, @params = application_setting, user, params.dup
end
end
end
module ApplicationSettings
class UpdateService < ApplicationSettings::BaseService
def execute
@application_setting.update(@params)
end
end
end
......@@ -3,9 +3,10 @@ module Notes
def execute
merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
note = project.notes.new(params)
note.author = current_user
note.system = false
note = Note.new(params)
note.project = project
note.author = current_user
note.system = false
if note.award_emoji?
noteable = note.noteable
......
......@@ -10,6 +10,9 @@ module Notes
# Skip system notes, like status changes and cross-references and awards
unless @note.system?
EventCreateService.new.leave_note(@note, @note.author)
return if @note.for_personal_snippet?
@note.create_cross_references!
execute_note_hooks
end
......
......@@ -12,7 +12,7 @@ module Notes
def self.supported?(note, current_user)
noteable_update_service(note) &&
current_user &&
current_user.can?(:"update_#{note.noteable_type.underscore}", note.noteable)
current_user.can?(:"update_#{note.to_ability_name}", note.noteable)
end
def supported?(note)
......
......@@ -178,8 +178,15 @@ class NotificationService
recipients = []
mentioned_users = note.mentioned_users
ability, subject = if note.for_personal_snippet?
[:read_personal_snippet, note.noteable]
else
[:read_project, note.project]
end
mentioned_users.select! do |user|
user.can?(:read_project, note.project)
user.can?(ability, subject)
end
# Add all users participating in the thread (author, assignee, comment authors)
......@@ -192,11 +199,13 @@ class NotificationService
recipients = recipients.concat(participants)
# Merge project watchers
recipients = add_project_watchers(recipients, note.project)
unless note.for_personal_snippet?
# Merge project watchers
recipients = add_project_watchers(recipients, note.project)
# Merge project with custom notification
recipients = add_custom_notifications(recipients, note.project, :new_note)
# Merge project with custom notification
recipients = add_custom_notifications(recipients, note.project, :new_note)
end
# Reject users with Mention notification level, except those mentioned in _this_ note.
recipients = reject_mention_users(recipients - mentioned_users, note.project)
......@@ -211,8 +220,7 @@ class NotificationService
recipients.delete(note.author)
recipients = recipients.uniq
# build notify method like 'note_commit_email'
notify_method = "note_#{note.noteable_type.underscore}_email".to_sym
notify_method = "note_#{note.to_ability_name}_email".to_sym
recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later
......
......@@ -22,17 +22,7 @@ module Projects
return @project
end
# Set project name from path
if @project.name.present? && @project.path.present?
# if both name and path set - everything is ok
elsif @project.path.present?
# Set project name from path
@project.name = @project.path.dup
elsif @project.name.present?
# For compatibility - set path from name
# TODO: remove this in 8.0
@project.path = @project.name.dup.parameterize
end
set_project_name_from_path
# get namespace id
namespace_id = params[:namespace_id]
......@@ -144,5 +134,19 @@ module Projects
service.save!
end
end
def set_project_name_from_path
# Set project name from path
if @project.name.present? && @project.path.present?
# if both name and path set - everything is ok
elsif @project.path.present?
# Set project name from path
@project.name = @project.path.dup
elsif @project.name.present?
# For compatibility - set path from name
# TODO: remove this in 8.0
@project.path = @project.name.dup.parameterize
end
end
end
end
......@@ -118,7 +118,8 @@ module Users
user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
user.groups_projects.select_for_project_authorization,
user.projects.select_for_project_authorization,
user.groups.joins(:shared_projects).select_for_project_authorization
user.groups.joins(:shared_projects).select_for_project_authorization,
user.nested_projects.select_for_project_authorization
]
Gitlab::SQL::Union.new(relations)
......
......@@ -4,7 +4,7 @@
- if @broadcast_message.message.present?
= render_broadcast_message(@broadcast_message)
- else
= "Your message here"
Your message here
= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f|
= form_errors(@broadcast_message)
......
%tr
%td
= "#{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})"
#{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})
%td
= identity.extern_uid
%td
......
......@@ -11,7 +11,7 @@
that for future communication.
%br
Registration token is
%code{ id: 'runners-token' } #{current_application_settings.runners_registration_token}
%code#runners-token= current_application_settings.runners_registration_token
.bs-callout.clearfix
.pull-left
......
......@@ -100,7 +100,7 @@
%td.build-link
- if project
= link_to ci_status_path(build.pipeline) do
%strong #{build.pipeline.short_sha}
%strong= build.pipeline.short_sha
%td.timestamp
- if build.finished_at
......
......@@ -10,7 +10,7 @@
%h4 CPU
.data
- if @cpus
%h1= "#{@cpus.length} cores"
%h1 #{@cpus.length} cores
- else
= icon('warning', class: 'text-warning')
Unable to collect CPU info
......@@ -19,7 +19,7 @@
%h4 Memory
.data
- if @memory
%h1= "#{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}"
%h1 #{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}
- else
= icon('warning', class: 'text-warning')
Unable to collect memory info
......@@ -28,6 +28,6 @@
%h4 Disks
.data
- @disks.each do |disk|
%h1= "#{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}"
%p= "#{disk[:disk_name]}"
%p= "#{disk[:mount_path]}"
%h1 #{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}
%p= disk[:disk_name]
%p= disk[:mount_path]
......@@ -186,6 +186,6 @@
- if @user.solo_owned_groups.present?
%p
This user is currently an owner in these groups:
%strong #{@user.solo_owned_groups.map(&:name).join(', ')}
%strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
You must transfer ownership or delete these groups before you can delete this user.
......@@ -13,7 +13,7 @@
.file-holder
.file-title.clearfix
Content of .gitlab-ci.yml
#ci-editor.ci-editor #{@content}
#ci-editor.ci-editor= @content
= text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
.col-sm-12
.pull-left.prepend-top-10
......
- expanded = discussion.expanded?
%li.note.note-discussion.timeline-entry
.timeline-entry-inner
.timeline-icon
= link_to user_path(discussion.author) do
= image_tag avatar_icon(discussion.author), class: "avatar s40"
.timeline-content
.discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } }
.discussion-header
......
......@@ -6,7 +6,7 @@
.content{ class: ('hide' unless discussion_left.expanded?) }
= render "discussions/notes", discussion: discussion_left, line_type: 'old'
- else
%td.notes_line.old= ""
%td.notes_line.old= ("")
%td.notes_content.parallel.old
.content
......@@ -16,6 +16,6 @@
.content{ class: ('hide' unless discussion_right.expanded?) }
= render "discussions/notes", discussion: discussion_right, line_type: 'new'
- else
%td.notes_line.new= ""
%td.notes_line.new= ("")
%td.notes_content.parallel.new
.content
......@@ -10,7 +10,7 @@
%p
= icon("exclamation-triangle fw")
You are an admin, which means granting access to
%strong #{@pre_auth.client.name}
%strong= @pre_auth.client.name
will allow them to interact with GitLab as an admin as well. Proceed with caution.
- if @pre_auth.scopes
......
......@@ -25,7 +25,7 @@
.panel.panel-default
.panel-heading
Users with access to
%strong #{@group.name}
%strong= @group.name
%span.badge= @members.total_count
%ul.content-list
= render partial: 'shared/members/member', collection: @members, as: :member
......
......@@ -18,7 +18,7 @@
.row-content-block.second-block
Only issues from the
%strong #{@group.name}
%strong= @group.name
group are listed here.
- if current_user
To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
......
......@@ -10,7 +10,7 @@
.row-content-block.second-block
Only merge requests from
%strong #{@group.name}
%strong= @group.name
group are listed here.
- if current_user
To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
......
......@@ -10,7 +10,7 @@
.row-content-block
Only milestones from
%strong #{@group.name}
%strong= @group.name
group are listed here.
.milestones
......
......@@ -16,7 +16,7 @@
%colgroup.import-jobs-status-col
%thead
%tr
%th= "From #{provider_title}"
%th From #{provider_title}
%th To GitLab
%th Status
%tbody
......
......@@ -37,7 +37,7 @@
%tbody
- @user_map.each do |id, user|
%tr
%td= id
%td= (id)
%td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control'
%td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control'
%td
......
......@@ -50,7 +50,7 @@
%td
= repo.name
%td.import-target
= "#{current_user.username}/#{repo.name}"
#{current_user.username}/#{repo.name}
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
Import
......
......@@ -55,7 +55,7 @@
%td
= link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
%td.import-target
= "#{current_user.username}/#{repo.name}"
#{current_user.username}/#{repo.name}
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
Import
......
......@@ -2,9 +2,9 @@
Assignee changed
- if @previous_assignee
from
%strong #{@previous_assignee.name}
%strong= @previous_assignee.name
to
- if issuable.assignee_id
%strong #{issuable.assignee_name}
%strong= issuable.assignee_name
- else
%strong Unassigned
%p
= "Issue was closed by #{@updated_by.name}"
Issue was closed by #{@updated_by.name}
= "Issue was closed by #{@updated_by.name}"
Issue was closed by #{@updated_by.name}
Issue ##{@issue.iid}: #{namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)}
%p
= "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}"
Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}
= "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}"
Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
......
%p
= "Issue was #{@issue_status} by #{@updated_by.name}"
Issue was #{@issue_status} by #{@updated_by.name}
%p
= "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}"
Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}
= "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}"
Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
......
%p
= "Merge Request #{@merge_request.to_reference} was merged"
Merge Request #{@merge_request.to_reference} was merged
= "Merge Request #{@merge_request.to_reference} was merged"
Merge Request #{@merge_request.to_reference} was merged
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
......
New comment for Snippet <%= @snippet.id %>
<%= url_for(snippet_url(@snippet, anchor: "note_#{@note.id}")) %>
Author: <%= @note.author_name %>
<%= @note.note %>
......@@ -139,7 +139,7 @@
had
= failed.size
failed
= "#{'build'.pluralize(failed.size)}."
#{'build'.pluralize(failed.size)}.
%tr.warning
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" }
Logs may contain sensitive data. Please consider before forwarding this email.
......
......@@ -138,9 +138,9 @@
%a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= "\##{@pipeline.id}"
successfully completed
= "#{build_count} #{'build'.pluralize(build_count)}"
#{build_count} #{'build'.pluralize(build_count)}
in
= "#{stage_count} #{'stage'.pluralize(stage_count)}."
#{stage_count} #{'stage'.pluralize(stage_count)}.
%tr.footer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
%img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
......
= "Project #{@project.name} couldn't be exported."
Project #{@project.name} couldn't be exported.
= "The errors we encountered were:"
The errors we encountered were:
- @errors.each do |error|
#{error}
......@@ -17,7 +17,7 @@
%ul
- @message.commits.each do |commit|
%li
%strong #{link_to(commit.short_id, namespace_project_commit_url(@message.project_namespace, @message.project, commit))}
%strong= link_to(commit.short_id, namespace_project_commit_url(@message.project_namespace, @message.project, commit))
%div
%span by #{commit.author_name}
%i at #{commit.committed_date.to_s(:iso8601)}
......
......@@ -102,7 +102,7 @@
= f.text_field :username, required: true, class: 'form-control'
.help-block
Current path:
= "#{root_url}#{current_user.username}"
#{root_url}#{current_user.username}
.prepend-top-default
= f.button class: "btn btn-warning", type: "submit" do
= icon "spinner spin", class: "hidden loading-username"
......@@ -128,7 +128,7 @@
- if @user.solo_owned_groups.present?
%p
Your account is currently an owner in these groups:
%strong #{@user.solo_owned_groups.map(&:name).join(', ')}
%strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
You must transfer ownership or delete these groups before you can delete your account.
.append-bottom-default
......@@ -62,7 +62,7 @@
%span.help-block
Please click the link in the confirmation email before continuing. It was sent to
= succeed "." do
%strong #{@user.unconfirmed_email}
%strong= @user.unconfirmed_email
%p
= link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post
......
......@@ -23,7 +23,7 @@
= markdown_toolbar_button({ icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
= markdown_toolbar_button({ icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
.toolbar-group
%button.toolbar-btn.js-zen-enter.has-tooltip.hidden-xs{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
%button.toolbar-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
= icon("arrows-alt fw")
.md-write-holder
......
......@@ -34,7 +34,7 @@
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
.file-editor.code
%pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]}
%pre.js-edit-mode-pane#editor= params[:content] || local_assigns[:blob_data]
- if local_assigns[:path]
.js-edit-mode-pane#preview.hide
.center
......
.file-content.image_file
- if blob.svg?
- if blob.size_within_svg_limits?
- # We need to scrub SVG but we cannot do so in the RawController: it would
- # be wrong/strange if RawController modified the data.
-# We need to scrub SVG but we cannot do so in the RawController: it would
-# be wrong/strange if RawController modified the data.
- blob.load_all_data!(@repository)
- blob = sanitize_svg(blob)
%img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}", alt: "#{blob.name}" }
......
......@@ -3,7 +3,7 @@
.modal-content
.modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title #{title}
%h3.page-title= title
.modal-body
= form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal' do
.dropzone
......
......@@ -85,7 +85,7 @@
- if build.finished_at
%p.finished-at
= icon("calendar")
%span #{time_ago_with_tooltip(build.finished_at)}
%span= time_ago_with_tooltip(build.finished_at)
%td.coverage
- if coverage && build.try(:coverage)
......
......@@ -78,7 +78,7 @@
.btn-group.inline
- if actions.any?
.btn-group
%button.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{ type: 'button', 'data-toggle' => 'dropdown' }
%button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual build', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Manual build' }
= custom_icon('icon_play')
= icon('caret-down', 'aria-hidden' => 'true')
%ul.dropdown-menu.dropdown-menu-align-right
......@@ -89,7 +89,7 @@
%span= build.name
- if artifacts.present?
.btn-group
%button.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{ type: 'button', 'data-toggle' => 'dropdown' }
%button.dropdown-toggle.btn.btn-default.build-artifacts.has-tooltip.js-pipeline-dropdown-download{ type: 'button', title: 'Artifacts', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Artifacts' }
= icon("download")
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
......@@ -102,8 +102,8 @@
- if can?(current_user, :update_pipeline, pipeline.project)
.cancel-retry-btns.inline
- if pipeline.retryable?
= link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do
= link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: 'Retry', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Retry' , method: :post do
= icon("repeat")
- if pipeline.cancelable?
= link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
= link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: 'Cancel', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Cancel' , method: :post do
= icon("remove")
......@@ -2,7 +2,7 @@
- commits, hidden = limited_commits(@commits)
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits|
%li.commit-header= "#{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}"
%li.commit-header #{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}
%li.commits-row
%ul.content-list.commit-list.table-list.table-wide
= render commits, project: project, ref: ref
......
......@@ -9,10 +9,13 @@
= render "head"
%div{ class: container_class }
.row-content-block.second-block.content-component-block
.row-content-block.second-block.content-component-block.flex-container-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits'
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
.block-controls.hidden-xs.hidden-sm
- if @merge_request.present?
.control
......@@ -30,8 +33,6 @@
.control
= link_to namespace_project_commits_path(@project.namespace, @project, @ref, { format: :atom, private_token: current_user.private_token }), title: "Commits Feed", class: 'btn' do
= icon("rss")
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
%div{ id: dom_id(@project) }
%ol#commits-list.list-unstyled.content_list
......
......@@ -16,9 +16,9 @@
There isn't anything to compare.
%p.slead
- if params[:to] == params[:from]
%span.label-branch #{params[:from]}
%span.label-branch= params[:from]
and
%span.label-branch #{params[:to]}
%span.label-branch= params[:to]
are the same.
- else
You'll need to use different branch names to get a valid comparison.
%tr.deployment
%td
%strong= "##{deployment.iid}"
%strong ##{deployment.iid}
%td
= render 'projects/deployments/commit', deployment: deployment
......@@ -8,7 +8,7 @@
%td.build-column
- if deployment.deployable
= link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do
= "#{deployment.deployable.name} (##{deployment.deployable.id})"
#{deployment.deployable.name} (##{deployment.deployable.id})
- if deployment.user
by
= user_avatar(user: deployment.user, size: 20)
......
.diff-content.diff-wrap-lines
- # Skip all non non-supported blobs
-# Skip all non non-supported blobs
- return unless blob.respond_to?(:text?)
- if diff_file.too_large?
.nothing-here-block This diff could not be displayed because it is too large.
......
......@@ -25,4 +25,4 @@
- if diff_file.mode_changed?
%small
= "#{diff_file.a_mode}#{diff_file.b_mode}"
#{diff_file.a_mode}#{diff_file.b_mode}
......@@ -9,7 +9,7 @@
%span.wrap
.frame{ class: image_diff_class(diff) }
%img{ src: diff.deleted_file ? old_file_raw_path : file_raw_path, alt: diff.new_path }
%p.image-info= "#{number_to_human_size file.size}"
%p.image-info= number_to_human_size(file.size)
- else
.image
.two-up.view
......@@ -18,7 +18,7 @@
%a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path)) }
%img{ src: old_file_raw_path, alt: diff.old_path }
%p.image-info.hide
%span.meta-filesize= "#{number_to_human_size old_file.size}"
%span.meta-filesize= number_to_human_size(old_file.size)
|
%b W:
%span.meta-width
......@@ -30,7 +30,7 @@
%a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path)) }
%img{ src: file_raw_path, alt: diff.new_path }
%p.image-info.hide
%span.meta-filesize= "#{number_to_human_size file.size}"
%span.meta-filesize= number_to_human_size(file.size)
|
%b W:
%span.meta-width
......
......@@ -2,7 +2,7 @@
.commit-stat-summary
Showing
= link_to '#', class: 'js-toggle-button' do
%strong #{pluralize(diff_files.size, "changed file")}
%strong= pluralize(diff_files.size, "changed file")
with
%strong.cgreen #{diff_files.sum(&:added_lines)} additions
and
......
.top-area
.nav-text
- full_count_title = "#{@public_forks_count} public and #{@private_forks_count} private"
= "#{pluralize(@total_forks_count, 'fork')}: #{full_count_title}"
#{pluralize(@total_forks_count, 'fork')}: #{full_count_title}
.nav-controls
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
......
......@@ -77,7 +77,7 @@
- if generic_commit_status.finished_at
%p.finished-at
= icon("calendar")
%span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
%span= time_ago_with_tooltip(generic_commit_status.finished_at)
%td.coverage
- if coverage && generic_commit_status.try(:coverage)
......
......@@ -11,7 +11,7 @@
%p.lead
Commit statistics for
%strong #{@ref}
%strong= @ref
#{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')}
.row
......@@ -19,19 +19,19 @@
%ul
%li
%p.lead
%strong #{@commits_graph.commits.size}
%strong= @commits_graph.commits.size
commits during
%strong #{@commits_graph.duration}
%strong= @commits_graph.duration
days
%li
%p.lead
Average
%strong #{@commits_graph.commit_per_day}
%strong= @commits_graph.commit_per_day
commits per day
%li
%p.lead
Contributed by
%strong #{@commits_graph.authors}
%strong= @commits_graph.authors
authors
.col-md-6
%div
......
......@@ -3,9 +3,9 @@
%p.slead
- source_title, target_title = format_mr_branch_names(@merge_request)
From
%strong.label-branch #{source_title}
%strong.label-branch= source_title
%span into
%strong.label-branch #{target_title}
%strong.label-branch= target_title
%span.pull-right
= link_to 'Change branches', mr_change_branches_path(@merge_request)
......@@ -43,7 +43,7 @@
#commits.commits.tab-pane.active
= render "projects/merge_requests/show/commits"
#diffs.diffs.tab-pane
- # This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
- if @pipelines.any?
#pipelines.pipelines.tab-pane
= render "projects/merge_requests/show/pipelines"
......
......@@ -92,11 +92,11 @@
= render "projects/merge_requests/discussion"
#commits.commits.tab-pane
- # This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
#pipelines.pipelines.tab-pane
- # This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
#diffs.diffs.tab-pane
- # This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
.mr-loading-status
= spinner
......
......@@ -25,7 +25,7 @@
latest version
- else
version #{version_index(merge_request_diff)}
.monospace #{short_sha(merge_request_diff.head_commit_sha)}
.monospace= short_sha(merge_request_diff.head_commit_sha)
%small
#{number_with_delimiter(merge_request_diff.commits_count)} #{'commit'.pluralize(merge_request_diff.commits_count)},
= time_ago_with_tooltip(merge_request_diff.created_at)
......@@ -55,14 +55,14 @@
latest version
- else
version #{version_index(merge_request_diff)}
.monospace #{short_sha(merge_request_diff.head_commit_sha)}
.monospace= short_sha(merge_request_diff.head_commit_sha)
%small
= time_ago_with_tooltip(merge_request_diff.created_at)
%li
= link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
%strong
#{@merge_request.target_branch} (base)
.monospace #{short_sha(@merge_request_diff.base_commit_sha)}
.monospace= short_sha(@merge_request_diff.base_commit_sha)
- if different_base?(@start_version, @merge_request_diff)
.content-block
......@@ -72,7 +72,7 @@
= link_to namespace_project_compare_path(@project.namespace, @project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do
new commits
from
%code #{@merge_request.target_branch}
%code= @merge_request.target_branch
- unless @merge_request_diff.latest? && !@start_sha
.comments-disabled-notif.content-block
......
......@@ -13,8 +13,8 @@
%span.ci-coverage
- elsif @merge_request.has_ci?
- # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
- # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API
-# Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
-# TODO, remove in later versions when services like Jenkins will set CI status via Commit status API
.mr-widget-heading
- %w[success skipped canceled failed running pending].each do |status|
.ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: "display:none" }
......
......@@ -69,7 +69,7 @@
- if note_editable
.original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
#{note.note}
%textarea.hidden.js-task-list-field.original-task-list #{note.note}
%textarea.hidden.js-task-list-field.original-task-list= note.note
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system
......
.panel.panel-default
.panel-heading
Group members with access to
%strong #{@group.name}
%strong= @group.name
%span.badge= members.size
- if can?(current_user, :admin_group_member, @group)
.controls
......
.panel.panel-default.project-members-groups
.panel-heading
Groups with access to
%strong #{@project.name}
%strong= @project.name
%span.badge= group_links.size
%ul.content-list
= render partial: 'shared/members/group', collection: group_links, as: :group_link
......@@ -5,9 +5,9 @@
.panel.panel-default
.panel-heading
Shared with
%strong #{shared_group.name}
%strong= shared_group.name
group, members with
%strong #{group_links.human_access}
%strong= group_links.human_access
role (#{shared_group_users_count})
- if can?(current_user, :admin_group, shared_group)
.panel-head-actions
......
.panel.panel-default
.panel-heading
Members with access to
%strong #{@project.name}
%strong= @project.name
%span.badge= @project_members.total_count
= form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
.form-group
......
......@@ -7,7 +7,7 @@
.oneline
.title
Release notes for tag
%strong #{@tag.name}
%strong= @tag.name
= form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f|
......
......@@ -9,10 +9,10 @@
(checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} for information on how to install it).
%li
Specify the following URL during the Runner setup:
%code #{ci_root_url(only_path: false)}
%code= ci_root_url(only_path: false)
%li
Use the following registration token during setup:
%code #{@project.runners_token}
%code= @project.runners_token
%li
Start the Runner!
......
......@@ -4,4 +4,4 @@
.col-sm-9.col-sm-offset-3
= link_to new_namespace_project_mattermost_path(@project.namespace, @project), class: 'btn btn-lg' do
= custom_icon('mattermost_logo', size: 15)
= 'Add to Mattermost'
Add to Mattermost
......@@ -3,4 +3,4 @@
%h4
= icon('search')
We couldn't find any results matching
%code #{@search_term}
%code= @search_term
......@@ -2,7 +2,7 @@
%h4
= link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do
%span.term.str-truncated= merge_request.title
.pull-right #{merge_request.to_reference}
.pull-right= merge_request.to_reference
- if merge_request.description.present?
.description.term
= preserve do
......
......@@ -9,7 +9,7 @@
= link_to user_snippets_path(snippet.author) do
= image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: ''
= snippet.author_name
%span.light #{time_ago_with_tooltip(snippet.created_at)}
%span.light= time_ago_with_tooltip(snippet.created_at)
%h4.snippet-title
- snippet_path = reliable_snippet_path(snippet)
= link_to snippet_path do
......
......@@ -20,4 +20,4 @@
= link_to user_snippets_path(snippet_title.author) do
= image_tag avatar_icon(snippet_title.author_email), class: "avatar avatar-inline s16", alt: ''
= snippet_title.author_name
%span.light #{time_ago_with_tooltip(snippet_title.created_at)}
%span.light= time_ago_with_tooltip(snippet_title.created_at)
......@@ -14,7 +14,7 @@
To prevent accidental actions we ask you to confirm your intention.
%br
Please type
%code.js-confirm-danger-match #{phrase}
%code.js-confirm-danger-match= phrase
to proceed or close this modal to cancel.
.form-group
......
......@@ -6,14 +6,14 @@
= link_to milestones_filter_path(state: 'opened') do
Open
- if @project
%span.badge #{counts[:opened]}
%span.badge= counts[:opened]
%li{ class: milestone_class_for_state(params[:state], 'closed') }>
= link_to milestones_filter_path(state: 'closed') do
Closed
- if @project
%span.badge #{counts[:closed]}
%span.badge= counts[:closed]
%li{ class: milestone_class_for_state(params[:state], 'all') }>
= link_to milestones_filter_path(state: 'all') do
All
- if @project
%span.badge #{counts[:all]}
%span.badge= counts[:all]
......@@ -32,7 +32,7 @@
- if group_member
as
%span #{group_member.human_access}
%span= group_member.human_access
- if group.description.present?
.description
......
......@@ -68,7 +68,7 @@
- if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project))
.inline.prepend-left-10
Please review the
%strong #{link_to 'contribution guidelines', guide_url}
%strong= link_to('contribution guidelines', guide_url)
for this project.
- if issuable.new_record?
......
......@@ -5,5 +5,5 @@
- scopes.each do |scope|
%fieldset
= check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
= label_tag "#{prefix}_scopes_#{scope}", scope
%span= "(#{t(scope, scope: [:doorkeeper, :scopes])})"
= label_tag ("#{prefix}_scopes_#{scope}"), scope
%span= t(scope, scope: [:doorkeeper, :scopes])
%h4.prepend-top-20
Contributions for
%strong #{@calendar_date.to_s(:short)}
%strong= @calendar_date.to_s(:short)
- if @events.any?
%ul.bordered-list
......
......@@ -110,16 +110,16 @@
= spinner
#groups.tab-pane
- # This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
#contributed.tab-pane
- # This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
#projects.tab-pane
- # This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
#snippets.tab-pane
- # This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
.loading-status
= spinner
......
---
title: Improve button accessibility on pipelines page
merge_request: 8561
author:
---
title: Fix tab index order on branch commits list page
merge_request:
author: Ryan Harris
---
title: Fix Sort by Recent Sign-in in Admin Area
merge_request: 8637
author: Poornima M
---
title: Fix mini-pipeline stage tooltip text wrapping
merge_request:
author:
---
title: Hide version check image if there is no internet connection
merge_request: 8355
author: Ken Ding
---
title: Make MR-review-discussions more reliable
merge_request:
author:
---
title: adds avatar for discussion note
merge_request: 8734
author:
---
title: Flag multiple empty lines in eslint, fix offenses.
merge_request: 8137
author:
---
title: Support notes when a project is not specified (personal snippet notes)
merge_request: 8468
author:
---
title: Display fullscreen button on small screens
merge_request: 5302
author: winniehell
---
title: Only show Merge Request button when user can create a MR
merge_request: 8639
author:
# Autoload all classes that we want to instrument, and instrument the methods we
# need. This takes the Gitlab::Metrics::Instrumentation module as an argument so
# that we can stub it for testing, as it is only called when metrics are
# enabled.
#
# rubocop:disable Metrics/AbcSize
def instrument_classes(instrumentation)
instrumentation.instrument_instance_methods(Gitlab::Shell)
instrumentation.instrument_methods(Gitlab::Git)
Gitlab::Git.constants.each do |name|
const = Gitlab::Git.const_get(name)
next unless const.is_a?(Module)
instrumentation.instrument_methods(const)
instrumentation.instrument_instance_methods(const)
end
# Path to search => prefix to strip from constant
paths_to_instrument = {
['app', 'finders'] => ['app', 'finders'],
['app', 'mailers', 'emails'] => ['app', 'mailers'],
['app', 'services', '**'] => ['app', 'services'],
['lib', 'gitlab', 'conflicts'] => ['lib'],
['lib', 'gitlab', 'diff'] => ['lib'],
['lib', 'gitlab', 'email', 'message'] => ['lib'],
['lib', 'gitlab', 'checks'] => ['lib']
}
paths_to_instrument.each do |(path, prefix)|
prefix = Rails.root.join(*prefix)
Dir[Rails.root.join(*path + ['*.rb'])].each do |file_path|
path = Pathname.new(file_path).relative_path_from(prefix)
const = path.to_s.sub('.rb', '').camelize.constantize
instrumentation.instrument_methods(const)
instrumentation.instrument_instance_methods(const)
end
end
instrumentation.instrument_methods(Premailer::Adapter::Nokogiri)
instrumentation.instrument_instance_methods(Premailer::Adapter::Nokogiri)
[
:Blame, :Branch, :BranchCollection, :Blob, :Commit, :Diff, :Repository,
:Tag, :TagCollection, :Tree
].each do |name|
const = Rugged.const_get(name)
instrumentation.instrument_methods(const)
instrumentation.instrument_instance_methods(const)
end
# Instruments all Banzai filters and reference parsers
{
Filter: Rails.root.join('lib', 'banzai', 'filter', '*.rb'),
ReferenceParser: Rails.root.join('lib', 'banzai', 'reference_parser', '*.rb')
}.each do |const_name, path|
Dir[path].each do |file|
klass = File.basename(file, File.extname(file)).camelize
const = Banzai.const_get(const_name).const_get(klass)
instrumentation.instrument_methods(const)
instrumentation.instrument_instance_methods(const)
end
end
instrumentation.instrument_methods(Banzai::Renderer)
instrumentation.instrument_methods(Banzai::Querying)
instrumentation.instrument_instance_methods(Banzai::ObjectRenderer)
instrumentation.instrument_instance_methods(Banzai::Redactor)
instrumentation.instrument_methods(Banzai::NoteRenderer)
[Issuable, Mentionable, Participable].each do |klass|
instrumentation.instrument_instance_methods(klass)
instrumentation.instrument_instance_methods(klass::ClassMethods)
end
instrumentation.instrument_methods(Gitlab::ReferenceExtractor)
instrumentation.instrument_instance_methods(Gitlab::ReferenceExtractor)
# Instrument the classes used for checking if somebody has push access.
instrumentation.instrument_instance_methods(Gitlab::GitAccess)
instrumentation.instrument_instance_methods(Gitlab::GitAccessWiki)
instrumentation.instrument_instance_methods(API::Helpers)
instrumentation.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker)
instrumentation.instrument_instance_methods(Rouge::Plugins::Redcarpet)
instrumentation.instrument_instance_methods(Rouge::Formatters::HTMLGitlab)
[:XML, :HTML].each do |namespace|
namespace_mod = Nokogiri.const_get(namespace)
instrumentation.instrument_methods(namespace_mod)
instrumentation.instrument_methods(namespace_mod::Document)
end
instrumentation.instrument_methods(Rinku)
instrumentation.instrument_instance_methods(Repository)
instrumentation.instrument_methods(Gitlab::Highlight)
instrumentation.instrument_instance_methods(Gitlab::Highlight)
# This is a Rails scope so we have to instrument it manually.
instrumentation.instrument_method(Project, :visible_to_user)
end
# rubocop:enable Metrics/AbcSize
if Gitlab::Metrics.enabled?
require 'pathname'
require 'influxdb'
......@@ -49,110 +163,7 @@ if Gitlab::Metrics.enabled?
end
Gitlab::Metrics::Instrumentation.configure do |config|
config.instrument_instance_methods(Gitlab::Shell)
config.instrument_methods(Gitlab::Git)
Gitlab::Git.constants.each do |name|
const = Gitlab::Git.const_get(name)
next unless const.is_a?(Module)
config.instrument_methods(const)
config.instrument_instance_methods(const)
end
# Path to search => prefix to strip from constant
paths_to_instrument = {
['app', 'finders'] => ['app', 'finders'],
['app', 'mailers', 'emails'] => ['app', 'mailers'],
['app', 'services', '**'] => ['app', 'services'],
['lib', 'gitlab', 'conflicts'] => ['lib'],
['lib', 'gitlab', 'diff'] => ['lib'],
['lib', 'gitlab', 'email', 'message'] => ['lib'],
['lib', 'gitlab', 'checks'] => ['lib']
}
paths_to_instrument.each do |(path, prefix)|
prefix = Rails.root.join(*prefix)
Dir[Rails.root.join(*path + ['*.rb'])].each do |file_path|
path = Pathname.new(file_path).relative_path_from(prefix)
const = path.to_s.sub('.rb', '').camelize.constantize
config.instrument_methods(const)
config.instrument_instance_methods(const)
end
end
config.instrument_methods(Premailer::Adapter::Nokogiri)
config.instrument_instance_methods(Premailer::Adapter::Nokogiri)
[
:Blame, :Branch, :BranchCollection, :Blob, :Commit, :Diff, :Repository,
:Tag, :TagCollection, :Tree
].each do |name|
const = Rugged.const_get(name)
config.instrument_methods(const)
config.instrument_instance_methods(const)
end
# Instruments all Banzai filters and reference parsers
{
Filter: Rails.root.join('lib', 'banzai', 'filter', '*.rb'),
ReferenceParser: Rails.root.join('lib', 'banzai', 'reference_parser', '*.rb')
}.each do |const_name, path|
Dir[path].each do |file|
klass = File.basename(file, File.extname(file)).camelize
const = Banzai.const_get(const_name).const_get(klass)
config.instrument_methods(const)
config.instrument_instance_methods(const)
end
end
config.instrument_methods(Banzai::Renderer)
config.instrument_methods(Banzai::Querying)
config.instrument_instance_methods(Banzai::ObjectRenderer)
config.instrument_instance_methods(Banzai::Redactor)
config.instrument_methods(Banzai::NoteRenderer)
[Issuable, Mentionable, Participable].each do |klass|
config.instrument_instance_methods(klass)
config.instrument_instance_methods(klass::ClassMethods)
end
config.instrument_methods(Gitlab::ReferenceExtractor)
config.instrument_instance_methods(Gitlab::ReferenceExtractor)
# Instrument the classes used for checking if somebody has push access.
config.instrument_instance_methods(Gitlab::GitAccess)
config.instrument_instance_methods(Gitlab::GitAccessWiki)
config.instrument_instance_methods(API::Helpers)
config.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker)
config.instrument_instance_methods(Rouge::Plugins::Redcarpet)
config.instrument_instance_methods(Rouge::Formatters::HTMLGitlab)
[:XML, :HTML].each do |namespace|
namespace_mod = Nokogiri.const_get(namespace)
config.instrument_methods(namespace_mod)
config.instrument_methods(namespace_mod::Document)
end
config.instrument_methods(Rinku)
config.instrument_instance_methods(Repository)
config.instrument_methods(Gitlab::Highlight)
config.instrument_instance_methods(Gitlab::Highlight)
# This is a Rails scope so we have to instrument it manually.
config.instrument_method(Project, :visible_to_user)
instrument_classes(config)
end
GC::Profiler.enable
......
......@@ -19,13 +19,11 @@ end
scope(path: 'groups/*id',
controller: :groups,
constraints: { id: Gitlab::Regex.namespace_route_regex }) do
constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ }) do
get :edit, as: :edit_group
get :issues, as: :issues_group
get :merge_requests, as: :merge_requests_group
get :projects, as: :projects_group
get :activity, as: :activity_group
get '/', action: :show, as: :group_canonical
end
# Must be last route in this file
get 'groups/*id' => 'groups#show', as: :group_canonical, constraints: { id: Gitlab::Regex.namespace_route_regex }
......@@ -3,7 +3,7 @@ class AddEstimateToIssuablesCe < ActiveRecord::Migration
DOWNTIME = false
def change
def up
unless column_exists?(:issues, :time_estimate)
add_column :issues, :time_estimate, :integer
end
......@@ -12,4 +12,14 @@ class AddEstimateToIssuablesCe < ActiveRecord::Migration
add_column :merge_requests, :time_estimate, :integer
end
end
def down
if column_exists?(:issues, :time_estimate)
remove_column :issues, :time_estimate
end
if column_exists?(:merge_requests, :time_estimate)
remove_column :merge_requests, :time_estimate
end
end
end
......@@ -3,7 +3,7 @@ class CreateTimelogsCe < ActiveRecord::Migration
DOWNTIME = false
def change
def up
unless table_exists?(:timelogs)
create_table :timelogs do |t|
t.integer :time_spent, null: false
......@@ -17,4 +17,8 @@ class CreateTimelogsCe < ActiveRecord::Migration
add_index :timelogs, :user_id
end
end
def down
drop_table :timelogs if table_exists?(:timelogs)
end
end
......@@ -27,6 +27,7 @@ Ruby Version: 2.1.5p273
Gem Version: 2.4.3
Bundler Version: 1.7.6
Rake Version: 10.3.2
Redis Version: 3.2.5
Sidekiq Version: 2.17.8
GitLab information
......
......@@ -78,7 +78,7 @@ PUT /projects/:id/environments/:environments_id
| `external_url` | string | no | The new external_url |
```bash
curl --request PUT --data "name=staging&external_url=https://staging.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1"
curl --request PUT --data "name=staging&external_url=https://staging.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1"
```
Example response:
......@@ -106,7 +106,7 @@ DELETE /projects/:id/environments/:environment_id
| `environment_id` | integer | yes | The ID of the environment |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1"
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1"
```
Example response:
......
......@@ -129,12 +129,7 @@ module API
end
end
# Delete all merged branches
#
# Parameters:
# id (required) - The ID of a project
# Example Request:
# DELETE /projects/:id/repository/branches/delete_merged
desc 'Delete all merged branches'
delete ":id/repository/merged_branches" do
DeleteMergedBranchesService.new(user_project, current_user).async_execute
......
......@@ -53,6 +53,10 @@ module Banzai
context[:project]
end
def skip_project_check?
context[:skip_project_check]
end
def reference_class(type)
"gfm gfm-#{type} has-tooltip"
end
......
......@@ -24,7 +24,7 @@ module Banzai
end
def call
return doc if project.nil?
return doc if project.nil? && !skip_project_check?
ref_pattern = User.reference_pattern
ref_pattern_start = /\A#{ref_pattern}\z/
......@@ -58,7 +58,7 @@ module Banzai
# have `gfm` and `gfm-project_member` class names attached for styling.
def user_link_filter(text, link_content: nil)
self.class.references_in(text) do |match, username|
if username == 'all'
if username == 'all' && !skip_project_check?
link_to_all(link_content: link_content)
elsif namespace = namespaces[username]
link_to_namespace(namespace, link_content: link_content) || match
......
......@@ -52,9 +52,9 @@ module Banzai
end
# Same as +render_field+, but without consulting or updating the cache field
def cacheless_render_field(object, field)
def cacheless_render_field(object, field, options = {})
text = object.__send__(field)
context = object.banzai_render_context(field)
context = object.banzai_render_context(field).merge(options)
cacheless_render(text, context)
end
......
......@@ -9,7 +9,9 @@ module Gitlab
end
def ensure_application_settings!
if connect_to_db?
return fake_application_settings unless connect_to_db?
unless ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
begin
settings = ::ApplicationSetting.current
# In case Redis isn't running or the Redis UNIX socket file is not available
......@@ -20,43 +22,23 @@ module Gitlab
settings ||= ::ApplicationSetting.create_from_defaults unless ActiveRecord::Migrator.needs_migration?
end
settings || fake_application_settings
settings || in_memory_application_settings
end
def sidekiq_throttling_enabled?
current_application_settings.sidekiq_throttling_enabled?
end
def in_memory_application_settings
@in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting::DEFAULTS)
# In case migrations the application_settings table is not created yet,
# we fallback to a simple OpenStruct
rescue ActiveRecord::StatementInvalid, ActiveRecord::UnknownAttributeError
fake_application_settings
end
def fake_application_settings
OpenStruct.new(
default_projects_limit: Settings.gitlab['default_projects_limit'],
default_branch_protection: Settings.gitlab['default_branch_protection'],
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
koding_enabled: false,
plantuml_enabled: false,
sign_in_text: nil,
after_sign_up_text: nil,
help_page_text: nil,
shared_runners_text: nil,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
session_expire_delay: Settings.gitlab['session_expire_delay'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
domain_whitelist: Settings.gitlab['domain_whitelist'],
import_sources: %w[gitea github bitbucket gitlab google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
two_factor_grace_period: 48,
akismet_enabled: false,
repository_checks_enabled: true,
container_registry_token_expire_delay: 5,
user_default_external: false,
sidekiq_throttling_enabled: false,
)
OpenStruct.new(::ApplicationSetting::DEFAULTS)
end
private
......
module Gitlab
module GithubImport
class ProjectCreator
include Gitlab::CurrentSettings
attr_reader :repo, :name, :namespace, :current_user, :session_data, :type
def initialize(repo, name, namespace, current_user, session_data, type: 'github')
......@@ -34,7 +36,7 @@ module Gitlab
end
def visibility_level
repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility
repo.private ? Gitlab::VisibilityLevel::PRIVATE : current_application_settings.default_project_visibility
end
#
......
......@@ -5,8 +5,6 @@
#
module Gitlab
module ImportSources
extend CurrentSettings
ImportSource = Struct.new(:name, :title, :importer)
ImportTable = [
......
......@@ -11,8 +11,10 @@ namespace :gitlab do
gem_version = run_command(%W(gem --version))
# check Bundler version
bunder_version = run_and_match(%W(bundle --version), /[\d\.]+/).try(:to_s)
# check Bundler version
# check Rake version
rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s)
# check redis version
redis_version = run_and_match(%W(redis-cli --version), /redis-cli (\d+\.\d+\.\d+)/).to_a
puts ""
puts "System information".color(:yellow)
......@@ -24,6 +26,7 @@ namespace :gitlab do
puts "Gem Version:\t#{gem_version || "unknown".color(:red)}"
puts "Bundler Version:#{bunder_version || "unknown".color(:red)}"
puts "Rake Version:\t#{rake_version || "unknown".color(:red)}"
puts "Redis Version:\t#{redis_version[1] || "unknown".color(:red)}"
puts "Sidekiq Version:#{Sidekiq::VERSION}"
......
require 'spec_helper'
describe HealthCheckController do
include StubENV
let(:token) { current_application_settings.health_check_access_token }
let(:json_response) { JSON.parse(response.body) }
let(:xml_response) { Hash.from_xml(response.body)['hash'] }
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
end
describe 'GET #index' do
context 'when services are up but NO access token' do
it 'returns a not found page' do
......
......@@ -13,6 +13,7 @@ FactoryGirl.define do
factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note]
factory :note_on_merge_request, traits: [:on_merge_request]
factory :note_on_project_snippet, traits: [:on_project_snippet]
factory :note_on_personal_snippet, traits: [:on_personal_snippet]
factory :system_note, traits: [:system]
factory :legacy_diff_note_on_commit, traits: [:on_commit, :legacy_diff_note], class: LegacyDiffNote
......@@ -70,6 +71,11 @@ FactoryGirl.define do
noteable { create(:project_snippet, project: project) }
end
trait :on_personal_snippet do
noteable { create(:personal_snippet) }
project nil
end
trait :system do
system true
end
......
require 'rails_helper'
feature 'Admin disables Git access protocol', feature: true do
include StubENV
let(:project) { create(:empty_project, :empty_repo) }
let(:admin) { create(:admin) }
background do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as(admin)
end
......
require 'spec_helper'
feature "Admin Health Check", feature: true do
include StubENV
include WaitForAjax
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as :admin
end
......@@ -12,11 +14,12 @@ feature "Admin Health Check", feature: true do
visit admin_health_check_path
end
it { page.has_text? 'Health Check' }
it { page.has_text? 'Health information can be retrieved' }
it 'has a health check access token' do
page.has_text? 'Health Check'
page.has_text? 'Health information can be retrieved'
token = current_application_settings.health_check_access_token
expect(page).to have_content("Access token is #{token}")
expect(page).to have_selector('#health-check-token', text: token)
end
......
require 'spec_helper'
describe "Admin Runners" do
include StubENV
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as :admin
end
......
require 'spec_helper'
feature 'Admin updates settings', feature: true do
before(:each) do
include StubENV
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as :admin
visit admin_application_settings_path
end
......
require 'rails_helper'
feature 'Admin uses repository checks', feature: true do
before { login_as :admin }
include StubENV
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as :admin
end
scenario 'to trigger a single check' do
project = create(:empty_project)
......@@ -29,7 +34,7 @@ feature 'Admin uses repository checks', feature: true do
scenario 'to clear all repository checks', js: true do
visit admin_application_settings_path
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
click_link 'Clear all repository checks'
......
......@@ -40,6 +40,16 @@ describe 'Dropdown label', js: true, feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
describe 'keyboard navigation' do
it 'selects label' do
send_keys_to_filtered_search('label:')
filtered_search.native.send_keys(:down, :down, :enter)
expect(filtered_search.value).to eq("label:~#{special_label.name} ")
end
end
describe 'behavior' do
it 'opens when the search bar has label:' do
filtered_search.set('label:')
......
......@@ -20,6 +20,22 @@ describe 'Search bar', js: true, feature: true do
left_style.to_s.gsub('left: ', '').to_f
end
describe 'keyboard navigation' do
it 'makes item active' do
filtered_search.native.send_keys(:down)
page.within '#js-dropdown-hint' do
expect(page).to have_selector('.dropdown-active')
end
end
it 'selects item' do
filtered_search.native.send_keys(:down, :down, :enter)
expect(filtered_search.value).to eq('author:')
end
end
describe 'clear search button' do
it 'clears text' do
search_text = 'search_text'
......
require 'spec_helper'
feature 'Merge Request button', feature: true do
shared_examples 'Merge Request button only shown when allowed' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:forked_project) { create(:project, :public, forked_from_project: project) }
context 'not logged in' do
it 'does not show Create Merge Request button' do
visit url
within("#content-body") do
expect(page).not_to have_link(label)
end
end
end
context 'logged in as developer' do
before do
login_as(user)
project.team << [user, :developer]
end
it 'shows Create Merge Request button' do
href = new_namespace_project_merge_request_path(project.namespace,
project,
merge_request: { source_branch: 'feature',
target_branch: 'master' })
visit url
within("#content-body") do
expect(page).to have_link(label, href: href)
end
end
context 'merge requests are disabled' do
before do
project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
end
it 'does not show Create Merge Request button' do
visit url
within("#content-body") do
expect(page).not_to have_link(label)
end
end
end
end
context 'logged in as non-member' do
before do
login_as(user)
end
it 'does not show Create Merge Request button' do
visit url
within("#content-body") do
expect(page).not_to have_link(label)
end
end
context 'on own fork of project' do
let(:user) { forked_project.owner }
it 'shows Create Merge Request button' do
href = new_namespace_project_merge_request_path(forked_project.namespace,
forked_project,
merge_request: { source_branch: 'feature',
target_branch: 'master' })
visit fork_url
within("#content-body") do
expect(page).to have_link(label, href: href)
end
end
end
end
end
context 'on branches page' do
it_behaves_like 'Merge Request button only shown when allowed' do
let(:label) { 'Merge Request' }
let(:url) { namespace_project_branches_path(project.namespace, project) }
let(:fork_url) { namespace_project_branches_path(forked_project.namespace, forked_project) }
end
end
context 'on compare page' do
it_behaves_like 'Merge Request button only shown when allowed' do
let(:label) { 'Create Merge Request' }
let(:url) { namespace_project_compare_path(project.namespace, project, from: 'master', to: 'feature') }
let(:fork_url) { namespace_project_compare_path(forked_project.namespace, forked_project, from: 'master', to: 'feature') }
end
end
context 'on commits page' do
it_behaves_like 'Merge Request button only shown when allowed' do
let(:label) { 'Create Merge Request' }
let(:url) { namespace_project_commits_path(project.namespace, project, 'feature') }
let(:fork_url) { namespace_project_commits_path(forked_project.namespace, forked_project, 'feature') }
end
end
end
require 'spec_helper'
require_relative '../../config/initializers/metrics'
describe 'instrument_classes', lib: true do
let(:config) { double(:config) }
before do
allow(config).to receive(:instrument_method)
allow(config).to receive(:instrument_methods)
allow(config).to receive(:instrument_instance_methods)
end
it 'can autoload and instrument all files' do
expect { instrument_classes(config) }.not_to raise_error
end
end
......@@ -21,7 +21,6 @@
messages = $('.abuse-reports .message');
});
it('should truncate long messages', () => {
const $longMessage = findMessage('LONG MESSAGE');
expect($longMessage.data('original-message')).toEqual(jasmine.anything());
......
......@@ -33,7 +33,6 @@ describe('Rollback Component', () => {
expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
});
it('Should render Rollback label when isLastDeployment is false', () => {
const component = new window.gl.environmentsList.RollbackComponent({
el: document.querySelector('.test-dom-element'),
......
......@@ -52,5 +52,22 @@
expect(value).toBe(null);
});
});
describe('gl.utils.normalizedHeaders', () => {
it('should upperCase all the header keys to keep them consistent', () => {
const apiHeaders = {
'X-Something-Workhorse': { workhorse: 'ok' },
'x-something-nginx': { nginx: 'ok' },
};
const normalized = gl.utils.normalizeHeaders(apiHeaders);
const WORKHORSE = 'X-SOMETHING-WORKHORSE';
const NGINX = 'X-SOMETHING-NGINX';
expect(normalized[WORKHORSE].workhorse).toBe('ok');
expect(normalized[NGINX].nginx).toBe('ok');
});
});
});
})();
......@@ -152,6 +152,30 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
end
context 'when a project is not specified' do
let(:project) { nil }
it 'does not link a User' do
doc = reference_filter("Hey #{reference}")
expect(doc).not_to include('a')
end
context 'when skip_project_check set to true' do
it 'links to a User' do
doc = reference_filter("Hey #{reference}", skip_project_check: true)
expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
end
it 'does not link users using @all reference' do
doc = reference_filter("Hey #{User.reference_prefix}all", skip_project_check: true)
expect(doc).not_to include('a')
end
end
end
describe '#namespaces' do
it 'returns a Hash containing all Namespaces' do
document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>")
......
require 'spec_helper'
describe Gitlab::CurrentSettings do
include StubENV
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
end
describe '#current_application_settings' do
it 'attempts to use cached values first' do
allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
expect(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults)
expect(ApplicationSetting).not_to receive(:last)
context 'with DB available' do
before do
allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
end
expect(current_application_settings).to be_a(ApplicationSetting)
end
it 'attempts to use cached values first' do
expect(ApplicationSetting).to receive(:current)
expect(ApplicationSetting).not_to receive(:last)
expect(current_application_settings).to be_a(ApplicationSetting)
end
it 'does not attempt to connect to DB or Redis' do
allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(false)
expect(ApplicationSetting).not_to receive(:current)
expect(ApplicationSetting).not_to receive(:last)
it 'falls back to DB if Redis returns an empty value' do
expect(ApplicationSetting).to receive(:last).and_call_original
expect(current_application_settings).to eq fake_application_settings
expect(current_application_settings).to be_a(ApplicationSetting)
end
it 'falls back to DB if Redis fails' do
expect(ApplicationSetting).to receive(:current).and_raise(::Redis::BaseError)
expect(ApplicationSetting).to receive(:last).and_call_original
expect(current_application_settings).to be_a(ApplicationSetting)
end
end
it 'falls back to DB if Redis returns an empty value' do
allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
expect(ApplicationSetting).to receive(:last).and_call_original
context 'with DB unavailable' do
before do
allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(false)
end
expect(current_application_settings).to be_a(ApplicationSetting)
it 'returns an in-memory ApplicationSetting object' do
expect(ApplicationSetting).not_to receive(:current)
expect(ApplicationSetting).not_to receive(:last)
expect(current_application_settings).to be_a(OpenStruct)
end
end
it 'falls back to DB if Redis fails' do
allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
expect(ApplicationSetting).to receive(:current).and_raise(::Redis::BaseError)
expect(ApplicationSetting).to receive(:last).and_call_original
context 'when ENV["IN_MEMORY_APPLICATION_SETTINGS"] is true' do
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'true')
end
it 'returns an in-memory ApplicationSetting object' do
expect(ApplicationSetting).not_to receive(:current)
expect(ApplicationSetting).not_to receive(:last)
expect(current_application_settings).to be_a(ApplicationSetting)
expect(current_application_settings).to be_a(ApplicationSetting)
expect(current_application_settings).not_to be_persisted
end
end
end
end
......@@ -171,6 +171,33 @@ describe Ability, lib: true do
end
end
describe '.users_that_can_read_personal_snippet' do
def users_for_snippet(snippet)
described_class.users_that_can_read_personal_snippet(users, snippet)
end
let(:users) { create_list(:user, 3) }
let(:author) { users[0] }
it 'private snippet is readable only by its author' do
snippet = create(:personal_snippet, :private, author: author)
expect(users_for_snippet(snippet)).to match_array([author])
end
it 'internal snippet is readable by all registered users' do
snippet = create(:personal_snippet, :public, author: author)
expect(users_for_snippet(snippet)).to match_array(users)
end
it 'public snippet is readable by all users' do
snippet = create(:personal_snippet, :public, author: author)
expect(users_for_snippet(snippet)).to match_array(users)
end
end
describe '.issues_readable_by_user' do
context 'with an admin user' do
it 'returns all given issues' do
......
......@@ -30,12 +30,20 @@ describe Issue, "Mentionable" do
describe '#mentioned_users' do
let!(:user) { create(:user, username: 'stranger') }
let!(:user2) { create(:user, username: 'john') }
let!(:issue) { create(:issue, description: "#{user.to_reference} mentioned") }
let!(:user3) { create(:user, username: 'jim') }
let(:issue) { create(:issue, description: "#{user.to_reference} mentioned") }
subject { issue.mentioned_users }
it { is_expected.to include(user) }
it { is_expected.not_to include(user2) }
it { expect(subject).to contain_exactly(user) }
context 'when a note on personal snippet' do
let!(:note) { create(:note_on_personal_snippet, note: "#{user.to_reference} mentioned #{user3.to_reference}") }
subject { note.mentioned_users }
it { expect(subject).to contain_exactly(user, user3) }
end
end
describe '#referenced_mentionables' do
......@@ -138,6 +146,16 @@ describe Issue, "Mentionable" do
issue.update_attributes(description: issues[1].to_reference)
issue.create_new_cross_references!
end
it 'notifies new references from project snippet note' do
snippet = create(:snippet, project: project)
note = create(:note, note: issues[0].to_reference, noteable: snippet, project: project, author: author)
expect(SystemNoteService).to receive(:cross_reference).with(issues[1], any_args)
note.update_attributes(note: issues[1].to_reference)
note.create_new_cross_references!
end
end
def create_issue(description:)
......
......@@ -68,4 +68,14 @@ describe Group, 'Routable' do
end
end
end
describe '.member_descendants' do
let!(:user) { create(:user) }
let!(:nested_group) { create(:group, parent: group) }
before { group.add_owner(user) }
subject { described_class.member_descendants(user.id) }
it { is_expected.to eq([nested_group]) }
end
end
......@@ -269,6 +269,12 @@ describe Group, models: true do
it 'returns the canonical URL' do
expect(group.web_url).to include("groups/#{group.name}")
end
context 'nested group' do
let(:nested_group) { create(:group, :nested) }
it { expect(nested_group.web_url).to include("groups/#{nested_group.full_path}") }
end
end
describe 'nested group' do
......
......@@ -5,6 +5,8 @@ describe Namespace, models: true do
it { is_expected.to have_many :projects }
it { is_expected.to have_many :project_statistics }
it { is_expected.to belong_to :parent }
it { is_expected.to have_many :children }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) }
......@@ -189,17 +191,31 @@ describe Namespace, models: true do
it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") }
end
describe '#parents' do
describe '#ancestors' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
let(:deep_nested_group) { create(:group, parent: nested_group) }
let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
it 'returns the correct parents' do
expect(very_deep_nested_group.parents).to eq([group, nested_group, deep_nested_group])
expect(deep_nested_group.parents).to eq([group, nested_group])
expect(nested_group.parents).to eq([group])
expect(group.parents).to eq([])
it 'returns the correct ancestors' do
expect(very_deep_nested_group.ancestors).to eq([group, nested_group, deep_nested_group])
expect(deep_nested_group.ancestors).to eq([group, nested_group])
expect(nested_group.ancestors).to eq([group])
expect(group.ancestors).to eq([])
end
end
describe '#descendants' do
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:deep_nested_group) { create(:group, parent: nested_group) }
let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
it 'returns the correct descendants' do
expect(very_deep_nested_group.descendants.to_a).to eq([])
expect(deep_nested_group.descendants.to_a).to eq([very_deep_nested_group])
expect(nested_group.descendants.to_a).to eq([deep_nested_group, very_deep_nested_group])
expect(group.descendants.to_a).to eq([nested_group, deep_nested_group, very_deep_nested_group])
end
end
end
......@@ -52,6 +52,19 @@ describe Note, models: true do
subject { create(:note) }
it { is_expected.to be_valid }
end
context 'when project is missing for a project related note' do
subject { build(:note, project: nil, noteable: build_stubbed(:issue)) }
it { is_expected.to be_invalid }
end
context 'when noteable is a personal snippet' do
subject { build(:note_on_personal_snippet) }
it 'is valid without project' do
is_expected.to be_valid
end
end
end
describe "Commit notes" do
......@@ -139,6 +152,7 @@ describe Note, models: true do
with([{
text: note1.note,
context: {
skip_project_check: false,
pipeline: :note,
cache_key: [note1, "note"],
project: note1.project,
......@@ -150,6 +164,7 @@ describe Note, models: true do
with([{
text: note2.note,
context: {
skip_project_check: false,
pipeline: :note,
cache_key: [note2, "note"],
project: note2.project,
......@@ -306,4 +321,70 @@ describe Note, models: true do
end
end
end
describe '#for_personal_snippet?' do
it 'returns false for a project snippet note' do
expect(build(:note_on_project_snippet).for_personal_snippet?).to be_falsy
end
it 'returns true for a personal snippet note' do
expect(build(:note_on_personal_snippet).for_personal_snippet?).to be_truthy
end
end
describe '#to_ability_name' do
it 'returns snippet for a project snippet note' do
expect(build(:note_on_project_snippet).to_ability_name).to eq('snippet')
end
it 'returns personal_snippet for a personal snippet note' do
expect(build(:note_on_personal_snippet).to_ability_name).to eq('personal_snippet')
end
it 'returns merge_request for an MR note' do
expect(build(:note_on_merge_request).to_ability_name).to eq('merge_request')
end
it 'returns issue for an issue note' do
expect(build(:note_on_issue).to_ability_name).to eq('issue')
end
it 'returns issue for a commit note' do
expect(build(:note_on_commit).to_ability_name).to eq('commit')
end
end
describe '#cache_markdown_field' do
let(:html) { '<p>some html</p>'}
context 'note for a project snippet' do
let(:note) { build(:note_on_project_snippet) }
before do
expect(Banzai::Renderer).to receive(:cacheless_render_field).
with(note, :note, { skip_project_check: false }).and_return(html)
note.save
end
it 'creates a note' do
expect(note.note_html).to eq(html)
end
end
context 'note for a personal snippet' do
let(:note) { build(:note_on_personal_snippet) }
before do
expect(Banzai::Renderer).to receive(:cacheless_render_field).
with(note, :note, { skip_project_check: true }).and_return(html)
note.save
end
it 'creates a note' do
expect(note.note_html).to eq(html)
end
end
end
end
......@@ -14,7 +14,7 @@ describe Route, models: true do
it { is_expected.to validate_uniqueness_of(:path) }
end
describe '#rename_children' do
describe '#rename_descendants' do
let!(:nested_group) { create(:group, path: "test", parent: group) }
let!(:deep_nested_group) { create(:group, path: "foo", parent: nested_group) }
let!(:similar_group) { create(:group, path: 'gitlab-org') }
......
......@@ -797,14 +797,14 @@ describe User, models: true do
describe '#avatar_type' do
let(:user) { create(:user) }
it "is true if avatar is image" do
it 'is true if avatar is image' do
user.update_attribute(:avatar, 'uploads/avatar.png')
expect(user.avatar_type).to be_truthy
end
it "is false if avatar is html page" do
it 'is false if avatar is html page' do
user.update_attribute(:avatar, 'uploads/avatar.html')
expect(user.avatar_type).to eq(["only images allowed"])
expect(user.avatar_type).to eq(['only images allowed'])
end
end
......@@ -926,8 +926,8 @@ describe User, models: true do
end
end
describe "#starred?" do
it "determines if user starred a project" do
describe '#starred?' do
it 'determines if user starred a project' do
user = create :user
project1 = create(:empty_project, :public)
project2 = create(:empty_project, :public)
......@@ -953,8 +953,8 @@ describe User, models: true do
end
end
describe "#toggle_star" do
it "toggles stars" do
describe '#toggle_star' do
it 'toggles stars' do
user = create :user
project = create(:empty_project, :public)
......@@ -966,31 +966,44 @@ describe User, models: true do
end
end
describe "#sort" do
describe '#sort' do
before do
User.delete_all
@user = create :user, created_at: Date.today, last_sign_in_at: Date.today, name: 'Alpha'
@user1 = create :user, created_at: Date.today - 1, last_sign_in_at: Date.today - 1, name: 'Omega'
@user2 = create :user, created_at: Date.today - 2, last_sign_in_at: nil, name: 'Beta'
end
it "sorts users by the recent sign-in time" do
expect(User.sort('recent_sign_in').first).to eq(@user)
context 'when sort by recent_sign_in' do
it 'sorts users by the recent sign-in time' do
expect(User.sort('recent_sign_in').first).to eq(@user)
end
it 'pushes users who never signed in to the end' do
expect(User.sort('recent_sign_in').third).to eq(@user2)
end
end
it "sorts users by the oldest sign-in time" do
expect(User.sort('oldest_sign_in').first).to eq(@user1)
context 'when sort by oldest_sign_in' do
it 'sorts users by the oldest sign-in time' do
expect(User.sort('oldest_sign_in').first).to eq(@user1)
end
it 'pushes users who never signed in to the end' do
expect(User.sort('oldest_sign_in').third).to eq(@user2)
end
end
it "sorts users in descending order by their creation time" do
it 'sorts users in descending order by their creation time' do
expect(User.sort('created_desc').first).to eq(@user)
end
it "sorts users in ascending order by their creation time" do
expect(User.sort('created_asc').first).to eq(@user1)
it 'sorts users in ascending order by their creation time' do
expect(User.sort('created_asc').first).to eq(@user2)
end
it "sorts users by id in descending order when nil is passed" do
expect(User.sort(nil).first).to eq(@user1)
it 'sorts users by id in descending order when nil is passed' do
expect(User.sort(nil).first).to eq(@user2)
end
end
......@@ -1350,6 +1363,39 @@ describe User, models: true do
end
end
describe '#nested_groups' do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
before do
group.add_owner(user)
# Add more data to ensure method does not include wrong groups
create(:group).add_owner(create(:user))
end
it { expect(user.nested_groups).to eq([nested_group]) }
end
describe '#nested_projects' do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:project) { create(:project, namespace: group) }
let!(:nested_project) { create(:project, namespace: nested_group) }
before do
group.add_owner(user)
# Add more data to ensure method does not include wrong projects
other_project = create(:project, namespace: create(:group, :nested))
other_project.add_developer(create(:user))
end
it { expect(user.nested_projects).to eq([nested_project]) }
end
describe '#refresh_authorized_projects', redis: true do
let(:project1) { create(:empty_project) }
let(:project2) { create(:empty_project) }
......
......@@ -337,8 +337,7 @@ describe API::Internal, api: true do
context 'ssh access has been disabled' do
before do
settings = ::ApplicationSetting.create_from_defaults
settings.update_attribute(:enabled_git_access_protocol, 'http')
stub_application_setting(enabled_git_access_protocol: 'http')
end
it 'rejects the SSH push' do
......@@ -360,8 +359,7 @@ describe API::Internal, api: true do
context 'http access has been disabled' do
before do
settings = ::ApplicationSetting.create_from_defaults
settings.update_attribute(:enabled_git_access_protocol, 'ssh')
stub_application_setting(enabled_git_access_protocol: 'ssh')
end
it 'rejects the HTTP push' do
......@@ -383,8 +381,7 @@ describe API::Internal, api: true do
context 'web actions are always allowed' do
it 'allows WEB push' do
settings = ::ApplicationSetting.create_from_defaults
settings.update_attribute(:enabled_git_access_protocol, 'ssh')
stub_application_setting(enabled_git_access_protocol: 'ssh')
project.team << [user, :developer]
push(key, project, 'web')
......
......@@ -13,7 +13,12 @@ describe 'cycle analytics events' do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
3.times { create_cycle }
3.times do |count|
Timecop.freeze(Time.now + count.days) do
create_cycle
end
end
deploy_master
login_as(user)
......
......@@ -15,39 +15,45 @@ describe Notes::CreateService, services: true do
context "valid params" do
it 'returns a valid note' do
note = Notes::CreateService.new(project, user, opts).execute
note = described_class.new(project, user, opts).execute
expect(note).to be_valid
end
it 'returns a persisted note' do
note = Notes::CreateService.new(project, user, opts).execute
note = described_class.new(project, user, opts).execute
expect(note).to be_persisted
end
it 'note has valid content' do
note = Notes::CreateService.new(project, user, opts).execute
note = described_class.new(project, user, opts).execute
expect(note.note).to eq(opts[:note])
end
it 'note belongs to the correct project' do
note = described_class.new(project, user, opts).execute
expect(note.project).to eq(project)
end
it 'TodoService#new_note is called' do
note = build(:note)
allow(project).to receive_message_chain(:notes, :new).with(opts) { note }
note = build(:note, project: project)
allow(Note).to receive(:new).with(opts) { note }
expect_any_instance_of(TodoService).to receive(:new_note).with(note, user)
Notes::CreateService.new(project, user, opts).execute
described_class.new(project, user, opts).execute
end
it 'enqueues NewNoteWorker' do
note = build(:note, id: 999)
allow(project).to receive_message_chain(:notes, :new).with(opts) { note }
note = build(:note, id: 999, project: project)
allow(Note).to receive(:new).with(opts) { note }
expect(NewNoteWorker).to receive(:perform_async).with(note.id)
Notes::CreateService.new(project, user, opts).execute
described_class.new(project, user, opts).execute
end
end
......@@ -75,6 +81,27 @@ describe Notes::CreateService, services: true do
end
end
end
describe 'personal snippet note' do
subject { described_class.new(nil, user, params).execute }
let(:snippet) { create(:personal_snippet) }
let(:params) do
{ note: 'comment', noteable_type: 'Snippet', noteable_id: snippet.id }
end
it 'returns a valid note' do
expect(subject).to be_valid
end
it 'returns a persisted note' do
expect(subject).to be_persisted
end
it 'note has valid content' do
expect(subject.note).to eq(params[:note])
end
end
end
describe "award emoji" do
......@@ -88,7 +115,7 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
noteable_id: issue.id
}
note = Notes::CreateService.new(project, user, opts).execute
note = described_class.new(project, user, opts).execute
expect(note).to be_valid
expect(note.name).to eq('smile')
......@@ -100,7 +127,7 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
noteable_id: issue.id
}
note = Notes::CreateService.new(project, user, opts).execute
note = described_class.new(project, user, opts).execute
expect(note).to be_valid
expect(note.note).to eq(opts[:note])
......@@ -115,7 +142,7 @@ describe Notes::CreateService, services: true do
expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user)
Notes::CreateService.new(project, user, opts).execute
described_class.new(project, user, opts).execute
end
end
end
......@@ -269,6 +269,55 @@ describe NotificationService, services: true do
end
end
context 'personal snippet note' do
let(:snippet) { create(:personal_snippet, :public, author: @u_snippet_author) }
let(:note) { create(:note_on_personal_snippet, noteable: snippet, note: '@mentioned note', author: @u_note_author) }
before do
@u_watcher = create_global_setting_for(create(:user), :watch)
@u_participant = create_global_setting_for(create(:user), :participating)
@u_disabled = create_global_setting_for(create(:user), :disabled)
@u_mentioned = create_global_setting_for(create(:user, username: 'mentioned'), :mention)
@u_mentioned_level = create_global_setting_for(create(:user, username: 'participator'), :mention)
@u_note_author = create(:user, username: 'note_author')
@u_snippet_author = create(:user, username: 'snippet_author')
@u_not_mentioned = create_global_setting_for(create(:user, username: 'regular'), :participating)
reset_delivered_emails!
end
let!(:notes) do
[
create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_watcher),
create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_participant),
create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_mentioned),
create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_disabled),
create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_note_author),
]
end
describe '#new_note' do
it 'notifies the participants' do
notification.new_note(note)
# it emails participants
should_email(@u_watcher)
should_email(@u_participant)
should_email(@u_watcher)
should_email(@u_snippet_author)
# it emails mentioned users
should_email(@u_mentioned)
# it does not email participants with mention notification level
should_not_email(@u_mentioned_level)
# it does not email note author
should_not_email(@u_note_author)
end
end
end
context 'commit note' do
let(:project) { create(:project, :public) }
let(:note) { create(:note_on_commit, project: project) }
......
......@@ -2,6 +2,7 @@ require './spec/simplecov_env'
SimpleCovEnv.start!
ENV["RAILS_ENV"] ||= 'test'
ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
......
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