Commit 5049eb42 authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'origin/master' into ce-to-ee-2017-10-11

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parents edb14ef4 bd7ba308
...@@ -20,7 +20,9 @@ class GeoNodeStatus { ...@@ -20,7 +20,9 @@ class GeoNodeStatus {
this.$repositoriesSynced = $('.js-repositories-synced', this.$status); this.$repositoriesSynced = $('.js-repositories-synced', this.$status);
this.$repositoriesFailed = $('.js-repositories-failed', this.$status); this.$repositoriesFailed = $('.js-repositories-failed', this.$status);
this.$lfsObjectsSynced = $('.js-lfs-objects-synced', this.$status); this.$lfsObjectsSynced = $('.js-lfs-objects-synced', this.$status);
this.$lfsObjectsFailed = $('.js-lfs-objects-failed', this.$status);
this.$attachmentsSynced = $('.js-attachments-synced', this.$status); this.$attachmentsSynced = $('.js-attachments-synced', this.$status);
this.$attachmentsFailed = $('.js-attachments-failed', this.$status);
this.$lastEventSeen = $('.js-last-event-seen', this.$status); this.$lastEventSeen = $('.js-last-event-seen', this.$status);
this.$lastCursorEvent = $('.js-last-cursor-event', this.$status); this.$lastCursorEvent = $('.js-last-cursor-event', this.$status);
this.$health = $('.js-health', this.$status); this.$health = $('.js-health', this.$status);
...@@ -78,15 +80,21 @@ class GeoNodeStatus { ...@@ -78,15 +80,21 @@ class GeoNodeStatus {
status.lfs_objects_count, status.lfs_objects_count,
status.lfs_objects_synced_in_percentage); status.lfs_objects_synced_in_percentage);
const lfsFailedText = gl.text.addDelimiter(status.lfs_objects_failed_count);
const attachmentText = GeoNodeStatus.formatCountAndPercentage( const attachmentText = GeoNodeStatus.formatCountAndPercentage(
status.attachments_synced_count, status.attachments_synced_count,
status.attachments_count, status.attachments_count,
status.attachments_synced_in_percentage); status.attachments_synced_in_percentage);
const attachmentFailedText = gl.text.addDelimiter(status.attachments_failed_count);
this.$repositoriesSynced.text(repoText); this.$repositoriesSynced.text(repoText);
this.$repositoriesFailed.text(repoFailedText); this.$repositoriesFailed.text(repoFailedText);
this.$lfsObjectsSynced.text(lfsText); this.$lfsObjectsSynced.text(lfsText);
this.$lfsObjectsFailed.text(lfsFailedText);
this.$attachmentsSynced.text(attachmentText); this.$attachmentsSynced.text(attachmentText);
this.$attachmentsFailed.text(attachmentFailedText);
const eventDate = gl.utils.formatDate(new Date(status.last_event_date)); const eventDate = gl.utils.formatDate(new Date(status.last_event_date));
const cursorDate = gl.utils.formatDate(new Date(status.cursor_last_event_date)); const cursorDate = gl.utils.formatDate(new Date(status.cursor_last_event_date));
......
...@@ -285,7 +285,7 @@ import CreateLabelDropdown from './create_label'; ...@@ -285,7 +285,7 @@ import CreateLabelDropdown from './create_label';
}, },
hidden: function() { hidden: function() {
var isIssueIndex, isMRIndex, page, selectedLabels; var isIssueIndex, isMRIndex, page, selectedLabels;
page = $('body').data('page'); page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index'; isMRIndex = page === 'projects:merge_requests:index';
$selectbox.hide(); $selectbox.hide();
...@@ -325,7 +325,7 @@ import CreateLabelDropdown from './create_label'; ...@@ -325,7 +325,7 @@ import CreateLabelDropdown from './create_label';
$loading.fadeOut(); $loading.fadeOut();
}; };
page = $('body').data('page'); page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index'; isMRIndex = page === 'projects:merge_requests:index';
......
export const getPagePath = (index = 0) => $('body').data('page').split(':')[index]; export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
export const isInGroupsPage = () => getPagePath() === 'groups'; export const isInGroupsPage = () => getPagePath() === 'groups';
......
...@@ -170,7 +170,7 @@ import _ from 'underscore'; ...@@ -170,7 +170,7 @@ import _ from 'underscore';
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore; var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
if (!selected) return; if (!selected) return;
page = $('body').data('page'); page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
isSelecting = (selected.name !== selectedMilestone); isSelecting = (selected.name !== selectedMilestone);
......
...@@ -1257,7 +1257,7 @@ export default class Notes { ...@@ -1257,7 +1257,7 @@ export default class Notes {
} }
static checkMergeRequestStatus() { static checkMergeRequestStatus() {
if (getPagePath(1) === 'merge_requests') { if (getPagePath(1) === 'merge_requests' && gl.mrWidget) {
gl.mrWidget.checkStatus(); gl.mrWidget.checkStatus();
} }
} }
......
...@@ -424,7 +424,7 @@ function UsersSelect(currentUser, els) { ...@@ -424,7 +424,7 @@ function UsersSelect(currentUser, els) {
} }
var isIssueIndex, isMRIndex, page, selected; var isIssueIndex, isMRIndex, page, selected;
page = $('body').data('page'); page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
......
...@@ -132,22 +132,6 @@ ...@@ -132,22 +132,6 @@
} }
} }
.disabled-comment .issuable-note-warning {
border: none;
border-radius: $label-border-radius;
padding-top: $gl-vert-padding;
padding-bottom: $gl-vert-padding;
.icon svg {
position: relative;
top: 2px;
margin-right: $btn-xs-side-margin;
width: $gl-font-size;
height: $gl-font-size;
fill: $orange-600;
}
}
.sidebar-item-value { .sidebar-item-value {
.fa { .fa {
background-color: inherit; background-color: inherit;
......
...@@ -558,6 +558,17 @@ a.deploy-project-label { ...@@ -558,6 +558,17 @@ a.deploy-project-label {
} }
} }
.fork-thumbnail-container {
display: flex;
flex-wrap: wrap;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
> h5 {
width: 100%;
}
}
.project-template { .project-template {
> .form-group { > .form-group {
margin-bottom: 0; margin-bottom: 0;
...@@ -646,8 +657,15 @@ a.deploy-project-label { ...@@ -646,8 +657,15 @@ a.deploy-project-label {
margin-bottom: 0; margin-bottom: 0;
} }
.import-btn-container {
margin-bottom: 0;
}
.toggle-import-form {
padding-bottom: 10px;
}
.import-buttons { .import-buttons {
padding-left: 0;
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
-webkit-flex-wrap: wrap; -webkit-flex-wrap: wrap;
......
class Geo::FileRegistry < Geo::BaseRegistry class Geo::FileRegistry < Geo::BaseRegistry
scope :failed, -> { where(success: false) }
scope :synced, -> { where(success: true) }
end end
...@@ -100,7 +100,7 @@ class GeoNodeStatus ...@@ -100,7 +100,7 @@ class GeoNodeStatus
def lfs_objects_synced_count def lfs_objects_synced_count
@lfs_objects_synced_count ||= begin @lfs_objects_synced_count ||= begin
relation = Geo::FileRegistry.where(file_type: :lfs) relation = Geo::FileRegistry.synced.where(file_type: :lfs)
if Gitlab::Geo.current_node.restricted_project_ids if Gitlab::Geo.current_node.restricted_project_ids
relation = relation.where(file_id: lfs_objects.pluck(:id)) relation = relation.where(file_id: lfs_objects.pluck(:id))
...@@ -114,6 +114,14 @@ class GeoNodeStatus ...@@ -114,6 +114,14 @@ class GeoNodeStatus
@lfs_objects_synced_count = value.to_i @lfs_objects_synced_count = value.to_i
end end
def lfs_objects_failed_count
@lfs_objects_failed_count ||= Geo::FileRegistry.failed.where(file_type: :lfs).count
end
def lfs_objects_failed_count=(value)
@lfs_objects_failed_count = value.to_i
end
def lfs_objects_synced_in_percentage def lfs_objects_synced_in_percentage
sync_percentage(lfs_objects_count, lfs_objects_synced_count) sync_percentage(lfs_objects_count, lfs_objects_synced_count)
end end
...@@ -129,7 +137,7 @@ class GeoNodeStatus ...@@ -129,7 +137,7 @@ class GeoNodeStatus
def attachments_synced_count def attachments_synced_count
@attachments_synced_count ||= begin @attachments_synced_count ||= begin
upload_ids = attachments.pluck(:id) upload_ids = attachments.pluck(:id)
synced_ids = Geo::FileRegistry.where(file_type: [:attachment, :avatar, :file]).pluck(:file_id) synced_ids = Geo::FileRegistry.synced.where(file_type: Geo::FileService::DEFAULT_OBJECT_TYPES).pluck(:file_id)
(synced_ids & upload_ids).length (synced_ids & upload_ids).length
end end
...@@ -139,6 +147,14 @@ class GeoNodeStatus ...@@ -139,6 +147,14 @@ class GeoNodeStatus
@attachments_synced_count = value.to_i @attachments_synced_count = value.to_i
end end
def attachments_failed_count
@attachments_failed_count ||= Geo::FileRegistry.failed.where(file_type: Geo::FileService::DEFAULT_OBJECT_TYPES).count
end
def attachments_failed_count=(value)
@attachments_failed_count = value.to_i
end
def attachments_synced_in_percentage def attachments_synced_in_percentage
sync_percentage(attachments_count, attachments_synced_count) sync_percentage(attachments_count, attachments_synced_count)
end end
......
...@@ -10,6 +10,7 @@ class GeoNodeStatusEntity < Grape::Entity ...@@ -10,6 +10,7 @@ class GeoNodeStatusEntity < Grape::Entity
expose :attachments_count expose :attachments_count
expose :attachments_synced_count expose :attachments_synced_count
expose :attachments_failed_count
expose :attachments_synced_in_percentage do |node| expose :attachments_synced_in_percentage do |node|
number_to_percentage(node.attachments_synced_in_percentage, precision: 2) number_to_percentage(node.attachments_synced_in_percentage, precision: 2)
end end
...@@ -18,6 +19,7 @@ class GeoNodeStatusEntity < Grape::Entity ...@@ -18,6 +19,7 @@ class GeoNodeStatusEntity < Grape::Entity
expose :lfs_objects_count expose :lfs_objects_count
expose :lfs_objects_synced_count expose :lfs_objects_synced_count
expose :lfs_objects_failed_count
expose :lfs_objects_synced_in_percentage do |node| expose :lfs_objects_synced_in_percentage do |node|
number_to_percentage(node.lfs_objects_synced_in_percentage, precision: 2) number_to_percentage(node.lfs_objects_synced_in_percentage, precision: 2)
end end
......
...@@ -11,7 +11,7 @@ module Geo ...@@ -11,7 +11,7 @@ module Geo
success: success, success: success,
bytes_downloaded: bytes_downloaded, bytes_downloaded: bytes_downloaded,
download_time_s: (Time.now - start_time).to_f.round(3)) download_time_s: (Time.now - start_time).to_f.round(3))
update_registry(bytes_downloaded) if success update_registry(bytes_downloaded, success: success)
end end
end end
...@@ -37,13 +37,14 @@ module Geo ...@@ -37,13 +37,14 @@ module Geo
end end
end end
def update_registry(bytes_downloaded) def update_registry(bytes_downloaded, success:)
transfer = Geo::FileRegistry.find_or_initialize_by( transfer = Geo::FileRegistry.find_or_initialize_by(
file_type: object_type, file_type: object_type,
file_id: object_db_id file_id: object_db_id
) )
transfer.bytes = bytes_downloaded transfer.bytes = bytes_downloaded
transfer.success = success
transfer.save transfer.save
end end
......
...@@ -62,10 +62,18 @@ ...@@ -62,10 +62,18 @@
%span.help-block %span.help-block
LFS objects synced: LFS objects synced:
%strong.node-info.js-lfs-objects-synced %strong.node-info.js-lfs-objects-synced
%p
%span.help-block
LFS objects failed:
%strong.node-info.js-lfs-objects-failed
%p %p
%span.help-block %span.help-block
Attachments synced: Attachments synced:
%strong.node-info.js-attachments-synced %strong.node-info.js-attachments-synced
%p
%span.help-block
Attachments failed:
%strong.node-info.js-attachments-failed
%p %p
.advanced-geo-node-status-container .advanced-geo-node-status-container
.advanced-status.hidden .advanced-status.hidden
......
...@@ -48,52 +48,54 @@ ...@@ -48,52 +48,54 @@
.tab-pane.import-project-pane{ id: 'import-project-pane', role: 'tabpanel' } .tab-pane.import-project-pane{ id: 'import-project-pane', role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f| = form_for @project, html: { class: 'new_project' } do |f|
- if import_sources_enabled? - if import_sources_enabled?
.project-import .project-import.row
.form-group.clearfix .col-sm-12
= f.label :visibility_level, class: 'label-light' do #the label here seems wrong .form-group.import-btn-container.clearfix
Import project from = f.label :visibility_level, class: 'label-light' do #the label here seems wrong
.col-sm-12.import-buttons Import project from
%div .import-buttons
- if github_import_enabled? %div
= link_to new_import_github_path, class: 'btn import_github' do - if github_import_enabled?
= icon('github', text: 'GitHub') = link_to new_import_github_path, class: 'btn import_github' do
%div = icon('github', text: 'GitHub')
- if bitbucket_import_enabled? %div
= link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do - if bitbucket_import_enabled?
= icon('bitbucket', text: 'Bitbucket') = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
- unless bitbucket_import_configured? = icon('bitbucket', text: 'Bitbucket')
= render 'bitbucket_import_modal' - unless bitbucket_import_configured?
%div = render 'bitbucket_import_modal'
- if gitlab_import_enabled? %div
= link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do - if gitlab_import_enabled?
= icon('gitlab', text: 'GitLab.com') = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
- unless gitlab_import_configured? = icon('gitlab', text: 'GitLab.com')
= render 'gitlab_import_modal' - unless gitlab_import_configured?
%div = render 'gitlab_import_modal'
- if google_code_import_enabled? %div
= link_to new_import_google_code_path, class: 'btn import_google_code' do - if google_code_import_enabled?
= icon('google', text: 'Google Code') = link_to new_import_google_code_path, class: 'btn import_google_code' do
%div = icon('google', text: 'Google Code')
- if fogbugz_import_enabled? %div
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do - if fogbugz_import_enabled?
= icon('bug', text: 'Fogbugz') = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
%div = icon('bug', text: 'Fogbugz')
- if gitea_import_enabled? %div
= link_to new_import_gitea_url, class: 'btn import_gitea' do - if gitea_import_enabled?
= custom_icon('go_logo') = link_to new_import_gitea_url, class: 'btn import_gitea' do
Gitea = custom_icon('go_logo')
%div Gitea
- if git_import_enabled? %div
%button.btn.js-toggle-button.import_git{ type: "button" } - if git_import_enabled?
= icon('git', text: 'Repo by URL') %button.btn.js-toggle-button.import_git{ type: "button" }
- if gitlab_project_import_enabled? = icon('git', text: 'Repo by URL')
.import_gitlab_project.has-tooltip{ data: { container: 'body' } } - if gitlab_project_import_enabled?
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
= icon('gitlab', text: 'GitLab export') = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
.col-lg-12 = icon('gitlab', text: 'GitLab export')
.js-toggle-content.hide .col-lg-12
%hr .js-toggle-content.hide.toggle-import-form
= render "shared/import_form", f: f %hr
= render "shared/import_form", f: f
= render 'new_project_fields', f: f, project_name_id: "import-url-name"
.save-project-loader.hide .save-project-loader.hide
.center .center
......
...@@ -18,8 +18,9 @@ ...@@ -18,8 +18,9 @@
.light .light
- if can?(current_user, :admin_project_member, @project) - if can?(current_user, :admin_project_member, @project)
%ul.nav-links.gitlab-tabs{ role: 'tablist' } %ul.nav-links.gitlab-tabs{ role: 'tablist' }
%li.active{ role: 'presentation' } - if !membership_locked?
%a{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member %li.active{ role: 'presentation' }
%a{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member
- if @project.allowed_to_share_with_group? - if @project.allowed_to_share_with_group?
%li{ role: 'presentation', class: ('active' if membership_locked?) } %li{ role: 'presentation', class: ('active' if membership_locked?) }
%a{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group %a{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group
......
...@@ -9,33 +9,47 @@ module Geo ...@@ -9,33 +9,47 @@ module Geo
end end
def load_pending_resources def load_pending_resources
unsynced = find_unsynced_objects
failed = find_failed_objects
interleave(unsynced, failed)
end
def find_unsynced_objects
lfs_object_ids = find_lfs_object_ids lfs_object_ids = find_lfs_object_ids
objects_ids = find_object_ids objects_ids = find_object_ids
interleave(lfs_object_ids, objects_ids) interleave(lfs_object_ids, objects_ids)
end end
def find_object_ids def find_failed_objects
downloaded_ids = find_downloaded_ids(Geo::FileService::DEFAULT_OBJECT_TYPES) Geo::FileRegistry
.failed
.limit(db_retrieve_batch_size)
.pluck(:file_id, :file_type)
end
unsynched_downloads = filter_downloaded_ids( def find_object_ids
current_node.uploads, downloaded_ids, Upload.table_name) unsynced_downloads = filter_registry_ids(
current_node.uploads,
Geo::FileService::DEFAULT_OBJECT_TYPES,
Upload.table_name
)
unsynched_downloads unsynced_downloads
.order(created_at: :desc)
.limit(db_retrieve_batch_size) .limit(db_retrieve_batch_size)
.pluck(:id, :uploader) .pluck(:id, :uploader)
.map { |id, uploader| [id, uploader.sub(/Uploader\z/, '').underscore] } .map { |id, uploader| [id, uploader.sub(/Uploader\z/, '').underscore] }
end end
def find_lfs_object_ids def find_lfs_object_ids
downloaded_ids = find_downloaded_ids([:lfs]) unsynced_downloads = filter_registry_ids(
current_node.lfs_objects,
[:lfs],
LfsObject.table_name
)
unsynched_downloads = filter_downloaded_ids( unsynced_downloads
current_node.lfs_objects, downloaded_ids, LfsObject.table_name)
unsynched_downloads
.order(created_at: :desc)
.limit(db_retrieve_batch_size) .limit(db_retrieve_batch_size)
.pluck(:id) .pluck(:id)
.map { |id| [id, :lfs] } .map { |id| [id, :lfs] }
...@@ -45,12 +59,14 @@ module Geo ...@@ -45,12 +59,14 @@ module Geo
# plucks a list of file IDs from one into the other. This will not scale # plucks a list of file IDs from one into the other. This will not scale
# well with the number of synchronized files--the query will increase # well with the number of synchronized files--the query will increase
# linearly in size--so this should be replaced with postgres_fdw ASAP. # linearly in size--so this should be replaced with postgres_fdw ASAP.
def filter_downloaded_ids(objects, downloaded_ids, table_name) def filter_registry_ids(objects, file_types, table_name)
return objects if downloaded_ids.empty? registry_ids = pluck_registry_ids(Geo::FileRegistry, file_types)
return objects if registry_ids.empty?
joined_relation = objects.joins(<<~SQL) joined_relation = objects.joins(<<~SQL)
LEFT OUTER JOIN LEFT OUTER JOIN
(VALUES #{downloaded_ids.map { |id| "(#{id}, 't')" }.join(',')}) (VALUES #{registry_ids.map { |id| "(#{id}, 't')" }.join(',')})
file_registry(file_id, registry_present) file_registry(file_id, registry_present)
ON #{table_name}.id = file_registry.file_id ON #{table_name}.id = file_registry.file_id
SQL SQL
...@@ -58,9 +74,9 @@ module Geo ...@@ -58,9 +74,9 @@ module Geo
joined_relation.where(file_registry: { registry_present: [nil, false] }) joined_relation.where(file_registry: { registry_present: [nil, false] })
end end
def find_downloaded_ids(file_types) def pluck_registry_ids(relation, file_types)
downloaded_ids = Geo::FileRegistry.where(file_type: file_types).pluck(:file_id) ids = relation.where(file_type: file_types).pluck(:file_id)
(downloaded_ids + scheduled_file_ids(file_types)).uniq (ids + scheduled_file_ids(file_types)).uniq
end end
def scheduled_file_ids(types) def scheduled_file_ids(types)
......
---
title: Prevent failed file syncs from stalling Geo backfill
merge_request: 3101
author:
type: fixed
---
title: Move group boards routes under - and remove "boards" from reserved paths
merge_request:
author:
type: fixed
---
title: Cleanup data-page attribute after each Karma test
merge_request: 14742
author:
type: fixed
...@@ -4,84 +4,87 @@ resources :groups, only: [:index, :new, :create] do ...@@ -4,84 +4,87 @@ resources :groups, only: [:index, :new, :create] do
post :preview_markdown post :preview_markdown
end end
scope(path: 'groups/*group_id', constraints(GroupUrlConstrainer.new) do
module: :groups, scope(path: 'groups/*id',
as: :group, controller: :groups,
constraints: { group_id: Gitlab::PathRegex.full_namespace_route_regex }) do constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ }) do
get :edit, as: :edit_group
## EE-specific get :issues, as: :issues_group
resource :analytics, only: [:show] get :merge_requests, as: :merge_requests_group
resource :ldap, only: [] do get :projects, as: :projects_group
member do get :activity, as: :activity_group
put :sync get :subgroups, as: :subgroups_group
end get '/', action: :show, as: :group_canonical
end end
resources :ldap_group_links, only: [:index, :create, :destroy] scope(path: 'groups/*group_id',
## EE-specific module: :groups,
as: :group,
resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do constraints: { group_id: Gitlab::PathRegex.full_namespace_route_regex }) do
post :resend_invite, on: :member
delete :leave, on: :collection
## EE-specific ## EE-specific
patch :override, on: :member resource :analytics, only: [:show]
resource :ldap, only: [] do
member do
put :sync
end
end
resources :ldap_group_links, only: [:index, :create, :destroy]
## EE-specific ## EE-specific
end
resource :avatar, only: [:destroy] resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :edit, :update, :new, :create] do post :resend_invite, on: :member
member do delete :leave, on: :collection
get :merge_requests
get :participants
get :labels
end
end
## EE-specific ## EE-specific
resource :notification_setting, only: [:update] patch :override, on: :member
resources :audit_events, only: [:index] ## EE-specific
resources :pipeline_quota, only: [:index] end
## EE-specific
## EE-specific resource :avatar, only: [:destroy]
resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :edit, :update, :new, :create] do
member do member do
get :test get :merge_requests
get :participants
get :labels
end
end end
end
## EE-specific
resources :labels, except: [:show] do ## EE-specific
post :toggle_subscription, on: :member resource :notification_setting, only: [:update]
end resources :audit_events, only: [:index]
resources :pipeline_quota, only: [:index]
## EE-specific
scope path: '-' do ## EE-specific
namespace :settings do resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
resource :ci_cd, only: [:show], controller: 'ci_cd' member do
get :test
end
end end
## EE-specific
resources :variables, only: [:index, :show, :update, :create, :destroy] resources :labels, except: [:show] do
resources :billings, only: [:index] post :toggle_subscription, on: :member
end end
## EE-specific scope path: '-' do
resources :boards, only: [:index, :show, :create, :update, :destroy] namespace :settings do
end resource :ci_cd, only: [:show], controller: 'ci_cd'
end
scope(path: 'groups/*id', resources :variables, only: [:index, :show, :update, :create, :destroy]
controller: :groups, resources :billings, only: [:index]
constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ }) do
get :edit, as: :edit_group ## EE-specific
get :issues, as: :issues_group resources :boards, only: [:index, :show, :create, :update, :destroy]
get :merge_requests, as: :merge_requests_group end
get :projects, as: :projects_group
get :activity, as: :activity_group ## EE-specific
get :subgroups, as: :subgroups_group get :boards, to: redirect('/groups/%{group_id}/-/boards')
get '/', action: :show, as: :group_canonical end
end
constraints(GroupUrlConstrainer.new) do
scope(path: '*id', scope(path: '*id',
as: :group, as: :group,
constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ }, constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ },
......
class AddFileRegistrySuccess < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
# Ensure existing rows are recorded as successes
add_column_with_default :file_registry, :success, :boolean, default: true, allow_null: false
change_column :file_registry, :success, :boolean, default: false
end
def down
# Prevent failures from being converted into successes
false_value = Arel::Nodes::False.new.to_sql(Geo::BaseRegistry)
connection.execute("DELETE FROM file_registry WHERE success = #{false_value}")
remove_column :file_registry, :success
end
end
class AddFileRegistrySuccessIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :file_registry, :success
end
def down
remove_concurrent_index :file_registry, :success
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: 20171005045404) do ActiveRecord::Schema.define(version: 20171009162209) 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"
...@@ -25,10 +25,12 @@ ActiveRecord::Schema.define(version: 20171005045404) do ...@@ -25,10 +25,12 @@ ActiveRecord::Schema.define(version: 20171005045404) do
t.integer "bytes", limit: 8 t.integer "bytes", limit: 8
t.string "sha256" t.string "sha256"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.boolean "success", default: false, null: false
end end
add_index "file_registry", ["file_type", "file_id"], name: "index_file_registry_on_file_type_and_file_id", unique: true, using: :btree add_index "file_registry", ["file_type", "file_id"], name: "index_file_registry_on_file_type_and_file_id", unique: true, using: :btree
add_index "file_registry", ["file_type"], name: "index_file_registry_on_file_type", using: :btree add_index "file_registry", ["file_type"], name: "index_file_registry_on_file_type", using: :btree
add_index "file_registry", ["success"], name: "index_file_registry_on_success", using: :btree
create_table "project_registry", force: :cascade do |t| create_table "project_registry", force: :cascade do |t|
t.integer "project_id", null: false t.integer "project_id", null: false
......
...@@ -22,7 +22,6 @@ ...@@ -22,7 +22,6 @@
## Backend guides ## Backend guides
- [Testing standards and style guidelines](testing_guide/index.md)
- [API styleguide](api_styleguide.md) Use this styleguide if you are - [API styleguide](api_styleguide.md) Use this styleguide if you are
contributing to the API. contributing to the API.
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers - [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
...@@ -68,6 +67,11 @@ ...@@ -68,6 +67,11 @@
- [Ordering table columns](ordering_table_columns.md) - [Ordering table columns](ordering_table_columns.md)
- [Verifying database capabilities](verifying_database_capabilities.md) - [Verifying database capabilities](verifying_database_capabilities.md)
## Testing guides
- [Testing standards and style guidelines](testing_guide/index.md)
- [Frontend testing standards and style guidelines](testing_guide/frontend_testing.md)
## Documentation guides ## Documentation guides
- [Documentation styleguide](doc_styleguide.md): Use this styleguide if you are - [Documentation styleguide](doc_styleguide.md): Use this styleguide if you are
......
...@@ -84,6 +84,7 @@ test should be re-implemented using RSpec instead. ...@@ -84,6 +84,7 @@ test should be re-implemented using RSpec instead.
[^1]: /ci/yaml/README.html#dependencies [^1]: /ci/yaml/README.html#dependencies
[rails]: http://rubyonrails.org/
[RSpec]: https://github.com/rspec/rspec-rails#feature-specs [RSpec]: https://github.com/rspec/rspec-rails#feature-specs
[Capybara]: https://github.com/teamcapybara/capybara [Capybara]: https://github.com/teamcapybara/capybara
[Karma]: http://karma-runner.github.io/ [Karma]: http://karma-runner.github.io/
......
# Google OAuth2 OmniAuth Provider # Google OAuth2 OmniAuth Provider
To enable the Google OAuth2 OmniAuth provider you must register your application with Google. Google will generate a client ID and secret key for you to use. To enable the Google OAuth2 OmniAuth provider you must register your application
with Google. Google will generate a client ID and secret key for you to use.
1. Sign in to the [Google Developers Console](https://console.developers.google.com/) with the Google account you want to use to register GitLab.
## Enabling Google OAuth
1. Select "Create Project".
In Google's side:
1. Provide the project information
- Project name: 'GitLab' works just fine here. 1. Navigate to the [cloud resource manager](https://console.cloud.google.com/cloud-resource-manager) page
- Project ID: Must be unique to all Google Developer registered applications. Google provides a randomly generated Project ID by default. You can use the randomly generated ID or choose a new one. 1. Select **Create Project**
1. Refresh the page. You should now see your new project in the list. Click on the project. 1. Provide the project information:
- **Project name** - "GitLab" works just fine here.
1. Select the "Google APIs" tab in the Overview. - **Project ID** - Must be unique to all Google Developer registered applications.
Google provides a randomly generated Project ID by default. You can use
1. Select and enable the following Google APIs - listed under "Popular APIs" the randomly generated ID or choose a new one.
- Enable `Contacts API` 1. Refresh the page and you should see your new project in the list
- Enable `Google+ API` 1. Go to the [Google API Console](https://console.developers.google.com/apis/dashboard)
1. Select the previously created project form the upper left corner
1. Select **Credentials** from the sidebar
1. Select **OAuth consent screen** and fill the form with the required information
1. In the **Credentials** tab, select **Create credentials > OAuth client ID**
1. Fill in the required information
- **Application type** - Choose "Web Application"
- **Name** - Use the default one or provide your own
- **Authorized JavaScript origins** -This isn't really used by GitLab but go
ahead and put `https://gitlab.example.com`
- **Authorized redirect URIs** - Enter your domain name followed by the
callback URIs one at a time:
1. Select "Credentials" in the submenu. ```
https://gitlab.example.com/users/auth/google_oauth2/callback
https://gitlab.exampl.com/-/google_api/auth/callback
```
1. Select "Create New Client ID". 1. You should now be able to see a Client ID and Client secret. Note them down
or keep this page open as you will need them later.
1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Google Cloud APIs > Container Engine API > Enable**
1. Fill in the required information On your GitLab server:
- Application type: "Web Application"
- Authorized JavaScript origins: This isn't really used by GitLab but go ahead and put 'https://gitlab.example.com' here.
- Authorized redirect URI: 'https://gitlab.example.com/users/auth/google_oauth2/callback'
1. Under the heading "Client ID for web application" you should see a Client ID and Client secret (see screenshot). Keep this page open as you continue configuration. ![Google app](img/google_app.png)
1. On your GitLab server, open the configuration file. 1. Open the configuration file.
For omnibus package: For Omnibus GitLab:
```sh ```sh
sudo editor /etc/gitlab/gitlab.rb sudo editor /etc/gitlab/gitlab.rb
``` ```
For installations from source: For installations from source:
```sh ```sh
cd /home/git/gitlab cd /home/git/gitlab
sudo -u git -H editor config/gitlab.yml
sudo -u git -H editor config/gitlab.yml
``` ```
1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. 1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
1. Add the provider configuration:
1. Add the provider configuration:
For omnibus package: For Omnibus GitLab:
```ruby ```ruby
gitlab_rails['omniauth_providers'] = [ gitlab_rails['omniauth_providers'] = [
{ {
"name" => "google_oauth2", "name" => "google_oauth2",
"app_id" => "YOUR_APP_ID", "app_id" => "YOUR_APP_ID",
"app_secret" => "YOUR_APP_SECRET", "app_secret" => "YOUR_APP_SECRET",
"args" => { "access_type" => "offline", "approval_prompt" => '' } "args" => { "access_type" => "offline", "approval_prompt" => '' }
} }
] ]
``` ```
For installations from source: For installations from source:
``` ```
- { name: 'google_oauth2', app_id: 'YOUR_APP_ID', - { name: 'google_oauth2', app_id: 'YOUR_APP_ID',
app_secret: 'YOUR_APP_SECRET', app_secret: 'YOUR_APP_SECRET',
args: { access_type: 'offline', approval_prompt: '' } } args: { access_type: 'offline', approval_prompt: '' } }
``` ```
1. Change 'YOUR_APP_ID' to the client ID from the Google Developer page from step 10. 1. Change `YOUR_APP_ID` to the client ID from the Google Developer page
1. Similarly, change `YOUR_APP_SECRET` to the client secret
1. Change 'YOUR_APP_SECRET' to the client secret from the Google Developer page from step 10. 1. Make sure that you configure GitLab to use an FQDN as Google will not accept
raw IP addresses.
1. Make sure that you configure GitLab to use an FQDN as Google will not accept raw IP addresses.
For Omnibus packages: For Omnibus packages:
```ruby ```ruby
external_url 'https://gitlab.example.com' external_url 'https://gitlab.example.com'
``` ```
For installations from source: For installations from source:
...@@ -88,21 +97,32 @@ To enable the Google OAuth2 OmniAuth provider you must register your application ...@@ -88,21 +97,32 @@ To enable the Google OAuth2 OmniAuth provider you must register your application
``` ```
1. Save the configuration file. 1. Save the configuration file.
1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you 1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
installed GitLab via Omnibus or from source respectively. installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a Google icon below the regular sign in form. Click the icon to begin the authentication process. Google will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. On the sign in page there should now be a Google icon below the regular sign in
form. Click the icon to begin the authentication process. Google will ask the
user to sign in and authorize the GitLab application. If everything goes well
the user will be returned to GitLab and will be signed in.
## Further Configuration ## Further Configuration
This further configuration is not required for Google authentication to function but it is strongly recommended. Taking these steps will increase usability for users by providing a little more recognition and branding. This further configuration is not required for Google authentication to function
but it is strongly recommended. Taking these steps will increase usability for
At this point, when users first try to authenticate to your GitLab installation with Google they will see a generic application name on the prompt screen. The prompt informs the user that "Project Default Service Account" would like to access their account. "Project Default Service Account" isn't very recognizable and may confuse or cause users to be concerned. This is easily changeable. users by providing a little more recognition and branding.
1. Select 'Consent screen' in the left menu. (See steps 1, 4 and 5 above for instructions on how to get here if you closed your window). At this point, when users first try to authenticate to your GitLab installation
1. Scroll down until you find "Product Name". Change the product name to something more descriptive. with Google they will see a generic application name on the prompt screen. The
1. Add any additional information as you wish - homepage, logo, privacy policy, etc. None of this is required, but it may help your users. prompt informs the user that "Project Default Service Account" would like to
access their account. "Project Default Service Account" isn't very recognizable
and may confuse or cause users to be concerned. This is easily changeable:
1. Select 'Consent screen' in the left menu. (See steps 1, 4 and 5 above for
instructions on how to get here if you closed your window).
1. Scroll down until you find "Product Name". Change the product name to
something more descriptive.
1. Add any additional information as you wish - homepage, logo, privacy policy,
etc. None of this is required, but it may help your users.
[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source [restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
# From Community Edition 10.1 to Enterprise Edition 10.1
This guide assumes you have a correctly configured and tested installation of
GitLab Community Edition 10.1. If you run into any trouble or if you have any
questions please contact us at [support@gitlab.com].
### 0. Backup
Make a backup just in case something goes wrong:
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
For installations using MySQL, this may require granting "LOCK TABLES"
privileges to the GitLab user on the database version.
### 1. Stop server
```bash
sudo service gitlab stop
```
### 2. Get the EE code
```bash
cd /home/git/gitlab
sudo -u git -H git remote add -f ee https://gitlab.com/gitlab-org/gitlab-ee.git
sudo -u git -H git checkout 10-1-stable-ee
```
### 3. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
# MySQL installations (note: the line below states '--without postgres')
sudo -u git -H bundle install --without postgres development test --deployment
# PostgreSQL installations (note: the line below states '--without mysql')
sudo -u git -H bundle install --without mysql development test --deployment
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
# Clean up assets and cache
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
### 4. Start application
```bash
sudo service gitlab start
sudo service nginx restart
```
### 5. Check application status
Check if GitLab and its environment are configured correctly:
```bash
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
```
To make sure you didn't miss anything run a more thorough check with:
```bash
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
```
If all items are green, then congratulations upgrade complete!
## Things went south? Revert to previous version (Community Edition 10.1)
### 1. Revert the code to the previous version
```bash
cd /home/git/gitlab
sudo -u git -H git checkout 10-1-stable
```
### 2. Restore from the backup
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
[support@gitlab.com]: mailto:support@gitlab.com
...@@ -190,6 +190,43 @@ load and will have a corresponding badge counter to match the counter on the ima ...@@ -190,6 +190,43 @@ load and will have a corresponding badge counter to match the counter on the ima
![Image resolved discussion](img/image_resolved_discussion.png) ![Image resolved discussion](img/image_resolved_discussion.png)
## Locking discussions
> [Introduced][ce-14531] in GitLab 10.1.
Sometimes a discussion is revolved around an image. With image discussions,
you can easily target a specific coordinate of an image and start a discussion
around it. Image discussions are available in merge requests and commit detail views.
To start an image discussion, hover your mouse over the image. Your mouse pointer
should convert into an icon, indicating that the image is available for commenting.
Simply click anywhere on the image to create a new discussion.
![Start image discussion](img/start_image_discussion.gif)
After you click on the image, a comment form will be displayed that would be the start
of your discussion. Once you save your comment, you will see a new badge displayed on
top of your image. This badge represents your discussion.
>**Note:**
This discussion badge is typically associated with a number that is only used as a visual
reference for each discussion. In the merge request discussion tab,
this badge will be indicated with a comment icon since each discussion will render a new
image section.
Image discussions also work on diffs that replace an existing image. In this diff view
mode, you can toggle the different view modes and still see the discussion point badges.
| 2-up | Swipe | Onion Skin |
| :-----------: | :----------: | :----------: |
| ![2-up view](img/two_up_view.png) | ![swipe view](img/swipe_view.png) | ![onion skin view](img/onion_skin_view.png) |
Image discussions also work well with resolvable discussions. Resolved discussions
on diffs (not on the merge request discussion tab) will appear collapsed on page
load and will have a corresponding badge counter to match the counter on the image.
![Image resolved discussion](img/image_resolved_discussion.png)
## Lock discussions ## Lock discussions
> [Introduced][ce-14531] in GitLab 10.1. > [Introduced][ce-14531] in GitLab 10.1.
......
...@@ -78,6 +78,7 @@ The following table depicts the various user permission levels in a project. ...@@ -78,6 +78,7 @@ The following table depicts the various user permission levels in a project.
| Force push to protected branches [^4] | | | | | | | Force push to protected branches [^4] | | | | | |
| Remove protected branches [^4] | | | | | | | Remove protected branches [^4] | | | | | |
| Remove pages | | | | | ✓ | | Remove pages | | | | | ✓ |
| Manage clusters | | | | ✓ | ✓ |
## Project features permissions ## Project features permissions
......
# Connecting GitLab with GKE
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/35954) in 10.1.
CAUTION: **Warning:**
The Cluster integration is currently in **Beta**.
Connect your project to Google Container Engine (GKE) in a few steps.
With a cluster associated to your project, you can use Review Apps, deploy your
applications, run your pipelines, and much more in an easy way.
NOTE: **Note:**
The Cluster integration will eventually supersede the
[Kubernetes integration](../integrations/kubernetes.md). For the moment,
you can create only one cluster.
## Prerequisites
In order to be able to manage your GKE cluster through GitLab, the following
prerequisites must be met:
- The [Google authentication integration](../../../integration/google.md) must
be enabled in GitLab at the instance level. If that's not the case, ask your
administrator to enable it.
- Your associated Google account must have the right privileges to manage
clusters on GKE. That would mean that a
[billing account](https://cloud.google.com/billing/docs/how-to/manage-billing-account)
must be set up.
- You must have Master [permissions] in order to be able to access the **Cluster**
page.
If all of the above requirements are met, you can proceed to add a new cluster.
## Adding a cluster
NOTE: **Note:**
You need Master [permissions] and above to add a cluster.
To add a new cluster:
1. Navigate to your project's **CI/CD > Cluster** page.
1. Connect your Google account if you haven't done already by clicking the
"Sign-in with Google" button.
1. Fill in the requested values:
- **Cluster name** (required) - The name you wish to give the cluster.
- **GCP project ID** (required) - The ID of the project you created in your GCP
console that will host the Kubernetes cluster. This must **not** be confused
with the project name. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects).
- **Zone** - The zone under which the cluster will be created. Read more about
[the available zones](https://cloud.google.com/compute/docs/regions-zones/).
- **Number of nodes** - The number of nodes you wish the cluster to have.
- **Machine type** - The machine type of the Virtual Machine instance that
the cluster will be based on. Read more about [the available machine types](https://cloud.google.com/compute/docs/machine-types).
- **Project namespace** - The unique namespace for this project. By default you
don't have to fill it in; by leaving it blank, GitLab will create one for you.
1. Click the **Create cluster** button.
After a few moments your cluster should be created. If something goes wrong,
you will be notified.
Now, you can proceed to [enable the Cluster integration](#enabling-or-disabling-the-cluster-integration).
## Enabling or disabling the Cluster integration
After you have successfully added your cluster information, you can enable the
Cluster integration:
1. Click the "Enabled/Disabled" switch
1. Hit **Save** for the changes to take effect
You can now start using your Kubernetes cluster for your deployments.
To disable the Cluster integration, follow the same procedure.
## Removing the Cluster integration
NOTE: **Note:**
You need Master [permissions] and above to remove a cluster integration.
NOTE: **Note:**
When you remove a cluster, you only remove its relation to GitLab, not the
cluster itself. To remove the cluster, you can do so by visiting the GKE
dashboard or using `kubectl`.
To remove the Cluster integration from your project, simply click on the
**Remove integration** button. You will then be able to follow the procedure
and [add a cluster](#adding-a-cluster) again.
[permissions]: ../../permissions.md
...@@ -63,6 +63,8 @@ common actions on issues or merge requests ...@@ -63,6 +63,8 @@ common actions on issues or merge requests
browse, and download job artifacts browse, and download job artifacts
- [Pipeline settings](pipelines/settings.md): Set up Git strategy (choose the default way your repository is fetched from GitLab in a job), - [Pipeline settings](pipelines/settings.md): Set up Git strategy (choose the default way your repository is fetched from GitLab in a job),
timeout (defines the maximum amount of time in minutes that a job is able run), custom path for `.gitlab-ci.yml`, test coverage parsing, pipeline's visibility, and much more timeout (defines the maximum amount of time in minutes that a job is able run), custom path for `.gitlab-ci.yml`, test coverage parsing, pipeline's visibility, and much more
- [GKE cluster integration](clusters/index.md): Connecting your GitLab project
with Google Container Engine
- [GitLab Pages](pages/index.md): Build, test, and deploy your static - [GitLab Pages](pages/index.md): Build, test, and deploy your static
website with GitLab Pages website with GitLab Pages
......
# Reducing the repository size using Git # Reducing the repository size using Git
A GitLab Entrerprise Edition administrator can set a [repository size limit][admin-repo-size] A GitLab Enterprise Edition administrator can set a [repository size limit][admin-repo-size]
which will prevent you to exceed it. which will prevent you to exceed it.
When a project has reached its size limit, you will not be able to push to it, When a project has reached its size limit, you will not be able to push to it,
......
...@@ -1018,8 +1018,10 @@ module API ...@@ -1018,8 +1018,10 @@ module API
expose :repositories_failed_count expose :repositories_failed_count
expose :lfs_objects_count expose :lfs_objects_count
expose :lfs_objects_synced_count expose :lfs_objects_synced_count
expose :lfs_objects_failed_count
expose :attachments_count expose :attachments_count
expose :attachments_synced_count expose :attachments_synced_count
expose :attachments_failed_count
expose :last_event_id expose :last_event_id
expose :last_event_date expose :last_event_date
expose :cursor_last_event_id expose :cursor_last_event_id
......
...@@ -119,7 +119,6 @@ module Gitlab ...@@ -119,7 +119,6 @@ module Gitlab
analytics analytics
audit_events audit_events
avatar avatar
boards
edit edit
group_members group_members
hooks hooks
......
...@@ -25,9 +25,9 @@ module Gitlab ...@@ -25,9 +25,9 @@ module Gitlab
end end
TEMPLATES_TABLE = [ TEMPLATES_TABLE = [
ProjectTemplate.new('rails', 'Ruby on Rails', 'Includes a MVC structure, gemfile, rakefile, and .gitlab-ci.yml file, along with many others, to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/rails'), ProjectTemplate.new('rails', 'Ruby on Rails', 'Includes an MVC structure, gemfile, rakefile, and .gitlab-ci.yml file, along with many others, to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/rails'),
ProjectTemplate.new('spring', 'Spring', 'Includes a MVC structure, mvnw, pom.xml, and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/spring'), ProjectTemplate.new('spring', 'Spring', 'Includes an MVC structure, mvnw, pom.xml, and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/spring'),
ProjectTemplate.new('express', 'NodeJS Express', 'Includes a MVC structure, and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/express') ProjectTemplate.new('express', 'NodeJS Express', 'Includes an MVC structure and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/express')
].freeze ].freeze
class << self class << self
......
...@@ -264,8 +264,10 @@ describe Admin::GeoNodesController, :postgresql do ...@@ -264,8 +264,10 @@ describe Admin::GeoNodesController, :postgresql do
id: 1, id: 1,
health: nil, health: nil,
attachments_count: 329, attachments_count: 329,
attachments_failed_count: 13,
attachments_synced_count: 141, attachments_synced_count: 141,
lfs_objects_count: 256, lfs_objects_count: 256,
lfs_objects_failed_count: 12,
lfs_objects_synced_count: 123, lfs_objects_synced_count: 123,
repositories_count: 10, repositories_count: 10,
repositories_synced_count: 5, repositories_synced_count: 5,
......
...@@ -2,6 +2,7 @@ FactoryGirl.define do ...@@ -2,6 +2,7 @@ FactoryGirl.define do
factory :geo_file_registry, class: Geo::FileRegistry do factory :geo_file_registry, class: Geo::FileRegistry do
sequence(:file_id) sequence(:file_id)
file_type :file file_type :file
success true
trait :avatar do trait :avatar do
file_type :avatar file_type :avatar
......
...@@ -5,8 +5,10 @@ ...@@ -5,8 +5,10 @@
"healthy", "healthy",
"health", "health",
"attachments_count", "attachments_count",
"attachments_failed_count",
"attachments_synced_count", "attachments_synced_count",
"lfs_objects_count", "lfs_objects_count",
"lfs_objects_failed_count",
"lfs_objects_synced_count", "lfs_objects_synced_count",
"db_replication_lag", "db_replication_lag",
"repositories_count", "repositories_count",
...@@ -22,10 +24,12 @@ ...@@ -22,10 +24,12 @@
"healthy": { "type": "boolean" }, "healthy": { "type": "boolean" },
"health": { "type": "string" }, "health": { "type": "string" },
"attachments_count": { "type": "integer" }, "attachments_count": { "type": "integer" },
"attachments_failed_count": { "type": "integer" },
"attachments_synced_count": { "type": "integer" }, "attachments_synced_count": { "type": "integer" },
"attachments_synced_in_percentage": { "type": "string" }, "attachments_synced_in_percentage": { "type": "string" },
"db_replication_lag": { "type": ["integer", "null"] }, "db_replication_lag": { "type": ["integer", "null"] },
"lfs_objects_count": { "type": "integer" }, "lfs_objects_count": { "type": "integer" },
"lfs_objects_failed_count": { "type": "integer" },
"lfs_objects_synced_count": { "type": "integer" }, "lfs_objects_synced_count": { "type": "integer" },
"lfs_objects_synced_in_percentage": { "type": "string" }, "lfs_objects_synced_in_percentage": { "type": "string" },
"repositories_count": { "type": "integer" }, "repositories_count": { "type": "integer" },
......
...@@ -28,7 +28,7 @@ import '~/lib/utils/common_utils'; ...@@ -28,7 +28,7 @@ import '~/lib/utils/common_utils';
preloadFixtures('merge_requests/diff_comment.html.raw'); preloadFixtures('merge_requests/diff_comment.html.raw');
beforeEach(function(done) { beforeEach(function(done) {
loadFixtures('merge_requests/diff_comment.html.raw'); loadFixtures('merge_requests/diff_comment.html.raw');
$('body').data('page', 'projects:merge_requests:show'); $('body').attr('data-page', 'projects:merge_requests:show');
loadAwardsHandler(true).then((obj) => { loadAwardsHandler(true).then((obj) => {
awardsHandler = obj; awardsHandler = obj;
spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb()); spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb());
...@@ -55,6 +55,9 @@ import '~/lib/utils/common_utils'; ...@@ -55,6 +55,9 @@ import '~/lib/utils/common_utils';
// restore original url root value // restore original url root value
gon.relative_url_root = urlRoot; gon.relative_url_root = urlRoot;
// Undo what we did to the shared <body>
$('body').removeAttr('data-page');
awardsHandler.destroy(); awardsHandler.destroy();
}); });
describe('::showEmojiMenu', function() { describe('::showEmojiMenu', function() {
......
...@@ -19,6 +19,11 @@ describe('Quick Submit behavior', () => { ...@@ -19,6 +19,11 @@ describe('Quick Submit behavior', () => {
this.textarea = $('.js-quick-submit textarea').first(); this.textarea = $('.js-quick-submit textarea').first();
}); });
afterEach(() => {
// Undo what we did to the shared <body>
$('body').removeAttr('data-page');
});
it('does not respond to other keyCodes', () => { it('does not respond to other keyCodes', () => {
this.textarea.trigger(keydownEvent({ this.textarea.trigger(keydownEvent({
keyCode: 32, keyCode: 32,
......
...@@ -23,12 +23,17 @@ describe('Merge request notes', () => { ...@@ -23,12 +23,17 @@ describe('Merge request notes', () => {
loadFixtures(discussionTabFixture); loadFixtures(discussionTabFixture);
gl.utils.disableButtonIfEmptyField = _.noop; gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads'; window.project_uploads_path = 'http://test.host/uploads';
$('body').data('page', 'projects:merge_requests:show'); $('body').attr('data-page', 'projects:merge_requests:show');
window.gon.current_user_id = $('.note:last').data('author-id'); window.gon.current_user_id = $('.note:last').data('author-id');
return new Notes('', []); return new Notes('', []);
}); });
afterEach(() => {
// Undo what we did to the shared <body>
$('body').removeAttr('data-page');
});
describe('up arrow', () => { describe('up arrow', () => {
it('edits last comment when triggered in main form', () => { it('edits last comment when triggered in main form', () => {
const upArrowEvent = $.Event('keydown'); const upArrowEvent = $.Event('keydown');
...@@ -71,12 +76,17 @@ describe('Merge request notes', () => { ...@@ -71,12 +76,17 @@ describe('Merge request notes', () => {
<textarea class="js-note-text"></textarea> <textarea class="js-note-text"></textarea>
</form>`; </form>`;
setFixtures(diffsResponse.html + noteFormHtml); setFixtures(diffsResponse.html + noteFormHtml);
$('body').data('page', 'projects:merge_requests:show'); $('body').attr('data-page', 'projects:merge_requests:show');
window.gon.current_user_id = $('.note:last').data('author-id'); window.gon.current_user_id = $('.note:last').data('author-id');
return new Notes('', []); return new Notes('', []);
}); });
afterEach(() => {
// Undo what we did to the shared <body>
$('body').removeAttr('data-page');
});
describe('up arrow', () => { describe('up arrow', () => {
it('edits last comment in discussion when triggered in discussion form', (done) => { it('edits last comment in discussion when triggered in discussion form', (done) => {
const upArrowEvent = $.Event('keydown'); const upArrowEvent = $.Event('keydown');
......
...@@ -277,7 +277,7 @@ import 'vendor/jquery.scrollTo'; ...@@ -277,7 +277,7 @@ import 'vendor/jquery.scrollTo';
describe('loadDiff', function () { describe('loadDiff', function () {
beforeEach(() => { beforeEach(() => {
loadFixtures('merge_requests/diff_comment.html.raw'); loadFixtures('merge_requests/diff_comment.html.raw');
spyOn(window.gl.utils, 'getPagePath').and.returnValue('merge_requests'); $('body').attr('data-page', 'projects:merge_requests:show');
window.gl.ImageFile = () => {}; window.gl.ImageFile = () => {};
window.notes = new Notes('', []); window.notes = new Notes('', []);
spyOn(window.notes, 'toggleDiffNote').and.callThrough(); spyOn(window.notes, 'toggleDiffNote').and.callThrough();
...@@ -286,6 +286,9 @@ import 'vendor/jquery.scrollTo'; ...@@ -286,6 +286,9 @@ import 'vendor/jquery.scrollTo';
afterEach(() => { afterEach(() => {
delete window.gl.ImageFile; delete window.gl.ImageFile;
delete window.notes; delete window.notes;
// Undo what we did to the shared <body>
$('body').removeAttr('data-page');
}); });
it('requires an absolute pathname', function () { it('requires an absolute pathname', function () {
......
...@@ -39,7 +39,12 @@ import '~/notes'; ...@@ -39,7 +39,12 @@ import '~/notes';
loadFixtures(commentsTemplate); loadFixtures(commentsTemplate);
gl.utils.disableButtonIfEmptyField = _.noop; gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads'; window.project_uploads_path = 'http://test.host/uploads';
$('body').data('page', 'projects:merge_requets:show'); $('body').attr('data-page', 'projects:merge_requets:show');
});
afterEach(() => {
// Undo what we did to the shared <body>
$('body').removeAttr('data-page');
}); });
describe('task lists', function() { describe('task lists', function() {
......
...@@ -6,7 +6,7 @@ import '~/lib/utils/common_utils'; ...@@ -6,7 +6,7 @@ import '~/lib/utils/common_utils';
import 'vendor/fuzzaldrin-plus'; import 'vendor/fuzzaldrin-plus';
(function() { (function() {
var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; var assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
var userName = 'root'; var userName = 'root';
widget = null; widget = null;
...@@ -29,25 +29,31 @@ import 'vendor/fuzzaldrin-plus'; ...@@ -29,25 +29,31 @@ import 'vendor/fuzzaldrin-plus';
groupName = 'Gitlab Org'; groupName = 'Gitlab Org';
const removeBodyAttributes = function() {
const $body = $('body');
$body.removeAttr('data-page');
$body.removeAttr('data-project');
$body.removeAttr('data-group');
};
// Add required attributes to body before starting the test. // Add required attributes to body before starting the test.
// section would be dashboard|group|project // section would be dashboard|group|project
addBodyAttributes = function(section) { const addBodyAttributes = function(section) {
var $body;
if (section == null) { if (section == null) {
section = 'dashboard'; section = 'dashboard';
} }
$body = $('body');
$body.removeAttr('data-page'); const $body = $('body');
$body.removeAttr('data-project'); removeBodyAttributes();
$body.removeAttr('data-group');
switch (section) { switch (section) {
case 'dashboard': case 'dashboard':
return $body.data('page', 'root:index'); return $body.attr('data-page', 'root:index');
case 'group': case 'group':
$body.data('page', 'groups:show'); $body.attr('data-page', 'groups:show');
return $body.data('group', 'gitlab-org'); return $body.data('group', 'gitlab-org');
case 'project': case 'project':
$body.data('page', 'projects:show'); $body.attr('data-page', 'projects:show');
return $body.data('project', 'gitlab-ce'); return $body.data('project', 'gitlab-ce');
} }
}; };
...@@ -108,7 +114,7 @@ import 'vendor/fuzzaldrin-plus'; ...@@ -108,7 +114,7 @@ import 'vendor/fuzzaldrin-plus';
preloadFixtures('static/search_autocomplete.html.raw'); preloadFixtures('static/search_autocomplete.html.raw');
beforeEach(function() { beforeEach(function() {
loadFixtures('static/search_autocomplete.html.raw'); loadFixtures('static/search_autocomplete.html.raw');
widget = new gl.SearchAutocomplete;
// Prevent turbolinks from triggering within gl_dropdown // Prevent turbolinks from triggering within gl_dropdown
spyOn(window.gl.utils, 'visitUrl').and.returnValue(true); spyOn(window.gl.utils, 'visitUrl').and.returnValue(true);
...@@ -120,6 +126,8 @@ import 'vendor/fuzzaldrin-plus'; ...@@ -120,6 +126,8 @@ import 'vendor/fuzzaldrin-plus';
}); });
afterEach(function() { afterEach(function() {
// Undo what we did to the shared <body>
removeBodyAttributes();
window.gon = {}; window.gon = {};
}); });
it('should show Dashboard specific dropdown menu', function() { it('should show Dashboard specific dropdown menu', function() {
......
...@@ -72,7 +72,14 @@ describe Gitlab::PathRegex do ...@@ -72,7 +72,14 @@ describe Gitlab::PathRegex do
route_set = Rails.application.routes route_set = Rails.application.routes
routes_collection = route_set.routes routes_collection = route_set.routes
routes_array = routes_collection.routes routes_array = routes_collection.routes
routes_array.map { |route| route.path.spec.to_s } non_deprecated_redirect_routes = routes_array.reject do |route|
app = route.app
# `app.app` is either another app, or `self`. We want to find the final app.
app = app.app while app.try(:app) && app.app != app
app.is_a?(ActionDispatch::Routing::PathRedirect) && app.block.include?('/-/')
end
non_deprecated_redirect_routes.map { |route| route.path.spec.to_s }
end end
let(:routes_without_format) { all_routes.map { |path| without_format(path) } } let(:routes_without_format) { all_routes.map { |path| without_format(path) } }
......
require 'spec_helper'
describe Geo::FileRegistry do
set(:failed) { create(:geo_file_registry, success: false) }
set(:synced) { create(:geo_file_registry, success: true) }
describe '.failed' do
it 'returns registries in the failed state' do
expect(described_class.failed).to contain_exactly(failed)
end
end
describe '.synced' do
it 'returns registries in the synced state' do
expect(described_class.synced).to contain_exactly(synced)
end
end
end
...@@ -39,6 +39,17 @@ describe GeoNodeStatus do ...@@ -39,6 +39,17 @@ describe GeoNodeStatus do
end end
describe '#attachments_synced_count' do describe '#attachments_synced_count' do
it 'only counts successful syncs' do
create_list(:user, 3, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png'))
uploads = Upload.all.pluck(:id)
create(:geo_file_registry, :avatar, file_id: uploads[0])
create(:geo_file_registry, :avatar, file_id: uploads[1])
create(:geo_file_registry, :avatar, file_id: uploads[2], success: false)
expect(subject.attachments_synced_count).to eq(2)
end
it 'does not count synced files that were replaced' do it 'does not count synced files that were replaced' do
user = create(:user, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png')) user = create(:user, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png'))
...@@ -68,6 +79,21 @@ describe GeoNodeStatus do ...@@ -68,6 +79,21 @@ describe GeoNodeStatus do
end end
end end
describe '#attachments_failed_count' do
it 'counts failed avatars, attachment, personal snippets and files' do
# These two should be ignored
create(:geo_file_registry, :lfs, success: false)
create(:geo_file_registry)
create(:geo_file_registry, file_type: :personal_file, success: false)
create(:geo_file_registry, file_type: :attachment, success: false)
create(:geo_file_registry, :avatar, success: false)
create(:geo_file_registry, success: false)
expect(subject.attachments_failed_count).to eq(4)
end
end
describe '#attachments_synced_in_percentage' do describe '#attachments_synced_in_percentage' do
let(:avatar) { fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) } let(:avatar) { fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) }
let(:upload_1) { create(:upload, model: group, path: avatar) } let(:upload_1) { create(:upload, model: group, path: avatar) }
...@@ -113,6 +139,20 @@ describe GeoNodeStatus do ...@@ -113,6 +139,20 @@ describe GeoNodeStatus do
end end
end end
describe '#lfs_objects_failed' do
it 'counts failed LFS objects' do
# These four should be ignored
create(:geo_file_registry, success: false)
create(:geo_file_registry, :avatar, success: false)
create(:geo_file_registry, file_type: :attachment, success: false)
create(:geo_file_registry, :lfs)
create(:geo_file_registry, :lfs, success: false)
expect(subject.lfs_objects_failed_count).to eq(1)
end
end
describe '#lfs_objects_synced_in_percentage' do describe '#lfs_objects_synced_in_percentage' do
let(:lfs_object_project) { create(:lfs_objects_project, project: project_1) } let(:lfs_object_project) { create(:lfs_objects_project, project: project_1) }
...@@ -218,8 +258,10 @@ describe GeoNodeStatus do ...@@ -218,8 +258,10 @@ describe GeoNodeStatus do
allow(Gitlab::Geo::HealthCheck).to receive(:db_replication_lag).and_return(nil) allow(Gitlab::Geo::HealthCheck).to receive(:db_replication_lag).and_return(nil)
subject.attachments_count = nil subject.attachments_count = nil
subject.attachments_synced_count = nil subject.attachments_synced_count = nil
subject.attachments_failed_count = nil
subject.lfs_objects_count = nil subject.lfs_objects_count = nil
subject.lfs_objects_synced_count = nil subject.lfs_objects_synced_count = nil
subject.lfs_objects_failed_count = nil
subject.repositories_count = nil subject.repositories_count = nil
subject.repositories_synced_count = nil subject.repositories_synced_count = nil
subject.repositories_failed_count = nil subject.repositories_failed_count = nil
...@@ -235,9 +277,11 @@ describe GeoNodeStatus do ...@@ -235,9 +277,11 @@ describe GeoNodeStatus do
expect(subject.repositories_failed_count).to be_zero expect(subject.repositories_failed_count).to be_zero
expect(subject.lfs_objects_count).to be_zero expect(subject.lfs_objects_count).to be_zero
expect(subject.lfs_objects_synced_count).to be_zero expect(subject.lfs_objects_synced_count).to be_zero
expect(subject.lfs_objects_failed_count).to be_zero
expect(subject.lfs_objects_synced_in_percentage).to be_zero expect(subject.lfs_objects_synced_in_percentage).to be_zero
expect(subject.attachments_count).to be_zero expect(subject.attachments_count).to be_zero
expect(subject.attachments_synced_count).to be_zero expect(subject.attachments_synced_count).to be_zero
expect(subject.attachments_failed_count).to be_zero
expect(subject.attachments_synced_in_percentage).to be_zero expect(subject.attachments_synced_in_percentage).to be_zero
expect(subject.last_event_id).to be_nil expect(subject.last_event_id).to be_nil
expect(subject.last_event_date).to be_nil expect(subject.last_event_date).to be_nil
......
...@@ -264,12 +264,6 @@ describe Repository do ...@@ -264,12 +264,6 @@ describe Repository do
expect_to_raise_storage_error { broken_repository.find_commits_by_message('s') } expect_to_raise_storage_error { broken_repository.find_commits_by_message('s') }
end end
end end
describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error { broken_repository.find_commits_by_message('s') }
end
end
end end
describe '#blob_at' do describe '#blob_at' do
......
require 'spec_helper'
describe 'Deprecated boards paths' do
it 'redirects to boards page' do
group = create :group, name: 'gitlabhq'
get('/groups/gitlabhq/boards')
expect(response).to redirect_to(group_boards_path(group))
end
end
require 'spec_helper'
describe 'Group routing' do
describe 'subgroup "boards"' do
it 'shows group show page' do
allow(Group).to receive(:find_by_full_path).with('gitlabhq/boards', any_args).and_return(true)
expect(get('/groups/gitlabhq/boards')).to route_to('groups#show', id: 'gitlabhq/boards')
end
it 'shows boards index page' do
allow(Group).to receive(:find_by_full_path).with('gitlabhq', any_args).and_return(true)
expect(get('/groups/gitlabhq/-/boards')).to route_to('groups/boards#index', group_id: 'gitlabhq')
end
end
end
...@@ -285,17 +285,15 @@ end ...@@ -285,17 +285,15 @@ end
describe "Groups", "routing" do describe "Groups", "routing" do
let(:name) { 'complex.group-namegit' } let(:name) { 'complex.group-namegit' }
let!(:group) { create(:group, name: name) }
before do
allow_any_instance_of(GroupUrlConstrainer).to receive(:matches?).and_return(true)
end
it "to #show" do it "to #show" do
expect(get("/groups/#{name}")).to route_to('groups#show', id: name) expect(get("/groups/#{name}")).to route_to('groups#show', id: name)
end end
it "also supports nested groups" do it "also supports nested groups" do
expect(get("/#{name}/#{name}")).to route_to('groups#show', id: "#{name}/#{name}") nested_group = create(:group, parent: group)
expect(get("/#{name}/#{nested_group.name}")).to route_to('groups#show', id: "#{name}/#{nested_group.name}")
end end
it "also display group#show on the short path" do it "also display group#show on the short path" do
...@@ -313,10 +311,6 @@ describe "Groups", "routing" do ...@@ -313,10 +311,6 @@ describe "Groups", "routing" do
it "to #members" do it "to #members" do
expect(get("/groups/#{name}/group_members")).to route_to('groups/group_members#index', group_id: name) expect(get("/groups/#{name}/group_members")).to route_to('groups/group_members#index', group_id: name)
end end
it "also display group#show with slash in the path" do
expect(get('/group/subgroup')).to route_to('groups#show', id: 'group/subgroup')
end
end end
describe HealthCheckController, 'routing' do describe HealthCheckController, 'routing' do
......
...@@ -6,8 +6,10 @@ describe GeoNodeStatusEntity, :postgresql do ...@@ -6,8 +6,10 @@ describe GeoNodeStatusEntity, :postgresql do
id: 1, id: 1,
health: '', health: '',
attachments_count: 329, attachments_count: 329,
attachments_failed_count: 25,
attachments_synced_count: 141, attachments_synced_count: 141,
lfs_objects_count: 256, lfs_objects_count: 256,
lfs_objects_failed_count: 12,
lfs_objects_synced_count: 123, lfs_objects_synced_count: 123,
repositories_count: 10, repositories_count: 10,
repositories_synced_count: 5, repositories_synced_count: 5,
...@@ -29,9 +31,11 @@ describe GeoNodeStatusEntity, :postgresql do ...@@ -29,9 +31,11 @@ describe GeoNodeStatusEntity, :postgresql do
it { is_expected.to have_key(:healthy) } it { is_expected.to have_key(:healthy) }
it { is_expected.to have_key(:health) } it { is_expected.to have_key(:health) }
it { is_expected.to have_key(:attachments_count) } it { is_expected.to have_key(:attachments_count) }
it { is_expected.to have_key(:attachments_failed_count) }
it { is_expected.to have_key(:attachments_synced_count) } it { is_expected.to have_key(:attachments_synced_count) }
it { is_expected.to have_key(:attachments_synced_in_percentage) } it { is_expected.to have_key(:attachments_synced_in_percentage) }
it { is_expected.to have_key(:lfs_objects_count) } it { is_expected.to have_key(:lfs_objects_count) }
it { is_expected.to have_key(:lfs_objects_failed_count) }
it { is_expected.to have_key(:lfs_objects_synced_count) } it { is_expected.to have_key(:lfs_objects_synced_count) }
it { is_expected.to have_key(:lfs_objects_synced_in_percentage) } it { is_expected.to have_key(:lfs_objects_synced_in_percentage) }
it { is_expected.to have_key(:repositories_count) } it { is_expected.to have_key(:repositories_count) }
......
...@@ -8,6 +8,8 @@ describe Geo::FileDownloadService do ...@@ -8,6 +8,8 @@ describe Geo::FileDownloadService do
before do before do
stub_current_geo_node(secondary) stub_current_geo_node(secondary)
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(true)
end end
describe '#execute' do describe '#execute' do
...@@ -15,15 +17,18 @@ describe Geo::FileDownloadService do ...@@ -15,15 +17,18 @@ describe Geo::FileDownloadService do
let(:user) { create(:user, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png')) } let(:user) { create(:user, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png')) }
let(:upload) { Upload.find_by(model: user, uploader: 'AvatarUploader') } let(:upload) { Upload.find_by(model: user, uploader: 'AvatarUploader') }
subject { described_class.new(:avatar, upload.id) } subject(:execute!) { described_class.new(:avatar, upload.id).execute }
it 'downloads a user avatar' do it 'downloads a user avatar' do
allow_any_instance_of(Gitlab::ExclusiveLease) stub_transfer(Gitlab::Geo::FileTransfer, 100)
.to receive(:try_obtain).and_return(true)
allow_any_instance_of(Gitlab::Geo::FileTransfer) expect { execute! }.to change { Geo::FileRegistry.synced.count }.by(1)
.to receive(:download_from_primary).and_return(100) end
it 'registers when the download fails' do
stub_transfer(Gitlab::Geo::FileTransfer, -1)
expect { subject.execute }.to change { Geo::FileRegistry.count }.by(1) expect { execute! }.to change { Geo::FileRegistry.failed.count }.by(1)
end end
end end
...@@ -31,15 +36,18 @@ describe Geo::FileDownloadService do ...@@ -31,15 +36,18 @@ describe Geo::FileDownloadService do
let(:group) { create(:group, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png')) } let(:group) { create(:group, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png')) }
let(:upload) { Upload.find_by(model: group, uploader: 'AvatarUploader') } let(:upload) { Upload.find_by(model: group, uploader: 'AvatarUploader') }
subject { described_class.new(:avatar, upload.id) } subject(:execute!) { described_class.new(:avatar, upload.id).execute }
it 'downloads a group avatar' do it 'downloads a group avatar' do
allow_any_instance_of(Gitlab::ExclusiveLease) stub_transfer(Gitlab::Geo::FileTransfer, 100)
.to receive(:try_obtain).and_return(true)
allow_any_instance_of(Gitlab::Geo::FileTransfer) expect { execute! }.to change { Geo::FileRegistry.synced.count }.by(1)
.to receive(:download_from_primary).and_return(100) end
it 'registers when the download fails' do
stub_transfer(Gitlab::Geo::FileTransfer, -1)
expect { subject.execute }.to change { Geo::FileRegistry.count }.by(1) expect { execute! }.to change { Geo::FileRegistry.failed.count }.by(1)
end end
end end
...@@ -47,15 +55,18 @@ describe Geo::FileDownloadService do ...@@ -47,15 +55,18 @@ describe Geo::FileDownloadService do
let(:project) { create(:project, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png')) } let(:project) { create(:project, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png')) }
let(:upload) { Upload.find_by(model: project, uploader: 'AvatarUploader') } let(:upload) { Upload.find_by(model: project, uploader: 'AvatarUploader') }
subject { described_class.new(:avatar, upload.id) } subject(:execute!) { described_class.new(:avatar, upload.id).execute }
it 'downloads a project avatar' do it 'downloads a project avatar' do
allow_any_instance_of(Gitlab::ExclusiveLease) stub_transfer(Gitlab::Geo::FileTransfer, 100)
.to receive(:try_obtain).and_return(true)
allow_any_instance_of(Gitlab::Geo::FileTransfer)
.to receive(:download_from_primary).and_return(100)
expect { subject.execute }.to change { Geo::FileRegistry.count }.by(1) expect { execute! }.to change { Geo::FileRegistry.synced.count }.by(1)
end
it 'registers when the download fails' do
stub_transfer(Gitlab::Geo::FileTransfer, -1)
expect { execute! }.to change { Geo::FileRegistry.failed.count }.by(1)
end end
end end
...@@ -63,30 +74,36 @@ describe Geo::FileDownloadService do ...@@ -63,30 +74,36 @@ describe Geo::FileDownloadService do
let(:note) { create(:note, :with_attachment) } let(:note) { create(:note, :with_attachment) }
let(:upload) { Upload.find_by(model: note, uploader: 'AttachmentUploader') } let(:upload) { Upload.find_by(model: note, uploader: 'AttachmentUploader') }
subject { described_class.new(:attachment, upload.id) } subject(:execute!) { described_class.new(:attachment, upload.id).execute }
it 'downloads the attachment' do it 'downloads the attachment' do
allow_any_instance_of(Gitlab::ExclusiveLease) stub_transfer(Gitlab::Geo::FileTransfer, 100)
.to receive(:try_obtain).and_return(true)
allow_any_instance_of(Gitlab::Geo::FileTransfer)
.to receive(:download_from_primary).and_return(100)
expect { subject.execute }.to change { Geo::FileRegistry.count }.by(1) expect { execute! }.to change { Geo::FileRegistry.synced.count }.by(1)
end
it 'registers when the download fails' do
stub_transfer(Gitlab::Geo::FileTransfer, -1)
expect { execute! }.to change { Geo::FileRegistry.failed.count }.by(1)
end end
end end
context 'with a snippet' do context 'with a snippet' do
let(:upload) { create(:upload, :personal_snippet) } let(:upload) { create(:upload, :personal_snippet) }
subject { described_class.new(:personal_file, upload.id) } subject(:execute!) { described_class.new(:personal_file, upload.id).execute }
it 'downloads the file' do it 'downloads the file' do
allow_any_instance_of(Gitlab::ExclusiveLease) stub_transfer(Gitlab::Geo::FileTransfer, 100)
.to receive(:try_obtain).and_return(true)
allow_any_instance_of(Gitlab::Geo::FileTransfer)
.to receive(:download_from_primary).and_return(100)
expect { subject.execute }.to change { Geo::FileRegistry.count }.by(1) expect { execute! }.to change { Geo::FileRegistry.synced.count }.by(1)
end
it 'registers when the download fails' do
stub_transfer(Gitlab::Geo::FileTransfer, -1)
expect { execute! }.to change { Geo::FileRegistry.failed.count }.by(1)
end end
end end
...@@ -101,12 +118,15 @@ describe Geo::FileDownloadService do ...@@ -101,12 +118,15 @@ describe Geo::FileDownloadService do
end end
it 'downloads the file' do it 'downloads the file' do
allow_any_instance_of(Gitlab::ExclusiveLease) stub_transfer(Gitlab::Geo::FileTransfer, 100)
.to receive(:try_obtain).and_return(true)
allow_any_instance_of(Gitlab::Geo::FileTransfer)
.to receive(:download_from_primary).and_return(100)
expect { subject.execute }.to change { Geo::FileRegistry.count }.by(1) expect { subject.execute }.to change { Geo::FileRegistry.synced.count }.by(1)
end
it 'registers when the download fails' do
stub_transfer(Gitlab::Geo::FileTransfer, -1)
expect { subject.execute }.to change { Geo::FileRegistry.failed.count }.by(1)
end end
end end
...@@ -115,18 +135,21 @@ describe Geo::FileDownloadService do ...@@ -115,18 +135,21 @@ describe Geo::FileDownloadService do
subject { described_class.new(:lfs, lfs_object.id) } subject { described_class.new(:lfs, lfs_object.id) }
before do it 'downloads an LFS object' do
allow_any_instance_of(Gitlab::ExclusiveLease) stub_transfer(Gitlab::Geo::LfsTransfer, 100)
.to receive(:try_obtain).and_return(true)
allow_any_instance_of(Gitlab::Geo::LfsTransfer) expect { subject.execute }.to change { Geo::FileRegistry.synced.count }.by(1)
.to receive(:download_from_primary).and_return(100)
end end
it 'downloads an LFS object' do it 'registers when the download fails' do
expect { subject.execute }.to change { Geo::FileRegistry.count }.by(1) stub_transfer(Gitlab::Geo::LfsTransfer, -1)
expect { subject.execute }.to change { Geo::FileRegistry.failed.count }.by(1)
end end
it 'logs a message' do it 'logs a message' do
stub_transfer(Gitlab::Geo::LfsTransfer, 100)
expect(Gitlab::Geo::Logger).to receive(:info).with(hash_including(:message, :download_time_s, success: true, bytes_downloaded: 100)).and_call_original expect(Gitlab::Geo::Logger).to receive(:info).with(hash_including(:message, :download_time_s, success: true, bytes_downloaded: 100)).and_call_original
subject.execute subject.execute
...@@ -138,5 +161,10 @@ describe Geo::FileDownloadService do ...@@ -138,5 +161,10 @@ describe Geo::FileDownloadService do
expect { described_class.new(:bad, 1).execute }.to raise_error(NameError) expect { described_class.new(:bad, 1).execute }.to raise_error(NameError)
end end
end end
def stub_transfer(kls, result)
instance = double("(instance of #{kls})", download_from_primary: result)
allow(kls).to receive(:new).and_return(instance)
end
end end
end end
...@@ -84,6 +84,27 @@ describe Geo::FileDownloadDispatchWorker, :postgresql do ...@@ -84,6 +84,27 @@ describe Geo::FileDownloadDispatchWorker, :postgresql do
end end
end end
context 'with a failed file' do
let!(:failed_registry) { create(:geo_file_registry, :lfs, file_id: 999, success: false) }
it 'does not stall backfill' do
unsynced = create(:lfs_object, :with_file)
stub_const('Geo::BaseSchedulerWorker::DB_RETRIEVE_BATCH_SIZE', 1)
expect(GeoFileDownloadWorker).not_to receive(:perform_async).with(:lfs, failed_registry.file_id)
expect(GeoFileDownloadWorker).to receive(:perform_async).with(:lfs, unsynced.id)
subject.perform
end
it 'retries failed files' do
expect(GeoFileDownloadWorker).to receive(:perform_async).with('lfs', failed_registry.file_id)
subject.perform
end
end
context 'when node has namespace restrictions' do context 'when node has namespace restrictions' do
let(:synced_group) { create(:group) } let(:synced_group) { create(:group) }
let!(:project_in_synced_group) { create(:project, group: synced_group) } let!(:project_in_synced_group) { create(:project, group: synced_group) }
......
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