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
variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
ELASTIC_URL: "http://elasticsearch:9200"
ELASTIC_URL: "http://elastic:changeme@docker.elastic.co-elasticsearch-elasticsearch:9200"
RAILS_ENV: "test"
NODE_ENV: "test"
SIMPLECOV: "true"
......@@ -63,13 +63,14 @@ stages:
services:
- postgres:9.2
- redis:alpine
- elasticsearch:5.3
- docker.elastic.co/elasticsearch/elasticsearch:5.3.2
.use-mysql: &use-mysql
services:
- mysql:latest
- 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:
......
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)
- GeoLogCursor is part of a new experimental Geo replication system. !1988
......
......@@ -2,6 +2,18 @@
documentation](doc/development/changelog.md) for instructions on adding your own
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)
- 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._
- [Issue tracker guidelines](#issue-tracker-guidelines)
- [Issue weight](#issue-weight)
- [Regression issues](#regression-issues)
- [Technical debt](#technical-debt)
- [Technical and UX debt](#technical-and-ux-debt)
- [Stewardship](#stewardship)
- [Merge requests](#merge-requests)
- [Merge request guidelines](#merge-request-guidelines)
......@@ -345,27 +345,29 @@ addressed.
[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
### Technical debt
### Technical and UX debt
In order to track things that can be improved in GitLab's codebase, we created
the ~"technical debt" label in [GitLab's issue tracker][ce-tracker].
In order to track things that can be improved in GitLab's codebase,
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,
shortcuts that have been taken, code that needs refactoring, features that need
additional attention, and all other things that have been left behind due to
high velocity of development.
These labels should be added to issues that describe things that can be improved,
shortcuts that have been taken, features that need additional attention, and all
other things that have been left behind due to 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
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.
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
for a release by the appropriate person.
Make sure to mention the merge request that the `technical debt` issue is
associated with in the description of the issue.
Make sure to mention the merge request that the ~"technical debt" issue or
~"UX debt" issue is associated with in the description of the issue.
### Stewardship
......
......@@ -175,7 +175,7 @@ gem 'rainbow', '~> 2.2'
gem 'settingslogic', '~> 2.0.9'
# Linear-time regex library for untrusted regular expressions
gem 're2', '~> 1.0.0'
gem 're2', '~> 1.1.0'
# Misc
......
......@@ -620,7 +620,7 @@ GEM
premailer-rails (1.9.7)
actionmailer (>= 3, < 6)
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)
pry (0.10.4)
coderay (~> 1.1.0)
......@@ -685,7 +685,7 @@ GEM
debugger-ruby_core_source (~> 1.3)
rdoc (4.2.2)
json (~> 1.4)
re2 (1.0.0)
re2 (1.1.0)
recaptcha (3.0.0)
json
recursive-open-struct (1.0.0)
......@@ -1093,7 +1093,7 @@ DEPENDENCIES
raindrops (~> 0.18)
rblineprof (~> 0.3.6)
rdoc (~> 4.2)
re2 (~> 1.0.0)
re2 (~> 1.1.0)
recaptcha (~> 3.0)
redcarpet (~> 3.4)
redis (~> 3.2)
......
......@@ -33,7 +33,7 @@ $(() => {
const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore;
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 || {};
......
/* 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 { placeholderImage } from './lazy_loader';
const gfmRules = {
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
......@@ -56,6 +57,11 @@ const gfmRules = {
return text;
},
},
ImageLazyLoadFilter: {
'img'(el, text) {
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
},
},
VideoLinkFilter: {
'.video-container'(el) {
const videoEl = el.querySelector('video');
......@@ -163,7 +169,9 @@ const gfmRules = {
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
},
'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) {
// 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';
import './labels';
import './labels_select';
import './layout_nav';
import LazyLoader from './lazy_loader';
import './line_highlighter';
import './logo';
import './member_expiration_date';
......@@ -174,6 +175,11 @@ window.addEventListener('load', function onLoad() {
gl.utils.handleLocationHash();
}, false);
gl.lazyLoader = new LazyLoader({
scrollContainer: window,
observerNode: '#content-body'
});
$(function () {
var $body = $('body');
var $document = $(document);
......@@ -292,13 +298,7 @@ $(function () {
return $container.remove();
// Commit show suppressed diff
});
$('.navbar-toggle').on('click', function () {
$('.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');
});
$('.navbar-toggle').on('click', () => $('.header-content').toggleClass('menu-expanded'));
// Show/hide comments on diff
$body.on('click', '.js-toggle-diff-comments', function (e) {
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 */
/* global Flash */
import { __, s__ } from './locale';
export default class Star {
constructor() {
$('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) {
......@@ -11,10 +13,10 @@ export default class Star {
toggleStar = function(isStarred) {
$this.parent().find('.star-count').text(data.star_count);
if (isStarred) {
$starSpan.removeClass('starred').text('Star');
$starSpan.removeClass('starred').text(s__('StarProject|Star'));
$starIcon.removeClass('fa-star').addClass('fa-star-o');
} else {
$starSpan.addClass('starred').text('Unstar');
$starSpan.addClass('starred').text(__('Unstar'));
$starIcon.removeClass('fa-star-o').addClass('fa-star');
}
};
......
......@@ -35,6 +35,8 @@
width: 40px;
height: 40px;
padding: 0;
background: $avatar-background;
overflow: hidden;
&.avatar-inline {
float: none;
......
......@@ -93,7 +93,7 @@
.is-selected .pika-day,
.pika-day:hover,
.is-today .pika-day:hover {
.is-today .pika-day {
background: $gl-primary;
color: $white-light;
box-shadow: none;
......
......@@ -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 {
position: absolute;
left: -10px;
......@@ -171,6 +187,19 @@ header {
min-height: $header-height;
padding-left: 30px;
&.menu-expanded {
@media (max-width: $screen-xs-max) {
.header-logo,
.title-container {
display: none;
}
.navbar-collapse {
display: block;
}
}
}
.dropdown-menu {
margin-top: -5px;
}
......
......@@ -182,6 +182,12 @@
}
}
&.nav-controls-new-nav {
> .dropdown {
margin-right: 0;
}
}
> .btn-grouped {
float: none;
}
......
......@@ -11,8 +11,17 @@
}
img {
max-width: 100%;
/*max-width: 100%;*/
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 {
......
......@@ -384,7 +384,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
* Avatar
*/
$avatar_radius: 50%;
$avatar-border: $border-color;
$avatar-border: $gray-normal;
$avatar-border-hover: $gray-darker;
$avatar-background: $gray-lightest;
$gl-avatar-size: 40px;
/*
......
......@@ -73,6 +73,7 @@
width: 100%;
height: 100%;
top: 0;
left: 0;
background: $white-light;
z-index: 500;
......@@ -479,7 +480,10 @@
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 {
top: 0;
bottom: 0;
......
......@@ -269,6 +269,14 @@
border-bottom: 2px solid $border-color;
}
}
&.has-downstream {
&::after {
content: '';
width: 0;
border: none;
}
}
}
}
......@@ -618,8 +626,8 @@
}
// Dropdown button in mini pipeline graph
.mini-pipeline-graph-dropdown-toggle,
.linked-pipeline-mini-item {
button.mini-pipeline-graph-dropdown-toggle,
a.linked-pipeline-mini-item {
border-radius: 100px;
background-color: $white-light;
border-width: 1px;
......@@ -630,6 +638,7 @@
padding: 0;
transition: all 0.2s linear;
position: relative;
vertical-align: middle;
> .fa.fa-caret-down {
position: absolute;
......@@ -934,12 +943,13 @@
&.is-downstream {
margin-left: -4px;
margin-right: 4px;
}
.arrow-icon {
display: inline-block;
vertical-align: middle;
margin: -2px 5px 0;
margin: -4px 2px 0 0;
svg {
fill: $gray-darkest;
......@@ -956,20 +966,23 @@
position: relative;
display: inline-block;
vertical-align: middle;
height: 20px;
width: 20px;
height: 22px;
width: 22px;
transition: margin .2s linear;
margin: 2px 5px 3px -12px;
margin: 2px 7px 3px -14px;
svg {
top: 0;
right: 0;
width: 18px;
height: 18px;
height: 22px;
width: 22px;
position: absolute;
top: -1px;
left: -1px;
z-index: 2;
overflow: visible;
}
// override dropdown-toggle width expansion
&:hover {
width: 20px;
width: 22px;
}
&:first-of-type:last-of-type {
......
class Admin::ApplicationSettingsController < Admin::ApplicationController
prepend EE::Admin::ApplicationSettingsController
before_action :set_application_setting
def show
......@@ -58,7 +60,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def application_setting_params
import_sources = params[:application_setting][:import_sources]
if import_sources.nil?
params[:application_setting][:import_sources] = []
else
......@@ -77,11 +78,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
params.require(:application_setting).permit(
application_setting_params_ce << application_setting_params_ee
application_setting_params_attributes
)
end
def application_setting_params_ce
def application_setting_params_attributes
[
:admin_notification_email,
:after_sign_out_path,
......@@ -166,30 +167,4 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
sidekiq_throttling_queues: []
]
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
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
end
def remote_mirror
return unless project.feature_available?(:repository_mirrors)
@remote_mirror = @project.remote_mirrors.first_or_initialize
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
def project
return @project if @project
return nil unless params[:project_id] || params[:id]
path = File.join(params[:namespace_id], params[:project_id] || params[:id])
auth_proc = ->(project) { !project.pending_delete? }
......
......@@ -5,6 +5,7 @@ class Projects::MirrorsController < Projects::ApplicationController
before_action :authorize_admin_project!, except: [:update_now]
before_action :authorize_push_code!, only: [:update_now]
before_action :remote_mirror, only: [:update]
before_action :check_repository_mirrors_available!
layout "project_settings"
......
class ProjectsController < Projects::ApplicationController
include IssuableCollections
include ExtractsPath
prepend EE::ProjectsController
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :project, except: [:index, :new, :create]
......@@ -297,10 +298,10 @@ class ProjectsController < Projects::ApplicationController
def project_params
params.require(:project)
.permit(project_params_ce << project_params_ee)
.permit(project_params_attributes)
end
def project_params_ce
def project_params_attributes
[
:avatar,
:build_allow_git_fetch,
......@@ -337,25 +338,6 @@ class ProjectsController < Projects::ApplicationController
]
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?
project.repository_exists? && !project.empty_repo? && project.repo
......
......@@ -5,6 +5,14 @@ class SessionsController < Devise::SessionsController
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 :authenticate_with_two_factor,
if: :two_factor_enabled?, only: [:create]
......
......@@ -11,17 +11,12 @@ module AvatarsHelper
def user_avatar_without_link(options = {})
avatar_size = options[:size] || 16
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)
data_attributes = { container: 'body' }
if options[:lazy]
data_attributes[:src] = avatar_url
end
image_tag(
options[:lazy] ? '' : avatar_url,
class: "avatar has-tooltip s#{avatar_size} #{css_class}",
avatar_url,
class: %W[avatar has-tooltip s#{avatar_size}].push(*options[:css_class]),
alt: "#{user_name}'s avatar",
title: user_name,
data: data_attributes
......
......@@ -61,8 +61,8 @@ module EmailsHelper
else
image_tag(
image_url('mailers/gitlab_header_logo.gif'),
size: "55x50",
alt: "GitLab"
size: '55x50',
alt: 'GitLab'
)
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
'discussion' => 'icon_comment_o',
'moved' => 'icon_arrow_circle_o_right',
'outdated' => 'icon_edit',
'duplicate' => 'icon_clone',
'approved' => 'icon_check',
'unapproved' => 'icon_fa_close',
'relate' => 'icon_anchor',
......
......@@ -2,7 +2,7 @@ module VersionCheckHelper
def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled
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
......@@ -11,7 +11,7 @@ module CacheMarkdownField
extend ActiveSupport::Concern
# 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
INVALIDATED_BY = %w[author project].freeze
......
......@@ -16,7 +16,7 @@ module EE
validates :mirror_max_delay,
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,
presence: true,
......@@ -50,6 +50,10 @@ module EE
private
def mirror_max_delay_in_minutes
::Gitlab::Mirror.min_delay_upper_bound / 60
end
def mirror_capacity_threshold_less_than
return unless mirror_max_capacity && mirror_capacity_threshold
......
......@@ -42,11 +42,6 @@ module EE
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
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)",
......@@ -84,6 +79,11 @@ module EE
end
end
def mirror
super && feature_available?(:repository_mirrors)
end
alias_method :mirror?, :mirror
def mirror_updated?
mirror? && self.mirror_last_update_at
end
......@@ -140,7 +140,7 @@ module EE
end
def has_remote_mirror?
remote_mirrors.enabled.exists?
feature_available?(:repository_mirrors) && remote_mirrors.enabled.exists?
end
def updating_remote_mirror?
......@@ -148,7 +148,9 @@ module EE
end
def update_remote_mirrors
remote_mirrors.each(&:sync)
return unless feature_available?(:repository_mirrors)
remote_mirrors.enabled.each(&:sync)
end
def mark_stuck_remote_mirrors_as_failed!
......
......@@ -186,10 +186,14 @@ class Group < Namespace
end
def has_owner?(user)
return false unless user
members_with_parents.owners.where(user_id: user).any?
end
def has_master?(user)
return false unless user
members_with_parents.masters.where(user_id: user).any?
end
......@@ -258,7 +262,7 @@ class Group < Namespace
end
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
def users_with_parents
......
......@@ -6,6 +6,7 @@ class License < ActiveRecord::Base
AUDITOR_USER_FEATURE = 'GitLab_Auditor_User'.freeze
BURNDOWN_CHARTS_FEATURE = 'GitLab_BurndownCharts'.freeze
CONTRIBUTION_ANALYTICS_FEATURE = 'GitLab_ContributionAnalytics'.freeze
DB_LOAD_BALANCING_FEATURE = 'GitLab_DbLoadBalancing'.freeze
DEPLOY_BOARD_FEATURE = 'GitLab_DeployBoard'.freeze
ELASTIC_SEARCH_FEATURE = 'GitLab_ElasticSearch'.freeze
EXPORT_ISSUES_FEATURE = 'GitLab_ExportIssues'.freeze
......@@ -27,6 +28,7 @@ class License < ActiveRecord::Base
PROTECTED_REFS_FOR_USERS_FEATURE = 'GitLab_RefPermissionsForUsers'.freeze
PUSH_RULES_FEATURE = 'GitLab_PushRules'.freeze
RELATED_ISSUES_FEATURE = 'GitLab_RelatedIssues'.freeze
REPOSITORY_MIRRORS_FEATURE = 'GitLab_RepositoryMirrors'.freeze
REPOSITORY_SIZE_LIMIT_FEATURE = 'GitLab_RepositorySizeLimit'.freeze
SERVICE_DESK_FEATURE = 'GitLab_ServiceDesk'.freeze
VARIABLE_ENVIRONMENT_SCOPE_FEATURE = 'GitLab_VariableEnvironmentScope'.freeze
......@@ -34,6 +36,7 @@ class License < ActiveRecord::Base
FEATURE_CODES = {
admin_audit_log: ADMIN_AUDIT_LOG_FEATURE,
auditor_user: AUDITOR_USER_FEATURE,
db_load_balancing: DB_LOAD_BALANCING_FEATURE,
elastic_search: ELASTIC_SEARCH_FEATURE,
geo: GEO_FEATURE,
object_storage: OBJECT_STORAGE_FEATURE,
......@@ -62,7 +65,8 @@ class License < ActiveRecord::Base
multiple_issue_assignees: MULTIPLE_ISSUE_ASSIGNEES_FEATURE,
multiple_issue_boards: MULTIPLE_ISSUE_BOARDS_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
STARTER_PLAN = 'starter'.freeze
......@@ -91,6 +95,7 @@ class License < ActiveRecord::Base
{ PUSH_RULES_FEATURE => 1 },
{ PROTECTED_REFS_FOR_USERS_FEATURE => 1 },
{ RELATED_ISSUES_FEATURE => 1 },
{ REPOSITORY_MIRRORS_FEATURE => 1 },
{ REPOSITORY_SIZE_LIMIT_FEATURE => 1 }
].freeze
......@@ -98,6 +103,7 @@ class License < ActiveRecord::Base
*EES_FEATURES,
{ ADMIN_AUDIT_LOG_FEATURE => 1 },
{ AUDITOR_USER_FEATURE => 1 },
{ DB_LOAD_BALANCING_FEATURE => 1 },
{ DEPLOY_BOARD_FEATURE => 1 },
{ FILE_LOCKS_FEATURE => 1 },
{ GEO_FEATURE => 1 },
......@@ -140,6 +146,7 @@ class License < ActiveRecord::Base
{ OBJECT_STORAGE_FEATURE => 1 },
{ PROTECTED_REFS_FOR_USERS_FEATURE => 1 },
{ PUSH_RULES_FEATURE => 1 },
{ REPOSITORY_MIRRORS_FEATURE => 1 },
{ SERVICE_DESK_FEATURE => 1 }
].freeze
......
......@@ -27,6 +27,7 @@ class ProjectMirrorData < ActiveRecord::Base
timestamp = Time.now
retry_factor = [1, self.retry_count].max
delay = [base_delay(timestamp) * retry_factor, Gitlab::Mirror.max_delay].min
delay = [delay, Gitlab::Mirror.min_delay].max
self.next_execution_timestamp = timestamp + delay
end
......
......@@ -77,13 +77,22 @@ class RemoteMirror < ActiveRecord::Base
end
def sync
return unless project && enabled
return if project.pending_delete?
return unless enabled?
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
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)
last_update_started_at && last_update_started_at > timestamp && !update_failed?
end
......
class SystemNoteMetadata < ActiveRecord::Base
ICON_TYPES = %w[
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
approved unapproved relate unrelate
].freeze
......
......@@ -12,7 +12,8 @@ class ProjectPolicy < BasePolicy
desc "User is a project owner"
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
desc "Project has public builds enabled"
......
......@@ -4,6 +4,8 @@ module Geo
EmptyCloneUrlPrefixError = Class.new(StandardError)
class BaseSyncService
include ::Gitlab::Geo::ProjectLogHelpers
class << self
attr_accessor :type
end
......@@ -19,9 +21,9 @@ module Geo
def execute
try_obtain_lease do
log("Started #{type} sync")
log_info("Started #{type} sync")
sync_repository
log("Finished #{type} sync")
log_info("Finished #{type} sync")
end
end
......@@ -46,11 +48,11 @@ module Geo
end
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
unless repository_lease
log("Could not obtain lease to sync #{type}")
log_info("Could not obtain lease to sync #{type}")
return
end
......@@ -59,14 +61,14 @@ module Geo
# 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
# 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)
end
def update_registry(type, started_at: nil, finished_at: nil)
return unless started_at || finished_at
log("Updating #{type} sync information")
log_info("Updating #{type} sync information")
attrs = {}
......@@ -83,9 +85,5 @@ module Geo
def type
self.class.type
end
def log(message)
Rails.logger.info("#{self.class.name}: #{message} for project #{project.path_with_namespace} (#{project.id})")
end
end
end
......@@ -33,7 +33,7 @@ module Geo
Geo::EventLog.create!("#{self.class.event_type}" => build_event)
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
private
......@@ -43,8 +43,13 @@ module Geo
"#{self.class} does not implement #{__method__}"
end
def log(message, error)
Rails.logger.error("#{self.class.name}: #{message} for project #{project.path_with_namespace} (#{project.id}): #{error}")
def log_error(message, 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
......@@ -15,8 +15,8 @@ module Geo
def downloader
klass = "Gitlab::Geo::#{service_klass_name}Downloader".constantize
klass.new(object_type, object_db_id)
rescue NameError
log("Unknown file type: #{object_type}")
rescue NameError => e
log_error('Unknown file type', e)
raise
end
......
......@@ -27,8 +27,24 @@ module Geo
klass_name.camelize
end
def log(message)
Rails.logger.info "#{self.class.name}: #{message}"
def log_info(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
......@@ -21,8 +21,8 @@ module Geo
def uploader_klass
"Gitlab::Geo::#{service_klass_name}Uploader".constantize
rescue NameError
log("Unknown file type: #{object_type}")
rescue NameError => e
log_error('Unknown file type', e)
raise
end
end
......
......@@ -20,10 +20,15 @@ module Geo
next unless node.enabled?
notify_url = node.send(notify_url_method.to_sym)
success, message = notify(notify_url, content)
success, details = notify(notify_url, content)
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)
end
end
......
......@@ -10,7 +10,7 @@ module Geo
end
def fetch_project_repository
log('Fetching project repository')
log_info('Fetching project repository')
update_registry(:repository, started_at: DateTime.now)
begin
......@@ -19,16 +19,16 @@ module Geo
update_registry(:repository, finished_at: DateTime.now)
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
Rails.logger.error("#{self.class.name}: Error invalid repository for project #{project.path_with_namespace}: #{e}")
log('Expiring caches')
log_error("Invalid repository", e)
log_info('Expiring caches')
project.repository.after_create
end
end
def expire_repository_caches
log('Expiring caches')
log_info('Expiring caches')
project.repository.after_sync
end
......
module Geo
class RepositoryUpdateService
include Gitlab::Geo::ProjectLogHelpers
attr_reader :project, :clone_url, :logger
LEASE_TIMEOUT = 1.hour.freeze
......@@ -21,21 +23,21 @@ module Geo
project.repository.expire_content_cache
end
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
logger.error "#{self.class.name}: Error invalid repository for project #{project.path_with_namespace}: #{e}"
logger.warn "#{self.class.name}: Invalidating cache for project #{project.path_with_namespace}"
log_error('Error invalid repository', e)
log_info('Invalidating cache for project')
project.repository.after_create
end
private
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
unless repository_lease.present?
log('Could not obtain lease to sync repository')
log_info('Could not obtain lease to sync repository')
return
end
......@@ -43,7 +45,7 @@ module Geo
begin
yield
ensure
log('Releasing leases to sync repository')
log_info('Releasing leases to sync repository')
Gitlab::ExclusiveLease.cancel(lease_key, repository_lease)
end
end
......@@ -51,9 +53,5 @@ module Geo
def lease_key
@lease_key ||= "#{LEASE_KEY_PREFIX}:#{project.id}"
end
def log(message)
logger.info("#{self.class.name}: #{message} for project #{project.path_with_namespace} (#{project.id})")
end
end
end
......@@ -9,7 +9,7 @@ module Geo
end
def fetch_wiki_repository
log('Fetching wiki repository')
log_info('Fetching wiki repository')
update_registry(:wiki, started_at: DateTime.now)
begin
......@@ -21,7 +21,7 @@ module Geo
Gitlab::Shell::Error,
ProjectWiki::CouldNotCreateWikiError,
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
......
......@@ -60,6 +60,7 @@ class IssuableBaseService < BaseService
params.delete(:assignee_ids)
params.delete(:assignee_id)
params.delete(:due_date)
params.delete(:canonical_issue_id)
end
filter_assignee(issuable)
......
......@@ -7,6 +7,14 @@ module Issues
issue_data
end
def reopen_service
Issues::ReopenService
end
def close_service
Issues::CloseService
end
private
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
def execute(issue)
handle_move_between_iids(issue)
filter_spam_check_params
change_issue_duplicate(issue)
update(issue)
end
......@@ -53,14 +54,6 @@ module Issues
end
end
def reopen_service
Issues::ReopenService
end
def close_service
Issues::CloseService
end
def handle_move_between_iids(issue)
return unless params[:move_between_iids]
......@@ -72,6 +65,15 @@ module Issues
issue.move_between(issue_before, issue_after)
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
def get_issue_if_allowed(project, iid)
......
......@@ -5,7 +5,7 @@ module Projects
def execute
unless project.mirror?
return error("The project has no mirror to update")
return success
end
unless can?(current_user, :push_code_to_protected_branches, project)
......
......@@ -6,6 +6,8 @@ module Projects
@mirror = remote_mirror
@errors = []
return success unless remote_mirror.enabled?
begin
repository.fetch_remote(mirror.ref_name, no_tags: true)
......
......@@ -472,6 +472,24 @@ module QuickActions
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)
return [] if params.nil?
......
......@@ -606,6 +606,44 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, noteable.project, user, body, action: 'unapproved'))
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
def notes_for_mentioner(mentioner, noteable, notes)
......
- if Gitlab.com?
- if Gitlab.com? && License.feature_available?(:repository_mirrors)
%fieldset
%legend Repository mirror settings
.form-group
= f.label :mirror_max_delay, class: 'control-label col-sm-2' do
Maximum delay (Hours)
Maximum delay (Minutes)
.col-sm-10
= f.number_field :mirror_max_delay, class: 'form-control', min: 0
%span.help-block#mirror_max_delay_help_block
......
......@@ -81,6 +81,6 @@
%button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
%span.sr-only Toggle navigation
= 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'
.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 @@
= link_to toggle_star_project_path(@project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do
- if current_user.starred?(@project)
= icon('star')
%span.starred= _('Unstar')
%span.starred= _('Unstar')
- else
= icon('star-o')
%span= s_('StarProject|Star')
......
......@@ -8,7 +8,7 @@
.image
%span.wrap
.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)
- else
.image
......@@ -16,7 +16,7 @@
%span.wrap
.frame.deleted
%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
%span.meta-filesize= number_to_human_size(old_blob.size)
|
......@@ -28,7 +28,7 @@
%span.wrap
.frame.added
%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
%span.meta-filesize= number_to_human_size(blob.size)
|
......@@ -41,10 +41,10 @@
.swipe.view.hide
.swipe-frame
.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
.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.top-handle
%span.bottom-handle
......@@ -52,9 +52,9 @@
.onion-skin.view.hide
.onion-skin-frame
.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
%img{ src: blob_raw_path, alt: diff_file.new_path }
%img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
.controls
.transparent
.drag-track
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('how_to_merge')
#modal_merge_info.modal
.modal-dialog
.modal-content
......@@ -50,14 +53,3 @@
= succeed '.' do
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'
: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
- page_title 'Milestones'
- if show_new_nav?
- if show_new_nav? && can?(current_user, :admin_milestone, @project)
- content_for :breadcrumbs_extra do
= link_to "New milestone", new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New milestone'
......@@ -11,10 +11,10 @@
.top-area
= 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'
- 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
.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?
%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'
%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'
- if @project.feature_available?(:repository_mirrors)
= render 'projects/mirrors/pull'
= render 'projects/mirrors/push'
<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
fail_stuck_mirrors!
Project.mirrors_to_sync.each(&:import_schedule) unless Gitlab::Mirror.max_mirror_capacity_reached?
schedule_mirrors!
cancel_lease(lease_uuid)
end
......@@ -22,6 +22,32 @@ class UpdateAllMirrorsWorker
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
def try_obtain_lease
......@@ -31,4 +57,17 @@ class UpdateAllMirrorsWorker
def cancel_lease(uuid)
::Gitlab::ExclusiveLease.cancel(LEASE_KEY, uuid)
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
---
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:
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'] ||= {}
Settings.gitlab['webhook_timeout'] ||= 10
Settings.gitlab['max_attachment_size'] ||= 10
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_capacity_threshold'] ||= 15
Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil?
......
if Gitlab::Database::LoadBalancing.enable?
Gitlab::Database.disable_prepared_statements
# We need to run this initializer after migrations are done so it doesn't fail on CI
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|
config.middleware.use(Gitlab::Database::LoadBalancing::RackMiddleware)
end
Gitlab::Application.configure do |config|
config.middleware.use(Gitlab::Database::LoadBalancing::RackMiddleware)
end
Gitlab::Database::LoadBalancing.configure_proxy
Gitlab::Database::LoadBalancing.configure_proxy
end
end
......@@ -45,6 +45,7 @@ var config = {
groups_list: './groups_list.js',
issuable: './issuable/issuable_bundle.js',
issues: './issues/issues_bundle.js',
how_to_merge: './how_to_merge.js',
issue_show: './issue_show/index.js',
integrations: './integrations',
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 @@
#
# 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
enable_extension "plpgsql"
......@@ -137,7 +137,7 @@ ActiveRecord::Schema.define(version: 20170717150329) do
t.string "clientside_sentry_dsn"
t.boolean "prometheus_metrics_enabled", 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_capacity_threshold", default: 50, null: false
t.boolean "authorized_keys_enabled", default: true, null: false
......
......@@ -133,6 +133,55 @@ reviewee.
tomorrow. When you are not able to find the right balance, ask other people
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
Largely based on the [thoughtbot code review guide].
......
......@@ -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
`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
### Page-specific JavaScript
......
......@@ -40,6 +40,7 @@ do.
| `/weight <1-9>` | Set the weight of the issue |
| `/clear_weight` | Clears the issue weight |
| `/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`
support multiple assignees.
......@@ -114,7 +114,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end
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
step 'I click on image link' do
......
......@@ -118,7 +118,7 @@ module Banzai
end
if path
content_tag(:img, nil, src: path, class: 'gfm')
content_tag(:img, nil, data: { src: path }, class: 'gfm')
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
link = doc.document.create_element(
'a',
class: 'no-attachment-icon',
href: img['src'],
href: img['data-src'] || img['src'],
target: '_blank',
rel: 'noopener noreferrer'
)
......
......@@ -22,6 +22,7 @@ module Banzai
doc.css('img, video').each do |el|
process_link_attr el.attribute('src')
process_link_attr el.attribute('data-src')
end
doc
......
......@@ -16,6 +16,7 @@ module Banzai
Filter::MathFilter,
Filter::UploadLinkFilter,
Filter::VideoLinkFilter,
Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter,
Filter::EmojiFilter,
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