diff options
Diffstat (limited to 'app')
28 files changed, 459 insertions, 374 deletions
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index d716218d9a4..5ff7edb6077 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,6 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ import { s__ } from './locale'; -/* global ProjectSelect */ +import projectSelect from './project_select'; import IssuableIndex from './issuable_index'; /* global Milestone */ import IssuableForm from './issuable_form'; @@ -26,8 +26,7 @@ import projectAvatar from './project_avatar'; /* global Compare */ /* global CompareAutocomplete */ /* global ProjectFindFile */ -/* global ProjectNew */ -/* global ProjectShow */ +import ProjectNew from './project_new'; import projectImport from './project_import'; import Labels from './labels'; import LabelManager from './label_manager'; @@ -91,6 +90,8 @@ import Members from './members'; import memberExpirationDate from './member_expiration_date'; import DueDateSelectors from './due_date_select'; import Diff from './diff'; +import ProjectLabelSubscription from './project_label_subscription'; +import ProjectVariables from './project_variables'; (function() { var Dispatcher; @@ -187,7 +188,7 @@ import Diff from './diff'; initIssuableSidebar(); break; case 'dashboard:milestones:index': - new ProjectSelect(); + projectSelect(); break; case 'projects:milestones:show': case 'groups:milestones:show': @@ -197,7 +198,7 @@ import Diff from './diff'; break; case 'dashboard:issues': case 'dashboard:merge_requests': - new ProjectSelect(); + projectSelect(); initLegacyFilters(); break; case 'groups:issues': @@ -206,7 +207,7 @@ import Diff from './diff'; const filteredSearchManager = new gl.FilteredSearchManager(page === 'groups:issues' ? 'issues' : 'merge_requests'); filteredSearchManager.setup(); } - new ProjectSelect(); + projectSelect(); break; case 'dashboard:todos:index': new Todos(); @@ -484,7 +485,7 @@ import Diff from './diff'; if ($el.find('.dropdown-group-label').length) { new GroupLabelSubscription($el); } else { - new gl.ProjectLabelSubscription($el); + new ProjectLabelSubscription($el); } }); break; @@ -520,7 +521,7 @@ import Diff from './diff'; // Initialize expandable settings panels initSettingsPanels(); case 'groups:settings:ci_cd:show': - new gl.ProjectVariables(); + new ProjectVariables(); break; case 'ci:lints:create': case 'ci:lints:show': @@ -623,7 +624,6 @@ import Diff from './diff'; case 'show': new Star(); new ProjectNew(); - new ProjectShow(); new NotificationsDropdown(); break; case 'wikis': diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 0035dd23011..cef79eec273 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -71,11 +71,6 @@ import './pager'; import './preview_markdown'; import './project_find_file'; import './project_import'; -import './project_label_subscription'; -import './project_new'; -import './project_select'; -import './project_show'; -import './project_variables'; import './projects_dropdown'; import './projects_list'; import './syntax_highlight'; diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index ddb78aaeea1..36b6a5ed376 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ -/* global ProjectSelect */ import Cookies from 'js-cookie'; +import projectSelect from './project_select'; export default class Project { constructor() { @@ -46,7 +46,7 @@ export default class Project { } static projectSelectDropdown () { - new ProjectSelect(); + projectSelect(); $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val())); } diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js index 0a811627600..b65521b278f 100644 --- a/app/assets/javascripts/project_label_subscription.js +++ b/app/assets/javascripts/project_label_subscription.js @@ -1,55 +1,50 @@ -/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, max-len, no-param-reassign */ +export default class ProjectLabelSubscription { + constructor(container) { + this.$container = $(container); + this.$buttons = this.$container.find('.js-subscribe-button'); -(function(global) { - class ProjectLabelSubscription { - constructor(container) { - this.$container = $(container); - this.$buttons = this.$container.find('.js-subscribe-button'); - - this.$buttons.on('click', this.toggleSubscription.bind(this)); - } + this.$buttons.on('click', this.toggleSubscription.bind(this)); + } - toggleSubscription(event) { - event.preventDefault(); + toggleSubscription(event) { + event.preventDefault(); - const $btn = $(event.currentTarget); - const $span = $btn.find('span'); - const url = $btn.attr('data-url'); - const oldStatus = $btn.attr('data-status'); + const $btn = $(event.currentTarget); + const $span = $btn.find('span'); + const url = $btn.attr('data-url'); + const oldStatus = $btn.attr('data-status'); - $btn.addClass('disabled'); - $span.toggleClass('hidden'); + $btn.addClass('disabled'); + $span.toggleClass('hidden'); - $.ajax({ - type: 'POST', - url: url - }).done(() => { - let newStatus, newAction; + $.ajax({ + type: 'POST', + url, + }).done(() => { + let newStatus; + let newAction; - if (oldStatus === 'unsubscribed') { - [newStatus, newAction] = ['subscribed', 'Unsubscribe']; - } else { - [newStatus, newAction] = ['unsubscribed', 'Subscribe']; - } + if (oldStatus === 'unsubscribed') { + [newStatus, newAction] = ['subscribed', 'Unsubscribe']; + } else { + [newStatus, newAction] = ['unsubscribed', 'Subscribe']; + } - $span.toggleClass('hidden'); - $btn.removeClass('disabled'); + $span.toggleClass('hidden'); + $btn.removeClass('disabled'); - this.$buttons.attr('data-status', newStatus); - this.$buttons.find('> span').text(newAction); + this.$buttons.attr('data-status', newStatus); + this.$buttons.find('> span').text(newAction); - this.$buttons.map((button) => { - const $button = $(button); + this.$buttons.map((button) => { + const $button = $(button); - if ($button.attr('data-original-title')) { - $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle'); - } + if ($button.attr('data-original-title')) { + $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle'); + } - return button; - }); + return button; }); - } + }); } - - global.ProjectLabelSubscription = ProjectLabelSubscription; -})(window.gl || (window.gl = {})); +} diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index fd89a1a85c3..ca548d011b6 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */ +/* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/ import VisibilitySelect from './visibility_select'; @@ -7,153 +7,145 @@ function highlightChanges($elm) { setTimeout(() => $elm.removeClass('highlight-changes'), 10); } -(function() { - this.ProjectNew = (function() { - function ProjectNew() { - this.toggleSettings = this.toggleSettings.bind(this); - this.$selects = $('.features select'); - this.$repoSelects = this.$selects.filter('.js-repo-select'); - this.$projectSelects = this.$selects.not('.js-repo-select'); - - $('.project-edit-container').on('ajax:before', (function(_this) { - return function() { - $('.project-edit-container').hide(); - return $('.save-project-loader').show(); - }; - })(this)); - - this.initVisibilitySelect(); - - this.toggleSettings(); - this.toggleSettingsOnclick(); - this.toggleRepoVisibility(); - } - - ProjectNew.prototype.initVisibilitySelect = function() { - const visibilityContainer = document.querySelector('.js-visibility-select'); - if (!visibilityContainer) return; - const visibilitySelect = new VisibilitySelect(visibilityContainer); - visibilitySelect.init(); - - const $visibilitySelect = $(visibilityContainer).find('select'); - let projectVisibility = $visibilitySelect.val(); - const PROJECT_VISIBILITY_PRIVATE = '0'; - - $visibilitySelect.on('change', () => { - const newProjectVisibility = $visibilitySelect.val(); - - if (projectVisibility !== newProjectVisibility) { - this.$projectSelects.each((idx, select) => { - const $select = $(select); - const $options = $select.find('option'); - const values = $.map($options, e => e.value); - - // if switched to "private", limit visibility options - if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) { - if ($select.val() !== values[0] && $select.val() !== values[1]) { - $select.val(values[1]).trigger('change'); - highlightChanges($select); - } - $options.slice(2).disable(); +export default class ProjectNew { + constructor() { + this.toggleSettings = this.toggleSettings.bind(this); + this.$selects = $('.features select'); + this.$repoSelects = this.$selects.filter('.js-repo-select'); + this.$projectSelects = this.$selects.not('.js-repo-select'); + + $('.project-edit-container').on('ajax:before', () => { + $('.project-edit-container').hide(); + return $('.save-project-loader').show(); + }); + + this.initVisibilitySelect(); + + this.toggleSettings(); + this.toggleSettingsOnclick(); + this.toggleRepoVisibility(); + } + + initVisibilitySelect() { + const visibilityContainer = document.querySelector('.js-visibility-select'); + if (!visibilityContainer) return; + const visibilitySelect = new VisibilitySelect(visibilityContainer); + visibilitySelect.init(); + + const $visibilitySelect = $(visibilityContainer).find('select'); + let projectVisibility = $visibilitySelect.val(); + const PROJECT_VISIBILITY_PRIVATE = '0'; + + $visibilitySelect.on('change', () => { + const newProjectVisibility = $visibilitySelect.val(); + + if (projectVisibility !== newProjectVisibility) { + this.$projectSelects.each((idx, select) => { + const $select = $(select); + const $options = $select.find('option'); + const values = $.map($options, e => e.value); + + // if switched to "private", limit visibility options + if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) { + if ($select.val() !== values[0] && $select.val() !== values[1]) { + $select.val(values[1]).trigger('change'); + highlightChanges($select); } + $options.slice(2).disable(); + } - // if switched from "private", increase visibility for non-disabled options - if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) { - $options.enable(); - if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) { - $select.val(values[values.length - 1]).trigger('change'); - highlightChanges($select); - } + // if switched from "private", increase visibility for non-disabled options + if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) { + $options.enable(); + if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) { + $select.val(values[values.length - 1]).trigger('change'); + highlightChanges($select); } - }); + } + }); - projectVisibility = newProjectVisibility; - } - }); - }; - - ProjectNew.prototype.toggleSettings = function() { - var self = this; - - this.$selects.each(function () { - var $select = $(this); - var className = $select.data('field') - .replace(/_/g, '-') - .replace('access-level', 'feature'); - self._showOrHide($select, '.' + className); - }); - }; - - ProjectNew.prototype.toggleSettingsOnclick = function() { - this.$selects.on('change', this.toggleSettings); - }; - - ProjectNew.prototype._showOrHide = function(checkElement, container) { - var $container = $(container); - - if ($(checkElement).val() !== '0') { - return $container.show(); - } else { - return $container.hide(); + projectVisibility = newProjectVisibility; } - }; - - ProjectNew.prototype.toggleRepoVisibility = function () { - var $repoAccessLevel = $('.js-repo-access-level select'); - var $lfsEnabledOption = $('.js-lfs-enabled select'); - var containerRegistry = document.querySelectorAll('.js-container-registry')[0]; - var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled'); - var prevSelectedVal = parseInt($repoAccessLevel.val(), 10); - - this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']") - .nextAll() - .hide(); - - $repoAccessLevel.off('change') - .on('change', function () { - var selectedVal = parseInt($repoAccessLevel.val(), 10); - - this.$repoSelects.each(function () { - var $this = $(this); - var repoSelectVal = parseInt($this.val(), 10); - - $this.find('option').enable(); - - if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) { - $this.val(selectedVal).trigger('change'); - highlightChanges($this); - } - - $this.find("option[value='" + selectedVal + "']").nextAll().disable(); - }); + }); + } + + toggleSettings() { + this.$selects.each(function () { + var $select = $(this); + var className = $select.data('field') + .replace(/_/g, '-') + .replace('access-level', 'feature'); + ProjectNew._showOrHide($select, '.' + className); + }); + } + + toggleSettingsOnclick() { + this.$selects.on('change', this.toggleSettings); + } + + static _showOrHide(checkElement, container) { + const $container = $(container); + + if ($(checkElement).val() !== '0') { + return $container.show(); + } + return $container.hide(); + } + + toggleRepoVisibility() { + var $repoAccessLevel = $('.js-repo-access-level select'); + var $lfsEnabledOption = $('.js-lfs-enabled select'); + var containerRegistry = document.querySelectorAll('.js-container-registry')[0]; + var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled'); + var prevSelectedVal = parseInt($repoAccessLevel.val(), 10); + + this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']") + .nextAll() + .hide(); + + $repoAccessLevel + .off('change') + .on('change', function () { + var selectedVal = parseInt($repoAccessLevel.val(), 10); + + this.$repoSelects.each(function () { + var $this = $(this); + var repoSelectVal = parseInt($this.val(), 10); + + $this.find('option').enable(); + + if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) { + $this.val(selectedVal).trigger('change'); + highlightChanges($this); + } - if (selectedVal) { - this.$repoSelects.removeClass('disabled'); + $this.find("option[value='" + selectedVal + "']").nextAll().disable(); + }); - if ($lfsEnabledOption.length) { - $lfsEnabledOption.removeClass('disabled'); - highlightChanges($lfsEnabledOption); - } - if (containerRegistry) { - containerRegistry.style.display = ''; - } - } else { - this.$repoSelects.addClass('disabled'); + if (selectedVal) { + this.$repoSelects.removeClass('disabled'); - if ($lfsEnabledOption.length) { - $lfsEnabledOption.val('false').addClass('disabled'); - highlightChanges($lfsEnabledOption); - } - if (containerRegistry) { - containerRegistry.style.display = 'none'; - containerRegistryCheckbox.checked = false; - } + if ($lfsEnabledOption.length) { + $lfsEnabledOption.removeClass('disabled'); + highlightChanges($lfsEnabledOption); + } + if (containerRegistry) { + containerRegistry.style.display = ''; } + } else { + this.$repoSelects.addClass('disabled'); - prevSelectedVal = selectedVal; - }.bind(this)); - }; + if ($lfsEnabledOption.length) { + $lfsEnabledOption.val('false').addClass('disabled'); + highlightChanges($lfsEnabledOption); + } + if (containerRegistry) { + containerRegistry.style.display = 'none'; + containerRegistryCheckbox.checked = false; + } + } - return ProjectNew; - })(); -}).call(window); + prevSelectedVal = selectedVal; + }.bind(this)); + } +} diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index bffc85e6315..07a49d1506c 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -2,79 +2,73 @@ import Api from './api'; import ProjectSelectComboButton from './project_select_combo_button'; -(function () { - this.ProjectSelect = (function () { - function ProjectSelect() { - $('.ajax-project-select').each(function(i, select) { - var placeholder; - const simpleFilter = $(select).data('simple-filter') || false; - this.groupId = $(select).data('group-id'); - this.includeGroups = $(select).data('include-groups'); - this.allProjects = $(select).data('all-projects') || false; - this.orderBy = $(select).data('order-by') || 'id'; - this.withIssuesEnabled = $(select).data('with-issues-enabled'); - this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled'); +export default function projectSelect() { + $('.ajax-project-select').each(function(i, select) { + var placeholder; + const simpleFilter = $(select).data('simple-filter') || false; + this.groupId = $(select).data('group-id'); + this.includeGroups = $(select).data('include-groups'); + this.allProjects = $(select).data('all-projects') || false; + this.orderBy = $(select).data('order-by') || 'id'; + this.withIssuesEnabled = $(select).data('with-issues-enabled'); + this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled'); - placeholder = "Search for project"; - if (this.includeGroups) { - placeholder += " or group"; - } + placeholder = "Search for project"; + if (this.includeGroups) { + placeholder += " or group"; + } - $(select).select2({ - placeholder: placeholder, - minimumInputLength: 0, - query: (function (_this) { - return function (query) { - var finalCallback, projectsCallback; - finalCallback = function (projects) { + $(select).select2({ + placeholder: placeholder, + minimumInputLength: 0, + query: (function (_this) { + return function (query) { + var finalCallback, projectsCallback; + finalCallback = function (projects) { + var data; + data = { + results: projects + }; + return query.callback(data); + }; + if (_this.includeGroups) { + projectsCallback = function (projects) { + var groupsCallback; + groupsCallback = function (groups) { var data; - data = { - results: projects - }; - return query.callback(data); + data = groups.concat(projects); + return finalCallback(data); }; - if (_this.includeGroups) { - projectsCallback = function (projects) { - var groupsCallback; - groupsCallback = function (groups) { - var data; - data = groups.concat(projects); - return finalCallback(data); - }; - return Api.groups(query.term, {}, groupsCallback); - }; - } else { - projectsCallback = finalCallback; - } - if (_this.groupId) { - return Api.groupProjects(_this.groupId, query.term, projectsCallback); - } else { - return Api.projects(query.term, { - order_by: _this.orderBy, - with_issues_enabled: _this.withIssuesEnabled, - with_merge_requests_enabled: _this.withMergeRequestsEnabled, - membership: !_this.allProjects, - }, projectsCallback); - } + return Api.groups(query.term, {}, groupsCallback); }; - })(this), - id: function(project) { - if (simpleFilter) return project.id; - return JSON.stringify({ - name: project.name, - url: project.web_url, - }); - }, - text: function (project) { - return project.name_with_namespace || project.name; - }, - dropdownCssClass: "ajax-project-dropdown" + } else { + projectsCallback = finalCallback; + } + if (_this.groupId) { + return Api.groupProjects(_this.groupId, query.term, projectsCallback); + } else { + return Api.projects(query.term, { + order_by: _this.orderBy, + with_issues_enabled: _this.withIssuesEnabled, + with_merge_requests_enabled: _this.withMergeRequestsEnabled, + membership: !_this.allProjects, + }, projectsCallback); + } + }; + })(this), + id: function(project) { + if (simpleFilter) return project.id; + return JSON.stringify({ + name: project.name, + url: project.web_url, }); - if (simpleFilter) return select; - return new ProjectSelectComboButton(select); - }); - } - - return ProjectSelect; - })(); -}).call(window); + }, + text: function (project) { + return project.name_with_namespace || project.name; + }, + dropdownCssClass: "ajax-project-dropdown" + }); + if (simpleFilter) return select; + return new ProjectSelectComboButton(select); + }); +} diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js deleted file mode 100644 index 3a51c1f26ac..00000000000 --- a/app/assets/javascripts/project_show.js +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife */ - -(function() { - this.ProjectShow = (function() { - function ProjectShow() {} - - return ProjectShow; - })(); -}).call(window); - -// I kept class for future diff --git a/app/assets/javascripts/project_variables.js b/app/assets/javascripts/project_variables.js index 4ee2e49306d..567c311f119 100644 --- a/app/assets/javascripts/project_variables.js +++ b/app/assets/javascripts/project_variables.js @@ -1,43 +1,39 @@ -(() => { - const HIDDEN_VALUE_TEXT = '******'; - class ProjectVariables { - constructor() { - this.$revealBtn = $('.js-btn-toggle-reveal-values'); - this.$revealBtn.on('click', this.toggleRevealState.bind(this)); - } +const HIDDEN_VALUE_TEXT = '******'; + +export default class ProjectVariables { + constructor() { + this.$revealBtn = $('.js-btn-toggle-reveal-values'); + this.$revealBtn.on('click', this.toggleRevealState.bind(this)); + } - toggleRevealState(e) { - e.preventDefault(); + toggleRevealState(e) { + e.preventDefault(); - const oldStatus = this.$revealBtn.attr('data-status'); - let newStatus = 'hidden'; - let newAction = 'Reveal Values'; + const oldStatus = this.$revealBtn.attr('data-status'); + let newStatus = 'hidden'; + let newAction = 'Reveal Values'; - if (oldStatus === 'hidden') { - newStatus = 'revealed'; - newAction = 'Hide Values'; - } + if (oldStatus === 'hidden') { + newStatus = 'revealed'; + newAction = 'Hide Values'; + } - this.$revealBtn.attr('data-status', newStatus); + this.$revealBtn.attr('data-status', newStatus); - const $variables = $('.variable-value'); + const $variables = $('.variable-value'); - $variables.each((_, variable) => { - const $variable = $(variable); - let newText = HIDDEN_VALUE_TEXT; + $variables.each((_, variable) => { + const $variable = $(variable); + let newText = HIDDEN_VALUE_TEXT; - if (newStatus === 'revealed') { - newText = $variable.attr('data-value'); - } + if (newStatus === 'revealed') { + newText = $variable.attr('data-value'); + } - $variable.text(newText); - }); + $variable.text(newText); + }); - this.$revealBtn.text(newAction); - } + this.$revealBtn.text(newAction); } - - window.gl = window.gl || {}; - window.gl.ProjectVariables = ProjectVariables; -})(); +} diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 9612b8d8514..56baa19f864 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -54,7 +54,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController if current_user log_audit_event(current_user, with: :saml) # Update SAML identity if data has changed. - identity = current_user.identities.find_by(extern_uid: oauth['uid'], provider: :saml) + identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take if identity.nil? current_user.identities.create(extern_uid: oauth['uid'], provider: :saml) redirect_to profile_account_path, notice: 'Authentication method updated' @@ -98,7 +98,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def handle_omniauth if current_user # Add new authentication method - current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider']) + current_user.identities + .with_extern_uid(oauth['provider'], oauth['uid']) + .first_or_create(extern_uid: oauth['uid']) log_audit_event(current_user, with: oauth['provider']) redirect_to profile_account_path, notice: 'Authentication method updated' else diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 28920877635..5f4afd2cdee 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -57,6 +57,7 @@ class Projects::CommitsController < Projects::ApplicationController @repository.commits(@ref, path: @path, limit: @limit, offset: @offset) end + @commits = @commits.with_pipeline_status @commits = prepare_commits_for_rendering(@commits) end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 22de6680511..abe4e5245b1 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -80,7 +80,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo def commits # Get commits from repository # or from cache if already merged - @commits = prepare_commits_for_rendering(@merge_request.commits) + @commits = + prepare_commits_for_rendering(@merge_request.commits.with_pipeline_status) render json: { html: view_to_html_string('projects/merge_requests/_commits') } end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index f7a9c98629d..b51261a49e0 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -74,7 +74,11 @@ class Projects::WikisController < Projects::ApplicationController def history @page = @project_wiki.find_page(params[:id]) - unless @page + if @page + @page_versions = Kaminari.paginate_array(@page.versions(page: params[:page]), + total_count: @page.count_versions) + .page(params[:page]) + else redirect_to( project_wiki_path(@project, :home), notice: "Page not found" @@ -101,7 +105,7 @@ class Projects::WikisController < Projects::ApplicationController # Call #wiki to make sure the Wiki Repo is initialized @project_wiki.wiki - @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15)) + @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages(limit: 15)) rescue ProjectWiki::CouldNotCreateWikiError flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." redirect_to project_path(@project) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 4dd573c61f1..636316da80a 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -6,11 +6,6 @@ # See 'detailed_status?` method and `Gitlab::Ci::Status` module. # module CiStatusHelper - def ci_status_path(pipeline) - project = pipeline.project - project_pipeline_path(project, pipeline) - end - def ci_label_for_status(status) if detailed_status?(status) return status.label diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 19814864e50..c77c837ff3f 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -149,34 +149,70 @@ module Ci end end - # ref can't be HEAD or SHA, can only be branch/tag name - scope :latest, ->(ref = nil) do - max_id = unscope(:select) - .select("max(#{quoted_table_name}.id)") - .group(:ref, :sha) - - if ref - where(ref: ref, id: max_id.where(ref: ref)) - else - where(id: max_id) - end - end scope :internal, -> { where(source: internal_sources) } + # Returns the pipelines in descending order (= newest first), optionally + # limited to a number of references. + # + # ref - The name (or names) of the branch(es)/tag(s) to limit the list of + # pipelines to. + def self.newest_first(ref = nil) + relation = order(id: :desc) + + ref ? relation.where(ref: ref) : relation + end + def self.latest_status(ref = nil) - latest(ref).status + newest_first(ref).pluck(:status).first end def self.latest_successful_for(ref) - success.latest(ref).order(id: :desc).first + newest_first(ref).success.take end def self.latest_successful_for_refs(refs) - success.latest(refs).order(id: :desc).each_with_object({}) do |pipeline, hash| + relation = newest_first(refs).success + + relation.each_with_object({}) do |pipeline, hash| hash[pipeline.ref] ||= pipeline end end + # Returns a Hash containing the latest pipeline status for every given + # commit. + # + # The keys of this Hash are the commit SHAs, the values the statuses. + # + # commits - The list of commit SHAs to get the status for. + # ref - The ref to scope the data to (e.g. "master"). If the ref is not + # given we simply get the latest status for the commits, regardless + # of what refs their pipelines belong to. + def self.latest_status_per_commit(commits, ref = nil) + p1 = arel_table + p2 = arel_table.alias + + # This LEFT JOIN will filter out all but the newest row for every + # combination of (project_id, sha) or (project_id, sha, ref) if a ref is + # given. + cond = p1[:sha].eq(p2[:sha]) + .and(p1[:project_id].eq(p2[:project_id])) + .and(p1[:id].lt(p2[:id])) + + cond = cond.and(p1[:ref].eq(p2[:ref])) if ref + join = p1.join(p2, Arel::Nodes::OuterJoin).on(cond) + + relation = select(:sha, :status) + .where(sha: commits) + .where(p2[:id].eq(nil)) + .joins(join.join_sources) + + relation = relation.where(ref: ref) if ref + + relation.each_with_object({}) do |row, hash| + hash[row[:sha]] = row[:status] + end + end + def self.truncate_sha(sha) sha[0...8] end diff --git a/app/models/commit.rb b/app/models/commit.rb index 6dba154a6ea..a31ebe9cc87 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -80,6 +80,7 @@ class Commit @raw = raw_commit @project = project + @statuses = {} end def id @@ -236,11 +237,13 @@ class Commit end def status(ref = nil) - @statuses ||= {} - return @statuses[ref] if @statuses.key?(ref) - @statuses[ref] = pipelines.latest_status(ref) + @statuses[ref] = project.pipelines.latest_status_per_commit(id, ref)[id] + end + + def set_status_for_ref(ref, status) + @statuses[ref] = status end def signature diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb new file mode 100644 index 00000000000..dd93af9df64 --- /dev/null +++ b/app/models/commit_collection.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# A collection of Commit instances for a specific project and Git reference. +class CommitCollection + include Enumerable + + attr_reader :project, :ref, :commits + + # project - The project the commits belong to. + # commits - The Commit instances to store. + # ref - The name of the ref (e.g. "master"). + def initialize(project, commits, ref = nil) + @project = project + @commits = commits + @ref = ref + end + + def each(&block) + commits.each(&block) + end + + # Sets the pipeline status for every commit. + # + # Setting this status ahead of time removes the need for running a query for + # every commit we're displaying. + def with_pipeline_status + statuses = project.pipelines.latest_status_per_commit(map(&:id), ref) + + each do |commit| + commit.set_status_for_ref(ref, statuses[commit.id]) + end + + self + end + + def respond_to_missing?(message, inc_private = false) + commits.respond_to?(message, inc_private) + end + + # rubocop:disable GitlabSecurity/PublicSend + def method_missing(message, *args, &block) + commits.public_send(message, *args, &block) + end +end diff --git a/app/models/fork_network_member.rb b/app/models/fork_network_member.rb index 6a9b52a1ef8..eb9417dc34f 100644 --- a/app/models/fork_network_member.rb +++ b/app/models/fork_network_member.rb @@ -4,4 +4,14 @@ class ForkNetworkMember < ActiveRecord::Base belongs_to :forked_from_project, class_name: 'Project' validates :fork_network, :project, presence: true + + after_destroy :cleanup_fork_network + + private + + def cleanup_fork_network + # Explicitly using `#count` makes sure we have the correct number if the + # relation was loaded in the fork_network. + fork_network.destroy if fork_network.fork_network_members.count == 0 + end end diff --git a/app/models/identity.rb b/app/models/identity.rb index ac8094b610e..ff811e19f8a 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -1,18 +1,27 @@ class Identity < ActiveRecord::Base include Sortable include CaseSensitivity + belongs_to :user validates :provider, presence: true - validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider } + validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider, case_sensitive: false } validates :user_id, uniqueness: { scope: :provider } + scope :with_provider, ->(provider) { where(provider: provider) } scope :with_extern_uid, ->(provider, extern_uid) do - extern_uid = Gitlab::LDAP::Person.normalize_dn(extern_uid) if provider.starts_with?('ldap') - where(extern_uid: extern_uid, provider: provider) + iwhere(extern_uid: normalize_uid(provider, extern_uid)).with_provider(provider) end def ldap? provider.starts_with?('ldap') end + + def self.normalize_uid(provider, uid) + if provider.to_s.starts_with?('ldap') + Gitlab::LDAP::Person.normalize_dn(uid) + else + uid.to_s + end + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 1eda0f9cbbd..5382f5cc627 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -284,8 +284,10 @@ class MergeRequestDiff < ActiveRecord::Base def load_commits commits = st_commits.presence || merge_request_diff_commits + commits = commits.map { |commit| Commit.from_hash(commit.to_hash, project) } - commits.map { |commit| Commit.from_hash(commit.to_hash, project) } + CommitCollection + .new(merge_request.source_project, commits, merge_request.source_branch) end def save_diffs diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 3eecbea8cbf..a0af749a93f 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -76,8 +76,8 @@ class ProjectWiki # Returns an Array of Gitlab WikiPage instances or an # empty Array if this Wiki has no pages. - def pages - wiki.pages.map { |page| WikiPage.new(self, page, true) } + def pages(limit: nil) + wiki.pages(limit: limit).map { |page| WikiPage.new(self, page, true) } end # Finds a page within the repository based on a tile diff --git a/app/models/repository.rb b/app/models/repository.rb index 3a89fa9264b..35ee12bdfa1 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -132,7 +132,8 @@ class Repository commits = Gitlab::Git::Commit.where(options) commits = Commit.decorate(commits, @project) if commits.present? - commits + + CommitCollection.new(project, commits, ref) end def commits_between(from, to) @@ -148,11 +149,14 @@ class Repository end raw_repository.gitaly_migrate(:commits_by_message) do |is_enabled| - if is_enabled - find_commits_by_message_by_gitaly(query, ref, path, limit, offset) - else - find_commits_by_message_by_shelling_out(query, ref, path, limit, offset) - end + commits = + if is_enabled + find_commits_by_message_by_gitaly(query, ref, path, limit, offset) + else + find_commits_by_message_by_shelling_out(query, ref, path, limit, offset) + end + + CommitCollection.new(project, commits, ref) end end diff --git a/app/models/user.rb b/app/models/user.rb index be8112749bf..71c34766451 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -269,8 +269,7 @@ class User < ActiveRecord::Base end def for_github_id(id) - joins(:identities) - .where(identities: { provider: :github, extern_uid: id.to_s }) + joins(:identities).merge(Identity.with_extern_uid(:github, id)) end # Find a User by their primary email or any associated secondary email diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 5f710961f95..bdfef677ef3 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -127,19 +127,24 @@ class WikiPage @version ||= @page.version end - # Returns an array of Gitlab Commit instances. - def versions + def versions(options = {}) return [] unless persisted? - wiki.wiki.page_versions(@page.path) + wiki.wiki.page_versions(@page.path, options) end - def commit - versions.first + def count_versions + return [] unless persisted? + + wiki.wiki.count_page_versions(@page.path) + end + + def last_version + @last_version ||= versions(limit: 1).first end def last_commit_sha - commit&.sha + last_version&.sha end # Returns the Date that this latest version was @@ -151,7 +156,7 @@ class WikiPage # Returns boolean True or False if this instance # is an old version of the page. def historical? - @page.historical? && versions.first.sha != version.sha + @page.historical? && last_version.sha != version.sha end # Returns boolean True or False if this instance diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 5957f612e84..d3a8ae8d7c6 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -60,15 +60,8 @@ module Projects # Notifications project.send_move_instructions(@old_path) - # Move main repository - # TODO: check storage type and NOOP when not using Legacy - unless move_repo_folder(@old_path, @new_path) - raise TransferError.new('Cannot move project') - end - - # Move wiki repo also if present - # TODO: check storage type and NOOP when not using Legacy - move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki") + # Directories on disk + move_project_folders(project) # Move missing group labels to project Labels::TransferService.new(current_user, @old_group, project).execute @@ -131,5 +124,20 @@ module Projects def execute_system_hooks SystemHooksService.new.execute_hooks_for(project, :transfer) end + + def move_project_folders(project) + return if project.hashed_storage?(:repository) + + # Move main repository + unless move_repo_folder(@old_path, @new_path) + raise TransferError.new("Cannot move project") + end + + # Disk path is changed; we need to ensure we reload it + project.reload_repository! + + # Move wiki repo also if present + move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki") + end end end diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index df2bf27be9d..6d8fad0eb8d 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -99,7 +99,7 @@ %td.build-link - if project - = link_to ci_status_path(build.pipeline) do + = link_to pipeline_path(build.pipeline) do %strong= build.pipeline.short_sha %td.timestamp diff --git a/app/views/projects/wikis/_pages_wiki_page.html.haml b/app/views/projects/wikis/_pages_wiki_page.html.haml index 0a1ccbc5f1c..efa16d38f84 100644 --- a/app/views/projects/wikis/_pages_wiki_page.html.haml +++ b/app/views/projects/wikis/_pages_wiki_page.html.haml @@ -2,4 +2,4 @@ = link_to wiki_page.title, project_wiki_path(@project, wiki_page) %small (#{wiki_page.format}) .pull-right - %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.commit.authored_date) }).html_safe + %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index 9ee09262324..969a1677d9a 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -21,7 +21,7 @@ %th= _("Last updated") %th= _("Format") %tbody - - @page.versions.each_with_index do |version, index| + - @page_versions.each_with_index do |version, index| - commit = version %tr %td @@ -37,5 +37,6 @@ %td %strong = version.format += paginate @page_versions, theme: 'gitlab' = render 'sidebar' diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index de15fc99eda..b3b83cee81a 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -11,8 +11,8 @@ .nav-text %h2.wiki-page-title= @page.title.capitalize %span.wiki-last-edit-by - = (_("Last edited by %{name}") % { name: "<strong>#{@page.commit.author_name}</strong>" }).html_safe - #{time_ago_with_tooltip(@page.commit.authored_date)} + = (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe + #{time_ago_with_tooltip(@page.last_version.authored_date)} .nav-controls = render 'main_links' |