Commit a0092e43 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into ce_upstream[ci skip]

parents 917438cc d964816b
...@@ -31,7 +31,7 @@ _This notice should stay as the first item in the CONTRIBUTING.MD file._ ...@@ -31,7 +31,7 @@ _This notice should stay as the first item in the CONTRIBUTING.MD file._
- [Issue tracker guidelines](#issue-tracker-guidelines) - [Issue tracker guidelines](#issue-tracker-guidelines)
- [Issue weight](#issue-weight) - [Issue weight](#issue-weight)
- [Regression issues](#regression-issues) - [Regression issues](#regression-issues)
- [Technical debt](#technical-debt) - [Technical and UX debt](#technical-and-ux-debt)
- [Stewardship](#stewardship) - [Stewardship](#stewardship)
- [Merge requests](#merge-requests) - [Merge requests](#merge-requests)
- [Merge request guidelines](#merge-request-guidelines) - [Merge request guidelines](#merge-request-guidelines)
...@@ -345,27 +345,29 @@ addressed. ...@@ -345,27 +345,29 @@ addressed.
[8.3 Regressions]: https://gitlab.com/gitlab-org/gitlab-ce/issues/4127 [8.3 Regressions]: https://gitlab.com/gitlab-org/gitlab-ce/issues/4127
[update the notes]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/pro-tips.md#update-the-regression-issue [update the notes]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/pro-tips.md#update-the-regression-issue
### Technical debt ### Technical and UX debt
In order to track things that can be improved in GitLab's codebase, we created In order to track things that can be improved in GitLab's codebase,
the ~"technical debt" label in [GitLab's issue tracker][ce-tracker]. we use the ~"technical debt" label in [GitLab's issue tracker][ce-tracker].
For user experience improvements, we use the ~"UX debt" label.
This label should be added to issues that describe things that can be improved, These labels should be added to issues that describe things that can be improved,
shortcuts that have been taken, code that needs refactoring, features that need shortcuts that have been taken, features that need additional attention, and all
additional attention, and all other things that have been left behind due to other things that have been left behind due to high velocity of development.
high velocity of development. For example, code that needs refactoring should use the ~"technical debt" label,
user experience refinements should use the ~"UX debt" label.
Everyone can create an issue, though you may need to ask for adding a specific Everyone can create an issue, though you may need to ask for adding a specific
label, if you do not have permissions to do it by yourself. Additional labels label, if you do not have permissions to do it by yourself. Additional labels
can be combined with the `technical debt` label, to make it easier to schedule can be combined with these labels, to make it easier to schedule
the improvements for a release. the improvements for a release.
Issues tagged with the `technical debt` label have the same priority like issues Issues tagged with these labels have the same priority like issues
that describe a new feature to be introduced in GitLab, and should be scheduled that describe a new feature to be introduced in GitLab, and should be scheduled
for a release by the appropriate person. for a release by the appropriate person.
Make sure to mention the merge request that the `technical debt` issue is Make sure to mention the merge request that the ~"technical debt" issue or
associated with in the description of the issue. ~"UX debt" issue is associated with in the description of the issue.
### Stewardship ### Stewardship
......
...@@ -175,7 +175,7 @@ gem 'rainbow', '~> 2.2' ...@@ -175,7 +175,7 @@ gem 'rainbow', '~> 2.2'
gem 'settingslogic', '~> 2.0.9' gem 'settingslogic', '~> 2.0.9'
# Linear-time regex library for untrusted regular expressions # Linear-time regex library for untrusted regular expressions
gem 're2', '~> 1.0.0' gem 're2', '~> 1.1.0'
# Misc # Misc
......
...@@ -620,7 +620,7 @@ GEM ...@@ -620,7 +620,7 @@ GEM
premailer-rails (1.9.7) premailer-rails (1.9.7)
actionmailer (>= 3, < 6) actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
prometheus-client-mmap (0.7.0.beta9) prometheus-client-mmap (0.7.0.beta10)
mmap2 (~> 2.2, >= 2.2.7) mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
...@@ -685,7 +685,7 @@ GEM ...@@ -685,7 +685,7 @@ GEM
debugger-ruby_core_source (~> 1.3) debugger-ruby_core_source (~> 1.3)
rdoc (4.2.2) rdoc (4.2.2)
json (~> 1.4) json (~> 1.4)
re2 (1.0.0) re2 (1.1.0)
recaptcha (3.0.0) recaptcha (3.0.0)
json json
recursive-open-struct (1.0.0) recursive-open-struct (1.0.0)
...@@ -1093,7 +1093,7 @@ DEPENDENCIES ...@@ -1093,7 +1093,7 @@ DEPENDENCIES
raindrops (~> 0.18) raindrops (~> 0.18)
rblineprof (~> 0.3.6) rblineprof (~> 0.3.6)
rdoc (~> 4.2) rdoc (~> 4.2)
re2 (~> 1.0.0) re2 (~> 1.1.0)
recaptcha (~> 3.0) recaptcha (~> 3.0)
redcarpet (~> 3.4) redcarpet (~> 3.4)
redis (~> 3.2) redis (~> 3.2)
......
/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ /* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
import './lib/utils/common_utils'; import './lib/utils/common_utils';
import { placeholderImage } from './lazy_loader';
const gfmRules = { const gfmRules = {
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
...@@ -56,6 +57,11 @@ const gfmRules = { ...@@ -56,6 +57,11 @@ const gfmRules = {
return text; return text;
}, },
}, },
ImageLazyLoadFilter: {
'img'(el, text) {
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
},
},
VideoLinkFilter: { VideoLinkFilter: {
'.video-container'(el) { '.video-container'(el) {
const videoEl = el.querySelector('video'); const videoEl = el.querySelector('video');
...@@ -163,7 +169,9 @@ const gfmRules = { ...@@ -163,7 +169,9 @@ const gfmRules = {
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
}, },
'img'(el) { 'img'(el) {
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; const imageSrc = el.src;
const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : (el.dataset.src || '');
return `![${el.getAttribute('alt')}](${imageUrl})`;
}, },
'a.anchor'(el, text) { 'a.anchor'(el, text) {
// Don't render a Markdown link for the anchor link inside a heading // Don't render a Markdown link for the anchor link inside a heading
......
document.addEventListener('DOMContentLoaded', () => {
const modal = $('#modal_merge_info').modal({
modal: true,
show: false,
});
$('.how_to_merge_link').bind('click', () => {
modal.show();
});
$('.modal-header .close').bind('click', () => {
modal.hide();
});
});
/* eslint-disable one-export, one-var, one-var-declaration-per-line */
import _ from 'underscore';
export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
const SCROLL_THRESHOLD = 300;
export default class LazyLoader {
constructor(options = {}) {
this.lazyImages = [];
this.observerNode = options.observerNode || '#content-body';
const throttledScrollCheck = _.throttle(() => this.scrollCheck(), 300);
const debouncedElementsInView = _.debounce(() => this.checkElementsInView(), 300);
window.addEventListener('scroll', throttledScrollCheck);
window.addEventListener('resize', debouncedElementsInView);
const scrollContainer = options.scrollContainer || window;
scrollContainer.addEventListener('load', () => this.loadCheck());
}
searchLazyImages() {
this.lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
this.checkElementsInView();
}
startContentObserver() {
const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
if (contentNode) {
const observer = new MutationObserver(() => this.searchLazyImages());
observer.observe(contentNode, {
childList: true,
subtree: true,
});
}
}
loadCheck() {
this.searchLazyImages();
this.startContentObserver();
}
scrollCheck() {
requestAnimationFrame(() => this.checkElementsInView());
}
checkElementsInView() {
const scrollTop = pageYOffset;
const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD;
let imgBoundRect, imgTop, imgBound;
// Loading Images which are in the current viewport or close to them
this.lazyImages = this.lazyImages.filter((selectedImage) => {
if (selectedImage.getAttribute('data-src')) {
imgBoundRect = selectedImage.getBoundingClientRect();
imgTop = scrollTop + imgBoundRect.top;
imgBound = imgTop + imgBoundRect.height;
if (scrollTop < imgBound && visHeight > imgTop) {
LazyLoader.loadImage(selectedImage);
return false;
}
return true;
}
return false;
});
}
static loadImage(img) {
if (img.getAttribute('data-src')) {
img.setAttribute('src', img.getAttribute('data-src'));
img.removeAttribute('data-src');
img.classList.remove('lazy');
img.classList.add('js-lazy-loaded');
}
}
}
...@@ -109,6 +109,7 @@ import './label_manager'; ...@@ -109,6 +109,7 @@ import './label_manager';
import './labels'; import './labels';
import './labels_select'; import './labels_select';
import './layout_nav'; import './layout_nav';
import LazyLoader from './lazy_loader';
import './line_highlighter'; import './line_highlighter';
import './logo'; import './logo';
import './member_expiration_date'; import './member_expiration_date';
...@@ -174,6 +175,11 @@ window.addEventListener('load', function onLoad() { ...@@ -174,6 +175,11 @@ window.addEventListener('load', function onLoad() {
gl.utils.handleLocationHash(); gl.utils.handleLocationHash();
}, false); }, false);
gl.lazyLoader = new LazyLoader({
scrollContainer: window,
observerNode: '#content-body'
});
$(function () { $(function () {
var $body = $('body'); var $body = $('body');
var $document = $(document); var $document = $(document);
...@@ -292,13 +298,7 @@ $(function () { ...@@ -292,13 +298,7 @@ $(function () {
return $container.remove(); return $container.remove();
// Commit show suppressed diff // Commit show suppressed diff
}); });
$('.navbar-toggle').on('click', function () { $('.navbar-toggle').on('click', () => $('.header-content').toggleClass('menu-expanded'));
$('.header-content .title, .header-content .navbar-sub-nav').toggle();
$('.header-content .header-logo').toggle();
$('.header-content .navbar-collapse').toggle();
$('.js-navbar-toggle-left, .js-navbar-toggle-right, .title-container').toggle();
return $('.navbar-toggle').toggleClass('active');
});
// Show/hide comments on diff // Show/hide comments on diff
$body.on('click', '.js-toggle-diff-comments', function (e) { $body.on('click', '.js-toggle-diff-comments', function (e) {
var $this = $(this); var $this = $(this);
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */
/* global Flash */ /* global Flash */
import { __, s__ } from './locale';
export default class Star { export default class Star {
constructor() { constructor() {
$('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) { $('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) {
...@@ -11,10 +13,10 @@ export default class Star { ...@@ -11,10 +13,10 @@ export default class Star {
toggleStar = function(isStarred) { toggleStar = function(isStarred) {
$this.parent().find('.star-count').text(data.star_count); $this.parent().find('.star-count').text(data.star_count);
if (isStarred) { if (isStarred) {
$starSpan.removeClass('starred').text('Star'); $starSpan.removeClass('starred').text(s__('StarProject|Star'));
$starIcon.removeClass('fa-star').addClass('fa-star-o'); $starIcon.removeClass('fa-star').addClass('fa-star-o');
} else { } else {
$starSpan.addClass('starred').text('Unstar'); $starSpan.addClass('starred').text(__('Unstar'));
$starIcon.removeClass('fa-star-o').addClass('fa-star'); $starIcon.removeClass('fa-star-o').addClass('fa-star');
} }
}; };
......
...@@ -35,6 +35,8 @@ ...@@ -35,6 +35,8 @@
width: 40px; width: 40px;
height: 40px; height: 40px;
padding: 0; padding: 0;
background: $avatar-background;
overflow: hidden;
&.avatar-inline { &.avatar-inline {
float: none; float: none;
......
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
.is-selected .pika-day, .is-selected .pika-day,
.pika-day:hover, .pika-day:hover,
.is-today .pika-day:hover { .is-today .pika-day {
background: $gl-primary; background: $gl-primary;
color: $white-light; color: $white-light;
box-shadow: none; box-shadow: none;
......
...@@ -132,6 +132,22 @@ header { ...@@ -132,6 +132,22 @@ header {
} }
} }
&.navbar-gitlab-new {
.fa-times {
display: none;
}
.menu-expanded {
.fa-ellipsis-v {
display: none;
}
.fa-times {
display: block;
}
}
}
.global-dropdown { .global-dropdown {
position: absolute; position: absolute;
left: -10px; left: -10px;
...@@ -171,6 +187,19 @@ header { ...@@ -171,6 +187,19 @@ header {
min-height: $header-height; min-height: $header-height;
padding-left: 30px; padding-left: 30px;
&.menu-expanded {
@media (max-width: $screen-xs-max) {
.header-logo,
.title-container {
display: none;
}
.navbar-collapse {
display: block;
}
}
}
.dropdown-menu { .dropdown-menu {
margin-top: -5px; margin-top: -5px;
} }
......
...@@ -182,6 +182,12 @@ ...@@ -182,6 +182,12 @@
} }
} }
&.nav-controls-new-nav {
> .dropdown {
margin-right: 0;
}
}
> .btn-grouped { > .btn-grouped {
float: none; float: none;
} }
......
...@@ -11,8 +11,17 @@ ...@@ -11,8 +11,17 @@
} }
img { img {
max-width: 100%; /*max-width: 100%;*/
margin: 0 0 8px; margin: 0 0 8px;
min-width: 200px;
min-height: 100px;
background-color: $gray-lightest;
}
img.js-lazy-loaded {
min-width: none;
min-height: none;
background-color: none;
} }
p a:not(.no-attachment-icon) img { p a:not(.no-attachment-icon) img {
......
...@@ -384,7 +384,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5); ...@@ -384,7 +384,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
* Avatar * Avatar
*/ */
$avatar_radius: 50%; $avatar_radius: 50%;
$avatar-border: $border-color; $avatar-border: $gray-normal;
$avatar-border-hover: $gray-darker;
$avatar-background: $gray-lightest;
$gl-avatar-size: 40px; $gl-avatar-size: 40px;
/* /*
......
...@@ -459,6 +459,7 @@ ...@@ -459,6 +459,7 @@
margin: 5px; margin: 5px;
} }
<<<<<<< HEAD
.boards-title-holder { .boards-title-holder {
padding: 25px 13px $gl-padding; padding: 25px 13px $gl-padding;
...@@ -480,6 +481,12 @@ ...@@ -480,6 +481,12 @@
} }
.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar { .page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar {
=======
.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar,
.page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar {
position: absolute;
>>>>>>> d964816b9fe56679ffc0b331e701f7b24db5c6a9
&.right-sidebar { &.right-sidebar {
top: 0; top: 0;
bottom: 0; bottom: 0;
......
...@@ -5,6 +5,14 @@ class SessionsController < Devise::SessionsController ...@@ -5,6 +5,14 @@ class SessionsController < Devise::SessionsController
skip_before_action :check_two_factor_requirement, only: [:destroy] skip_before_action :check_two_factor_requirement, only: [:destroy]
# Explicitly call protect from forgery before anything else. Otherwise the
# CSFR-token might be cleared before authentication is done. This was the case
# when LDAP was enabled and the `OmniauthCallbacksController` is loaded
#
# *Note:* `prepend: true` is the default for rails4, but this will be changed
# to `prepend: false` in rails5.
protect_from_forgery prepend: true, with: :exception
prepend_before_action :check_initial_setup, only: [:new] prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor, prepend_before_action :authenticate_with_two_factor,
if: :two_factor_enabled?, only: [:create] if: :two_factor_enabled?, only: [:create]
......
...@@ -11,17 +11,12 @@ module AvatarsHelper ...@@ -11,17 +11,12 @@ module AvatarsHelper
def user_avatar_without_link(options = {}) def user_avatar_without_link(options = {})
avatar_size = options[:size] || 16 avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name] user_name = options[:user].try(:name) || options[:user_name]
css_class = options[:css_class] || ''
avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size) avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size)
data_attributes = { container: 'body' } data_attributes = { container: 'body' }
if options[:lazy]
data_attributes[:src] = avatar_url
end
image_tag( image_tag(
options[:lazy] ? '' : avatar_url, avatar_url,
class: "avatar has-tooltip s#{avatar_size} #{css_class}", class: %W[avatar has-tooltip s#{avatar_size}].push(*options[:css_class]),
alt: "#{user_name}'s avatar", alt: "#{user_name}'s avatar",
title: user_name, title: user_name,
data: data_attributes data: data_attributes
......
...@@ -61,8 +61,8 @@ module EmailsHelper ...@@ -61,8 +61,8 @@ module EmailsHelper
else else
image_tag( image_tag(
image_url('mailers/gitlab_header_logo.gif'), image_url('mailers/gitlab_header_logo.gif'),
size: "55x50", size: '55x50',
alt: "GitLab" alt: 'GitLab'
) )
end end
end end
......
module LazyImageTagHelper
def placeholder_image
"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
end
# Override the default ActionView `image_tag` helper to support lazy-loading
def image_tag(source, options = {})
options = options.symbolize_keys
unless options.delete(:lazy) == false
options[:data] ||= {}
options[:data][:src] = path_to_image(source)
options[:class] ||= ""
options[:class] << " lazy"
source = placeholder_image
end
super(source, options)
end
# Required for Banzai::Filter::ImageLazyLoadFilter
module_function :placeholder_image
end
...@@ -2,7 +2,7 @@ module VersionCheckHelper ...@@ -2,7 +2,7 @@ module VersionCheckHelper
def version_status_badge def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled if Rails.env.production? && current_application_settings.version_check_enabled
image_url = VersionCheck.new.url image_url = VersionCheck.new.url
image_tag image_url, class: 'js-version-status-badge' image_tag image_url, class: 'js-version-status-badge', lazy: false
end end
end end
end end
...@@ -11,7 +11,7 @@ module CacheMarkdownField ...@@ -11,7 +11,7 @@ module CacheMarkdownField
extend ActiveSupport::Concern extend ActiveSupport::Concern
# Increment this number every time the renderer changes its output # Increment this number every time the renderer changes its output
CACHE_VERSION = 1 CACHE_VERSION = 2
# changes to these attributes cause the cache to be invalidates # changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze INVALIDATED_BY = %w[author project].freeze
......
...@@ -186,10 +186,14 @@ class Group < Namespace ...@@ -186,10 +186,14 @@ class Group < Namespace
end end
def has_owner?(user) def has_owner?(user)
return false unless user
members_with_parents.owners.where(user_id: user).any? members_with_parents.owners.where(user_id: user).any?
end end
def has_master?(user) def has_master?(user)
return false unless user
members_with_parents.masters.where(user_id: user).any? members_with_parents.masters.where(user_id: user).any?
end end
...@@ -258,7 +262,7 @@ class Group < Namespace ...@@ -258,7 +262,7 @@ class Group < Namespace
end end
def members_with_parents def members_with_parents
GroupMember.non_request.where(source_id: ancestors.pluck(:id).push(id)) GroupMember.active.where(source_id: ancestors.pluck(:id).push(id)).where.not(user_id: nil)
end end
def users_with_parents def users_with_parents
......
...@@ -12,7 +12,8 @@ class ProjectPolicy < BasePolicy ...@@ -12,7 +12,8 @@ class ProjectPolicy < BasePolicy
desc "User is a project owner" desc "User is a project owner"
condition :owner do condition :owner do
@user && project.owner == @user || (project.group && project.group.has_owner?(@user)) (project.owner.present? && project.owner == @user) ||
project.group&.has_owner?(@user)
end end
desc "Project has public builds enabled" desc "Project has public builds enabled"
......
...@@ -81,6 +81,6 @@ ...@@ -81,6 +81,6 @@
%button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' } %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
%span.sr-only Toggle navigation %span.sr-only Toggle navigation
= icon('ellipsis-v', class: 'js-navbar-toggle-right') = icon('ellipsis-v', class: 'js-navbar-toggle-right')
= icon('times', class: 'js-navbar-toggle-left', style: 'display: none;') = icon('times', class: 'js-navbar-toggle-left')
= render 'shared/outdated_browser' = render 'shared/outdated_browser'
.file-content.image_file .file-content.image_file
%img{ src: blob_raw_url, alt: viewer.blob.name } %img{ 'data-src': blob_raw_url, alt: viewer.blob.name }
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
= link_to toggle_star_project_path(@project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do = link_to toggle_star_project_path(@project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do
- if current_user.starred?(@project) - if current_user.starred?(@project)
= icon('star') = icon('star')
%span.starred= _('Unstar') %span.starred= _('Unstar')
- else - else
= icon('star-o') = icon('star-o')
%span= s_('StarProject|Star') %span= s_('StarProject|Star')
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.image .image
%span.wrap %span.wrap
.frame{ class: (diff_file.deleted_file? ? 'deleted' : 'added') } .frame{ class: (diff_file.deleted_file? ? 'deleted' : 'added') }
%img{ src: blob_raw_path, alt: diff_file.file_path } %img{ 'data-src': blob_raw_path, alt: diff_file.file_path }
%p.image-info= number_to_human_size(blob.size) %p.image-info= number_to_human_size(blob.size)
- else - else
.image .image
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
%span.wrap %span.wrap
.frame.deleted .frame.deleted
%a{ href: project_blob_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path)) } %a{ href: project_blob_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path)) }
%img{ src: old_blob_raw_path, alt: diff_file.old_path } %img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
%p.image-info.hide %p.image-info.hide
%span.meta-filesize= number_to_human_size(old_blob.size) %span.meta-filesize= number_to_human_size(old_blob.size)
| |
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
%span.wrap %span.wrap
.frame.added .frame.added
%a{ href: project_blob_path(@project, tree_join(diff_file.content_sha, diff_file.new_path)) } %a{ href: project_blob_path(@project, tree_join(diff_file.content_sha, diff_file.new_path)) }
%img{ src: blob_raw_path, alt: diff_file.new_path } %img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
%p.image-info.hide %p.image-info.hide
%span.meta-filesize= number_to_human_size(blob.size) %span.meta-filesize= number_to_human_size(blob.size)
| |
...@@ -41,10 +41,10 @@ ...@@ -41,10 +41,10 @@
.swipe.view.hide .swipe.view.hide
.swipe-frame .swipe-frame
.frame.deleted .frame.deleted
%img{ src: old_blob_raw_path, alt: diff_file.old_path } %img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
.swipe-wrap .swipe-wrap
.frame.added .frame.added
%img{ src: blob_raw_path, alt: diff_file.new_path } %img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
%span.swipe-bar %span.swipe-bar
%span.top-handle %span.top-handle
%span.bottom-handle %span.bottom-handle
...@@ -52,9 +52,9 @@ ...@@ -52,9 +52,9 @@
.onion-skin.view.hide .onion-skin.view.hide
.onion-skin-frame .onion-skin-frame
.frame.deleted .frame.deleted
%img{ src: old_blob_raw_path, alt: diff_file.old_path } %img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
.frame.added .frame.added
%img{ src: blob_raw_path, alt: diff_file.new_path } %img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
.controls .controls
.transparent .transparent
.drag-track .drag-track
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('how_to_merge')
#modal_merge_info.modal #modal_merge_info.modal
.modal-dialog .modal-dialog
.modal-content .modal-content
...@@ -50,14 +53,3 @@ ...@@ -50,14 +53,3 @@
= succeed '.' do = succeed '.' do
You can also checkout merge requests locally by You can also checkout merge requests locally by
= link_to 'following these guidelines', help_page_path('user/project/merge_requests/index.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer' = link_to 'following these guidelines', help_page_path('user/project/merge_requests/index.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer'
:javascript
$(function(){
var modal = $('#modal_merge_info').modal({modal: true, show:false});
$('.how_to_merge_link').bind("click", function(){
modal.show();
});
$('.modal-header .close').bind("click", function(){
modal.hide();
})
})
- @no_container = true - @no_container = true
- page_title 'Milestones' - page_title 'Milestones'
- if show_new_nav? - if show_new_nav? && can?(current_user, :admin_milestone, @project)
- content_for :breadcrumbs_extra do - content_for :breadcrumbs_extra do
= link_to "New milestone", new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New milestone' = link_to "New milestone", new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New milestone'
...@@ -11,10 +11,10 @@ ...@@ -11,10 +11,10 @@
.top-area .top-area
= render 'shared/milestones_filter', counts: milestone_counts(@project.milestones) = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
.nav-controls .nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) }
= render 'shared/milestones_sort_dropdown' = render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project) - if can?(current_user, :admin_milestone, @project)
= link_to new_project_milestone_path(@project), class: 'btn btn-new', title: 'New milestone' do = link_to new_project_milestone_path(@project), class: "btn btn-new #{("visible-xs" if show_new_nav?)}", title: 'New milestone' do
New milestone New milestone
.milestones .milestones
......
---
title: Add Slack and JIRA services counts to Usage Data
merge_request:
author:
---
title: Lazy load images for better Frontend performance
merge_request: 12503
author:
---
title: Fix translations for Star/Unstar in JS file
merge_request:
author:
---
title: Fix anonymous access to public projects in groups with pending invites
merge_request:
author:
---
title: Fix cross site request protection when logging in as a regular user when LDAP
is enabled
merge_request: 13049
author:
---
title: Fix today day highlight in calendar
merge_request: 13048
author:
---
title: Fixed issue boards sidebar close icon size
merge_request:
author:
---
title: Fixed duplicate new milestone buttons when new navigation is turned on
merge_request:
author:
---
title: Fix bug causing metrics files to be truncated
merge_request: 35420
author:
...@@ -43,8 +43,12 @@ var config = { ...@@ -43,8 +43,12 @@ var config = {
group: './group.js', group: './group.js',
groups: './groups/index.js', groups: './groups/index.js',
groups_list: './groups_list.js', groups_list: './groups_list.js',
<<<<<<< HEAD
issuable: './issuable/issuable_bundle.js', issuable: './issuable/issuable_bundle.js',
issues: './issues/issues_bundle.js', issues: './issues/issues_bundle.js',
=======
how_to_merge: './how_to_merge.js',
>>>>>>> d964816b9fe56679ffc0b331e701f7b24db5c6a9
issue_show: './issue_show/index.js', issue_show: './issue_show/index.js',
integrations: './integrations', integrations: './integrations',
job_details: './jobs/job_details_bundle.js', job_details: './jobs/job_details_bundle.js',
......
...@@ -133,6 +133,55 @@ reviewee. ...@@ -133,6 +133,55 @@ reviewee.
tomorrow. When you are not able to find the right balance, ask other people tomorrow. When you are not able to find the right balance, ask other people
about their opinion. about their opinion.
### GitLab-specific concerns
GitLab is used in a lot of places. Many users use
our [Omnibus packages](https://about.gitlab.com/installation/), but some use
the [Docker images](https://docs.gitlab.com/omnibus/docker/), some are
[installed from source](https://docs.gitlab.com/ce/install/installation.html),
and there are other installation methods available. GitLab.com itself is a large
Enterprise Edition instance. This has some implications:
1. **Query changes** should be tested to ensure that they don't result in worse
performance at the scale of GitLab.com:
1. Generating large quantities of data locally can help.
2. Asking for query plans from GitLab.com is the most reliable way to validate
these.
2. **Database migrations** must be:
1. Reversible.
2. Performant at the scale of GitLab.com - ask a maintainer to test the
migration on the staging environment if you aren't sure.
3. Categorised correctly:
- Regular migrations run before the new code is running on the instance.
- [Post-deployment migrations](post_deployment_migrations.md) run _after_
the new code is deployed, when the instance is configured to do that.
- [Background migrations](background_migrations.md) run in Sidekiq, and
should only be done for migrations that would take an extreme amount of
time at GitLab.com scale.
3. **Sidekiq workers**
[cannot change in a backwards-incompatible way](sidekiq_style_guide.md#removing-or-renaming-queues):
1. Sidekiq queues are not drained before a deploy happens, so there will be
workers in the queue from the previous version of GitLab.
2. If you need to change a method signature, try to do so across two releases,
and accept both the old and new arguments in the first of those.
3. Similarly, if you need to remove a worker, stop it from being scheduled in
one release, then remove it in the next. This will allow existing jobs to
execute.
4. Don't forget, not every instance will upgrade to every intermediate version
(some people may go from X.1.0 to X.10.0, or even try bigger upgrades!), so
try to be liberal in accepting the old format if it is cheap to do so.
4. **Cached values** may persist across releases. If you are changing the type a
cached value returns (say, from a string or nil to an array), change the
cache key at the same time.
5. **Settings** should be added as a
[last resort](https://about.gitlab.com/handbook/product/#convention-over-configuration).
If you're adding a new setting in `gitlab.yml`:
1. Try to avoid that, and add to `ApplicationSetting` instead.
2. Ensure that it is also
[added to Omnibus](https://docs.gitlab.com/omnibus/settings/gitlab.yml.html#adding-a-new-setting-to-gitlab-yml).
6. **Filesystem access** can be slow, so try to avoid
[shared files](shared_files.md) when an alternative solution is available.
### Credits ### Credits
Largely based on the [thoughtbot code review guide]. Largely based on the [thoughtbot code review guide].
......
...@@ -23,6 +23,18 @@ controlled by the server. ...@@ -23,6 +23,18 @@ controlled by the server.
1. The backend code will most likely be using etags. You do not and should not check for status 1. The backend code will most likely be using etags. You do not and should not check for status
`304 Not Modified`. The browser will transform it for you. `304 Not Modified`. The browser will transform it for you.
### Lazy Loading
To improve the time to first render we are using lazy loading for images. This works by setting
the actual image source on the `data-src` attribute. After the HTML is rendered and JavaScript is loaded,
the value of `data-src` will be moved to `src` automatically if the image is in the current viewport.
* Prepare images in HTML for lazy loading by renaming the `src` attribute to `data-src`
* If you are using the Rails `image_tag` helper, all images will be lazy-loaded by default unless `lazy: false` is provided.
If you are asynchronously adding content which contains lazy images then you need to call the function
`gl.lazyLoader.searchLazyImages()` which will search for lazy images and load them if needed.
## Reducing Asset Footprint ## Reducing Asset Footprint
### Page-specific JavaScript ### Page-specific JavaScript
......
...@@ -114,7 +114,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps ...@@ -114,7 +114,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end end
step 'Image should be shown on the page' do step 'Image should be shown on the page' do
expect(page).to have_xpath("//img[@src=\"image.jpg\"]") expect(page).to have_xpath("//img[@data-src=\"image.jpg\"]")
end end
step 'I click on image link' do step 'I click on image link' do
......
...@@ -29,12 +29,15 @@ module API ...@@ -29,12 +29,15 @@ module API
render_api_error!(errors, 400) render_api_error!(errors, 400)
end end
<<<<<<< HEAD
def check_sha_param!(params, merge_request) def check_sha_param!(params, merge_request)
if params[:sha] && merge_request.diff_head_sha != params[:sha] if params[:sha] && merge_request.diff_head_sha != params[:sha]
render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409) render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
end end
end end
=======
>>>>>>> d964816b9fe56679ffc0b331e701f7b24db5c6a9
def find_merge_requests(args = {}) def find_merge_requests(args = {})
args = params.merge(args) args = params.merge(args)
......
...@@ -118,7 +118,7 @@ module Banzai ...@@ -118,7 +118,7 @@ module Banzai
end end
if path if path
content_tag(:img, nil, src: path, class: 'gfm') content_tag(:img, nil, data: { src: path }, class: 'gfm')
end end
end end
......
module Banzai
module Filter
# HTML filter that moves the value of the src attribute to the data-src attribute so it can be lazy loaded
class ImageLazyLoadFilter < HTML::Pipeline::Filter
def call
doc.xpath('descendant-or-self::img').each do |img|
img['class'] ||= '' << 'lazy'
img['data-src'] = img['src']
img['src'] = LazyImageTagHelper.placeholder_image
end
doc
end
end
end
end
...@@ -10,7 +10,7 @@ module Banzai ...@@ -10,7 +10,7 @@ module Banzai
link = doc.document.create_element( link = doc.document.create_element(
'a', 'a',
class: 'no-attachment-icon', class: 'no-attachment-icon',
href: img['src'], href: img['data-src'] || img['src'],
target: '_blank', target: '_blank',
rel: 'noopener noreferrer' rel: 'noopener noreferrer'
) )
......
...@@ -22,6 +22,7 @@ module Banzai ...@@ -22,6 +22,7 @@ module Banzai
doc.css('img, video').each do |el| doc.css('img, video').each do |el|
process_link_attr el.attribute('src') process_link_attr el.attribute('src')
process_link_attr el.attribute('data-src')
end end
doc doc
......
...@@ -16,6 +16,7 @@ module Banzai ...@@ -16,6 +16,7 @@ module Banzai
Filter::MathFilter, Filter::MathFilter,
Filter::UploadLinkFilter, Filter::UploadLinkFilter,
Filter::VideoLinkFilter, Filter::VideoLinkFilter,
Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter, Filter::ImageLinkFilter,
Filter::EmojiFilter, Filter::EmojiFilter,
Filter::TableOfContentsFilter, Filter::TableOfContentsFilter,
......
...@@ -22,33 +22,9 @@ module Gitlab ...@@ -22,33 +22,9 @@ module Gitlab
end end
def scan(text) def scan(text)
text = text.dup # modified in-place matches = scan_regexp.scan(text).to_a
results = [] matches.map!(&:first) if regexp.number_of_capturing_groups.zero?
matches
loop do
match = scan_regexp.match(text)
break unless match
# Ruby scan returns empty strings, not nil
groups = match.to_a.map(&:to_s)
results <<
if regexp.number_of_capturing_groups.zero?
groups[0]
else
groups[1..-1]
end
matchsize = match.end(0)
# No further matches
break unless matchsize.present?
text.slice!(0, matchsize)
break unless text.present?
end
results
end end
def replace(text, rewrite) def replace(text, rewrite)
......
...@@ -44,7 +44,6 @@ module Gitlab ...@@ -44,7 +44,6 @@ module Gitlab
pages_domains: PagesDomain.count, pages_domains: PagesDomain.count,
projects: Project.count, projects: Project.count,
projects_imported_from_github: Project.where(import_type: 'github').count, projects_imported_from_github: Project.where(import_type: 'github').count,
projects_prometheus_active: PrometheusService.active.count,
protected_branches: ProtectedBranch.count, protected_branches: ProtectedBranch.count,
releases: Release.count, releases: Release.count,
remote_mirrors: RemoteMirror.count, remote_mirrors: RemoteMirror.count,
...@@ -53,6 +52,7 @@ module Gitlab ...@@ -53,6 +52,7 @@ module Gitlab
todos: Todo.count, todos: Todo.count,
uploads: Upload.count, uploads: Upload.count,
web_hooks: WebHook.count web_hooks: WebHook.count
<<<<<<< HEAD
}.merge(service_desk_counts) }.merge(service_desk_counts)
} }
end end
...@@ -67,6 +67,9 @@ module Gitlab ...@@ -67,6 +67,9 @@ module Gitlab
service_desk_issues: Issue.where(project: projects_with_service_desk, service_desk_issues: Issue.where(project: projects_with_service_desk,
author: User.support_bot, author: User.support_bot,
confidential: true).count confidential: true).count
=======
}.merge(services_usage)
>>>>>>> d964816b9fe56679ffc0b331e701f7b24db5c6a9
} }
end end
...@@ -97,6 +100,7 @@ module Gitlab ...@@ -97,6 +100,7 @@ module Gitlab
usage_data usage_data
end end
<<<<<<< HEAD
def license_edition(plan) def license_edition(plan)
case plan case plan
when 'premium' when 'premium'
...@@ -106,6 +110,18 @@ module Gitlab ...@@ -106,6 +110,18 @@ module Gitlab
else # Older licenses else # Older licenses
'EE' 'EE'
end end
=======
def services_usage
types = {
JiraService: :projects_jira_active,
SlackService: :projects_slack_notifications_active,
SlackSlashCommandsService: :projects_slack_slash_active,
PrometheusService: :projects_prometheus_active
}
results = Service.unscoped.where(type: types.keys, active: true).group(:type).count
results.each_with_object({}) { |(key, value), response| response[types[key.to_sym]] = value }
>>>>>>> d964816b9fe56679ffc0b331e701f7b24db5c6a9
end end
end end
end end
......
...@@ -62,7 +62,7 @@ module Gitlab ...@@ -62,7 +62,7 @@ module Gitlab
end end
def send_git_blob(repository, blob) def send_git_blob(repository, blob)
params = if Gitlab::GitalyClient.feature_enabled?(:project_raw_show) params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_raw_show)
{ {
'GitalyServer' => gitaly_server_hash(repository), 'GitalyServer' => gitaly_server_hash(repository),
'GetBlobRequest' => { 'GetBlobRequest' => {
......
...@@ -63,11 +63,11 @@ feature 'Admin Appearance', feature: true do ...@@ -63,11 +63,11 @@ feature 'Admin Appearance', feature: true do
end end
def logo_selector def logo_selector
'//img[@src^="/uploads/-/system/appearance/logo"]' '//img[data-src^="/uploads/-/system/appearance/logo"]'
end end
def header_logo_selector def header_logo_selector
'//img[@src^="/uploads/-/system/appearance/header_logo"]' '//img[data-src^="/uploads/-/system/appearance/header_logo"]'
end end
def logo_fixture def logo_fixture
......
...@@ -100,7 +100,7 @@ describe 'GitLab Markdown', feature: true do ...@@ -100,7 +100,7 @@ describe 'GitLab Markdown', feature: true do
end end
it 'permits img elements' do it 'permits img elements' do
expect(doc).to have_selector('img[src*="smile.png"]') expect(doc).to have_selector('img[data-src*="smile.png"]')
end end
it 'permits br elements' do it 'permits br elements' do
......
...@@ -18,7 +18,7 @@ feature 'User uploads avatar to group', feature: true do ...@@ -18,7 +18,7 @@ feature 'User uploads avatar to group', feature: true do
visit group_path(group) visit group_path(group)
expect(page).to have_selector(%Q(img[src$="/uploads/-/system/group/avatar/#{group.id}/dk.png"])) expect(page).to have_selector(%Q(img[data-src$="/uploads/-/system/group/avatar/#{group.id}/dk.png"]))
# Cheating here to verify something that isn't user-facing, but is important # Cheating here to verify something that isn't user-facing, but is important
expect(group.reload.avatar.file).to exist expect(group.reload.avatar.file).to exist
......
...@@ -16,7 +16,7 @@ feature 'User uploads avatar to profile', feature: true do ...@@ -16,7 +16,7 @@ feature 'User uploads avatar to profile', feature: true do
visit user_path(user) visit user_path(user)
expect(page).to have_selector(%Q(img[src$="/uploads/-/system/user/avatar/#{user.id}/dk.png"])) expect(page).to have_selector(%Q(img[data-src$="/uploads/-/system/user/avatar/#{user.id}/dk.png"]))
# Cheating here to verify something that isn't user-facing, but is important # Cheating here to verify something that isn't user-facing, but is important
expect(user.reload.avatar.file).to exist expect(user.reload.avatar.file).to exist
......
...@@ -62,13 +62,13 @@ describe ApplicationHelper do ...@@ -62,13 +62,13 @@ describe ApplicationHelper do
avatar_url = "/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif" avatar_url = "/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon(project.full_path).to_s) expect(helper.project_icon(project.full_path).to_s)
.to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />" .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host) allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
avatar_url = "#{gitlab_host}/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif" avatar_url = "#{gitlab_host}/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon(project.full_path).to_s) expect(helper.project_icon(project.full_path).to_s)
.to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />" .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
end end
it 'gives uploaded icon when present' do it 'gives uploaded icon when present' do
...@@ -77,7 +77,8 @@ describe ApplicationHelper do ...@@ -77,7 +77,8 @@ describe ApplicationHelper do
allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true) allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
avatar_url = "#{gitlab_host}#{project_avatar_path(project)}" avatar_url = "#{gitlab_host}#{project_avatar_path(project)}"
expect(helper.project_icon(project.full_path).to_s).to match(image_tag(avatar_url)) expect(helper.project_icon(project.full_path).to_s)
.to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
end end
end end
......
...@@ -27,11 +27,11 @@ describe AvatarsHelper do ...@@ -27,11 +27,11 @@ describe AvatarsHelper do
it 'displays user avatar' do it 'displays user avatar' do
is_expected.to eq image_tag( is_expected.to eq image_tag(
avatar_icon(user, 16), LazyImageTagHelper.placeholder_image,
class: 'avatar has-tooltip s16 ', class: 'avatar has-tooltip s16 lazy',
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, title: user.name,
data: { container: 'body' } data: { container: 'body', src: avatar_icon(user, 16) }
) )
end end
...@@ -40,22 +40,8 @@ describe AvatarsHelper do ...@@ -40,22 +40,8 @@ describe AvatarsHelper do
it 'uses provided css_class' do it 'uses provided css_class' do
is_expected.to eq image_tag( is_expected.to eq image_tag(
avatar_icon(user, 16), LazyImageTagHelper.placeholder_image,
class: "avatar has-tooltip s16 #{options[:css_class]}", class: "avatar has-tooltip s16 #{options[:css_class]} lazy",
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body' }
)
end
end
context 'with lazy parameter' do
let(:options) { { user: user, lazy: true } }
it 'uses data-src instead of src' do
is_expected.to eq image_tag(
'',
class: 'avatar has-tooltip s16 ',
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, title: user.name,
data: { container: 'body', src: avatar_icon(user, 16) } data: { container: 'body', src: avatar_icon(user, 16) }
...@@ -68,11 +54,11 @@ describe AvatarsHelper do ...@@ -68,11 +54,11 @@ describe AvatarsHelper do
it 'uses provided size' do it 'uses provided size' do
is_expected.to eq image_tag( is_expected.to eq image_tag(
avatar_icon(user, options[:size]), LazyImageTagHelper.placeholder_image,
class: "avatar has-tooltip s#{options[:size]} ", class: "avatar has-tooltip s#{options[:size]} lazy",
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, title: user.name,
data: { container: 'body' } data: { container: 'body', src: avatar_icon(user, options[:size]) }
) )
end end
end end
...@@ -82,11 +68,11 @@ describe AvatarsHelper do ...@@ -82,11 +68,11 @@ describe AvatarsHelper do
it 'uses provided url' do it 'uses provided url' do
is_expected.to eq image_tag( is_expected.to eq image_tag(
options[:url], LazyImageTagHelper.placeholder_image,
class: 'avatar has-tooltip s16 ', class: 'avatar has-tooltip s16 lazy',
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, title: user.name,
data: { container: 'body' } data: { container: 'body', src: options[:url] }
) )
end end
end end
...@@ -99,22 +85,22 @@ describe AvatarsHelper do ...@@ -99,22 +85,22 @@ describe AvatarsHelper do
it 'prefers user parameter' do it 'prefers user parameter' do
is_expected.to eq image_tag( is_expected.to eq image_tag(
avatar_icon(user, 16), LazyImageTagHelper.placeholder_image,
class: 'avatar has-tooltip s16 ', class: 'avatar has-tooltip s16 lazy',
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, title: user.name,
data: { container: 'body' } data: { container: 'body', src: avatar_icon(user, 16) }
) )
end end
end end
it 'uses user_name and user_email parameter if user is not present' do it 'uses user_name and user_email parameter if user is not present' do
is_expected.to eq image_tag( is_expected.to eq image_tag(
avatar_icon(options[:user_email], 16), LazyImageTagHelper.placeholder_image,
class: 'avatar has-tooltip s16 ', class: 'avatar has-tooltip s16 lazy',
alt: "#{options[:user_name]}'s avatar", alt: "#{options[:user_name]}'s avatar",
title: options[:user_name], title: options[:user_name],
data: { container: 'body' } data: { container: 'body', src: avatar_icon(options[:user_email], 16) }
) )
end end
end end
......
import LazyLoader from '~/lazy_loader';
let lazyLoader = null;
describe('LazyLoader', function () {
preloadFixtures('issues/issue_with_comment.html.raw');
beforeEach(function () {
loadFixtures('issues/issue_with_comment.html.raw');
lazyLoader = new LazyLoader({
observerNode: 'body',
});
// Doing everything that happens normally in onload
lazyLoader.loadCheck();
});
describe('behavior', function () {
it('should copy value from data-src to src for img 1', function (done) {
const img = document.querySelectorAll('img[data-src]')[0];
const originalDataSrc = img.getAttribute('data-src');
img.scrollIntoView();
setTimeout(() => {
expect(img.getAttribute('src')).toBe(originalDataSrc);
expect(document.getElementsByClassName('js-lazy-loaded').length).toBeGreaterThan(0);
done();
}, 100);
});
it('should lazy load dynamically added data-src images', function (done) {
const newImg = document.createElement('img');
const testPath = '/img/testimg.png';
newImg.className = 'lazy';
newImg.setAttribute('data-src', testPath);
document.body.appendChild(newImg);
newImg.scrollIntoView();
setTimeout(() => {
expect(newImg.getAttribute('src')).toBe(testPath);
expect(document.getElementsByClassName('js-lazy-loaded').length).toBeGreaterThan(0);
done();
}, 100);
});
it('should not alter normal images', function (done) {
const newImg = document.createElement('img');
const testPath = '/img/testimg.png';
newImg.setAttribute('src', testPath);
document.body.appendChild(newImg);
newImg.scrollIntoView();
setTimeout(() => {
expect(newImg).not.toHaveClass('js-lazy-loaded');
done();
}, 100);
});
});
});
...@@ -22,7 +22,7 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do ...@@ -22,7 +22,7 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do
tag = '[[images/image.jpg]]' tag = '[[images/image.jpg]]'
doc = filter("See #{tag}", project_wiki: project_wiki) doc = filter("See #{tag}", project_wiki: project_wiki)
expect(doc.at_css('img')['src']).to eq "#{project_wiki.wiki_base_path}/images/image.jpg" expect(doc.at_css('img')['data-src']).to eq "#{project_wiki.wiki_base_path}/images/image.jpg"
end end
it 'does not creates img tag if image does not exist' do it 'does not creates img tag if image does not exist' do
...@@ -40,7 +40,7 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do ...@@ -40,7 +40,7 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do
tag = '[[http://example.com/image.jpg]]' tag = '[[http://example.com/image.jpg]]'
doc = filter("See #{tag}", project_wiki: project_wiki) doc = filter("See #{tag}", project_wiki: project_wiki)
expect(doc.at_css('img')['src']).to eq "http://example.com/image.jpg" expect(doc.at_css('img')['data-src']).to eq "http://example.com/image.jpg"
end end
it 'does not creates img tag for invalid URL' do it 'does not creates img tag for invalid URL' do
......
require 'spec_helper'
describe Banzai::Filter::ImageLazyLoadFilter, lib: true do
include FilterSpecHelper
def image(path)
%(<img src="#{path}" />)
end
it 'transforms the image src to a data-src' do
doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
expect(doc.at_css('img')['data-src']).to eq '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'
end
it 'works with external images' do
doc = filter(image('https://i.imgur.com/DfssX9C.jpg'))
expect(doc.at_css('img')['data-src']).to eq 'https://i.imgur.com/DfssX9C.jpg'
end
end
...@@ -308,6 +308,20 @@ describe Gitlab::Ci::Trace::Stream do ...@@ -308,6 +308,20 @@ describe Gitlab::Ci::Trace::Stream do
it { is_expected.to eq('65') } it { is_expected.to eq('65') }
end end
context 'long line' do
let(:data) { 'a' * 80000 + '100%' + 'a' * 80000 }
let(:regex) { '\d+\%' }
it { is_expected.to eq('100') }
end
context 'many lines' do
let(:data) { "foo\n" * 80000 + "100%\n" + "foo\n" * 80000 }
let(:regex) { '\d+\%' }
it { is_expected.to eq('100') }
end
context 'empty regex' do context 'empty regex' do
let(:data) { 'foo' } let(:data) { 'foo' }
let(:regex) { '' } let(:regex) { '' }
......
...@@ -54,8 +54,8 @@ describe Gitlab::UntrustedRegexp do ...@@ -54,8 +54,8 @@ describe Gitlab::UntrustedRegexp do
let(:regexp) { '' } let(:regexp) { '' }
let(:text) { 'foo' } let(:text) { 'foo' }
it 'returns an array of empty matches' do it 'returns an array of nil matches' do
is_expected.to eq(['']) is_expected.to eq([nil, nil, nil, nil])
end end
end end
...@@ -63,8 +63,8 @@ describe Gitlab::UntrustedRegexp do ...@@ -63,8 +63,8 @@ describe Gitlab::UntrustedRegexp do
let(:regexp) { '()' } let(:regexp) { '()' }
let(:text) { 'foo' } let(:text) { 'foo' }
it 'returns an array of empty matches in an array' do it 'returns an array of nil matches in an array' do
is_expected.to eq([['']]) is_expected.to eq([[nil], [nil], [nil], [nil]])
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::UsageData do describe Gitlab::UsageData do
let!(:project) { create(:empty_project) } let(:projects) { create_list(:project, 3) }
let!(:project2) { create(:empty_project) } let!(:board) { create(:board, project: projects[0]) }
let!(:board) { create(:board, project: project) }
describe '#data' do describe '#data' do
before do
create(:jira_service, project: projects[0])
create(:jira_service, project: projects[1])
create(:prometheus_service, project: projects[1])
create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true)
create(:service, project: projects[1], type: 'SlackService', active: true)
create(:service, project: projects[2], type: 'SlackService', active: true)
end
subject { described_class.data } subject { described_class.data }
it "gathers usage data" do it "gathers usage data" do
...@@ -32,7 +40,7 @@ describe Gitlab::UsageData do ...@@ -32,7 +40,7 @@ describe Gitlab::UsageData do
count_data = subject[:counts] count_data = subject[:counts]
expect(count_data[:boards]).to eq(1) expect(count_data[:boards]).to eq(1)
expect(count_data[:projects]).to eq(2) expect(count_data[:projects]).to eq(3)
expect(count_data.keys).to match_array(%i( expect(count_data.keys).to match_array(%i(
boards boards
...@@ -60,6 +68,9 @@ describe Gitlab::UsageData do ...@@ -60,6 +68,9 @@ describe Gitlab::UsageData do
notes notes
projects projects
projects_imported_from_github projects_imported_from_github
projects_jira_active
projects_slack_notifications_active
projects_slack_slash_active
projects_prometheus_active projects_prometheus_active
pages_domains pages_domains
protected_branches protected_branches
...@@ -72,6 +83,16 @@ describe Gitlab::UsageData do ...@@ -72,6 +83,16 @@ describe Gitlab::UsageData do
web_hooks web_hooks
)) ))
end end
it 'gathers projects data correctly' do
count_data = subject[:counts]
expect(count_data[:projects]).to eq(3)
expect(count_data[:projects_prometheus_active]).to eq(1)
expect(count_data[:projects_jira_active]).to eq(2)
expect(count_data[:projects_slack_notifications_active]).to eq(2)
expect(count_data[:projects_slack_slash_active]).to eq(1)
end
end end
describe '#license_usage_data' do describe '#license_usage_data' do
......
...@@ -325,7 +325,7 @@ describe Gitlab::Workhorse, lib: true do ...@@ -325,7 +325,7 @@ describe Gitlab::Workhorse, lib: true do
subject { described_class.send_git_blob(repository, blob) } subject { described_class.send_git_blob(repository, blob) }
context 'when Gitaly project_raw_show feature is enabled' do context 'when Gitaly workhorse_raw_show feature is enabled' do
it 'sets the header correctly' do it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject) key, command, params = decode_workhorse_header(subject)
...@@ -345,7 +345,7 @@ describe Gitlab::Workhorse, lib: true do ...@@ -345,7 +345,7 @@ describe Gitlab::Workhorse, lib: true do
end end
end end
context 'when Gitaly project_raw_show feature is disabled', skip_gitaly_mock: true do context 'when Gitaly workhorse_raw_show feature is disabled', skip_gitaly_mock: true do
it 'sets the header correctly' do it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject) key, command, params = decode_workhorse_header(subject)
......
...@@ -98,7 +98,7 @@ describe Ability, lib: true do ...@@ -98,7 +98,7 @@ describe Ability, lib: true do
user2 = build(:user, external: true) user2 = build(:user, external: true)
users = [user1, user2] users = [user1, user2]
expect(project).to receive(:owner).twice.and_return(user1) expect(project).to receive(:owner).at_least(:once).and_return(user1)
expect(described_class.users_that_can_read_project(users, project)) expect(described_class.users_that_can_read_project(users, project))
.to eq([user1]) .to eq([user1])
...@@ -109,7 +109,7 @@ describe Ability, lib: true do ...@@ -109,7 +109,7 @@ describe Ability, lib: true do
user2 = build(:user, external: true) user2 = build(:user, external: true)
users = [user1, user2] users = [user1, user2]
expect(project.team).to receive(:members).twice.and_return([user1]) expect(project.team).to receive(:members).at_least(:once).and_return([user1])
expect(described_class.users_that_can_read_project(users, project)) expect(described_class.users_that_can_read_project(users, project))
.to eq([user1]) .to eq([user1])
...@@ -140,7 +140,7 @@ describe Ability, lib: true do ...@@ -140,7 +140,7 @@ describe Ability, lib: true do
user2 = build(:user, external: true) user2 = build(:user, external: true)
users = [user1, user2] users = [user1, user2]
expect(project).to receive(:owner).twice.and_return(user1) expect(project).to receive(:owner).at_least(:once).and_return(user1)
expect(described_class.users_that_can_read_project(users, project)) expect(described_class.users_that_can_read_project(users, project))
.to eq([user1]) .to eq([user1])
...@@ -151,7 +151,7 @@ describe Ability, lib: true do ...@@ -151,7 +151,7 @@ describe Ability, lib: true do
user2 = build(:user, external: true) user2 = build(:user, external: true)
users = [user1, user2] users = [user1, user2]
expect(project.team).to receive(:members).twice.and_return([user1]) expect(project.team).to receive(:members).at_least(:once).and_return([user1])
expect(described_class.users_that_can_read_project(users, project)) expect(described_class.users_that_can_read_project(users, project))
.to eq([user1]) .to eq([user1])
......
...@@ -255,6 +255,7 @@ describe Group, models: true do ...@@ -255,6 +255,7 @@ describe Group, models: true do
describe '#has_owner?' do describe '#has_owner?' do
before do before do
@members = setup_group_members(group) @members = setup_group_members(group)
create(:group_member, :invited, :owner, group: group)
end end
it { expect(group.has_owner?(@members[:owner])).to be_truthy } it { expect(group.has_owner?(@members[:owner])).to be_truthy }
...@@ -263,11 +264,13 @@ describe Group, models: true do ...@@ -263,11 +264,13 @@ describe Group, models: true do
it { expect(group.has_owner?(@members[:reporter])).to be_falsey } it { expect(group.has_owner?(@members[:reporter])).to be_falsey }
it { expect(group.has_owner?(@members[:guest])).to be_falsey } it { expect(group.has_owner?(@members[:guest])).to be_falsey }
it { expect(group.has_owner?(@members[:requester])).to be_falsey } it { expect(group.has_owner?(@members[:requester])).to be_falsey }
it { expect(group.has_owner?(nil)).to be_falsey }
end end
describe '#has_master?' do describe '#has_master?' do
before do before do
@members = setup_group_members(group) @members = setup_group_members(group)
create(:group_member, :invited, :master, group: group)
end end
it { expect(group.has_master?(@members[:owner])).to be_falsey } it { expect(group.has_master?(@members[:owner])).to be_falsey }
...@@ -276,6 +279,7 @@ describe Group, models: true do ...@@ -276,6 +279,7 @@ describe Group, models: true do
it { expect(group.has_master?(@members[:reporter])).to be_falsey } it { expect(group.has_master?(@members[:reporter])).to be_falsey }
it { expect(group.has_master?(@members[:guest])).to be_falsey } it { expect(group.has_master?(@members[:guest])).to be_falsey }
it { expect(group.has_master?(@members[:requester])).to be_falsey } it { expect(group.has_master?(@members[:requester])).to be_falsey }
it { expect(group.has_master?(nil)).to be_falsey }
end end
describe '#lfs_enabled?' do describe '#lfs_enabled?' do
......
...@@ -160,12 +160,21 @@ describe MergeRequest, models: true do ...@@ -160,12 +160,21 @@ describe MergeRequest, models: true do
subject.project.has_external_issue_tracker = true subject.project.has_external_issue_tracker = true
subject.project.save! subject.project.save!
end end
<<<<<<< HEAD
it 'does not cache issues from external trackers' do it 'does not cache issues from external trackers' do
issue = ExternalIssue.new('JIRA-123', subject.project) issue = ExternalIssue.new('JIRA-123', subject.project)
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}") commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit]) allow(subject).to receive(:commits).and_return([commit])
=======
it 'does not cache issues from external trackers' do
issue = ExternalIssue.new('JIRA-123', subject.project)
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
>>>>>>> d964816b9fe56679ffc0b331e701f7b24db5c6a9
expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count) expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
end end
......
...@@ -582,6 +582,7 @@ describe Project, models: true do ...@@ -582,6 +582,7 @@ describe Project, models: true do
let!(:internal_issue) { create(:issue, project: project) } let!(:internal_issue) { create(:issue, project: project) }
before do before do
allow(project).to receive(:external_issue_tracker).and_return(true) allow(project).to receive(:external_issue_tracker).and_return(true)
<<<<<<< HEAD
end end
context 'when internal issues are enabled' do context 'when internal issues are enabled' do
...@@ -602,6 +603,28 @@ describe Project, models: true do ...@@ -602,6 +603,28 @@ describe Project, models: true do
end end
end end
=======
end
context 'when internal issues are enabled' do
it 'returns interlan issue' do
issue = project.get_issue(internal_issue.iid, user)
expect(issue).to be_kind_of(Issue)
expect(issue.iid).to eq(internal_issue.iid)
expect(issue.project).to eq(project)
end
it 'returns an ExternalIssue when internal issue does not exists' do
issue = project.get_issue('FOO-1234', user)
expect(issue).to be_kind_of(ExternalIssue)
expect(issue.iid).to eq('FOO-1234')
expect(issue.project).to eq(project)
end
end
>>>>>>> d964816b9fe56679ffc0b331e701f7b24db5c6a9
context 'when internal issues are disabled' do context 'when internal issues are disabled' do
before do before do
project.issues_enabled = false project.issues_enabled = false
......
...@@ -139,6 +139,27 @@ describe ProjectPolicy, models: true do ...@@ -139,6 +139,27 @@ describe ProjectPolicy, models: true do
end end
end end
<<<<<<< HEAD
=======
context 'when a project has pending invites, and the current user is anonymous' do
let(:group) { create(:group, :public) }
let(:project) { create(:empty_project, :public, namespace: group) }
let(:user_permissions) { [:create_project, :create_issue, :create_note, :upload_file] }
let(:anonymous_permissions) { guest_permissions - user_permissions }
subject { described_class.new(nil, project) }
before do
create(:group_member, :invited, group: group)
end
it 'does not grant owner access' do
expect_allowed(*anonymous_permissions)
expect_disallowed(*user_permissions)
end
end
>>>>>>> d964816b9fe56679ffc0b331e701f7b24db5c6a9
context 'abilities for non-public projects' do context 'abilities for non-public projects' do
let(:project) { create(:empty_project, namespace: owner.namespace) } let(:project) { create(:empty_project, namespace: owner.namespace) }
......
...@@ -536,6 +536,7 @@ describe GitPushService, services: true do ...@@ -536,6 +536,7 @@ describe GitPushService, services: true do
let(:message) { "this is some work.\n\ncloses #1" } let(:message) { "this is some work.\n\ncloses #1" }
it "does not initiates one api call to jira server to close the issue" do it "does not initiates one api call to jira server to close the issue" do
<<<<<<< HEAD
execute_service(project, commit_author, oldrev, newrev, ref ) execute_service(project, commit_author, oldrev, newrev, ref )
expect(WebMock).not_to have_requested(:post, jira_api_transition_url('JIRA-1')) expect(WebMock).not_to have_requested(:post, jira_api_transition_url('JIRA-1'))
...@@ -562,6 +563,34 @@ describe GitPushService, services: true do ...@@ -562,6 +563,34 @@ describe GitPushService, services: true do
it "initiates one api call to jira server to comment on the jira issue" do it "initiates one api call to jira server to comment on the jira issue" do
execute_service(project, commit_author, oldrev, newrev, ref ) execute_service(project, commit_author, oldrev, newrev, ref )
=======
execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).not_to have_requested(:post, jira_api_transition_url('JIRA-1'))
end
it "does not initiates one api call to jira server to comment on the issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).not_to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
body: comment_body
).once
end
end
context 'when internal issues are enabled' do
let(:issue) { create(:issue, project: project) }
let(:message) { "this is some work.\n\ncloses JIRA-1 \n\n closes #{issue.to_reference}" }
it "initiates one api call to jira server to close the jira issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).to have_requested(:post, jira_api_transition_url('JIRA-1')).once
end
it "initiates one api call to jira server to comment on the jira issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
>>>>>>> d964816b9fe56679ffc0b331e701f7b24db5c6a9
expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with( expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
body: comment_body body: comment_body
...@@ -569,14 +598,22 @@ describe GitPushService, services: true do ...@@ -569,14 +598,22 @@ describe GitPushService, services: true do
end end
it "closes the internal issue" do it "closes the internal issue" do
<<<<<<< HEAD
execute_service(project, commit_author, oldrev, newrev, ref ) execute_service(project, commit_author, oldrev, newrev, ref )
=======
execute_service(project, commit_author, @oldrev, @newrev, @ref )
>>>>>>> d964816b9fe56679ffc0b331e701f7b24db5c6a9
expect(issue.reload).to be_closed expect(issue.reload).to be_closed
end end
it "adds a note indicating that the issue is now closed" do it "adds a note indicating that the issue is now closed" do
expect(SystemNoteService).to receive(:change_status) expect(SystemNoteService).to receive(:change_status)
.with(issue, project, commit_author, "closed", closing_commit) .with(issue, project, commit_author, "closed", closing_commit)
<<<<<<< HEAD
execute_service(project, commit_author, oldrev, newrev, ref ) execute_service(project, commit_author, oldrev, newrev, ref )
=======
execute_service(project, commit_author, @oldrev, @newrev, @ref )
>>>>>>> d964816b9fe56679ffc0b331e701f7b24db5c6a9
end end
end end
end end
......
...@@ -17,7 +17,7 @@ module MarkdownMatchers ...@@ -17,7 +17,7 @@ module MarkdownMatchers
image = actual.at_css('img[alt="Relative Image"]') image = actual.at_css('img[alt="Relative Image"]')
expect(link['href']).to end_with('master/doc/README.md') expect(link['href']).to end_with('master/doc/README.md')
expect(image['src']).to end_with('master/app/assets/images/touch-icon-ipad.png') expect(image['data-src']).to end_with('master/app/assets/images/touch-icon-ipad.png')
end end
end end
...@@ -70,7 +70,7 @@ module MarkdownMatchers ...@@ -70,7 +70,7 @@ module MarkdownMatchers
# GollumTagsFilter # GollumTagsFilter
matcher :parse_gollum_tags do matcher :parse_gollum_tags do
def have_image(src) def have_image(src)
have_css("img[src$='#{src}']") have_css("img[data-src$='#{src}']")
end end
prefix = '/namespace1/gitlabhq/wikis' prefix = '/namespace1/gitlabhq/wikis'
......
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