Commit 7c60d2db authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch 'master' into geo-base-scheduler-worker

parents d045cbef 89d7c75a
...@@ -18,7 +18,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git ...@@ -18,7 +18,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git
variables: variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1" MYSQL_ALLOW_EMPTY_PASSWORD: "1"
ELASTIC_URL: "http://elasticsearch:9200" ELASTIC_URL: "http://elastic:changeme@docker.elastic.co-elasticsearch-elasticsearch:9200"
RAILS_ENV: "test" RAILS_ENV: "test"
NODE_ENV: "test" NODE_ENV: "test"
SIMPLECOV: "true" SIMPLECOV: "true"
...@@ -63,13 +63,14 @@ stages: ...@@ -63,13 +63,14 @@ stages:
services: services:
- postgres:9.2 - postgres:9.2
- redis:alpine - redis:alpine
- elasticsearch:5.3 - docker.elastic.co/elasticsearch/elasticsearch:5.3.2
.use-mysql: &use-mysql .use-mysql: &use-mysql
services: services:
- mysql:latest - mysql:latest
- redis:alpine - redis:alpine
- elasticsearch:5.3 - docker.elastic.co/elasticsearch/elasticsearch:5.3.2
.only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql .only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql
only: only:
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 9.4.1 (2017-07-25)
- Cleans up mirror capacity in project destroy service if project is a scheduled mirror. !2445
- Fixes unscoping of imposed capacity limit by find_each method on Mirror scheduler. !2460
- Remove text underline from suggested approvers.
## 9.4.0 (2017-07-22) ## 9.4.0 (2017-07-22)
- GeoLogCursor is part of a new experimental Geo replication system. !1988 - GeoLogCursor is part of a new experimental Geo replication system. !1988
......
...@@ -2,6 +2,18 @@ ...@@ -2,6 +2,18 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 9.4.1 (2017-07-25)
- Fix pipeline_schedules pages throwing error 500 (when ref is empty). !12983
- Fix editing project with container images present. !13028
- Fix some invalid entries in PO files. !13032
- Fix cross site request protection when logging in as a regular user when LDAP is enabled. !13049
- Fix bug causing metrics files to be truncated. !35420
- Fix anonymous access to public projects in groups with pending invites.
- Fixed issue boards sidebar close icon size.
- Fixed duplicate new milestone buttons when new navigation is turned on.
- Fix margins in the mini graph for pipeline in commits box.
## 9.4.0 (2017-07-22) ## 9.4.0 (2017-07-22)
- Add blame view age mapping. !7198 (Jeff Stubler) - Add blame view age mapping. !7198 (Jeff Stubler)
......
...@@ -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)
......
...@@ -33,7 +33,7 @@ $(() => { ...@@ -33,7 +33,7 @@ $(() => {
const $boardApp = document.getElementById('board-app'); const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
const ModalStore = gl.issueBoards.ModalStore; const ModalStore = gl.issueBoards.ModalStore;
const issueBoardsContent = document.querySelector('.js-focus-mode-board'); const issueBoardsContent = document.querySelector('.content-wrapper > .js-focus-mode-board');
window.gl = window.gl || {}; window.gl = window.gl || {};
......
/* 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;
/* /*
......
...@@ -73,6 +73,7 @@ ...@@ -73,6 +73,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
top: 0; top: 0;
left: 0;
background: $white-light; background: $white-light;
z-index: 500; z-index: 500;
...@@ -479,7 +480,10 @@ ...@@ -479,7 +480,10 @@
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
} }
.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;
&.right-sidebar { &.right-sidebar {
top: 0; top: 0;
bottom: 0; bottom: 0;
......
...@@ -269,6 +269,14 @@ ...@@ -269,6 +269,14 @@
border-bottom: 2px solid $border-color; border-bottom: 2px solid $border-color;
} }
} }
&.has-downstream {
&::after {
content: '';
width: 0;
border: none;
}
}
} }
} }
...@@ -618,8 +626,8 @@ ...@@ -618,8 +626,8 @@
} }
// Dropdown button in mini pipeline graph // Dropdown button in mini pipeline graph
.mini-pipeline-graph-dropdown-toggle, button.mini-pipeline-graph-dropdown-toggle,
.linked-pipeline-mini-item { a.linked-pipeline-mini-item {
border-radius: 100px; border-radius: 100px;
background-color: $white-light; background-color: $white-light;
border-width: 1px; border-width: 1px;
...@@ -630,6 +638,7 @@ ...@@ -630,6 +638,7 @@
padding: 0; padding: 0;
transition: all 0.2s linear; transition: all 0.2s linear;
position: relative; position: relative;
vertical-align: middle;
> .fa.fa-caret-down { > .fa.fa-caret-down {
position: absolute; position: absolute;
...@@ -934,12 +943,13 @@ ...@@ -934,12 +943,13 @@
&.is-downstream { &.is-downstream {
margin-left: -4px; margin-left: -4px;
margin-right: 4px;
} }
.arrow-icon { .arrow-icon {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
margin: -2px 5px 0; margin: -4px 2px 0 0;
svg { svg {
fill: $gray-darkest; fill: $gray-darkest;
...@@ -956,20 +966,23 @@ ...@@ -956,20 +966,23 @@
position: relative; position: relative;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
height: 20px; height: 22px;
width: 20px; width: 22px;
transition: margin .2s linear; transition: margin .2s linear;
margin: 2px 5px 3px -12px; margin: 2px 7px 3px -14px;
svg { svg {
top: 0; height: 22px;
right: 0; width: 22px;
width: 18px; position: absolute;
height: 18px; top: -1px;
left: -1px;
z-index: 2;
overflow: visible;
} }
// override dropdown-toggle width expansion // override dropdown-toggle width expansion
&:hover { &:hover {
width: 20px; width: 22px;
} }
&:first-of-type:last-of-type { &:first-of-type:last-of-type {
......
class Admin::ApplicationSettingsController < Admin::ApplicationController class Admin::ApplicationSettingsController < Admin::ApplicationController
prepend EE::Admin::ApplicationSettingsController
before_action :set_application_setting before_action :set_application_setting
def show def show
...@@ -58,7 +60,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -58,7 +60,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def application_setting_params def application_setting_params
import_sources = params[:application_setting][:import_sources] import_sources = params[:application_setting][:import_sources]
if import_sources.nil? if import_sources.nil?
params[:application_setting][:import_sources] = [] params[:application_setting][:import_sources] = []
else else
...@@ -77,11 +78,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -77,11 +78,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file] params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
params.require(:application_setting).permit( params.require(:application_setting).permit(
application_setting_params_ce << application_setting_params_ee application_setting_params_attributes
) )
end end
def application_setting_params_ce def application_setting_params_attributes
[ [
:admin_notification_email, :admin_notification_email,
:after_sign_out_path, :after_sign_out_path,
...@@ -166,30 +167,4 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -166,30 +167,4 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
sidekiq_throttling_queues: [] sidekiq_throttling_queues: []
] ]
end end
def application_setting_params_ee
[
:help_text,
:elasticsearch_url,
:elasticsearch_indexing,
:elasticsearch_aws,
:elasticsearch_aws_access_key,
:elasticsearch_aws_secret_access_key,
:elasticsearch_aws_region,
:elasticsearch_search,
:repository_size_limit,
:shared_runners_minutes,
:geo_status_timeout,
:elasticsearch_experimental_indexer,
:check_namespace_plan,
:mirror_max_delay,
:mirror_max_capacity,
:mirror_capacity_threshold,
:authorized_keys_enabled,
:slack_app_enabled,
:slack_app_id,
:slack_app_secret,
:slack_app_verification_token
]
end
end end
module EE
module Admin
module ApplicationSettingsController
def application_setting_params_attributes
attrs = super + application_setting_params_attributes_ee
attrs += repository_mirrors_params_attributes if License.feature_available?(:repository_mirrors)
attrs
end
private
def application_setting_params_attributes_ee
[
:help_text,
:elasticsearch_url,
:elasticsearch_indexing,
:elasticsearch_aws,
:elasticsearch_aws_access_key,
:elasticsearch_aws_secret_access_key,
:elasticsearch_aws_region,
:elasticsearch_search,
:repository_size_limit,
:shared_runners_minutes,
:geo_status_timeout,
:elasticsearch_experimental_indexer,
:check_namespace_plan,
:authorized_keys_enabled,
:slack_app_enabled,
:slack_app_id,
:slack_app_secret,
:slack_app_verification_token
]
end
def repository_mirrors_params_attributes
[
:mirror_max_delay,
:mirror_max_capacity,
:mirror_capacity_threshold
]
end
end
end
end
...@@ -19,6 +19,8 @@ module EE ...@@ -19,6 +19,8 @@ module EE
end end
def remote_mirror def remote_mirror
return unless project.feature_available?(:repository_mirrors)
@remote_mirror = @project.remote_mirrors.first_or_initialize @remote_mirror = @project.remote_mirrors.first_or_initialize
end end
......
module EE
module ProjectsController
def project_params_attributes
attrs = super + project_params_ee
attrs += repository_mirrors_params if project&.feature_available?(:repository_mirrors)
attrs
end
private
def project_params_ee
%i[
approvals_before_merge
approver_group_ids
approver_ids
issues_template
merge_method
merge_requests_template
disable_overriding_approvers_per_merge_request
repository_size_limit
reset_approvals_on_push
service_desk_enabled
]
end
def repository_mirrors_params
%i[
mirror
mirror_trigger_builds
mirror_user_id
]
end
end
end
...@@ -22,6 +22,7 @@ class Projects::ApplicationController < ApplicationController ...@@ -22,6 +22,7 @@ class Projects::ApplicationController < ApplicationController
def project def project
return @project if @project return @project if @project
return nil unless params[:project_id] || params[:id]
path = File.join(params[:namespace_id], params[:project_id] || params[:id]) path = File.join(params[:namespace_id], params[:project_id] || params[:id])
auth_proc = ->(project) { !project.pending_delete? } auth_proc = ->(project) { !project.pending_delete? }
......
...@@ -5,6 +5,7 @@ class Projects::MirrorsController < Projects::ApplicationController ...@@ -5,6 +5,7 @@ class Projects::MirrorsController < Projects::ApplicationController
before_action :authorize_admin_project!, except: [:update_now] before_action :authorize_admin_project!, except: [:update_now]
before_action :authorize_push_code!, only: [:update_now] before_action :authorize_push_code!, only: [:update_now]
before_action :remote_mirror, only: [:update] before_action :remote_mirror, only: [:update]
before_action :check_repository_mirrors_available!
layout "project_settings" layout "project_settings"
......
class ProjectsController < Projects::ApplicationController class ProjectsController < Projects::ApplicationController
include IssuableCollections include IssuableCollections
include ExtractsPath include ExtractsPath
prepend EE::ProjectsController
before_action :authenticate_user!, except: [:index, :show, :activity, :refs] before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :project, except: [:index, :new, :create] before_action :project, except: [:index, :new, :create]
...@@ -297,10 +298,10 @@ class ProjectsController < Projects::ApplicationController ...@@ -297,10 +298,10 @@ class ProjectsController < Projects::ApplicationController
def project_params def project_params
params.require(:project) params.require(:project)
.permit(project_params_ce << project_params_ee) .permit(project_params_attributes)
end end
def project_params_ce def project_params_attributes
[ [
:avatar, :avatar,
:build_allow_git_fetch, :build_allow_git_fetch,
...@@ -337,25 +338,6 @@ class ProjectsController < Projects::ApplicationController ...@@ -337,25 +338,6 @@ class ProjectsController < Projects::ApplicationController
] ]
end end
def project_params_ee
%i[
approvals_before_merge
approvals
approver_group_ids
approver_ids
issues_template
merge_method
merge_requests_template
mirror
mirror_trigger_builds
mirror_user_id
disable_overriding_approvers_per_merge_request
repository_size_limit
reset_approvals_on_push
service_desk_enabled
]
end
def repo_exists? def repo_exists?
project.repository_exists? && !project.empty_repo? && project.repo project.repository_exists? && !project.empty_repo? && project.repo
......
...@@ -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
...@@ -19,6 +19,7 @@ module SystemNoteHelper ...@@ -19,6 +19,7 @@ module SystemNoteHelper
'discussion' => 'icon_comment_o', 'discussion' => 'icon_comment_o',
'moved' => 'icon_arrow_circle_o_right', 'moved' => 'icon_arrow_circle_o_right',
'outdated' => 'icon_edit', 'outdated' => 'icon_edit',
'duplicate' => 'icon_clone',
'approved' => 'icon_check', 'approved' => 'icon_check',
'unapproved' => 'icon_fa_close', 'unapproved' => 'icon_fa_close',
'relate' => 'icon_anchor', 'relate' => 'icon_anchor',
......
...@@ -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
......
...@@ -16,7 +16,7 @@ module EE ...@@ -16,7 +16,7 @@ module EE
validates :mirror_max_delay, validates :mirror_max_delay,
presence: true, presence: true,
numericality: { allow_nil: true, only_integer: true, greater_than: 0 } numericality: { allow_nil: true, only_integer: true, greater_than: :mirror_max_delay_in_minutes }
validates :mirror_max_capacity, validates :mirror_max_capacity,
presence: true, presence: true,
...@@ -50,6 +50,10 @@ module EE ...@@ -50,6 +50,10 @@ module EE
private private
def mirror_max_delay_in_minutes
::Gitlab::Mirror.min_delay_upper_bound / 60
end
def mirror_capacity_threshold_less_than def mirror_capacity_threshold_less_than
return unless mirror_max_capacity && mirror_capacity_threshold return unless mirror_max_capacity && mirror_capacity_threshold
......
...@@ -42,11 +42,6 @@ module EE ...@@ -42,11 +42,6 @@ module EE
scope :with_shared_runners_limit_enabled, -> { with_shared_runners.non_public_only } scope :with_shared_runners_limit_enabled, -> { with_shared_runners.non_public_only }
scope :mirrors_to_sync, -> do
mirror.joins(:mirror_data).where("next_execution_timestamp <= ? AND import_status NOT IN ('scheduled', 'started')", Time.now)
.order_by(:next_execution_timestamp).limit(::Gitlab::Mirror.available_capacity)
end
scope :stuck_mirrors, -> do scope :stuck_mirrors, -> do
mirror.joins(:mirror_data) mirror.joins(:mirror_data)
.where("(import_status = 'started' AND project_mirror_data.last_update_started_at < :limit) OR (import_status = 'scheduled' AND project_mirror_data.last_update_scheduled_at < :limit)", .where("(import_status = 'started' AND project_mirror_data.last_update_started_at < :limit) OR (import_status = 'scheduled' AND project_mirror_data.last_update_scheduled_at < :limit)",
...@@ -84,6 +79,11 @@ module EE ...@@ -84,6 +79,11 @@ module EE
end end
end end
def mirror
super && feature_available?(:repository_mirrors)
end
alias_method :mirror?, :mirror
def mirror_updated? def mirror_updated?
mirror? && self.mirror_last_update_at mirror? && self.mirror_last_update_at
end end
...@@ -140,7 +140,7 @@ module EE ...@@ -140,7 +140,7 @@ module EE
end end
def has_remote_mirror? def has_remote_mirror?
remote_mirrors.enabled.exists? feature_available?(:repository_mirrors) && remote_mirrors.enabled.exists?
end end
def updating_remote_mirror? def updating_remote_mirror?
...@@ -148,7 +148,9 @@ module EE ...@@ -148,7 +148,9 @@ module EE
end end
def update_remote_mirrors def update_remote_mirrors
remote_mirrors.each(&:sync) return unless feature_available?(:repository_mirrors)
remote_mirrors.enabled.each(&:sync)
end end
def mark_stuck_remote_mirrors_as_failed! def mark_stuck_remote_mirrors_as_failed!
......
...@@ -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
......
...@@ -6,6 +6,7 @@ class License < ActiveRecord::Base ...@@ -6,6 +6,7 @@ class License < ActiveRecord::Base
AUDITOR_USER_FEATURE = 'GitLab_Auditor_User'.freeze AUDITOR_USER_FEATURE = 'GitLab_Auditor_User'.freeze
BURNDOWN_CHARTS_FEATURE = 'GitLab_BurndownCharts'.freeze BURNDOWN_CHARTS_FEATURE = 'GitLab_BurndownCharts'.freeze
CONTRIBUTION_ANALYTICS_FEATURE = 'GitLab_ContributionAnalytics'.freeze CONTRIBUTION_ANALYTICS_FEATURE = 'GitLab_ContributionAnalytics'.freeze
DB_LOAD_BALANCING_FEATURE = 'GitLab_DbLoadBalancing'.freeze
DEPLOY_BOARD_FEATURE = 'GitLab_DeployBoard'.freeze DEPLOY_BOARD_FEATURE = 'GitLab_DeployBoard'.freeze
ELASTIC_SEARCH_FEATURE = 'GitLab_ElasticSearch'.freeze ELASTIC_SEARCH_FEATURE = 'GitLab_ElasticSearch'.freeze
EXPORT_ISSUES_FEATURE = 'GitLab_ExportIssues'.freeze EXPORT_ISSUES_FEATURE = 'GitLab_ExportIssues'.freeze
...@@ -27,6 +28,7 @@ class License < ActiveRecord::Base ...@@ -27,6 +28,7 @@ class License < ActiveRecord::Base
PROTECTED_REFS_FOR_USERS_FEATURE = 'GitLab_RefPermissionsForUsers'.freeze PROTECTED_REFS_FOR_USERS_FEATURE = 'GitLab_RefPermissionsForUsers'.freeze
PUSH_RULES_FEATURE = 'GitLab_PushRules'.freeze PUSH_RULES_FEATURE = 'GitLab_PushRules'.freeze
RELATED_ISSUES_FEATURE = 'GitLab_RelatedIssues'.freeze RELATED_ISSUES_FEATURE = 'GitLab_RelatedIssues'.freeze
REPOSITORY_MIRRORS_FEATURE = 'GitLab_RepositoryMirrors'.freeze
REPOSITORY_SIZE_LIMIT_FEATURE = 'GitLab_RepositorySizeLimit'.freeze REPOSITORY_SIZE_LIMIT_FEATURE = 'GitLab_RepositorySizeLimit'.freeze
SERVICE_DESK_FEATURE = 'GitLab_ServiceDesk'.freeze SERVICE_DESK_FEATURE = 'GitLab_ServiceDesk'.freeze
VARIABLE_ENVIRONMENT_SCOPE_FEATURE = 'GitLab_VariableEnvironmentScope'.freeze VARIABLE_ENVIRONMENT_SCOPE_FEATURE = 'GitLab_VariableEnvironmentScope'.freeze
...@@ -34,6 +36,7 @@ class License < ActiveRecord::Base ...@@ -34,6 +36,7 @@ class License < ActiveRecord::Base
FEATURE_CODES = { FEATURE_CODES = {
admin_audit_log: ADMIN_AUDIT_LOG_FEATURE, admin_audit_log: ADMIN_AUDIT_LOG_FEATURE,
auditor_user: AUDITOR_USER_FEATURE, auditor_user: AUDITOR_USER_FEATURE,
db_load_balancing: DB_LOAD_BALANCING_FEATURE,
elastic_search: ELASTIC_SEARCH_FEATURE, elastic_search: ELASTIC_SEARCH_FEATURE,
geo: GEO_FEATURE, geo: GEO_FEATURE,
object_storage: OBJECT_STORAGE_FEATURE, object_storage: OBJECT_STORAGE_FEATURE,
...@@ -62,7 +65,8 @@ class License < ActiveRecord::Base ...@@ -62,7 +65,8 @@ class License < ActiveRecord::Base
multiple_issue_assignees: MULTIPLE_ISSUE_ASSIGNEES_FEATURE, multiple_issue_assignees: MULTIPLE_ISSUE_ASSIGNEES_FEATURE,
multiple_issue_boards: MULTIPLE_ISSUE_BOARDS_FEATURE, multiple_issue_boards: MULTIPLE_ISSUE_BOARDS_FEATURE,
protected_refs_for_users: PROTECTED_REFS_FOR_USERS_FEATURE, protected_refs_for_users: PROTECTED_REFS_FOR_USERS_FEATURE,
push_rules: PUSH_RULES_FEATURE push_rules: PUSH_RULES_FEATURE,
repository_mirrors: REPOSITORY_MIRRORS_FEATURE
}.freeze }.freeze
STARTER_PLAN = 'starter'.freeze STARTER_PLAN = 'starter'.freeze
...@@ -91,6 +95,7 @@ class License < ActiveRecord::Base ...@@ -91,6 +95,7 @@ class License < ActiveRecord::Base
{ PUSH_RULES_FEATURE => 1 }, { PUSH_RULES_FEATURE => 1 },
{ PROTECTED_REFS_FOR_USERS_FEATURE => 1 }, { PROTECTED_REFS_FOR_USERS_FEATURE => 1 },
{ RELATED_ISSUES_FEATURE => 1 }, { RELATED_ISSUES_FEATURE => 1 },
{ REPOSITORY_MIRRORS_FEATURE => 1 },
{ REPOSITORY_SIZE_LIMIT_FEATURE => 1 } { REPOSITORY_SIZE_LIMIT_FEATURE => 1 }
].freeze ].freeze
...@@ -98,6 +103,7 @@ class License < ActiveRecord::Base ...@@ -98,6 +103,7 @@ class License < ActiveRecord::Base
*EES_FEATURES, *EES_FEATURES,
{ ADMIN_AUDIT_LOG_FEATURE => 1 }, { ADMIN_AUDIT_LOG_FEATURE => 1 },
{ AUDITOR_USER_FEATURE => 1 }, { AUDITOR_USER_FEATURE => 1 },
{ DB_LOAD_BALANCING_FEATURE => 1 },
{ DEPLOY_BOARD_FEATURE => 1 }, { DEPLOY_BOARD_FEATURE => 1 },
{ FILE_LOCKS_FEATURE => 1 }, { FILE_LOCKS_FEATURE => 1 },
{ GEO_FEATURE => 1 }, { GEO_FEATURE => 1 },
...@@ -140,6 +146,7 @@ class License < ActiveRecord::Base ...@@ -140,6 +146,7 @@ class License < ActiveRecord::Base
{ OBJECT_STORAGE_FEATURE => 1 }, { OBJECT_STORAGE_FEATURE => 1 },
{ PROTECTED_REFS_FOR_USERS_FEATURE => 1 }, { PROTECTED_REFS_FOR_USERS_FEATURE => 1 },
{ PUSH_RULES_FEATURE => 1 }, { PUSH_RULES_FEATURE => 1 },
{ REPOSITORY_MIRRORS_FEATURE => 1 },
{ SERVICE_DESK_FEATURE => 1 } { SERVICE_DESK_FEATURE => 1 }
].freeze ].freeze
......
...@@ -27,6 +27,7 @@ class ProjectMirrorData < ActiveRecord::Base ...@@ -27,6 +27,7 @@ class ProjectMirrorData < ActiveRecord::Base
timestamp = Time.now timestamp = Time.now
retry_factor = [1, self.retry_count].max retry_factor = [1, self.retry_count].max
delay = [base_delay(timestamp) * retry_factor, Gitlab::Mirror.max_delay].min delay = [base_delay(timestamp) * retry_factor, Gitlab::Mirror.max_delay].min
delay = [delay, Gitlab::Mirror.min_delay].max
self.next_execution_timestamp = timestamp + delay self.next_execution_timestamp = timestamp + delay
end end
......
...@@ -77,13 +77,22 @@ class RemoteMirror < ActiveRecord::Base ...@@ -77,13 +77,22 @@ class RemoteMirror < ActiveRecord::Base
end end
def sync def sync
return unless project && enabled return unless enabled?
return if project.pending_delete?
return if Gitlab::Geo.secondary? return if Gitlab::Geo.secondary?
RepositoryUpdateRemoteMirrorWorker.perform_in(BACKOFF_DELAY, self.id, Time.now) if project&.repository_exists? RepositoryUpdateRemoteMirrorWorker.perform_in(BACKOFF_DELAY, self.id, Time.now)
end end
def enabled
return false unless project && super
return false unless project.repository_exists?
return false if project.pending_delete?
# Sync is only enabled when the license permits it
project.feature_available?(:repository_mirrors)
end
alias_method :enabled?, :enabled
def updated_since?(timestamp) def updated_since?(timestamp)
last_update_started_at && last_update_started_at > timestamp && !update_failed? last_update_started_at && last_update_started_at > timestamp && !update_failed?
end end
......
class SystemNoteMetadata < ActiveRecord::Base class SystemNoteMetadata < ActiveRecord::Base
ICON_TYPES = %w[ ICON_TYPES = %w[
commit description merge confidential visible label assignee cross_reference commit description merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved opened closed merged title time_tracking branch milestone discussion task moved
opened closed merged duplicate
outdated outdated
approved unapproved relate unrelate approved unapproved relate unrelate
].freeze ].freeze
......
...@@ -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"
......
...@@ -4,6 +4,8 @@ module Geo ...@@ -4,6 +4,8 @@ module Geo
EmptyCloneUrlPrefixError = Class.new(StandardError) EmptyCloneUrlPrefixError = Class.new(StandardError)
class BaseSyncService class BaseSyncService
include ::Gitlab::Geo::ProjectLogHelpers
class << self class << self
attr_accessor :type attr_accessor :type
end end
...@@ -19,9 +21,9 @@ module Geo ...@@ -19,9 +21,9 @@ module Geo
def execute def execute
try_obtain_lease do try_obtain_lease do
log("Started #{type} sync") log_info("Started #{type} sync")
sync_repository sync_repository
log("Finished #{type} sync") log_info("Finished #{type} sync")
end end
end end
...@@ -46,11 +48,11 @@ module Geo ...@@ -46,11 +48,11 @@ module Geo
end end
def try_obtain_lease def try_obtain_lease
log("Trying to obtain lease to sync #{type}") log_info("Trying to obtain lease to sync #{type}")
repository_lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT).try_obtain repository_lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT).try_obtain
unless repository_lease unless repository_lease
log("Could not obtain lease to sync #{type}") log_info("Could not obtain lease to sync #{type}")
return return
end end
...@@ -59,14 +61,14 @@ module Geo ...@@ -59,14 +61,14 @@ module Geo
# We should release the lease for a repository, only if we have obtained # We should release the lease for a repository, only if we have obtained
# it. If something went wrong when syncing the repository, we should wait # it. If something went wrong when syncing the repository, we should wait
# for the lease timeout to try again. # for the lease timeout to try again.
log("Releasing leases to sync #{type}") log_info("Releasing leases to sync #{type}")
Gitlab::ExclusiveLease.cancel(lease_key, repository_lease) Gitlab::ExclusiveLease.cancel(lease_key, repository_lease)
end end
def update_registry(type, started_at: nil, finished_at: nil) def update_registry(type, started_at: nil, finished_at: nil)
return unless started_at || finished_at return unless started_at || finished_at
log("Updating #{type} sync information") log_info("Updating #{type} sync information")
attrs = {} attrs = {}
...@@ -83,9 +85,5 @@ module Geo ...@@ -83,9 +85,5 @@ module Geo
def type def type
self.class.type self.class.type
end end
def log(message)
Rails.logger.info("#{self.class.name}: #{message} for project #{project.path_with_namespace} (#{project.id})")
end
end end
end end
...@@ -33,7 +33,7 @@ module Geo ...@@ -33,7 +33,7 @@ module Geo
Geo::EventLog.create!("#{self.class.event_type}" => build_event) Geo::EventLog.create!("#{self.class.event_type}" => build_event)
rescue ActiveRecord::RecordInvalid, NoMethodError => e rescue ActiveRecord::RecordInvalid, NoMethodError => e
log("#{self.event_type.to_s.humanize} could not be created", e) log_error("#{self.event_type.to_s.humanize} could not be created", e)
end end
private private
...@@ -43,8 +43,13 @@ module Geo ...@@ -43,8 +43,13 @@ module Geo
"#{self.class} does not implement #{__method__}" "#{self.class} does not implement #{__method__}"
end end
def log(message, error) def log_error(message, error)
Rails.logger.error("#{self.class.name}: #{message} for project #{project.path_with_namespace} (#{project.id}): #{error}") Gitlab::Geo::Logger.error(
class: self.class.name,
message: message,
error: error,
project_id: project.id,
project_path: project.path_with_namespace)
end end
end end
end end
...@@ -15,8 +15,8 @@ module Geo ...@@ -15,8 +15,8 @@ module Geo
def downloader def downloader
klass = "Gitlab::Geo::#{service_klass_name}Downloader".constantize klass = "Gitlab::Geo::#{service_klass_name}Downloader".constantize
klass.new(object_type, object_db_id) klass.new(object_type, object_db_id)
rescue NameError rescue NameError => e
log("Unknown file type: #{object_type}") log_error('Unknown file type', e)
raise raise
end end
......
...@@ -27,8 +27,24 @@ module Geo ...@@ -27,8 +27,24 @@ module Geo
klass_name.camelize klass_name.camelize
end end
def log(message) def log_info(message)
Rails.logger.info "#{self.class.name}: #{message}" data = log_base_data(message)
Gitlab::Geo::Logger.info(data)
end
def log_error(message, error)
data = log_base_data(message)
data[:error] = error
Gitlab::Geo::Logger.error(data)
end
def log_base_data(message)
{
class: self.class.name,
object_type: object_type,
object_db_id: object_db_id,
message: message
}
end end
end end
end end
...@@ -21,8 +21,8 @@ module Geo ...@@ -21,8 +21,8 @@ module Geo
def uploader_klass def uploader_klass
"Gitlab::Geo::#{service_klass_name}Uploader".constantize "Gitlab::Geo::#{service_klass_name}Uploader".constantize
rescue NameError rescue NameError => e
log("Unknown file type: #{object_type}") log_error('Unknown file type', e)
raise raise
end end
end end
......
...@@ -20,10 +20,15 @@ module Geo ...@@ -20,10 +20,15 @@ module Geo
next unless node.enabled? next unless node.enabled?
notify_url = node.send(notify_url_method.to_sym) notify_url = node.send(notify_url_method.to_sym)
success, message = notify(notify_url, content) success, details = notify(notify_url, content)
unless success unless success
Rails.logger.error("GitLab failed to notify #{node.url} to #{notify_url} : #{message}") Gitlab::Geo::Logger.error(
class: self.class.name,
message: "GitLab failed to notify",
error: details,
node_url: node.url,
notify_url: notify_url)
queue.store_batched_data(projects) queue.store_batched_data(projects)
end end
end end
......
...@@ -10,7 +10,7 @@ module Geo ...@@ -10,7 +10,7 @@ module Geo
end end
def fetch_project_repository def fetch_project_repository
log('Fetching project repository') log_info('Fetching project repository')
update_registry(:repository, started_at: DateTime.now) update_registry(:repository, started_at: DateTime.now)
begin begin
...@@ -19,16 +19,16 @@ module Geo ...@@ -19,16 +19,16 @@ module Geo
update_registry(:repository, finished_at: DateTime.now) update_registry(:repository, finished_at: DateTime.now)
rescue Gitlab::Shell::Error, Geo::EmptyCloneUrlPrefixError => e rescue Gitlab::Shell::Error, Geo::EmptyCloneUrlPrefixError => e
Rails.logger.error("#{self.class.name}: Error syncing repository for project #{project.path_with_namespace}: #{e}") log_error("Error syncing repository", e)
rescue Gitlab::Git::Repository::NoRepository => e rescue Gitlab::Git::Repository::NoRepository => e
Rails.logger.error("#{self.class.name}: Error invalid repository for project #{project.path_with_namespace}: #{e}") log_error("Invalid repository", e)
log('Expiring caches') log_info('Expiring caches')
project.repository.after_create project.repository.after_create
end end
end end
def expire_repository_caches def expire_repository_caches
log('Expiring caches') log_info('Expiring caches')
project.repository.after_sync project.repository.after_sync
end end
......
module Geo module Geo
class RepositoryUpdateService class RepositoryUpdateService
include Gitlab::Geo::ProjectLogHelpers
attr_reader :project, :clone_url, :logger attr_reader :project, :clone_url, :logger
LEASE_TIMEOUT = 1.hour.freeze LEASE_TIMEOUT = 1.hour.freeze
...@@ -21,21 +23,21 @@ module Geo ...@@ -21,21 +23,21 @@ module Geo
project.repository.expire_content_cache project.repository.expire_content_cache
end end
rescue Gitlab::Shell::Error => e rescue Gitlab::Shell::Error => e
logger.error "#{self.class.name}: Error fetching repository for project #{project.path_with_namespace}: #{e}" log_error('Error fetching repository for project', e)
rescue Gitlab::Git::Repository::NoRepository => e rescue Gitlab::Git::Repository::NoRepository => e
logger.error "#{self.class.name}: Error invalid repository for project #{project.path_with_namespace}: #{e}" log_error('Error invalid repository', e)
logger.warn "#{self.class.name}: Invalidating cache for project #{project.path_with_namespace}" log_info('Invalidating cache for project')
project.repository.after_create project.repository.after_create
end end
private private
def try_obtain_lease def try_obtain_lease
log('Trying to obtain lease to sync repository') log_info('Trying to obtain lease to sync repository')
repository_lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT).try_obtain repository_lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT).try_obtain
unless repository_lease.present? unless repository_lease.present?
log('Could not obtain lease to sync repository') log_info('Could not obtain lease to sync repository')
return return
end end
...@@ -43,7 +45,7 @@ module Geo ...@@ -43,7 +45,7 @@ module Geo
begin begin
yield yield
ensure ensure
log('Releasing leases to sync repository') log_info('Releasing leases to sync repository')
Gitlab::ExclusiveLease.cancel(lease_key, repository_lease) Gitlab::ExclusiveLease.cancel(lease_key, repository_lease)
end end
end end
...@@ -51,9 +53,5 @@ module Geo ...@@ -51,9 +53,5 @@ module Geo
def lease_key def lease_key
@lease_key ||= "#{LEASE_KEY_PREFIX}:#{project.id}" @lease_key ||= "#{LEASE_KEY_PREFIX}:#{project.id}"
end end
def log(message)
logger.info("#{self.class.name}: #{message} for project #{project.path_with_namespace} (#{project.id})")
end
end end
end end
...@@ -9,7 +9,7 @@ module Geo ...@@ -9,7 +9,7 @@ module Geo
end end
def fetch_wiki_repository def fetch_wiki_repository
log('Fetching wiki repository') log_info('Fetching wiki repository')
update_registry(:wiki, started_at: DateTime.now) update_registry(:wiki, started_at: DateTime.now)
begin begin
...@@ -21,7 +21,7 @@ module Geo ...@@ -21,7 +21,7 @@ module Geo
Gitlab::Shell::Error, Gitlab::Shell::Error,
ProjectWiki::CouldNotCreateWikiError, ProjectWiki::CouldNotCreateWikiError,
Geo::EmptyCloneUrlPrefixError => e Geo::EmptyCloneUrlPrefixError => e
Rails.logger.error("#{self.class.name}: Error syncing wiki repository for project #{project.path_with_namespace}: #{e}") log_error("Error syncing wiki repository", e)
end end
end end
......
...@@ -60,6 +60,7 @@ class IssuableBaseService < BaseService ...@@ -60,6 +60,7 @@ class IssuableBaseService < BaseService
params.delete(:assignee_ids) params.delete(:assignee_ids)
params.delete(:assignee_id) params.delete(:assignee_id)
params.delete(:due_date) params.delete(:due_date)
params.delete(:canonical_issue_id)
end end
filter_assignee(issuable) filter_assignee(issuable)
......
...@@ -7,6 +7,14 @@ module Issues ...@@ -7,6 +7,14 @@ module Issues
issue_data issue_data
end end
def reopen_service
Issues::ReopenService
end
def close_service
Issues::CloseService
end
private private
def create_assignee_note(issue, old_assignees) def create_assignee_note(issue, old_assignees)
......
module Issues
class DuplicateService < Issues::BaseService
def execute(duplicate_issue, canonical_issue)
return if canonical_issue == duplicate_issue
return unless can?(current_user, :update_issue, duplicate_issue)
return unless can?(current_user, :create_note, canonical_issue)
create_issue_duplicate_note(duplicate_issue, canonical_issue)
create_issue_canonical_note(canonical_issue, duplicate_issue)
close_service.new(project, current_user, {}).execute(duplicate_issue)
end
private
def create_issue_duplicate_note(duplicate_issue, canonical_issue)
SystemNoteService.mark_duplicate_issue(duplicate_issue, duplicate_issue.project, current_user, canonical_issue)
end
def create_issue_canonical_note(canonical_issue, duplicate_issue)
SystemNoteService.mark_canonical_issue_of_duplicate(canonical_issue, canonical_issue.project, current_user, duplicate_issue)
end
end
end
...@@ -5,6 +5,7 @@ module Issues ...@@ -5,6 +5,7 @@ module Issues
def execute(issue) def execute(issue)
handle_move_between_iids(issue) handle_move_between_iids(issue)
filter_spam_check_params filter_spam_check_params
change_issue_duplicate(issue)
update(issue) update(issue)
end end
...@@ -53,14 +54,6 @@ module Issues ...@@ -53,14 +54,6 @@ module Issues
end end
end end
def reopen_service
Issues::ReopenService
end
def close_service
Issues::CloseService
end
def handle_move_between_iids(issue) def handle_move_between_iids(issue)
return unless params[:move_between_iids] return unless params[:move_between_iids]
...@@ -72,6 +65,15 @@ module Issues ...@@ -72,6 +65,15 @@ module Issues
issue.move_between(issue_before, issue_after) issue.move_between(issue_before, issue_after)
end end
def change_issue_duplicate(issue)
canonical_issue_id = params.delete(:canonical_issue_id)
canonical_issue = IssuesFinder.new(current_user).find_by(id: canonical_issue_id)
if canonical_issue
Issues::DuplicateService.new(project, current_user).execute(issue, canonical_issue)
end
end
private private
def get_issue_if_allowed(project, iid) def get_issue_if_allowed(project, iid)
......
...@@ -5,7 +5,7 @@ module Projects ...@@ -5,7 +5,7 @@ module Projects
def execute def execute
unless project.mirror? unless project.mirror?
return error("The project has no mirror to update") return success
end end
unless can?(current_user, :push_code_to_protected_branches, project) unless can?(current_user, :push_code_to_protected_branches, project)
......
...@@ -6,6 +6,8 @@ module Projects ...@@ -6,6 +6,8 @@ module Projects
@mirror = remote_mirror @mirror = remote_mirror
@errors = [] @errors = []
return success unless remote_mirror.enabled?
begin begin
repository.fetch_remote(mirror.ref_name, no_tags: true) repository.fetch_remote(mirror.ref_name, no_tags: true)
......
...@@ -472,6 +472,24 @@ module QuickActions ...@@ -472,6 +472,24 @@ module QuickActions
end end
end end
desc 'Mark this issue as a duplicate of another issue'
explanation do |duplicate_reference|
"Marks this issue as a duplicate of #{duplicate_reference}."
end
params '#issue'
condition do
issuable.is_a?(Issue) &&
issuable.persisted? &&
current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
end
command :duplicate do |duplicate_param|
canonical_issue = extract_references(duplicate_param, :issue).first
if canonical_issue.present?
@updates[:canonical_issue_id] = canonical_issue.id
end
end
def extract_users(params) def extract_users(params)
return [] if params.nil? return [] if params.nil?
......
...@@ -606,6 +606,44 @@ module SystemNoteService ...@@ -606,6 +606,44 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, noteable.project, user, body, action: 'unapproved')) create_note(NoteSummary.new(noteable, noteable.project, user, body, action: 'unapproved'))
end end
# Called when a Noteable has been marked as a duplicate of another Issue
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
# canonical_issue - Issue that this is a duplicate of
#
# Example Note text:
#
# "marked this issue as a duplicate of #1234"
#
# "marked this issue as a duplicate of other_project#5678"
#
# Returns the created Note object
def mark_duplicate_issue(noteable, project, author, canonical_issue)
body = "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
end
# Called when a Noteable has been marked as the canonical Issue of a duplicate
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
# duplicate_issue - Issue that was a duplicate of this
#
# Example Note text:
#
# "marked #1234 as a duplicate of this issue"
#
# "marked other_project#5678 as a duplicate of this issue"
#
# Returns the created Note object
def mark_canonical_issue_of_duplicate(noteable, project, author, duplicate_issue)
body = "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue"
create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
end
private private
def notes_for_mentioner(mentioner, noteable, notes) def notes_for_mentioner(mentioner, noteable, notes)
......
- if Gitlab.com? - if Gitlab.com? && License.feature_available?(:repository_mirrors)
%fieldset %fieldset
%legend Repository mirror settings %legend Repository mirror settings
.form-group .form-group
= f.label :mirror_max_delay, class: 'control-label col-sm-2' do = f.label :mirror_max_delay, class: 'control-label col-sm-2' do
Maximum delay (Hours) Maximum delay (Minutes)
.col-sm-10 .col-sm-10
= f.number_field :mirror_max_delay, class: 'form-control', min: 0 = f.number_field :mirror_max_delay, class: 'form-control', min: 0
%span.help-block#mirror_max_delay_help_block %span.help-block#mirror_max_delay_help_block
......
...@@ -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
......
- expanded = Rails.env.test?
%section.settings.project-mirror-settings
.settings-header
%h4
Pull from a remote repository
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Set up your project to automatically have its branches, tags, and commits
updated from an upstream repository.
= link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pulling-from-a-remote-repository'), target: '_blank'
.settings-content.no-animate{ class: ('expanded' if expanded) }
= form_for @project, url: project_mirror_path(@project) do |f|
%div
= form_errors(@project)
%h5
Set up mirror repository
= render "shared/mirror_update_button"
- if @project.mirror_last_update_failed?
.panel.panel-danger
.panel-heading
The repository failed to update #{time_ago_with_tooltip(@project.mirror_last_update_at)}.
- if @project.mirror_ever_updated_successfully?
Last successful update #{time_ago_with_tooltip(@project.mirror_last_successful_update_at)}.
.panel-body
%pre
:preserve
#{h(@project.import_error.try(:strip))}
.form-group
= f.check_box :mirror, class: "pull-left"
.prepend-left-20
= f.label :mirror, "Mirror repository", class: "label-light append-bottom-0"
.form-group
= f.label :import_url, "Git repository URL", class: "label-light"
= f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
= render "projects/mirrors/instructions"
.form-group
= f.label :mirror_user_id, "Mirror user", class: "label-light"
= select_tag('project[mirror_user_id]', options_for_mirror_user, class: "select2 lg", required: true)
.help-block
This user will be the author of all events in the activity feed that are the result of an update,
like new branches being created or new commits being pushed to existing branches.
You can only assign yourself to be the mirror user.
- if @project.builds_enabled?
= render "shared/mirror_trigger_builds_setting", f: f
= f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror'
- expanded = Rails.env.test?
%section.settings
.settings-header
%h4
Push to a remote repository
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Set up the remote repository that you want to update with the content of the current repository
every time someone pushes to it.
= link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pushing-to-a-remote-repository'), target: '_blank'
.settings-content.no-animate{ class: ('expanded' if expanded) }
= form_for @project, url: project_mirror_path(@project) do |f|
%div
= form_errors(@project)
= render "shared/remote_mirror_update_button", remote_mirror: @remote_mirror
- if @remote_mirror.last_error.present?
.panel.panel-danger
.panel-heading
The remote repository failed to update #{time_ago_with_tooltip(@remote_mirror.last_update_at)}.
- if @remote_mirror.last_successful_update_at
Last successful update #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}.
.panel-body
%pre
:preserve
#{h(@remote_mirror.last_error.strip)}
= f.fields_for :remote_mirrors, @remote_mirror do |rm_form|
.form-group
= rm_form.check_box :enabled, class: "pull-left"
.prepend-left-20
= rm_form.label :enabled, "Remote mirror repository", class: "label-light append-bottom-0"
%p.light.append-bottom-0
Automatically update the remote mirror's branches, tags, and commits from this repository every time someone pushes to it.
.form-group.has-feedback
= rm_form.label :url, "Git repository URL", class: "label-light"
= rm_form.text_field :url, class: "form-control", placeholder: 'https://username:password@gitlab.company.com/group/project.git'
= render "projects/mirrors/instructions"
= f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror'
- expanded = Rails.env.test? - if @project.feature_available?(:repository_mirrors)
%section.settings.project-mirror-settings = render 'projects/mirrors/pull'
.settings-header = render 'projects/mirrors/push'
%h4
Pull from a remote repository
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Set up your project to automatically have its branches, tags, and commits
updated from an upstream repository.
= link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pulling-from-a-remote-repository'), target: '_blank'
.settings-content.no-animate{ class: ('expanded' if expanded) }
= form_for @project, url: project_mirror_path(@project) do |f|
%div
= form_errors(@project)
%h5
Set up mirror repository
= render "shared/mirror_update_button"
- if @project.mirror_last_update_failed?
.panel.panel-danger
.panel-heading
The repository failed to update #{time_ago_with_tooltip(@project.mirror_last_update_at)}.
- if @project.mirror_ever_updated_successfully?
Last successful update #{time_ago_with_tooltip(@project.mirror_last_successful_update_at)}.
.panel-body
%pre
:preserve
#{h(@project.import_error.try(:strip))}
.form-group
= f.check_box :mirror, class: "pull-left"
.prepend-left-20
= f.label :mirror, "Mirror repository", class: "label-light append-bottom-0"
.form-group
= f.label :import_url, "Git repository URL", class: "label-light"
= f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
= render "projects/mirrors/instructions"
.form-group
= f.label :mirror_user_id, "Mirror user", class: "label-light"
= select_tag('project[mirror_user_id]', options_for_mirror_user, class: "select2 lg", required: true)
.help-block
This user will be the author of all events in the activity feed that are the result of an update,
like new branches being created or new commits being pushed to existing branches.
You can only assign yourself to be the mirror user.
- if @project.builds_enabled?
= render "shared/mirror_trigger_builds_setting", f: f
= f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror'
%section.settings
.settings-header
%h4
Push to a remote repository
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Set up the remote repository that you want to update with the content of the current repository
every time someone pushes to it.
= link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pushing-to-a-remote-repository'), target: '_blank'
.settings-content.no-animate{ class: ('expanded' if expanded) }
= form_for @project, url: project_mirror_path(@project) do |f|
%div
= form_errors(@project)
= render "shared/remote_mirror_update_button", remote_mirror: @remote_mirror
- if @remote_mirror.last_error.present?
.panel.panel-danger
.panel-heading
The remote repository failed to update #{time_ago_with_tooltip(@remote_mirror.last_update_at)}.
- if @remote_mirror.last_successful_update_at
Last successful update #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}.
.panel-body
%pre
:preserve
#{h(@remote_mirror.last_error.strip)}
= f.fields_for :remote_mirrors, @remote_mirror do |rm_form|
.form-group
= rm_form.check_box :enabled, class: "pull-left"
.prepend-left-20
= rm_form.label :enabled, "Remote mirror repository", class: "label-light append-bottom-0"
%p.light.append-bottom-0
Automatically update the remote mirror's branches, tags, and commits from this repository every time someone pushes to it.
.form-group.has-feedback
= rm_form.label :url, "Git repository URL", class: "label-light"
= rm_form.text_field :url, class: "form-control", placeholder: 'https://username:password@gitlab.company.com/group/project.git'
= render "projects/mirrors/instructions"
= f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror'
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14" height="14" viewBox="0 0 14 14">
<path d="M13 12.75v-8.5q0-0.102-0.074-0.176t-0.176-0.074h-8.5q-0.102 0-0.176 0.074t-0.074 0.176v8.5q0 0.102 0.074 0.176t0.176 0.074h8.5q0.102 0 0.176-0.074t0.074-0.176zM14 4.25v8.5q0 0.516-0.367 0.883t-0.883 0.367h-8.5q-0.516 0-0.883-0.367t-0.367-0.883v-8.5q0-0.516 0.367-0.883t0.883-0.367h8.5q0.516 0 0.883 0.367t0.367 0.883zM11 1.25v1.25h-1v-1.25q0-0.102-0.074-0.176t-0.176-0.074h-8.5q-0.102 0-0.176 0.074t-0.074 0.176v8.5q0 0.102 0.074 0.176t0.176 0.074h1.25v1h-1.25q-0.516 0-0.883-0.367t-0.367-0.883v-8.5q0-0.516 0.367-0.883t0.883-0.367h8.5q0.516 0 0.883 0.367t0.367 0.883z"></path>
</svg>
...@@ -11,7 +11,7 @@ class UpdateAllMirrorsWorker ...@@ -11,7 +11,7 @@ class UpdateAllMirrorsWorker
fail_stuck_mirrors! fail_stuck_mirrors!
Project.mirrors_to_sync.each(&:import_schedule) unless Gitlab::Mirror.max_mirror_capacity_reached? schedule_mirrors!
cancel_lease(lease_uuid) cancel_lease(lease_uuid)
end end
...@@ -22,6 +22,32 @@ class UpdateAllMirrorsWorker ...@@ -22,6 +22,32 @@ class UpdateAllMirrorsWorker
end end
end end
def schedule_mirrors!
capacity = batch_size = Gitlab::Mirror.available_capacity
# Ignore mirrors that become due for scheduling once work begins, so we
# can't end up in an infinite loop
now = Time.now
last = nil
# Normally, this will complete in 1-2 batches. One batch will be added per
# `batch_size` unlicensed projects in the database.
while capacity > 0
projects = pull_mirrors_batch(freeze_at: now, batch_size: batch_size, offset_at: last)
break if projects.empty?
last = projects.last.mirror_data.next_execution_timestamp
projects.each do |project|
next unless project.feature_available?(:repository_mirrors)
capacity -= 1
project.import_schedule
break unless capacity > 0
end
end
end
private private
def try_obtain_lease def try_obtain_lease
...@@ -31,4 +57,17 @@ class UpdateAllMirrorsWorker ...@@ -31,4 +57,17 @@ class UpdateAllMirrorsWorker
def cancel_lease(uuid) def cancel_lease(uuid)
::Gitlab::ExclusiveLease.cancel(LEASE_KEY, uuid) ::Gitlab::ExclusiveLease.cancel(LEASE_KEY, uuid)
end end
def pull_mirrors_batch(freeze_at:, batch_size:, offset_at: nil)
relation = Project
.mirror
.joins(:mirror_data)
.where("next_execution_timestamp <= ? AND import_status NOT IN ('scheduled', 'started')", freeze_at)
.reorder('project_mirror_data.next_execution_timestamp')
.limit(batch_size)
relation = relation.where('next_execution_timestamp > ?', offset_at) if offset_at
relation
end
end end
---
title: Fix CSS for mini graph with downstream pipeline
merge_request:
author:
---
title: Adds lower bound to pull mirror scheduling feature
merge_request: 2366
author:
---
title: Fixes unscoping of imposed capacity limit by find_each method on Mirror scheduler
merge_request: 2460
author:
---
title: Cleans up mirror capacity in project destroy service if project is a scheduled
mirror
merge_request: 2445
author:
---
title: Fixed issue boards focus mode when new navigation is turned on
merge_request:
author:
---
title: Namespace license checks for Repository Mirrors
merge_request: 2328
author:
---
title: Added /duplicate quick action to close a duplicate issue
merge_request: 12845
author: Ryan Scott
---
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: Remove text underline from suggested approvers title: Fix translations for Star/Unstar in JS file
merge_request: merge_request:
author: author:
---
title: Fix margins in the mini graph for pipeline in commits box
merge_request:
author:
---
title: Fix some invalid entries in PO files
merge_request: 13032
author:
---
title: Fix today day highlight in calendar
merge_request: 13048
author:
---
title: Fix editing project with container images present
merge_request: 13028
author:
---
title: Fix pipeline_schedules pages throwing error 500 (when ref is empty)
merge_request: 12983
author:
...@@ -265,7 +265,7 @@ Settings.gitlab['default_projects_features'] ||= {} ...@@ -265,7 +265,7 @@ Settings.gitlab['default_projects_features'] ||= {}
Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['webhook_timeout'] ||= 10
Settings.gitlab['max_attachment_size'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10
Settings.gitlab['session_expire_delay'] ||= 10080 Settings.gitlab['session_expire_delay'] ||= 10080
Settings.gitlab['mirror_max_delay'] ||= 5 Settings.gitlab['mirror_max_delay'] ||= 300
Settings.gitlab['mirror_max_capacity'] ||= 30 Settings.gitlab['mirror_max_capacity'] ||= 30
Settings.gitlab['mirror_capacity_threshold'] ||= 15 Settings.gitlab['mirror_capacity_threshold'] ||= 15
Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil?
......
if Gitlab::Database::LoadBalancing.enable? # We need to run this initializer after migrations are done so it doesn't fail on CI
Gitlab::Database.disable_prepared_statements if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('licenses')
if Gitlab::Database::LoadBalancing.enable?
Gitlab::Database.disable_prepared_statements
Gitlab::Application.configure do |config| Gitlab::Application.configure do |config|
config.middleware.use(Gitlab::Database::LoadBalancing::RackMiddleware) config.middleware.use(Gitlab::Database::LoadBalancing::RackMiddleware)
end end
Gitlab::Database::LoadBalancing.configure_proxy Gitlab::Database::LoadBalancing.configure_proxy
end
end end
...@@ -45,6 +45,7 @@ var config = { ...@@ -45,6 +45,7 @@ var config = {
groups_list: './groups_list.js', groups_list: './groups_list.js',
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',
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',
......
class ConvertMaxMirrorDelayToMinutesInApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
change_column_default :application_settings, :mirror_max_delay, 300
execute 'UPDATE application_settings SET mirror_max_delay = COALESCE(mirror_max_delay, 5) * 60'
end
def down
change_column_default :application_settings, :mirror_max_delay, 5
execute 'UPDATE application_settings SET mirror_max_delay = COALESCE(mirror_max_delay, 300) / 60'
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170717150329) do ActiveRecord::Schema.define(version: 20170719182937) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -137,7 +137,7 @@ ActiveRecord::Schema.define(version: 20170717150329) do ...@@ -137,7 +137,7 @@ ActiveRecord::Schema.define(version: 20170717150329) do
t.string "clientside_sentry_dsn" t.string "clientside_sentry_dsn"
t.boolean "prometheus_metrics_enabled", default: false, null: false t.boolean "prometheus_metrics_enabled", default: false, null: false
t.boolean "check_namespace_plan", default: false, null: false t.boolean "check_namespace_plan", default: false, null: false
t.integer "mirror_max_delay", default: 5, null: false t.integer "mirror_max_delay", default: 300, null: false
t.integer "mirror_max_capacity", default: 100, null: false t.integer "mirror_max_capacity", default: 100, null: false
t.integer "mirror_capacity_threshold", default: 50, null: false t.integer "mirror_capacity_threshold", default: 50, null: false
t.boolean "authorized_keys_enabled", default: true, null: false t.boolean "authorized_keys_enabled", default: true, null: false
......
...@@ -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
......
...@@ -40,6 +40,7 @@ do. ...@@ -40,6 +40,7 @@ do.
| `/weight <1-9>` | Set the weight of the issue | | `/weight <1-9>` | Set the weight of the issue |
| `/clear_weight` | Clears the issue weight | | `/clear_weight` | Clears the issue weight |
| `/board_move ~column` | Move issue to column on the board | | `/board_move ~column` | Move issue to column on the board |
| `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue |
Note: In GitLab EES every issue can have more than one assignee, so commands `/assign`, `/unassign` and `/reassign` Note: In GitLab EES every issue can have more than one assignee, so commands `/assign`, `/unassign` and `/reassign`
support multiple assignees. support multiple assignees.
...@@ -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
......
...@@ -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,
......
...@@ -45,6 +45,8 @@ module Gitlab ...@@ -45,6 +45,8 @@ module Gitlab
# Returns true if load balancing is to be enabled. # Returns true if load balancing is to be enabled.
def self.enable? def self.enable?
return false unless ::License.feature_available?(:db_load_balancing)
program_name != 'rake' && !hosts.empty? && !Sidekiq.server? && program_name != 'rake' && !hosts.empty? && !Sidekiq.server? &&
Database.postgresql? Database.postgresql?
end end
......
module Gitlab
module Geo
class Logger < ::Gitlab::JsonLogger
def self.file_name_noext
'geo'
end
end
end
end
module Gitlab
module Geo
module ProjectLogHelpers
def log_info(message)
data = base_log_data(message)
Gitlab::Geo::Logger.info(data)
end
def log_error(message, error)
data = base_log_data(message)
data[:error] = error
Gitlab::Geo::Logger.error(data)
end
private
def base_log_data(message)
{
class: self.class.name,
project_id: project.id,
project_path: project.path_with_namespace,
message: message
}
end
end
end
end
module Gitlab
class JsonLogger < ::Gitlab::Logger
def self.file_name_noext
raise NotImplementedError
end
def format_message(severity, timestamp, progname, message)
data = {}
data[:severity] = severity
data[:time] = timestamp.utc.iso8601(3)
case message
when String
data[:message] = message
when Hash
data.merge!(message)
end
data.to_json + "\n"
end
end
end
...@@ -5,7 +5,8 @@ module Gitlab ...@@ -5,7 +5,8 @@ module Gitlab
# Runs scheduler every minute # Runs scheduler every minute
SCHEDULER_CRON = '* * * * *'.freeze SCHEDULER_CRON = '* * * * *'.freeze
PULL_CAPACITY_KEY = 'MIRROR_PULL_CAPACITY'.freeze PULL_CAPACITY_KEY = 'MIRROR_PULL_CAPACITY'.freeze
UPPER_JITTER = 1.minute JITTER = 1.minute
MIN_DELAY = 15.minutes
class << self class << self
def configure_cron_job! def configure_cron_job!
...@@ -49,7 +50,15 @@ module Gitlab ...@@ -49,7 +50,15 @@ module Gitlab
end end
def max_delay def max_delay
current_application_settings.mirror_max_delay.hours + rand(UPPER_JITTER) current_application_settings.mirror_max_delay.minutes + rand(JITTER)
end
def min_delay_upper_bound
MIN_DELAY + JITTER
end
def min_delay
MIN_DELAY + rand(JITTER)
end end
def max_capacity def max_capacity
......
...@@ -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,7 +52,7 @@ module Gitlab ...@@ -53,7 +52,7 @@ module Gitlab
todos: Todo.count, todos: Todo.count,
uploads: Upload.count, uploads: Upload.count,
web_hooks: WebHook.count web_hooks: WebHook.count
}.merge(service_desk_counts) }.merge(service_desk_counts).merge(services_usage)
} }
end end
...@@ -107,6 +106,18 @@ module Gitlab ...@@ -107,6 +106,18 @@ module Gitlab
'EE' 'EE'
end end
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 }
end
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' => {
......
...@@ -102,9 +102,11 @@ namespace :gitlab do ...@@ -102,9 +102,11 @@ namespace :gitlab do
def setup def setup
warn_user_is_not_gitlab warn_user_is_not_gitlab
ensure_write_to_authorized_keys_is_enabled
unless ENV['force'] == 'yes' unless ENV['force'] == 'yes'
puts "This will rebuild an authorized_keys file." puts "This task will now rebuild the authorized_keys file."
puts "You will lose any data stored in authorized_keys file." puts "You will lose any data stored in the authorized_keys file."
ask_to_continue ask_to_continue
puts "" puts ""
end end
...@@ -128,4 +130,47 @@ namespace :gitlab do ...@@ -128,4 +130,47 @@ namespace :gitlab do
puts "Quitting...".color(:red) puts "Quitting...".color(:red)
exit 1 exit 1
end end
def ensure_write_to_authorized_keys_is_enabled
return if current_application_settings.authorized_keys_enabled
puts authorized_keys_is_disabled_warning
answer = prompt('Do you want to permanently enable the "Write to authorized_keys file" setting now (yes/no)? '.color(:blue), %w{yes no})
if answer == 'yes'
puts 'Enabling the "Write to authorized_keys file" setting...'
uncached_settings = ApplicationSetting.last
uncached_settings.authorized_keys_enabled = true
uncached_settings.save!
puts 'Successfully enabled "Write to authorized_keys file"!'
puts ''
else
puts 'Leaving the "Write to authorized_keys file" setting disabled.'
puts 'Failed to rebuild authorized_keys file...'.color(:red)
exit 1
end
end
def authorized_keys_is_disabled_warning
<<-MSG.strip_heredoc
WARNING
The "Write to authorized_keys file" setting is disabled, which prevents
the file from being rebuilt!
It should be enabled for most GitLab installations. Large installations
may wish to disable it as part of speeding up SSH operations.
See https://docs.gitlab.com/ee/administration/operations/speed_up_ssh.html
If you did not intentionally disable this option in Admin Area > Settings,
then you may have been affected by the 9.3.0 bug in which the new setting
was disabled by default.
https://gitlab.com/gitlab-org/gitlab-ee/issues/2738
It was reverted in 9.3.1 and fixed in 9.3.3, however, if Settings were
saved while the setting was unchecked, then it is still disabled.
MSG
end
end end
require 'spec_helper'
describe Admin::ApplicationSettingsController do # rubocop:disable RSpec/FilePath
include StubENV
let(:admin) { create(:admin) }
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
end
describe 'PUT #update' do
before do
sign_in(admin)
end
it 'updates the EE specific application settings' do
settings = {
help_text: 'help_text',
elasticsearch_url: 'http://my-elastic.search:9200',
elasticsearch_indexing: true,
elasticsearch_aws: true,
elasticsearch_aws_access_key: 'elasticsearch_aws_access_key',
elasticsearch_aws_secret_access_key: 'elasticsearch_aws_secret_access_key',
elasticsearch_aws_region: 'elasticsearch_aws_region',
elasticsearch_search: true,
repository_size_limit: 1024,
shared_runners_minutes: 60,
geo_status_timeout: 30,
elasticsearch_experimental_indexer: true,
check_namespace_plan: true,
authorized_keys_enabled: true,
slack_app_enabled: true,
slack_app_id: 'slack_app_id',
slack_app_secret: 'slack_app_secret',
slack_app_verification_token: 'slack_app_verification_token'
}
put :update, application_setting: settings
expect(response).to redirect_to(admin_application_settings_path)
settings.except(:elasticsearch_url, :repository_size_limit).each do |setting, value|
expect(ApplicationSetting.current.public_send(setting)).to eq(value)
end
expect(ApplicationSetting.current.repository_size_limit).to eq(settings[:repository_size_limit].megabytes)
expect(ApplicationSetting.current.elasticsearch_url).to contain_exactly(settings[:elasticsearch_url])
end
it 'does not update mirror settings when repository mirrors unlicensed' do
stub_licensed_features(repository_mirrors: false)
settings = {
mirror_max_delay: 12,
mirror_max_capacity: 2,
mirror_capacity_threshold: 2
}
settings.each do |setting, _value|
expect do
put :update, application_setting: settings
end.not_to change(ApplicationSetting.current.reload, setting)
end
end
it 'updates mirror settings when repository mirrors is licensed' do
stub_licensed_features(repository_mirrors: true)
settings = {
mirror_max_delay: 12,
mirror_max_capacity: 2,
mirror_capacity_threshold: 2
}
put :update, application_setting: settings
settings.each do |setting, value|
expect(ApplicationSetting.current.public_send(setting)).to eq(value)
end
end
end
end
require('spec_helper')
describe ProjectsController do # rubocop:disable RSpec/FilePath
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
before do
project.add_master(user)
sign_in(user)
end
describe 'PUT #update' do
before do
controller.instance_variable_set(:@project, project)
end
it 'updates EE attributes' do
params = {
repository_size_limit: 1024
}
put :update,
namespace_id: project.namespace,
id: project.id,
project: params
expect(response).to have_http_status(302)
params.except(:repository_size_limit).each do |param, value|
expect(project.public_send(param)).to eq(value)
end
expect(project.repository_size_limit).to eq(params[:repository_size_limit].megabytes)
end
it 'updates Merge Request Approvers attributes' do
params = {
approvals_before_merge: 50,
approver_group_ids: create(:group).id,
approver_ids: create(:user).id,
reset_approvals_on_push: false
}
put :update,
namespace_id: project.namespace,
id: project.id,
project: params
expect(response).to have_http_status(302)
expect(project.approver_groups.pluck(:group_id)).to contain_exactly(params[:approver_group_ids])
expect(project.approvers.pluck(:user_id)).to contain_exactly(params[:approver_ids])
end
it 'updates Issuable Default Templates attributes' do
params = {
issues_template: 'You got issues?',
merge_requests_template: 'I got tissues'
}
put :update,
namespace_id: project.namespace,
id: project.id,
project: params
expect(response).to have_http_status(302)
params.each do |param, value|
expect(project.public_send(param)).to eq(value)
end
end
it 'updates Fast Forward Merge attributes' do
params = {
merge_method: :ff
}
put :update,
namespace_id: project.namespace,
id: project.id,
project: params
expect(response).to have_http_status(302)
params.each do |param, value|
expect(project.public_send(param)).to eq(value)
end
end
it 'updates Fast Forward Merge attributes' do
params = {
merge_method: :ff
}
put :update,
namespace_id: project.namespace,
id: project.id,
project: params
expect(response).to have_http_status(302)
params.each do |param, value|
expect(project.public_send(param)).to eq(value)
end
end
it 'updates Service Desk attributes' do
allow(Gitlab::IncomingEmail).to receive(:enabled?) { true }
allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true }
stub_licensed_features(service_desk: true)
params = {
service_desk_enabled: true
}
put :update,
namespace_id: project.namespace,
id: project.id,
project: params
expect(response).to have_http_status(302)
expect(project.service_desk_enabled).to eq(true)
end
context 'repository mirrors licensed' do
before do
stub_licensed_features(repository_mirrors: true)
end
it 'updates repository mirror attributes' do
params = {
mirror: true,
mirror_trigger_builds: true,
mirror_user_id: user.id
}
put :update,
namespace_id: project.namespace,
id: project.id,
project: params
params.each do |param, value|
expect(project.public_send(param)).to eq(value)
end
end
end
context 'repository mirrors unlicensed' do
before do
stub_licensed_features(repository_mirrors: false)
end
it 'does not update repository mirror attributes' do
params = {
mirror: true,
mirror_trigger_builds: true,
mirror_user_id: user.id
}
params.each do |param, _value|
expect do
put :update,
namespace_id: project.namespace,
id: project.id,
project: params
end.not_to change(project, param)
end
end
end
end
end
...@@ -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
......
...@@ -194,5 +194,42 @@ feature 'Issues > User uses quick actions', feature: true, js: true do ...@@ -194,5 +194,42 @@ feature 'Issues > User uses quick actions', feature: true, js: true do
end end
end end
end end
describe 'mark issue as duplicate' do
let(:issue) { create(:issue, project: project) }
let(:original_issue) { create(:issue, project: project) }
context 'when the current user can update issues' do
it 'does not create a note, and marks the issue as a duplicate' do
write_note("/duplicate ##{original_issue.to_reference}")
expect(page).not_to have_content "/duplicate #{original_issue.to_reference}"
expect(page).to have_content 'Commands applied'
expect(page).to have_content "marked this issue as a duplicate of #{original_issue.to_reference}"
expect(issue.reload).to be_closed
end
end
context 'when the current user cannot update the issue' do
let(:guest) { create(:user) }
before do
project.team << [guest, :guest]
gitlab_sign_out
sign_in(guest)
visit project_issue_path(project, issue)
end
it 'does not create a note, and does not mark the issue as a duplicate' do
write_note("/duplicate ##{original_issue.to_reference}")
expect(page).to have_content "/duplicate ##{original_issue.to_reference}"
expect(page).not_to have_content 'Commands applied'
expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}"
expect(issue.reload).to be_open
end
end
end
end end
end end
...@@ -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
......
...@@ -10,6 +10,18 @@ feature 'Project mirror', feature: true do ...@@ -10,6 +10,18 @@ feature 'Project mirror', feature: true do
sign_in user sign_in user
end end
context 'unlicensed' do
before do
stub_licensed_features(repository_mirrors: false)
end
it 'returns 404' do
visit project_mirror_path(project)
expect(page.status_code).to eq(404)
end
end
context 'with Update now button' do context 'with Update now button' do
let(:timestamp) { Time.now } let(:timestamp) { Time.now }
......
require 'spec_helper' require 'spec_helper'
describe 'Project settings > [EE] repository', feature: true do describe 'Project settings > [EE] repository', feature: true do
include Select2Helper
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project_empty_repo) } let(:project) { create(:project_empty_repo) }
...@@ -44,24 +42,4 @@ describe 'Project settings > [EE] repository', feature: true do ...@@ -44,24 +42,4 @@ describe 'Project settings > [EE] repository', feature: true do
end end
end end
end end
describe 'mirror settings', :js do
let(:user2) { create(:user) }
before do
project.team << [user2, :master]
visit project_settings_repository_path(project)
end
it 'sets mirror user' do
page.within('.project-mirror-settings') do
select2(user2.id, from: '#project_mirror_user_id')
click_button('Save changes')
expect(find('.select2-chosen')).to have_content(user.name)
end
end
end
end end
require 'spec_helper'
describe 'Project settings > [EE] repository', feature: true do
include Select2Helper
let(:user) { create(:user) }
let(:project) { create(:project_empty_repo) }
before do
project.add_master(user)
gitlab_sign_in(user)
end
context 'unlicensed' do
before do
stub_licensed_features(repository_mirrors: false)
visit project_settings_repository_path(project)
end
it 'does not show pull mirror settings' do
expect(page).to have_no_selector('#project_mirror')
expect(page).to have_no_selector('#project_import_url')
expect(page).to have_no_selector('#project_mirror_user_id', visible: false)
expect(page).to have_no_selector('#project_mirror_trigger_builds')
end
it 'does not show push mirror settings' do
expect(page).to have_no_selector('#project_remote_mirrors_attributes_0_enabled')
expect(page).to have_no_selector('#project_remote_mirrors_attributes_0_url')
end
end
describe 'mirror settings', :js do
let(:user2) { create(:user) }
before do
project.team << [user2, :master]
visit project_settings_repository_path(project)
end
it 'shows pull mirror settings' do
expect(page).to have_selector('#project_mirror')
expect(page).to have_selector('#project_import_url')
expect(page).to have_selector('#project_mirror_user_id', visible: false)
expect(page).to have_selector('#project_mirror_trigger_builds')
end
it 'shows push mirror settings' do
expect(page).to have_selector('#project_remote_mirrors_attributes_0_enabled')
expect(page).to have_selector('#project_remote_mirrors_attributes_0_url')
end
it 'sets mirror user' do
page.within('.project-mirror-settings') do
select2(user2.id, from: '#project_mirror_user_id')
click_button('Save changes')
expect(find('.select2-chosen')).to have_content(user.name)
end
end
end
end
...@@ -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) { '' }
......
...@@ -26,6 +26,8 @@ describe Gitlab::Database::LoadBalancing do ...@@ -26,6 +26,8 @@ describe Gitlab::Database::LoadBalancing do
end end
describe '.enable?' do describe '.enable?' do
let!(:license) { create(:license, plan: ::License::PREMIUM_PLAN) }
it 'returns false when no hosts are specified' do it 'returns false when no hosts are specified' do
allow(described_class).to receive(:hosts).and_return([]) allow(described_class).to receive(:hosts).and_return([])
...@@ -60,6 +62,36 @@ describe Gitlab::Database::LoadBalancing do ...@@ -60,6 +62,36 @@ describe Gitlab::Database::LoadBalancing do
expect(described_class.enable?).to eq(true) expect(described_class.enable?).to eq(true)
end end
context 'without a license' do
before do
License.destroy_all
end
it 'is disabled' do
expect(described_class.enable?).to eq(false)
end
end
context 'with an EES license' do
let!(:license) { create(:license, plan: ::License::STARTER_PLAN) }
it 'is disabled' do
expect(described_class.enable?).to eq(false)
end
end
context 'with an EEP license' do
let!(:license) { create(:license, plan: ::License::PREMIUM_PLAN) }
it 'is enabled' do
allow(described_class).to receive(:hosts).and_return(%w(foo))
allow(Sidekiq).to receive(:server?).and_return(false)
allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
expect(described_class.enable?).to eq(true)
end
end
end end
describe '.program_name' do describe '.program_name' do
......
...@@ -157,4 +157,10 @@ describe Gitlab::Mirror do ...@@ -157,4 +157,10 @@ describe Gitlab::Mirror do
expect(described_class.max_delay).to be_within(1.minute).of(5.hours) expect(described_class.max_delay).to be_within(1.minute).of(5.hours)
end end
end end
describe '#min_delay' do
it 'returns min delay with some jitter' do
expect(described_class.min_delay).to be_within(1.minute).of(15.minutes)
end
end
end end
...@@ -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(:project) { create(:empty_project) }
let!(:project2) { create(:empty_project) } let(:projects) { create_list(:project, 3) }
let!(:board) { create(:board, project: project) } let!(:board) { create(:board, project: projects[0]) }
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 +41,7 @@ describe Gitlab::UsageData do ...@@ -32,7 +41,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 +69,9 @@ describe Gitlab::UsageData do ...@@ -60,6 +69,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 +84,16 @@ describe Gitlab::UsageData do ...@@ -72,6 +84,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
...@@ -124,7 +146,7 @@ describe Gitlab::UsageData do ...@@ -124,7 +146,7 @@ describe Gitlab::UsageData do
allow(License).to receive(:feature_available?).with(:service_desk).and_return(true) allow(License).to receive(:feature_available?).with(:service_desk).and_return(true)
allow(::EE::Gitlab::ServiceDesk).to receive(:enabled?).with(anything).and_return(true) allow(::EE::Gitlab::ServiceDesk).to receive(:enabled?).with(anything).and_return(true)
expect(subject).to eq(service_desk_enabled_projects: 2, expect(subject).to eq(service_desk_enabled_projects: 4,
service_desk_issues: 3) service_desk_issues: 3)
end end
end end
......
...@@ -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)
......
...@@ -97,7 +97,7 @@ describe UpdateAuthorizedKeysFile, :migration do ...@@ -97,7 +97,7 @@ describe UpdateAuthorizedKeysFile, :migration do
ActiveRecord::Base.connection.change_column_null :application_settings, :authorized_keys_enabled, true ActiveRecord::Base.connection.change_column_null :application_settings, :authorized_keys_enabled, true
ActiveRecord::Base.connection.change_column :application_settings, :authorized_keys_enabled, :boolean, default: nil ActiveRecord::Base.connection.change_column :application_settings, :authorized_keys_enabled, :boolean, default: nil
ApplicationSetting.first.update(authorized_keys_enabled: nil) ApplicationSetting.first.update(authorized_keys_enabled: nil, mirror_max_delay: 300)
end end
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
...@@ -105,7 +105,7 @@ describe UpdateAuthorizedKeysFile, :migration do ...@@ -105,7 +105,7 @@ describe UpdateAuthorizedKeysFile, :migration do
context 'when authorized_keys_enabled is explicitly false' do context 'when authorized_keys_enabled is explicitly false' do
before do before do
ApplicationSetting.first.update!(authorized_keys_enabled: false) ApplicationSetting.first.update!(authorized_keys_enabled: false, mirror_max_delay: 300)
end end
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
......
...@@ -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])
......
...@@ -21,25 +21,6 @@ describe ApplicationSetting, models: true do ...@@ -21,25 +21,6 @@ describe ApplicationSetting, models: true do
it { is_expected.to allow_value(https).for(:after_sign_out_path) } it { is_expected.to allow_value(https).for(:after_sign_out_path) }
it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) } it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) }
it { is_expected.to allow_value(10).for(:mirror_max_delay) }
it { is_expected.not_to allow_value(nil).for(:mirror_max_delay) }
it { is_expected.not_to allow_value(0).for(:mirror_max_delay) }
it { is_expected.not_to allow_value(1.0).for(:mirror_max_delay) }
it { is_expected.not_to allow_value(-1).for(:mirror_max_delay) }
it { is_expected.to allow_value(10).for(:mirror_max_capacity) }
it { is_expected.not_to allow_value(nil).for(:mirror_max_capacity) }
it { is_expected.not_to allow_value(0).for(:mirror_max_capacity) }
it { is_expected.not_to allow_value(1.0).for(:mirror_max_capacity) }
it { is_expected.not_to allow_value(-1).for(:mirror_max_capacity) }
it { is_expected.to allow_value(10).for(:mirror_capacity_threshold) }
it { is_expected.not_to allow_value(nil).for(:mirror_capacity_threshold) }
it { is_expected.not_to allow_value(0).for(:mirror_capacity_threshold) }
it { is_expected.not_to allow_value(1.0).for(:mirror_capacity_threshold) }
it { is_expected.not_to allow_value(-1).for(:mirror_capacity_threshold) }
it { is_expected.not_to allow_value(subject.mirror_max_capacity + 1).for(:mirror_capacity_threshold) }
describe 'disabled_oauth_sign_in_sources validations' do describe 'disabled_oauth_sign_in_sources validations' do
before do before do
allow(Devise).to receive(:omniauth_providers).and_return([:github]) allow(Devise).to receive(:omniauth_providers).and_return([:github])
......
...@@ -3,6 +3,28 @@ require 'spec_helper' ...@@ -3,6 +3,28 @@ require 'spec_helper'
describe ApplicationSetting do describe ApplicationSetting do
let(:setting) { described_class.create_from_defaults } let(:setting) { described_class.create_from_defaults }
describe 'validations' do
it { is_expected.to allow_value(100).for(:mirror_max_delay) }
it { is_expected.not_to allow_value(nil).for(:mirror_max_delay) }
it { is_expected.not_to allow_value(0).for(:mirror_max_delay) }
it { is_expected.not_to allow_value(1.0).for(:mirror_max_delay) }
it { is_expected.not_to allow_value(-1).for(:mirror_max_delay) }
it { is_expected.not_to allow_value((Gitlab::Mirror::MIN_DELAY - 1.minute) / 60).for(:mirror_max_delay) }
it { is_expected.to allow_value(10).for(:mirror_max_capacity) }
it { is_expected.not_to allow_value(nil).for(:mirror_max_capacity) }
it { is_expected.not_to allow_value(0).for(:mirror_max_capacity) }
it { is_expected.not_to allow_value(1.0).for(:mirror_max_capacity) }
it { is_expected.not_to allow_value(-1).for(:mirror_max_capacity) }
it { is_expected.to allow_value(10).for(:mirror_capacity_threshold) }
it { is_expected.not_to allow_value(nil).for(:mirror_capacity_threshold) }
it { is_expected.not_to allow_value(0).for(:mirror_capacity_threshold) }
it { is_expected.not_to allow_value(1.0).for(:mirror_capacity_threshold) }
it { is_expected.not_to allow_value(-1).for(:mirror_capacity_threshold) }
it { is_expected.not_to allow_value(subject.mirror_max_capacity + 1).for(:mirror_capacity_threshold) }
end
describe '#should_check_namespace_plan?' do describe '#should_check_namespace_plan?' do
before do before do
stub_application_setting(check_namespace_plan: check_namespace_plan_column) stub_application_setting(check_namespace_plan: check_namespace_plan_column)
......
...@@ -217,6 +217,62 @@ describe Project, models: true do ...@@ -217,6 +217,62 @@ describe Project, models: true do
end end
end end
describe '#has_remote_mirror?' do
let(:project) { create(:empty_project, :remote_mirror, :import_started) }
subject { project.has_remote_mirror? }
before do
allow_any_instance_of(RemoteMirror).to receive(:refresh_remote)
end
it 'returns true when a remote mirror is enabled' do
is_expected.to be_truthy
end
it 'returns false when unlicensed' do
stub_licensed_features(repository_mirrors: false)
is_expected.to be_falsy
end
it 'returns false when remote mirror is disabled' do
project.remote_mirrors.first.update_attributes(enabled: false)
is_expected.to be_falsy
end
end
describe '#update_remote_mirrors' do
let(:project) { create(:empty_project, :remote_mirror, :import_started) }
delegate :update_remote_mirrors, to: :project
before do
allow_any_instance_of(RemoteMirror).to receive(:refresh_remote)
end
it 'syncs enabled remote mirror' do
expect_any_instance_of(RemoteMirror).to receive(:sync)
update_remote_mirrors
end
it 'does nothing when unlicensed' do
stub_licensed_features(repository_mirrors: false)
expect_any_instance_of(RemoteMirror).not_to receive(:sync)
update_remote_mirrors
end
it 'does not sync disabled remote mirrors' do
project.remote_mirrors.first.update_attributes(enabled: false)
expect_any_instance_of(RemoteMirror).not_to receive(:sync)
update_remote_mirrors
end
end
describe '#any_runners_limit' do describe '#any_runners_limit' do
let(:project) { create(:empty_project, shared_runners_enabled: shared_runners_enabled) } let(:project) { create(:empty_project, shared_runners_enabled: shared_runners_enabled) }
let(:specific_runner) { create(:ci_runner) } let(:specific_runner) { create(:ci_runner) }
......
...@@ -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
......
...@@ -164,6 +164,7 @@ describe MergeRequest, models: true do ...@@ -164,6 +164,7 @@ describe MergeRequest, models: true do
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])
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)
......
...@@ -80,31 +80,60 @@ describe ProjectMirrorData, type: :model do ...@@ -80,31 +80,60 @@ describe ProjectMirrorData, type: :model do
end end
end end
context 'when base delay is higher than mirror_max_delay' do context 'when boundaries are surpassed' do
let!(:upper_jitter) { 30.seconds } let!(:mirror_jitter) { 30.seconds }
let(:max_timestamp) { timestamp + current_application_settings.mirror_max_delay.hours }
before do context 'when base delay is lower than mirror min_delay' do
allow_any_instance_of(Gitlab::Mirror).to receive(:rand).and_return(upper_jitter) before do
mirror_data.last_update_started_at = timestamp - 1.hour allow_any_instance_of(Gitlab::Mirror).to receive(:rand).and_return(mirror_jitter)
end mirror_data.last_update_started_at = timestamp - 1.second
end
context 'when reseting retry count' do context 'when resetting retry count' do
it 'applies transition successfully' do it 'applies transition successfully' do
expect do expect do
mirror_data.set_next_execution_timestamp! mirror_data.set_next_execution_timestamp!
end.to change { mirror_data.next_execution_timestamp }.to be_within(interval).of(max_timestamp + upper_jitter) end.to change { mirror_data.next_execution_timestamp }.to be_within(interval).of(timestamp + 15.minutes)
end
end
context 'when incrementing retry count' do
it 'applies transition successfully' do
mirror_data.retry_count = 2
mirror_data.increment_retry_count!
expect do
mirror_data.set_next_execution_timestamp!
end.to change { mirror_data.next_execution_timestamp }.to be_within(interval).of(timestamp + 15.minutes)
end
end end
end end
context 'when incrementing retry count' do context 'when base delay is higher than mirror_max_delay' do
it 'applies transition successfully' do let(:max_timestamp) { timestamp + current_application_settings.mirror_max_delay.minutes }
mirror_data.retry_count = 2
mirror_data.increment_retry_count!
expect do before do
mirror_data.set_next_execution_timestamp! allow_any_instance_of(Gitlab::Mirror).to receive(:rand).and_return(mirror_jitter)
end.to change { mirror_data.next_execution_timestamp }.to be_within(interval).of(max_timestamp + upper_jitter) mirror_data.last_update_started_at = timestamp - 1.hour
end
context 'when resetting retry count' do
it 'applies transition successfully' do
expect do
mirror_data.set_next_execution_timestamp!
end.to change { mirror_data.next_execution_timestamp }.to be_within(interval).of(max_timestamp + mirror_jitter)
end
end
context 'when incrementing retry count' do
it 'applies transition successfully' do
mirror_data.retry_count = 2
mirror_data.increment_retry_count!
expect do
mirror_data.set_next_execution_timestamp!
end.to change { mirror_data.next_execution_timestamp }.to be_within(interval).of(max_timestamp + mirror_jitter)
end
end end
end end
end end
......
...@@ -96,6 +96,18 @@ describe RemoteMirror do ...@@ -96,6 +96,18 @@ describe RemoteMirror do
Timecop.return Timecop.return
end end
context 'repository mirrors not licensed' do
before do
stub_licensed_features(repository_mirrors: false)
end
it 'does not schedule RepositoryUpdateRemoteMirrorWorker' do
expect(RepositoryUpdateRemoteMirrorWorker).not_to receive(:perform_in)
remote_mirror.sync
end
end
context 'with remote mirroring enabled' do context 'with remote mirroring enabled' do
it 'schedules a RepositoryUpdateRemoteMirrorWorker to run within a certain backoff delay' do it 'schedules a RepositoryUpdateRemoteMirrorWorker to run within a certain backoff delay' do
expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_in).with(RemoteMirror::BACKOFF_DELAY, remote_mirror.id, Time.now) expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_in).with(RemoteMirror::BACKOFF_DELAY, remote_mirror.id, Time.now)
......
...@@ -139,6 +139,24 @@ describe ProjectPolicy, models: true do ...@@ -139,6 +139,24 @@ describe ProjectPolicy, models: true do
end end
end end
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) { [:read_issue_link, :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
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) }
......
require 'spec_helper'
describe Issues::DuplicateService, services: true do
let(:user) { create(:user) }
let(:canonical_project) { create(:empty_project) }
let(:duplicate_project) { create(:empty_project) }
let(:canonical_issue) { create(:issue, project: canonical_project) }
let(:duplicate_issue) { create(:issue, project: duplicate_project) }
subject { described_class.new(duplicate_project, user, {}) }
describe '#execute' do
context 'when the issues passed are the same' do
it 'does nothing' do
expect(subject).not_to receive(:close_service)
expect(SystemNoteService).not_to receive(:mark_duplicate_issue)
expect(SystemNoteService).not_to receive(:mark_canonical_issue_of_duplicate)
subject.execute(duplicate_issue, duplicate_issue)
end
end
context 'when the user cannot update the duplicate issue' do
before do
canonical_project.add_reporter(user)
end
it 'does nothing' do
expect(subject).not_to receive(:close_service)
expect(SystemNoteService).not_to receive(:mark_duplicate_issue)
expect(SystemNoteService).not_to receive(:mark_canonical_issue_of_duplicate)
subject.execute(duplicate_issue, canonical_issue)
end
end
context 'when the user cannot comment on the canonical issue' do
before do
duplicate_project.add_reporter(user)
end
it 'does nothing' do
expect(subject).not_to receive(:close_service)
expect(SystemNoteService).not_to receive(:mark_duplicate_issue)
expect(SystemNoteService).not_to receive(:mark_canonical_issue_of_duplicate)
subject.execute(duplicate_issue, canonical_issue)
end
end
context 'when the user can mark the issue as a duplicate' do
before do
canonical_project.add_reporter(user)
duplicate_project.add_reporter(user)
end
it 'closes the duplicate issue' do
subject.execute(duplicate_issue, canonical_issue)
expect(duplicate_issue.reload).to be_closed
expect(canonical_issue.reload).to be_open
end
it 'adds a system note to the duplicate issue' do
expect(SystemNoteService)
.to receive(:mark_duplicate_issue).with(duplicate_issue, duplicate_project, user, canonical_issue)
subject.execute(duplicate_issue, canonical_issue)
end
it 'adds a system note to the canonical issue' do
expect(SystemNoteService)
.to receive(:mark_canonical_issue_of_duplicate).with(canonical_issue, canonical_project, user, duplicate_issue)
subject.execute(duplicate_issue, canonical_issue)
end
end
end
end
...@@ -491,6 +491,27 @@ describe Issues::UpdateService, services: true do ...@@ -491,6 +491,27 @@ describe Issues::UpdateService, services: true do
include_examples 'updating mentions', Issues::UpdateService include_examples 'updating mentions', Issues::UpdateService
end end
context 'duplicate issue' do
let(:canonical_issue) { create(:issue, project: project) }
context 'invalid canonical_issue_id' do
it 'does not call the duplicate service' do
expect(Issues::DuplicateService).not_to receive(:new)
update_issue(canonical_issue_id: 123456789)
end
end
context 'valid canonical_issue_id' do
it 'calls the duplicate service with both issues' do
expect_any_instance_of(Issues::DuplicateService)
.to receive(:execute).with(issue, canonical_issue)
update_issue(canonical_issue_id: canonical_issue.id)
end
end
end
include_examples 'issuable update service' do include_examples 'issuable update service' do
let(:open_issuable) { issue } let(:open_issuable) { issue }
let(:closed_issuable) { create(:closed_issue, project: project) } let(:closed_issuable) { create(:closed_issue, project: project) }
......
...@@ -4,6 +4,21 @@ describe Projects::UpdateMirrorService do ...@@ -4,6 +4,21 @@ describe Projects::UpdateMirrorService do
let(:project) { create(:project, :mirror, import_url: Project::UNKNOWN_IMPORT_URL) } let(:project) { create(:project, :mirror, import_url: Project::UNKNOWN_IMPORT_URL) }
describe "#execute" do describe "#execute" do
context 'unlicensed' do
before do
stub_licensed_features(repository_mirrors: false)
end
it 'does nothing' do
allow_any_instance_of(EE::Project).to receive(:destroy_mirror_data)
expect(project).not_to receive(:fetch_mirror)
result = described_class.new(project, project.owner).execute
expect(result[:status]).to eq(:success)
end
end
it "fetches the upstream repository" do it "fetches the upstream repository" do
expect(project).to receive(:fetch_mirror) expect(project).to receive(:fetch_mirror)
...@@ -111,12 +126,12 @@ describe Projects::UpdateMirrorService do ...@@ -111,12 +126,12 @@ describe Projects::UpdateMirrorService do
describe "when is no mirror" do describe "when is no mirror" do
let(:project) { build_stubbed(:project) } let(:project) { build_stubbed(:project) }
it "fails" do it "success" do
expect(project.mirror?).to eq(false) expect(project.mirror?).to eq(false)
result = described_class.new(project, build_stubbed(:user)).execute result = described_class.new(project, build_stubbed(:user)).execute
expect(result[:status]).to eq(:error) expect(result[:status]).to eq(:success)
end end
end end
end end
......
...@@ -4,7 +4,7 @@ describe Projects::UpdateRemoteMirrorService do ...@@ -4,7 +4,7 @@ describe Projects::UpdateRemoteMirrorService do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:remote_project) { create(:forked_project_with_submodules) } let(:remote_project) { create(:forked_project_with_submodules) }
let(:repository) { project.repository } let(:repository) { project.repository }
let(:remote_mirror) { project.remote_mirrors.create!(url: remote_project.http_url_to_repo) } let(:remote_mirror) { project.remote_mirrors.create!(url: remote_project.http_url_to_repo, enabled: true) }
subject { described_class.new(project, project.creator) } subject { described_class.new(project, project.creator) }
...@@ -18,6 +18,14 @@ describe Projects::UpdateRemoteMirrorService do ...@@ -18,6 +18,14 @@ describe Projects::UpdateRemoteMirrorService do
allow(gitlab_shell).to receive(:push_remote_branches).and_return(true) allow(gitlab_shell).to receive(:push_remote_branches).and_return(true)
end end
it 'does nothing when unlicensed' do
stub_licensed_features(repository_mirrors: false)
expect(project.repository).not_to receive(:fetch_remote)
subject.execute(remote_mirror)
end
it "fetches the remote repository" do it "fetches the remote repository" do
expect(repository).to receive(:fetch_remote).with(remote_mirror.ref_name, no_tags: true) do expect(repository).to receive(:fetch_remote).with(remote_mirror.ref_name, no_tags: true) do
sync_remote(repository, remote_mirror.ref_name, local_branch_names) sync_remote(repository, remote_mirror.ref_name, local_branch_names)
......
...@@ -280,6 +280,15 @@ describe QuickActions::InterpretService, services: true do ...@@ -280,6 +280,15 @@ describe QuickActions::InterpretService, services: true do
end end
end end
shared_examples 'duplicate command' do
it 'fetches issue and populates canonical_issue_id if content contains /duplicate issue_reference' do
issue_duplicate # populate the issue
_, updates = service.execute(content, issuable)
expect(updates).to eq(canonical_issue_id: issue_duplicate.id)
end
end
it_behaves_like 'reopen command' do it_behaves_like 'reopen command' do
let(:content) { '/reopen' } let(:content) { '/reopen' }
let(:issuable) { issue } let(:issuable) { issue }
...@@ -720,6 +729,41 @@ describe QuickActions::InterpretService, services: true do ...@@ -720,6 +729,41 @@ describe QuickActions::InterpretService, services: true do
end end
end end
context '/duplicate command' do
it_behaves_like 'duplicate command' do
let(:issue_duplicate) { create(:issue, project: project) }
let(:content) { "/duplicate #{issue_duplicate.to_reference}" }
let(:issuable) { issue }
end
it_behaves_like 'empty command' do
let(:content) { '/duplicate' }
let(:issuable) { issue }
end
context 'cross project references' do
it_behaves_like 'duplicate command' do
let(:other_project) { create(:empty_project, :public) }
let(:issue_duplicate) { create(:issue, project: other_project) }
let(:content) { "/duplicate #{issue_duplicate.to_reference(project)}" }
let(:issuable) { issue }
end
it_behaves_like 'empty command' do
let(:content) { "/duplicate imaginary#1234" }
let(:issuable) { issue }
end
it_behaves_like 'empty command' do
let(:other_project) { create(:empty_project, :private) }
let(:issue_duplicate) { create(:issue, project: other_project) }
let(:content) { "/duplicate #{issue_duplicate.to_reference(project)}" }
let(:issuable) { issue }
end
end
end
context 'when current_user cannot :admin_issue' do context 'when current_user cannot :admin_issue' do
let(:visitor) { create(:user) } let(:visitor) { create(:user) }
let(:issue) { create(:issue, project: project, author: visitor) } let(:issue) { create(:issue, project: project, author: visitor) }
...@@ -769,6 +813,11 @@ describe QuickActions::InterpretService, services: true do ...@@ -769,6 +813,11 @@ describe QuickActions::InterpretService, services: true do
let(:content) { '/remove_due_date' } let(:content) { '/remove_due_date' }
let(:issuable) { issue } let(:issuable) { issue }
end end
it_behaves_like 'empty command' do
let(:content) { '/duplicate #{issue.to_reference}' }
let(:issuable) { issue }
end
end end
context '/award command' do context '/award command' do
......
...@@ -1239,4 +1239,54 @@ describe SystemNoteService, services: true do ...@@ -1239,4 +1239,54 @@ describe SystemNoteService, services: true do
expect(subject.note).to include(diffs_project_merge_request_url(project, merge_request, diff_id: diff_id, anchor: line_code)) expect(subject.note).to include(diffs_project_merge_request_url(project, merge_request, diff_id: diff_id, anchor: line_code))
end end
end end
describe '.mark_duplicate_issue' do
subject { described_class.mark_duplicate_issue(noteable, project, author, canonical_issue) }
context 'within the same project' do
let(:canonical_issue) { create(:issue, project: project) }
it_behaves_like 'a system note' do
let(:action) { 'duplicate' }
end
it { expect(subject.note).to eq "marked this issue as a duplicate of #{canonical_issue.to_reference}" }
end
context 'across different projects' do
let(:other_project) { create(:empty_project) }
let(:canonical_issue) { create(:issue, project: other_project) }
it_behaves_like 'a system note' do
let(:action) { 'duplicate' }
end
it { expect(subject.note).to eq "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}" }
end
end
describe '.mark_canonical_issue_of_duplicate' do
subject { described_class.mark_canonical_issue_of_duplicate(noteable, project, author, duplicate_issue) }
context 'within the same project' do
let(:duplicate_issue) { create(:issue, project: project) }
it_behaves_like 'a system note' do
let(:action) { 'duplicate' }
end
it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference} as a duplicate of this issue" }
end
context 'across different projects' do
let(:other_project) { create(:empty_project) }
let(:duplicate_issue) { create(:issue, project: other_project) }
it_behaves_like 'a system note' do
let(:action) { 'duplicate' }
end
it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue" }
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'
......
require 'rails_helper' require 'spec_helper'
describe UpdateAllMirrorsWorker do describe UpdateAllMirrorsWorker do
subject(:worker) { described_class.new } subject(:worker) { described_class.new }
...@@ -23,6 +23,12 @@ describe UpdateAllMirrorsWorker do ...@@ -23,6 +23,12 @@ describe UpdateAllMirrorsWorker do
worker.perform worker.perform
end end
it 'schedules mirrors' do
expect(worker).to receive(:schedule_mirrors!)
worker.perform
end
end end
describe '#fail_stuck_mirrors!' do describe '#fail_stuck_mirrors!' do
...@@ -63,4 +69,80 @@ describe UpdateAllMirrorsWorker do ...@@ -63,4 +69,80 @@ describe UpdateAllMirrorsWorker do
expect(project.reload.import_error).to eq 'The mirror update took too long to complete.' expect(project.reload.import_error).to eq 'The mirror update took too long to complete.'
end end
end end
describe '#schedule_mirrors!' do
def schedule_mirrors!(capacity:)
allow(Gitlab::Mirror).to receive_messages(available_capacity: capacity)
Sidekiq::Testing.fake! do
worker.schedule_mirrors!
end
end
def expect_import_status(project, status)
expect(project.reload.import_status).to eq(status)
end
def expect_import_scheduled(*projects)
projects.each { |project| expect_import_status(project, 'scheduled') }
end
def expect_import_not_scheduled(*projects)
projects.each { |project| expect_import_status(project, 'none') }
end
context 'unlicensed' do
it 'does not schedule when project does not have repository mirrors available' do
project = create(:empty_project, :mirror)
stub_licensed_features(repository_mirrors: false)
schedule_mirrors!(capacity: 5)
expect_import_not_scheduled(project)
end
end
context 'licensed' do
def scheduled_mirror(at:, licensed:)
namespace = create(:group, :public, plan: (Namespace::BRONZE_PLAN if licensed))
project = create(:empty_project, :public, :mirror, namespace: namespace)
project.mirror_data.update!(next_execution_timestamp: at)
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
project
end
before do
stub_licensed_features(repository_mirrors: true)
stub_application_setting(check_namespace_plan: true)
allow(Gitlab).to receive_messages(com?: true)
end
let!(:unlicensed_project) { scheduled_mirror(at: 4.weeks.ago, licensed: false) }
let!(:earliest_project) { scheduled_mirror(at: 3.weeks.ago, licensed: true) }
let!(:latest_project) { scheduled_mirror(at: 2.weeks.ago, licensed: true) }
it "schedules all available mirrors when capacity is in excess" do
schedule_mirrors!(capacity: 3)
expect_import_scheduled(earliest_project, latest_project)
expect_import_not_scheduled(unlicensed_project)
end
it "schedules all available mirrors when capacity is sufficient" do
schedule_mirrors!(capacity: 2)
expect_import_scheduled(earliest_project, latest_project)
expect_import_not_scheduled(unlicensed_project)
end
it 'schedules mirrors by next_execution_timestamp when capacity is insufficient' do
schedule_mirrors!(capacity: 1)
expect_import_scheduled(earliest_project)
expect_import_not_scheduled(unlicensed_project, latest_project)
end
end
end
end end
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