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,
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment