Commit 24f22fa2 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-10-15

# Conflicts:
#	app/services/clusters/create_service.rb
#	locale/gitlab.pot
#	spec/requests/api/settings_spec.rb

[ci skip]
parents c7177a4c 0602fcb6
...@@ -86,9 +86,6 @@ gem 'net-ldap' ...@@ -86,9 +86,6 @@ gem 'net-ldap'
# Only used to compute wiki page slugs # Only used to compute wiki page slugs
gem 'gitlab-gollum-lib', '~> 4.2', require: false gem 'gitlab-gollum-lib', '~> 4.2', require: false
# Language detection
gem 'github-linguist', '~> 5.3.3', require: 'linguist'
# API # API
gem 'grape', '~> 1.1' gem 'grape', '~> 1.1'
gem 'grape-entity', '~> 0.7.1' gem 'grape-entity', '~> 0.7.1'
...@@ -156,6 +153,7 @@ gem 'rouge', '~> 3.1' ...@@ -156,6 +153,7 @@ gem 'rouge', '~> 3.1'
gem 'truncato', '~> 0.7.9' gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0' gem 'bootstrap_form', '~> 2.7.0'
gem 'nokogiri', '~> 1.8.2' gem 'nokogiri', '~> 1.8.2'
gem 'escape_utils', '~> 1.1'
# Calendar rendering # Calendar rendering
gem 'icalendar' gem 'icalendar'
......
...@@ -301,11 +301,6 @@ GEM ...@@ -301,11 +301,6 @@ GEM
gitaly-proto (0.118.1) gitaly-proto (0.118.1)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
mime-types (>= 1.19)
rugged (>= 0.25.1)
github-markup (1.7.0) github-markup (1.7.0)
gitlab-flowdock-git-hook (1.0.1) gitlab-flowdock-git-hook (1.0.1)
flowdock (~> 0.7) flowdock (~> 0.7)
...@@ -1039,6 +1034,7 @@ DEPENDENCIES ...@@ -1039,6 +1034,7 @@ DEPENDENCIES
elasticsearch-rails (~> 0.1.9) elasticsearch-rails (~> 0.1.9)
email_reply_trimmer (~> 0.1) email_reply_trimmer (~> 0.1)
email_spec (~> 2.2.0) email_spec (~> 2.2.0)
escape_utils (~> 1.1)
factory_bot_rails (~> 4.8.2) factory_bot_rails (~> 4.8.2)
faraday (~> 0.12) faraday (~> 0.12)
faraday_middleware-aws-signers-v4 faraday_middleware-aws-signers-v4
...@@ -1062,7 +1058,6 @@ DEPENDENCIES ...@@ -1062,7 +1058,6 @@ DEPENDENCIES
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.118.1) gitaly-proto (~> 0.118.1)
github-linguist (~> 5.3.3)
github-markup (~> 1.7.0) github-markup (~> 1.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
...@@ -1224,4 +1219,4 @@ DEPENDENCIES ...@@ -1224,4 +1219,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.16.4 1.16.6
...@@ -304,11 +304,6 @@ GEM ...@@ -304,11 +304,6 @@ GEM
gitaly-proto (0.118.1) gitaly-proto (0.118.1)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
mime-types (>= 1.19)
rugged (>= 0.25.1)
github-markup (1.7.0) github-markup (1.7.0)
gitlab-flowdock-git-hook (1.0.1) gitlab-flowdock-git-hook (1.0.1)
flowdock (~> 0.7) flowdock (~> 0.7)
...@@ -1048,6 +1043,7 @@ DEPENDENCIES ...@@ -1048,6 +1043,7 @@ DEPENDENCIES
elasticsearch-rails (~> 0.1.9) elasticsearch-rails (~> 0.1.9)
email_reply_trimmer (~> 0.1) email_reply_trimmer (~> 0.1)
email_spec (~> 2.2.0) email_spec (~> 2.2.0)
escape_utils (~> 1.1)
factory_bot_rails (~> 4.8.2) factory_bot_rails (~> 4.8.2)
faraday (~> 0.12) faraday (~> 0.12)
faraday_middleware-aws-signers-v4 faraday_middleware-aws-signers-v4
...@@ -1071,7 +1067,6 @@ DEPENDENCIES ...@@ -1071,7 +1067,6 @@ DEPENDENCIES
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.118.1) gitaly-proto (~> 0.118.1)
github-linguist (~> 5.3.3)
github-markup (~> 1.7.0) github-markup (~> 1.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
...@@ -1233,4 +1228,4 @@ DEPENDENCIES ...@@ -1233,4 +1228,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.16.4 1.16.6
...@@ -13,11 +13,11 @@ export default () => { ...@@ -13,11 +13,11 @@ export default () => {
if (editBlobForm.length) { if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot'); const urlRoot = editBlobForm.data('relativeUrlRoot');
const assetsPath = editBlobForm.data('assetsPrefix'); const assetsPath = editBlobForm.data('assetsPrefix');
const blobLanguage = editBlobForm.data('blobLanguage'); const filePath = editBlobForm.data('blobFilename')
const currentAction = $('.js-file-title').data('currentAction'); const currentAction = $('.js-file-title').data('currentAction');
const projectId = editBlobForm.data('project-id'); const projectId = editBlobForm.data('project-id');
new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction, projectId); new EditBlob(`${urlRoot}${assetsPath}`, filePath, currentAction, projectId);
new NewCommitForm(editBlobForm); new NewCommitForm(editBlobForm);
} }
......
...@@ -5,6 +5,7 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -5,6 +5,7 @@ import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import TemplateSelectorMediator from '../blob/file_template_mediator'; import TemplateSelectorMediator from '../blob/file_template_mediator';
import getModeByFileExtension from '~/lib/utils/ace_utils';
export default class EditBlob { export default class EditBlob {
constructor(assetsPath, aceMode, currentAction, projectId) { constructor(assetsPath, aceMode, currentAction, projectId) {
...@@ -14,9 +15,10 @@ export default class EditBlob { ...@@ -14,9 +15,10 @@ export default class EditBlob {
this.initFileSelectors(currentAction, projectId); this.initFileSelectors(currentAction, projectId);
} }
configureAceEditor(aceMode, assetsPath) { configureAceEditor(filePath, assetsPath) {
ace.config.set('modePath', `${assetsPath}/ace`); ace.config.set('modePath', `${assetsPath}/ace`);
ace.config.loadModule('ace/ext/searchbox'); ace.config.loadModule('ace/ext/searchbox');
ace.config.loadModule('ace/ext/modelist');
this.editor = ace.edit('editor'); this.editor = ace.edit('editor');
...@@ -25,8 +27,8 @@ export default class EditBlob { ...@@ -25,8 +27,8 @@ export default class EditBlob {
this.editor.focus(); this.editor.focus();
if (aceMode) { if (filePath) {
this.editor.getSession().setMode(`ace/mode/${aceMode}`); this.editor.getSession().setMode(getModeByFileExtension(filePath));
} }
} }
......
import DirtySubmitForm from './dirty_submit_form';
class DirtySubmitCollection {
constructor(forms) {
this.forms = forms;
this.dirtySubmits = [];
this.forms.forEach(form => this.dirtySubmits.push(new DirtySubmitForm(form)));
}
}
export default DirtySubmitCollection;
import DirtySubmitCollection from './dirty_submit_collection';
import DirtySubmitForm from './dirty_submit_form';
export default function dirtySubmitFactory(formOrForms) {
const isCollection = formOrForms instanceof NodeList || formOrForms instanceof Array;
const DirtySubmitClass = isCollection ? DirtySubmitCollection : DirtySubmitForm;
return new DirtySubmitClass(formOrForms);
}
import _ from 'underscore';
class DirtySubmitForm {
constructor(form) {
this.form = form;
this.dirtyInputs = [];
this.isDisabled = true;
this.init();
}
init() {
this.inputs = this.form.querySelectorAll('input, textarea, select');
this.submits = this.form.querySelectorAll('input[type=submit], button[type=submit]');
this.inputs.forEach(DirtySubmitForm.initInput);
this.toggleSubmission();
this.registerListeners();
}
registerListeners() {
const throttledUpdateDirtyInput = _.throttle(
event => this.updateDirtyInput(event),
DirtySubmitForm.THROTTLE_DURATION,
);
this.form.addEventListener('input', throttledUpdateDirtyInput);
this.form.addEventListener('submit', event => this.formSubmit(event));
}
updateDirtyInput(event) {
const input = event.target;
if (!input.dataset.dirtySubmitOriginalValue) return;
this.updateDirtyInputs(input);
this.toggleSubmission();
}
updateDirtyInputs(input) {
const { name } = input;
const isDirty =
input.dataset.dirtySubmitOriginalValue !== DirtySubmitForm.inputCurrentValue(input);
const indexOfInputName = this.dirtyInputs.indexOf(name);
const isExisting = indexOfInputName !== -1;
if (isDirty && !isExisting) this.dirtyInputs.push(name);
if (!isDirty && isExisting) this.dirtyInputs.splice(indexOfInputName, 1);
}
toggleSubmission() {
this.isDisabled = this.dirtyInputs.length === 0;
this.submits.forEach(element => {
element.disabled = this.isDisabled;
});
}
formSubmit(event) {
if (this.isDisabled) {
event.preventDefault();
event.stopImmediatePropagation();
}
return !this.isDisabled;
}
static initInput(element) {
element.dataset.dirtySubmitOriginalValue = DirtySubmitForm.inputCurrentValue(element);
}
static isInputCheckable(input) {
return input.type === 'checkbox' || input.type === 'radio';
}
static inputCurrentValue(input) {
return DirtySubmitForm.isInputCheckable(input) ? input.checked.toString() : input.value;
}
}
DirtySubmitForm.THROTTLE_DURATION = 500;
export default DirtySubmitForm;
/*= require ace/ace */ /*= require ace/ace */
/*= require ace/ext-modelist */
/*= require ace/ext-searchbox */ /*= require ace/ext-searchbox */
/*= require ./ace/ace_config_paths */ /*= require ./ace/ace_config_paths */
/* global ace */
export default function getModeByFileExtension(path) {
const modelist = ace.require("ace/ext/modelist");
return modelist.getModeForPath(path).mode;
};
...@@ -205,7 +205,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -205,7 +205,6 @@ document.addEventListener('DOMContentLoaded', () => {
$('.navbar-toggler').on('click', () => { $('.navbar-toggler').on('click', () => {
$('.header-content').toggleClass('menu-expanded'); $('.header-content').toggleClass('menu-expanded');
gl.lazyLoader.loadCheck();
}); });
// Show/hide comments on diff // Show/hide comments on diff
......
...@@ -5,6 +5,7 @@ import Vue from 'vue'; ...@@ -5,6 +5,7 @@ import Vue from 'vue';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import flash from '~/flash'; import flash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import getModeByFileExtension from '~/lib/utils/ace_utils';
(global => { (global => {
global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts = global.mergeConflicts || {};
...@@ -72,7 +73,7 @@ import { __ } from '~/locale'; ...@@ -72,7 +73,7 @@ import { __ } from '~/locale';
this.fileLoaded = true; this.fileLoaded = true;
this.editor = ace.edit(content); this.editor = ace.edit(content);
this.editor.$blockScrolling = Infinity; // Turn off annoying warning this.editor.$blockScrolling = Infinity; // Turn off annoying warning
this.editor.getSession().setMode(`ace/mode/${data.blob_ace_mode}`); this.editor.getSession().setMode(getModeByFileExtension(data.new_path));
this.editor.on('change', () => { this.editor.on('change', () => {
this.saveDiffResolution(); this.saveDiffResolution();
}); });
......
...@@ -2,6 +2,7 @@ import groupAvatar from '~/group_avatar'; ...@@ -2,6 +2,7 @@ import groupAvatar from '~/group_avatar';
import TransferDropdown from '~/groups/transfer_dropdown'; import TransferDropdown from '~/groups/transfer_dropdown';
import initConfirmDangerModal from '~/confirm_danger_modal'; import initConfirmDangerModal from '~/confirm_danger_modal';
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import { GROUP_BADGE } from '~/badges/constants'; import { GROUP_BADGE } from '~/badges/constants';
...@@ -10,5 +11,8 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -10,5 +11,8 @@ document.addEventListener('DOMContentLoaded', () => {
new TransferDropdown(); // eslint-disable-line no-new new TransferDropdown(); // eslint-disable-line no-new
initConfirmDangerModal(); initConfirmDangerModal();
initSettingsPanels(); initSettingsPanels();
dirtySubmitFactory(
document.querySelectorAll('.js-general-settings-form, .js-general-permissions-form'),
);
mountBadgeSettings(GROUP_BADGE); mountBadgeSettings(GROUP_BADGE);
}); });
...@@ -57,8 +57,6 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { ...@@ -57,8 +57,6 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
$allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right'); $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
$('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
$('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); $('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
} }
$this.attr('data-original-title', tooltipLabel); $this.attr('data-original-title', tooltipLabel);
......
import $ from 'jquery'; import $ from 'jquery';
import { __ } from './locale';
function expandSection($section) { function expandSection($section) {
$section.find('.js-settings-toggle').text('Collapse'); $section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Collapse'));
$section.find('.settings-content').off('scroll.expandSection').scrollTop(0); $section.find('.settings-content').off('scroll.expandSection').scrollTop(0);
$section.addClass('expanded'); $section.addClass('expanded');
if (!$section.hasClass('no-animate')) { if (!$section.hasClass('no-animate')) {
...@@ -11,7 +12,7 @@ function expandSection($section) { ...@@ -11,7 +12,7 @@ function expandSection($section) {
} }
function closeSection($section) { function closeSection($section) {
$section.find('.js-settings-toggle').text('Expand'); $section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Expand'));
$section.find('.settings-content').on('scroll.expandSection', () => expandSection($section)); $section.find('.settings-content').on('scroll.expandSection', () => expandSection($section));
$section.removeClass('expanded'); $section.removeClass('expanded');
if (!$section.hasClass('no-animate')) { if (!$section.hasClass('no-animate')) {
......
...@@ -42,6 +42,10 @@ ...@@ -42,6 +42,10 @@
margin-top: 0; margin-top: 0;
} }
.settings-title {
cursor: pointer;
}
button { button {
position: absolute; position: absolute;
top: 20px; top: 20px;
......
...@@ -2,31 +2,20 @@ ...@@ -2,31 +2,20 @@
class Projects::Clusters::ApplicationsController < Projects::ApplicationController class Projects::Clusters::ApplicationsController < Projects::ApplicationController
before_action :cluster before_action :cluster
before_action :application_class, only: [:create]
before_action :authorize_read_cluster! before_action :authorize_read_cluster!
before_action :authorize_create_cluster!, only: [:create] before_action :authorize_create_cluster!, only: [:create]
# rubocop: disable CodeReuse/ActiveRecord
def create def create
application = @application_class.find_or_initialize_by(cluster: @cluster) Clusters::Applications::CreateService
.new(@cluster, current_user, create_cluster_application_params)
if application.has_attribute?(:hostname) .execute(request)
application.hostname = params[:hostname]
end
if application.respond_to?(:oauth_application)
application.oauth_application = create_oauth_application(application)
end
application.save!
Clusters::Applications::ScheduleInstallationService.new(project, current_user).execute(application)
head :no_content head :no_content
rescue Clusters::Applications::CreateService::InvalidApplicationError
render_404
rescue StandardError rescue StandardError
head :bad_request head :bad_request
end end
# rubocop: enable CodeReuse/ActiveRecord
private private
...@@ -34,18 +23,7 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll ...@@ -34,18 +23,7 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll
@cluster ||= project.clusters.find(params[:id]) || render_404 @cluster ||= project.clusters.find(params[:id]) || render_404
end end
def application_class def create_cluster_application_params
@application_class ||= Clusters::Cluster::APPLICATIONS[params[:application]] || render_404 params.permit(:application, :hostname)
end
def create_oauth_application(application)
oauth_application_params = {
name: params[:application],
redirect_uri: application.callback_url,
scopes: 'api read_user openid',
owner: current_user
}
Applications::CreateService.new(current_user, oauth_application_params).execute(request)
end end
end end
...@@ -195,7 +195,7 @@ module BlobHelper ...@@ -195,7 +195,7 @@ module BlobHelper
{ {
'relative-url-root' => Rails.application.config.relative_url_root, 'relative-url-root' => Rails.application.config.relative_url_root,
'assets-prefix' => Gitlab::Application.config.assets.prefix, 'assets-prefix' => Gitlab::Application.config.assets.prefix,
'blob-language' => @blob && @blob.language.try(:ace_mode), 'blob-filename' => @blob && @blob.path,
'project-id' => project.id 'project-id' => project.id
} }
end end
......
...@@ -164,7 +164,7 @@ class Blob < SimpleDelegator ...@@ -164,7 +164,7 @@ class Blob < SimpleDelegator
if stored_externally? if stored_externally?
if rich_viewer if rich_viewer
rich_viewer.binary? rich_viewer.binary?
elsif Linguist::Language.find_by_extension(name).any? elsif known_extension?
false false
elsif _mime_type elsif _mime_type
_mime_type.binary? _mime_type.binary?
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module BlobLike module BlobLike
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Linguist::BlobHelper include Gitlab::BlobHelper
def id def id
raise NotImplementedError raise NotImplementedError
......
...@@ -880,10 +880,12 @@ class Repository ...@@ -880,10 +880,12 @@ class Repository
delegate :merged_branch_names, to: :raw_repository delegate :merged_branch_names, to: :raw_repository
def merge_base(first_commit_id, second_commit_id) def merge_base(*commits_or_ids)
first_commit_id = commit(first_commit_id).try(:id) || first_commit_id commit_ids = commits_or_ids.map do |commit_or_id|
second_commit_id = commit(second_commit_id).try(:id) || second_commit_id commit_or_id.is_a?(::Commit) ? commit_or_id.id : commit_or_id
raw_repository.merge_base(first_commit_id, second_commit_id) end
raw_repository.merge_base(*commit_ids)
end end
def ancestor?(ancestor_id, descendant_id) def ancestor?(ancestor_id, descendant_id)
......
...@@ -43,6 +43,6 @@ class IssueEntity < IssuableEntity ...@@ -43,6 +43,6 @@ class IssueEntity < IssuableEntity
end end
expose :preview_note_path do |issue| expose :preview_note_path do |issue|
preview_markdown_path(issue.project, quick_actions_target_type: 'Issue', quick_actions_target_id: issue.id) preview_markdown_path(issue.project, quick_actions_target_type: 'Issue', quick_actions_target_id: issue.iid)
end end
end end
...@@ -224,7 +224,7 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -224,7 +224,7 @@ class MergeRequestWidgetEntity < IssuableEntity
end end
expose :preview_note_path do |merge_request| expose :preview_note_path do |merge_request|
preview_markdown_path(merge_request.project, quick_actions_target_type: 'MergeRequest', quick_actions_target_id: merge_request.id) preview_markdown_path(merge_request.project, quick_actions_target_type: 'MergeRequest', quick_actions_target_id: merge_request.iid)
end end
expose :merge_commit_path do |merge_request| expose :merge_commit_path do |merge_request|
......
...@@ -11,6 +11,7 @@ module Applications ...@@ -11,6 +11,7 @@ module Applications
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# EE would override and use `request` arg
def execute(request) def execute(request)
Doorkeeper::Application.create(@params) Doorkeeper::Application.create(@params)
end end
......
# frozen_string_literal: true
module Clusters
module Applications
class CreateService
InvalidApplicationError = Class.new(StandardError)
attr_reader :cluster, :current_user, :params
def initialize(cluster, user, params = {})
@cluster = cluster
@current_user = user
@params = params.dup
end
def execute(request)
create_application.tap do |application|
if application.has_attribute?(:hostname)
application.hostname = params[:hostname]
end
if application.respond_to?(:oauth_application)
application.oauth_application = create_oauth_application(application, request)
end
application.save!
Clusters::Applications::ScheduleInstallationService.new(application).execute
end
end
private
def create_application
builder.call(@cluster)
end
def builder
builders[application_name] || raise(InvalidApplicationError, "invalid application: #{application_name}")
end
def builders
{
"helm" => -> (cluster) { cluster.application_helm || cluster.build_application_helm },
"ingress" => -> (cluster) { cluster.application_ingress || cluster.build_application_ingress },
"prometheus" => -> (cluster) { cluster.application_prometheus || cluster.build_application_prometheus },
"runner" => -> (cluster) { cluster.application_runner || cluster.build_application_runner },
"jupyter" => -> (cluster) { cluster.application_jupyter || cluster.build_application_jupyter }
}
end
def application_name
params[:application]
end
def create_oauth_application(application, request)
oauth_application_params = {
name: params[:application],
redirect_uri: application.callback_url,
scopes: 'api read_user openid',
owner: current_user
}
::Applications::CreateService.new(current_user, oauth_application_params).execute(request)
end
end
end
end
...@@ -2,8 +2,14 @@ ...@@ -2,8 +2,14 @@
module Clusters module Clusters
module Applications module Applications
class ScheduleInstallationService < ::BaseService class ScheduleInstallationService
def execute(application) attr_reader :application
def initialize(application)
@application = application
end
def execute
application.make_scheduled! application.make_scheduled!
ClusterInstallAppWorker.perform_async(application.name, application.id) ClusterInstallAppWorker.perform_async(application.name, application.id)
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
module Clusters module Clusters
class CreateService class CreateService
<<<<<<< HEAD
prepend EE::Clusters::CreateService prepend EE::Clusters::CreateService
attr_reader :current_user, :params attr_reader :current_user, :params
...@@ -18,6 +19,22 @@ module Clusters ...@@ -18,6 +19,22 @@ module Clusters
provider[:access_token] = access_token provider[:access_token] = access_token
end end
=======
attr_reader :current_user, :params
def initialize(user = nil, params = {})
@current_user, @params = user, params.dup
end
def execute(project:, access_token: nil)
raise ArgumentError.new(_('Instance does not support multiple Kubernetes clusters')) unless can_create_cluster?(project)
cluster_params = params.merge(user: current_user, projects: [project])
cluster_params[:provider_gcp_attributes].try do |provider|
provider[:access_token] = access_token
end
>>>>>>> upstream/master
create_cluster(cluster_params).tap do |cluster| create_cluster(cluster_params).tap do |cluster|
ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted? ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
end end
......
...@@ -10,11 +10,11 @@ ...@@ -10,11 +10,11 @@
.col-sm-10 .col-sm-10
= render 'shared/choose_group_avatar_button', f: f = render 'shared/choose_group_avatar_button', f: f
= render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group = render 'shared/old_visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false
.form-group.row .form-group.row
.offset-sm-2.col-sm-10 .offset-sm-2.col-sm-10
= render 'shared/allow_request_access', form: f = render 'shared/allow_request_access', form: f, bold_label: true
= render 'groups/group_admin_settings', f: f = render 'groups/group_admin_settings', f: f
......
...@@ -3,31 +3,31 @@ ...@@ -3,31 +3,31 @@
- expanded = Rails.env.test? - expanded = Rails.env.test?
%section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded' if expanded) } %section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded') }
.settings-header .settings-header
%h4 %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' }
= _('General') = _('Naming, visibility')
%button.btn.js-settings-toggle{ type: 'button' } %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand') = _('Collapse')
%p %p
= _('Update your group name, description, avatar, and other general settings.') = _('Update your group name, description, avatar, and visibility.')
.settings-content .settings-content
= render 'groups/settings/general' = render 'groups/settings/general'
%section.settings.gs-permissions.no-animate#js-permissions-settings{ class: ('expanded' if expanded) } %section.settings.gs-permissions.no-animate#js-permissions-settings{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' }
= _('Permissions') = _('Permissions, LFS, 2FA')
%button.btn.js-settings-toggle{ type: 'button' } %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand') = expanded ? _('Collapse') : _('Expand')
%p %p
= _('Enable or disable certain group features and choose access levels.') = _('Advanced permissions, Large File Storage and Two-Factor authentication settings.')
.settings-content .settings-content
= render 'groups/settings/permissions' = render 'groups/settings/permissions'
%section.settings.no-animate{ class: ('expanded' if expanded) } %section.settings.no-animate#js-badge-settings{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' }
= s_('GroupSettings|Badges') = s_('GroupSettings|Badges')
%button.btn.js-settings-toggle{ type: 'button' } %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
...@@ -39,8 +39,8 @@ ...@@ -39,8 +39,8 @@
%section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) } %section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' }
= _('Advanced') = _('Path, transfer, remove')
%button.btn.js-settings-toggle{ type: 'button' } %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand') = expanded ? _('Collapse') : _('Expand')
%p %p
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
.col-sm-10 .col-sm-10
= render 'shared/choose_group_avatar_button', f: f = render 'shared/choose_group_avatar_button', f: f
= render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group = render 'shared/old_visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false
= render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
......
...@@ -23,16 +23,6 @@ ...@@ -23,16 +23,6 @@
= f.submit 'Change group path', class: 'btn btn-warning' = f.submit 'Change group path', class: 'btn btn-warning'
.sub-section
%h4.danger-title Remove group
= form_tag(@group, method: :delete) do
%p
Removing group will cause all child projects and resources to be removed.
%br
%strong Removed group can not be restored!
= button_to 'Remove group', '#', class: 'btn btn-remove js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(@group) }
- if supports_nested_groups? - if supports_nested_groups?
.sub-section .sub-section
%h4.warning-title Transfer group %h4.warning-title Transfer group
...@@ -47,3 +37,13 @@ ...@@ -47,3 +37,13 @@
%li You will need to update your local repositories to point to the new location. %li You will need to update your local repositories to point to the new location.
%li If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility. %li If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.
= f.submit 'Transfer group', class: 'btn btn-warning' = f.submit 'Transfer group', class: 'btn btn-warning'
.sub-section
%h4.danger-title= _('Remove group')
= form_tag(@group, method: :delete) do
%p
= _('Removing group will cause all child projects and resources to be removed.')
%br
%strong= _('Removed group can not be restored!')
= button_to _('Remove group'), '#', class: 'btn btn-remove js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(@group) }
= form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f| = form_for @group, html: { multipart: true, class: 'gl-show-field-errors js-general-settings-form' }, authenticity_token: true do |f|
%input{ type: 'hidden', name: 'update_section', value: 'js-general-settings' } %input{ type: 'hidden', name: 'update_section', value: 'js-general-settings' }
= form_errors(@group) = form_errors(@group)
%fieldset %fieldset
.row .row
.form-group.col-md-9 .form-group.col-md-5
= f.label :name, class: 'label-bold' do = f.label :name, _('Group name'), class: 'label-bold'
Group name
= f.text_field :name, class: 'form-control' = f.text_field :name, class: 'form-control'
.form-group.col-md-3 .form-group.col-md-7
= f.label :id, class: 'label-bold' do = f.label :id, _('Group ID'), class: 'label-bold'
Group ID = f.text_field :id, class: 'form-control w-auto', readonly: true
= f.text_field :id, class: 'form-control', readonly: true
.form-group .row.prepend-top-8
= f.label :description, class: 'label-bold' do .form-group.col-md-9.append-bottom-0
Group description = f.label :description, _('Group description (optional)'), class: 'label-bold'
%span.light (optional) = f.text_area :description, class: 'form-control', rows: 3, maxlength: 250
= f.text_area :description, class: 'form-control', rows: 3, maxlength: 250
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
.form-group.row .form-group.prepend-top-default.append-bottom-20
.col-sm-12 .avatar-container.s90
.avatar-container.s160 = group_icon(@group, alt: '', class: 'avatar group-avatar s90')
= group_icon(@group, alt: '', class: 'avatar group-avatar s160') = f.label :avatar, _('Group avatar'), class: 'label-bold d-block'
%p.light = render 'shared/choose_group_avatar_button', f: f
- if @group.avatar? - if @group.avatar?
You can change the group avatar here %hr
- else = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-danger btn-inverted'
You can upload a group avatar here
= render 'shared/choose_group_avatar_button', f: f
- if @group.avatar?
%hr
= link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-danger btn-inverted'
= f.submit 'Save group', class: 'btn btn-success' = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
= f.submit _('Save changes'), class: 'btn btn-success mt-4 js-dirty-submit'
- docs_link_url = help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: docs_link_url }
%h5= _('Large File Storage')
%p= s_('Check the %{docs_link_start}documentation%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe }
.form-group.append-bottom-default
.form-check
= f.check_box :lfs_enabled, checked: @group.lfs_enabled?, class: 'form-check-input'
= f.label :lfs_enabled, class: 'form-check-label' do
%span
= _('Allow projects within this group to use Git LFS')
%br/
%span.text-muted= _('This setting can be overridden in each project.')
= form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f| = form_for @group, html: { multipart: true, class: 'gl-show-field-errors js-general-permissions-form' }, authenticity_token: true do |f|
%input{ type: 'hidden', name: 'update_section', value: 'js-permissions-settings' } %input{ type: 'hidden', name: 'update_section', value: 'js-permissions-settings' }
= form_errors(@group) = form_errors(@group)
%fieldset %fieldset
= render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group %h5= _('Permissions')
.form-group
= render 'shared/allow_request_access', form: f
.form-group.row .form-group.append-bottom-default
.offset-sm-2.col-sm-10 .form-check
= render 'shared/allow_request_access', form: f = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input'
= f.label :share_with_group_lock, class: 'form-check-label' do
.form-group.row %span
%label.col-form-label.col-sm-2.pt-0 - group_link = link_to @group.name, group_path(@group)
= s_('GroupSettings|Share with group lock') = s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link }
.col-sm-10 %br
.form-check %span.descr.text-muted= share_with_group_lock_help_text(@group)
= f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input'
= f.label :share_with_group_lock, class: 'form-check-label' do
%strong
- group_link = link_to @group.name, group_path(@group)
= s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link }
%br
%span.descr= share_with_group_lock_help_text(@group)
= render 'groups/group_admin_settings', f: f
= render 'groups/settings/lfs', f: f
= render 'groups/settings/two_factor_auth', f: f
= render_if_exists 'groups/member_lock_setting', f: f, group: @group = render_if_exists 'groups/member_lock_setting', f: f, group: @group
= f.submit 'Save group', class: 'btn btn-success' = f.submit _('Save changes'), class: 'btn btn-success prepend-top-default js-dirty-submit'
- docs_link_url = help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group')
- docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: docs_link_url }
%h5= _('Two-factor authentication')
%p= s_('Check the %{docs_link_start}documentation%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe }
.form-group
.form-check
= f.check_box :require_two_factor_authentication, class: 'form-check-input'
= f.label :require_two_factor_authentication, class: 'form-check-label' do
%span= _('Require all users in this group to setup Two-factor authentication')
.form-group
= f.label :two_factor_grace_period, _('Time before enforced'), class: 'label-bold'
= f.text_field :two_factor_grace_period, class: 'form-control form-control-sm w-auto'
.form-text.text-muted= _('Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication')
- label_class = local_assigns.fetch(:bold_label, false) ? 'font-weight-bold' : ''
.form-check .form-check
= form.check_box :request_access_enabled, class: 'form-check-input' = form.check_box :request_access_enabled, class: 'form-check-input'
= form.label :request_access_enabled, class: 'form-check-label' do = form.label :request_access_enabled, class: 'form-check-label' do
%strong Allow users to request access %span{ class: label_class }= _('Allow users to request access')
%br %br
%span.descr Allow users to request access if visibility is public or internal. %span.text-muted= _('Allow users to request access if visibility is public or internal.')
.form-group.row
.col-sm-2.col-form-label
= _('Visibility level')
= link_to icon('question-circle'), help_page_path("public_access/public_access")
.col-sm-10
= render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_visibility_level, form_model: form_model, with_label: with_label
- with_label = local_assigns.fetch(:with_label, true) - with_label = local_assigns.fetch(:with_label, true)
.form-group.row.visibility-level-setting .form-group.visibility-level-setting
- if with_label - if with_label
= f.label :visibility_level, class: 'col-form-label col-sm-2 pt-0' do = f.label :visibility_level, _('Visibility level'), class: 'label-bold append-bottom-0'
Visibility Level %p
= link_to icon('question-circle'), help_page_path("public_access/public_access") = _('Who can see this group?')
%div{ :class => (with_label ? "col-sm-10" : "col-sm-12") } - visibility_docs_path = help_page_path('public_access/public_access')
- if can_change_visibility_level - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: visibility_docs_path }
= render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model) = s_('Check the %{docs_link_start}documentation%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe }
- else - if can_change_visibility_level
%div = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model)
%span.info - else
= visibility_level_icon(visibility_level) %div
%strong %span.info
= visibility_level_label(visibility_level) = visibility_level_icon(visibility_level)
.light= visibility_level_description(visibility_level, form_model) %strong
= visibility_level_label(visibility_level)
.light= visibility_level_description(visibility_level, form_model)
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
= render 'shared/form_elements/description', model: @snippet, project: @project, form: f = render 'shared/form_elements/description', model: @snippet, project: @project, form: f
= render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet = render 'shared/old_visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet, with_label: false
.file-editor .file-editor
.form-group.row .form-group.row
......
---
title: Update group settings/edit page to new design
merge_request: 21115
author:
type: other
---
title: Allow Issue and Merge Request sidebar to be toggled from collapsed state
merge_request: 22353
author:
type: fixed
---
title: Fixes close/reopen quick actions preview for issues and merge_requests
merge_request: 22343
author: Jacopo Beschi @jacopo-beschi
type: fixed
---
title: Allow finding the common ancestor for multiple revisions through the API
merge_request: 22295
author:
type: changed
---
title: Remove Koding integration and documentation
merge_request: 22334
author:
type: removed
---
title: Remove Linguist gem, reducing Rails memory usage by 128MB per process
merge_request: 21008
author:
type: changed
...@@ -216,7 +216,7 @@ GET /projects/:id/repository/merge_base ...@@ -216,7 +216,7 @@ GET /projects/:id/repository/merge_base
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `refs` | array | yes | The refs to find the common ancestor of, for now only 2 refs are supported | | `refs` | array | yes | The refs to find the common ancestor of, multiple refs can be passed |
```bash ```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/merge_base?refs[]=304d257dcb821665ab5110318fc58a007bd104ed&refs[]=0031876facac3f2b2702a0e53a26e89939a42209" curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/merge_base?refs[]=304d257dcb821665ab5110318fc58a007bd104ed&refs[]=0031876facac3f2b2702a0e53a26e89939a42209"
......
...@@ -29,7 +29,7 @@ There are a few rules to get your merge request accepted: ...@@ -29,7 +29,7 @@ There are a few rules to get your merge request accepted:
to ask one of the [Merge request coaches][team]. to ask one of the [Merge request coaches][team].
1. It is recommended that you assign a maintainer that is from a different team than your own. 1. It is recommended that you assign a maintainer that is from a different team than your own.
This ensures that all code across GitLab is consistent and can be easily understood by all contributors. This ensures that all code across GitLab is consistent and can be easily understood by all contributors.
1. Keep in mind that maintainers are also going to perform a final code review. 1. Keep in mind that maintainers are also going to perform a final code review.
The ideal scenario is that the reviewer has already addressed any concerns The ideal scenario is that the reviewer has already addressed any concerns
the maintainer would have found, and the maintainer only has to perform the the maintainer would have found, and the maintainer only has to perform the
...@@ -97,6 +97,20 @@ first time. ...@@ -97,6 +97,20 @@ first time.
branch. Do not squash until the branch is ready to merge. Reviewers should be branch. Do not squash until the branch is ready to merge. Reviewers should be
able to read individual updates based on their earlier feedback. able to read individual updates based on their earlier feedback.
### Assigning a merge request for a review
If you want to have your merge request reviewed you can assign it to any reviewer. The list of reviewers can be found on [Engineering projects](https://about.gitlab.com/handbook/engineering/projects/) page.
You can also use `ready for review` label. That means that your merge request is ready to be reviewed and any reviewer can pick it. It is recommended to use that label only if there isn't time pressure and make sure the merge request is assigned to a reviewer.
When your merge request was reviewed and can be passed to a maintainer you can either pick a specific maintainer or use a label `ready for merge`.
It is responsibility of the author of a merge request that the merge request is reviewed. If it stays in `ready for review` state too long it is recommended to assign it to a specific reviewer.
### List of merge requests ready for review
Developers who have capacity can regularly check the list of [merge requests to review](https://gitlab.com/groups/gitlab-org/-/merge_requests?scope=all&utf8=%E2%9C%93&state=opened&label_name%5B%5D=ready%20for%20review&assignee_id=0) and assign any merge request they want to review.
### Reviewing code ### Reviewing code
Understand why the change is necessary (fixes a bug, improves the user Understand why the change is necessary (fixes a bug, improves the user
......
...@@ -19,6 +19,10 @@ Guidance on topics related to development. ...@@ -19,6 +19,10 @@ Guidance on topics related to development.
Learn about all the dependencies that make up our frontend, including some of our own custom built libraries. Learn about all the dependencies that make up our frontend, including some of our own custom built libraries.
## [Modules](modules/index.md)
Learn about all the internal JavaScript modules that make up our frontend.
## [Style guides](style/index.md) ## [Style guides](style/index.md)
Style guides to keep our code consistent. Style guides to keep our code consistent.
......
# Dirty Submit
> [Introduced][ce-21115] in GitLab 11.3.
> [dirty_submit][dirty-submit]
## Summary
Prevent submitting forms with no changes.
Currently handles `input`, `textarea` and `select` elements.
## Usage
```js
import dirtySubmitFactory from './dirty_submit/dirty_submit_form';
new DirtySubmitForm(document.querySelector('form'));
// or
new DirtySubmitForm(document.querySelectorAll('form'));
```
[ce-21115]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21115
[dirty-submit]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/dirty_submit/
\ No newline at end of file
# Modules
* [DirtySubmit](dirty_submit.md)
Disable form submits until there are unsaved changes.
\ No newline at end of file
...@@ -130,18 +130,13 @@ module API ...@@ -130,18 +130,13 @@ module API
success Entities::Commit success Entities::Commit
end end
params do params do
# For now we just support 2 refs passed, but `merge-base` supports
# multiple defining this as an Array instead of 2 separate params will
# make sure we don't need to deprecate this API in favor of one
# supporting multiple commits when this functionality gets added to
# Gitaly
requires :refs, type: Array[String] requires :refs, type: Array[String]
end end
get ':id/repository/merge_base' do get ':id/repository/merge_base' do
refs = params[:refs] refs = params[:refs]
unless refs.size == 2 if refs.size < 2
render_api_error!('Provide exactly 2 refs', 400) render_api_error!('Provide at least 2 refs', 400)
end end
merge_base = Gitlab::Git::MergeBase.new(user_project.repository, refs) merge_base = Gitlab::Git::MergeBase.new(user_project.repository, refs)
......
# This has been extracted from https://github.com/github/linguist/blob/master/lib/linguist/blob_helper.rb
module Gitlab
module BlobHelper
def extname
File.extname(name.to_s)
end
def known_extension?
LanguageData.extensions.include?(extname)
end
def viewable?
!large? && text?
end
MEGABYTE = 1024 * 1024
def large?
size.to_i > MEGABYTE
end
def binary?
# Large blobs aren't even loaded into memory
if data.nil?
true
# Treat blank files as text
elsif data == ""
false
# Charlock doesn't know what to think
elsif encoding.nil?
true
# If Charlock says its binary
else
detect_encoding[:type] == :binary
end
end
def text?
!binary?
end
def image?
['.png', '.jpg', '.jpeg', '.gif'].include?(extname.downcase)
end
# Internal: Lookup mime type for extension.
#
# Returns a MIME::Type
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def _mime_type
if defined? @_mime_type
@_mime_type
else
guesses = ::MIME::Types.type_for(extname.to_s)
# Prefer text mime types over binary
@_mime_type = guesses.detect { |type| type.ascii? } || guesses.first
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
# Public: Get the actual blob mime type
#
# Examples
#
# # => 'text/plain'
# # => 'text/html'
#
# Returns a mime type String.
def mime_type
_mime_type ? _mime_type.to_s : 'text/plain'
end
def binary_mime_type?
_mime_type ? _mime_type.binary? : false
end
def lines
@lines ||=
if viewable? && data
# `data` is usually encoded as ASCII-8BIT even when the content has
# been detected as a different encoding. However, we are not allowed
# to change the encoding of `data` because we've made the implicit
# guarantee that each entry in `lines` is encoded the same way as
# `data`.
#
# Instead, we re-encode each possible newline sequence as the
# detected encoding, then force them back to the encoding of `data`
# (usually a binary encoding like ASCII-8BIT). This means that the
# byte sequence will match how newlines are likely encoded in the
# file, but we don't have to change the encoding of `data` as far as
# Ruby is concerned. This allows us to correctly parse out each line
# without changing the encoding of `data`, and
# also--importantly--without having to duplicate many (potentially
# large) strings.
begin
data.split(encoded_newlines_re, -1)
rescue Encoding::ConverterNotFoundError
# The data is not splittable in the detected encoding. Assume it's
# one big line.
[data]
end
else
[]
end
end
def content_type
# rubocop:disable Style/MultilineTernaryOperator
# rubocop:disable Style/NestedTernaryOperator
@content_type ||= binary_mime_type? || binary? ? mime_type :
(encoding ? "text/plain; charset=#{encoding.downcase}" : "text/plain")
# rubocop:enable Style/NestedTernaryOperator
# rubocop:enable Style/MultilineTernaryOperator
end
def encoded_newlines_re
@encoded_newlines_re ||=
Regexp.union(["\r\n", "\r", "\n"].map { |nl| nl.encode(ruby_encoding, "ASCII-8BIT").force_encoding(data.encoding) })
end
def ruby_encoding
if hash = detect_encoding
hash[:ruby_encoding]
end
end
def encoding
if hash = detect_encoding
hash[:encoding]
end
end
def detect_encoding
@detect_encoding ||= CharlockHolmes::EncodingDetector.new.detect(data) if data # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def empty?
data.nil? || data == ""
end
end
end
...@@ -158,7 +158,6 @@ module Gitlab ...@@ -158,7 +158,6 @@ module Gitlab
json_hash.tap do |json_hash| json_hash.tap do |json_hash|
if opts[:full_content] if opts[:full_content]
json_hash[:content] = content json_hash[:content] = content
json_hash[:blob_ace_mode] = our_blob && our_blob.language.try(:ace_mode)
else else
json_hash[:sections] = sections if type.text? json_hash[:sections] = sections if type.text?
json_hash[:type] = type json_hash[:type] = type
......
...@@ -3,13 +3,13 @@ ...@@ -3,13 +3,13 @@
module Gitlab module Gitlab
module Git module Git
class Blob class Blob
include Linguist::BlobHelper include Gitlab::BlobHelper
include Gitlab::EncodingHelper include Gitlab::EncodingHelper
# This number is the maximum amount of data that we want to display to # This number is the maximum amount of data that we want to display to
# the user. We load as much as we can for encoding detection # the user. We load as much as we can for encoding detection and LFS
# (Linguist) and LFS pointer parsing. All other cases where we need full # pointer parsing. All other cases where we need full blob data should
# blob data should use load_all_data!. # use load_all_data!.
MAX_DATA_DISPLAY_SIZE = 10.megabytes MAX_DATA_DISPLAY_SIZE = 10.megabytes
# These limits are used as a heuristic to ignore files which can't be LFS # These limits are used as a heuristic to ignore files which can't be LFS
......
# Gitaly note: JV: no RPC's here.
module Gitlab
module Git
class BlobSnippet
include Linguist::BlobHelper
attr_accessor :ref
attr_accessor :lines
attr_accessor :filename
attr_accessor :startline
def initialize(ref, lines, startline, filename)
@ref, @lines, @startline, @filename = ref, lines, startline, filename
end
def data
lines&.join("\n")
end
def name
filename
end
def size
data.length
end
def mode
nil
end
end
end
end
...@@ -382,9 +382,9 @@ module Gitlab ...@@ -382,9 +382,9 @@ module Gitlab
end end
# Returns the SHA of the most recent common ancestor of +from+ and +to+ # Returns the SHA of the most recent common ancestor of +from+ and +to+
def merge_base(from, to) def merge_base(*commits)
wrapped_gitaly_errors do wrapped_gitaly_errors do
gitaly_repository_client.find_merge_base(from, to) gitaly_repository_client.find_merge_base(*commits)
end end
end end
......
# frozen_string_literal: true
module Gitlab
module LanguageData
EXTENSION_MUTEX = Mutex.new
class << self
include Gitlab::Utils::StrongMemoize
def extensions
EXTENSION_MUTEX.synchronize do
strong_memoize(:extensions) do
Set.new.tap do |set|
YAML.load_file(Rails.root.join('vendor', 'languages.yml')).each do |_name, details|
details['extensions']&.each do |ext|
next unless ext.start_with?('.')
set << ext.downcase
end
end
end
end
end
end
def clear_extensions!
EXTENSION_MUTEX.synchronize do
clear_memoization(:extensions)
end
end
end
end
end
...@@ -490,7 +490,7 @@ msgstr "" ...@@ -490,7 +490,7 @@ msgstr ""
msgid "AdminUsers|To confirm, type %{username}" msgid "AdminUsers|To confirm, type %{username}"
msgstr "" msgstr ""
msgid "Advanced" msgid "Advanced permissions, Large File Storage and Two-Factor authentication settings."
msgstr "" msgstr ""
msgid "Advanced settings" msgid "Advanced settings"
...@@ -511,6 +511,9 @@ msgstr "" ...@@ -511,6 +511,9 @@ msgstr ""
msgid "Allow commits from members who can merge to the target branch." msgid "Allow commits from members who can merge to the target branch."
msgstr "" msgstr ""
msgid "Allow projects within this group to use Git LFS"
msgstr ""
msgid "Allow public access to pipelines and job details, including output logs and artifacts" msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr "" msgstr ""
...@@ -520,6 +523,12 @@ msgstr "" ...@@ -520,6 +523,12 @@ msgstr ""
msgid "Allow requests to the local network from hooks and services." msgid "Allow requests to the local network from hooks and services."
msgstr "" msgstr ""
msgid "Allow users to request access"
msgstr ""
msgid "Allow users to request access if visibility is public or internal."
msgstr ""
msgid "Allows you to add and manage Kubernetes clusters." msgid "Allows you to add and manage Kubernetes clusters."
msgstr "" msgstr ""
...@@ -535,7 +544,11 @@ msgstr "" ...@@ -535,7 +544,11 @@ msgstr ""
msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import." msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "An SSH key will be automatically generated when the form is submitted. For more information, please refer to the documentation." msgid "An SSH key will be automatically generated when the form is submitted. For more information, please refer to the documentation."
=======
msgid "Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication"
>>>>>>> upstream/master
msgstr "" msgstr ""
msgid "An application called %{link_to_client} is requesting access to your GitLab account." msgid "An application called %{link_to_client} is requesting access to your GitLab account."
...@@ -1395,6 +1408,9 @@ msgstr "" ...@@ -1395,6 +1408,9 @@ msgstr ""
msgid "Chat" msgid "Chat"
msgstr "" msgstr ""
msgid "Check the %{docs_link_start}documentation%{docs_link_end}."
msgstr ""
msgid "Checking %{text} availability…" msgid "Checking %{text} availability…"
msgstr "" msgstr ""
...@@ -2886,12 +2902,15 @@ msgstr "" ...@@ -2886,12 +2902,15 @@ msgstr ""
msgid "Enable group Runners" msgid "Enable group Runners"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "Enable or disable certain group features and choose access levels." msgid "Enable or disable certain group features and choose access levels."
msgstr "" msgstr ""
msgid "Enable or disable the Pseudonymizer data collection." msgid "Enable or disable the Pseudonymizer data collection."
msgstr "" msgstr ""
=======
>>>>>>> upstream/master
msgid "Enable or disable version check and usage ping." msgid "Enable or disable version check and usage ping."
msgstr "" msgstr ""
...@@ -3879,6 +3898,9 @@ msgstr "" ...@@ -3879,6 +3898,9 @@ msgstr ""
msgid "Group avatar" msgid "Group avatar"
msgstr "" msgstr ""
msgid "Group description (optional)"
msgstr ""
msgid "Group details" msgid "Group details"
msgstr "" msgstr ""
...@@ -3888,6 +3910,9 @@ msgstr "" ...@@ -3888,6 +3910,9 @@ msgstr ""
msgid "Group maintainers can register group runners in the %{link}" msgid "Group maintainers can register group runners in the %{link}"
msgstr "" msgstr ""
msgid "Group name"
msgstr ""
msgid "Group: %{group_name}" msgid "Group: %{group_name}"
msgstr "" msgstr ""
...@@ -3939,9 +3964,6 @@ msgstr "" ...@@ -3939,9 +3964,6 @@ msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups" msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr "" msgstr ""
msgid "GroupSettings|Share with group lock"
msgstr ""
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup." msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
msgstr "" msgstr ""
...@@ -4480,6 +4502,9 @@ msgstr "" ...@@ -4480,6 +4502,9 @@ msgstr ""
msgid "Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. Existing project labels with the same title will be merged. This action cannot be reversed." msgid "Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. Existing project labels with the same title will be merged. This action cannot be reversed."
msgstr "" msgstr ""
msgid "Large File Storage"
msgstr ""
msgid "Last %d day" msgid "Last %d day"
msgid_plural "Last %d days" msgid_plural "Last %d days"
msgstr[0] "" msgstr[0] ""
...@@ -5108,6 +5133,9 @@ msgstr "" ...@@ -5108,6 +5133,9 @@ msgstr ""
msgid "Name:" msgid "Name:"
msgstr "" msgstr ""
msgid "Naming, visibility"
msgstr ""
msgid "Nav|Help" msgid "Nav|Help"
msgstr "" msgstr ""
...@@ -5553,6 +5581,9 @@ msgstr "" ...@@ -5553,6 +5581,9 @@ msgstr ""
msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key." msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
msgstr "" msgstr ""
msgid "Path, transfer, remove"
msgstr ""
msgid "Path:" msgid "Path:"
msgstr "" msgstr ""
...@@ -5580,6 +5611,9 @@ msgstr "" ...@@ -5580,6 +5611,9 @@ msgstr ""
msgid "Permissions" msgid "Permissions"
msgstr "" msgstr ""
msgid "Permissions, LFS, 2FA"
msgstr ""
msgid "Personal Access Token" msgid "Personal Access Token"
msgstr "" msgstr ""
...@@ -6488,12 +6522,21 @@ msgstr "" ...@@ -6488,12 +6522,21 @@ msgstr ""
msgid "Remove avatar" msgid "Remove avatar"
msgstr "" msgstr ""
msgid "Remove group"
msgstr ""
msgid "Remove priority" msgid "Remove priority"
msgstr "" msgstr ""
msgid "Remove project" msgid "Remove project"
msgstr "" msgstr ""
msgid "Removed group can not be restored!"
msgstr ""
msgid "Removing group will cause all child projects and resources to be removed."
msgstr ""
msgid "Rename" msgid "Rename"
msgstr "" msgstr ""
...@@ -6593,6 +6636,9 @@ msgstr "" ...@@ -6593,6 +6636,9 @@ msgstr ""
msgid "Requests Profiles" msgid "Requests Profiles"
msgstr "" msgstr ""
msgid "Require all users in this group to setup Two-factor authentication"
msgstr ""
msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab." msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
msgstr "" msgstr ""
...@@ -7841,6 +7887,9 @@ msgstr "" ...@@ -7841,6 +7887,9 @@ msgstr ""
msgid "This runner will only run on pipelines triggered on protected branches" msgid "This runner will only run on pipelines triggered on protected branches"
msgstr "" msgstr ""
msgid "This setting can be overridden in each project."
msgstr ""
msgid "This source diff could not be displayed because it is too large." msgid "This source diff could not be displayed because it is too large."
msgstr "" msgstr ""
...@@ -7868,6 +7917,9 @@ msgstr "" ...@@ -7868,6 +7917,9 @@ msgstr ""
msgid "Time before an issue starts implementation" msgid "Time before an issue starts implementation"
msgstr "" msgstr ""
msgid "Time before enforced"
msgstr ""
msgid "Time between merge request creation and merge/close" msgid "Time between merge request creation and merge/close"
msgstr "" msgstr ""
...@@ -8193,6 +8245,9 @@ msgstr "" ...@@ -8193,6 +8245,9 @@ msgstr ""
msgid "Twitter" msgid "Twitter"
msgstr "" msgstr ""
msgid "Two-factor authentication"
msgstr ""
msgid "Type" msgid "Type"
msgstr "" msgstr ""
...@@ -8268,7 +8323,7 @@ msgstr "" ...@@ -8268,7 +8323,7 @@ msgstr ""
msgid "Update now" msgid "Update now"
msgstr "" msgstr ""
msgid "Update your group name, description, avatar, and other general settings." msgid "Update your group name, description, avatar, and visibility."
msgstr "" msgstr ""
msgid "Updating" msgid "Updating"
...@@ -8454,6 +8509,9 @@ msgstr "" ...@@ -8454,6 +8509,9 @@ msgstr ""
msgid "Visibility and access controls" msgid "Visibility and access controls"
msgstr "" msgstr ""
msgid "Visibility level"
msgstr ""
msgid "Visibility level:" msgid "Visibility level:"
msgstr "" msgstr ""
...@@ -8508,7 +8566,11 @@ msgstr "" ...@@ -8508,7 +8566,11 @@ msgstr ""
msgid "When enabled, users cannot use GitLab until the terms have been accepted." msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "When leaving the URL blank, classification labels can still be specified without disabling cross project features or performing external authorization checks." msgid "When leaving the URL blank, classification labels can still be specified without disabling cross project features or performing external authorization checks."
=======
msgid "Who can see this group?"
>>>>>>> upstream/master
msgstr "" msgstr ""
msgid "Wiki" msgid "Wiki"
......
...@@ -150,7 +150,6 @@ describe Projects::MergeRequests::ConflictsController do ...@@ -150,7 +150,6 @@ describe Projects::MergeRequests::ConflictsController do
'new_path' => path, 'new_path' => path,
'blob_icon' => 'file-text-o', 'blob_icon' => 'file-text-o',
'blob_path' => a_string_ending_with(path), 'blob_path' => a_string_ending_with(path),
'blob_ace_mode' => 'ruby',
'content' => content) 'content' => content)
end end
end end
......
...@@ -125,7 +125,7 @@ describe 'Edit group settings' do ...@@ -125,7 +125,7 @@ describe 'Edit group settings' do
def save_group def save_group
page.within('.gs-general') do page.within('.gs-general') do
click_button 'Save group' click_button 'Save changes'
end end
end end
end end
...@@ -60,14 +60,14 @@ describe 'Group share with group lock' do ...@@ -60,14 +60,14 @@ describe 'Group share with group lock' do
def enable_group_lock def enable_group_lock
page.within('.gs-permissions') do page.within('.gs-permissions') do
check 'group_share_with_group_lock' check 'group_share_with_group_lock'
click_on 'Save group' click_on 'Save changes'
end end
end end
def disable_group_lock def disable_group_lock
page.within('.gs-permissions') do page.within('.gs-permissions') do
uncheck 'group_share_with_group_lock' uncheck 'group_share_with_group_lock'
click_on 'Save group' click_on 'Save changes'
end end
end end
end end
...@@ -140,10 +140,13 @@ describe 'Group' do ...@@ -140,10 +140,13 @@ describe 'Group' do
visit path visit path
end end
it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="group[name]"]' },
{ form: '.js-general-permissions-form', input: 'input[name="group[two_factor_grace_period]"]' }]
it 'saves new settings' do it 'saves new settings' do
page.within('.gs-general') do page.within('.gs-general') do
fill_in 'group_name', with: new_name fill_in 'group_name', with: new_name
click_button 'Save group' click_button 'Save changes'
end end
expect(page).to have_content 'successfully updated' expect(page).to have_content 'successfully updated'
......
...@@ -15,7 +15,7 @@ describe 'User uploads avatar to group' do ...@@ -15,7 +15,7 @@ describe 'User uploads avatar to group' do
) )
page.within('.gs-general') do page.within('.gs-general') do
click_button 'Save group' click_button 'Save changes'
end end
visit group_path(group) visit group_path(group)
......
import DirtySubmitCollection from '~/dirty_submit/dirty_submit_collection';
import { setInput, createForm } from './helper';
describe('DirtySubmitCollection', () => {
it('disables submits until there are changes', done => {
const testElementsCollection = [createForm(), createForm()];
const forms = testElementsCollection.map(testElements => testElements.form);
new DirtySubmitCollection(forms); // eslint-disable-line no-new
testElementsCollection.forEach(testElements => {
const { input, submit } = testElements;
const originalValue = input.value;
expect(submit.disabled).toBe(true);
return setInput(input, `${originalValue} changes`)
.then(() => expect(submit.disabled).toBe(false))
.then(() => setInput(input, originalValue))
.then(() => expect(submit.disabled).toBe(true))
.then(done)
.catch(done.fail);
});
});
});
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import DirtySubmitForm from '~/dirty_submit/dirty_submit_form';
import DirtySubmitCollection from '~/dirty_submit/dirty_submit_collection';
import { createForm } from './helper';
describe('DirtySubmitCollection', () => {
it('returns a DirtySubmitForm instance for single form elements', () => {
const { form } = createForm();
expect(dirtySubmitFactory(form) instanceof DirtySubmitForm).toBe(true);
});
it('returns a DirtySubmitCollection instance for a collection of form elements', () => {
const forms = [createForm().form, createForm().form];
expect(dirtySubmitFactory(forms) instanceof DirtySubmitCollection).toBe(true);
});
});
import DirtySubmitForm from '~/dirty_submit/dirty_submit_form';
import { setInput, createForm } from './helper';
describe('DirtySubmitForm', () => {
it('disables submit until there are changes', done => {
const { form, input, submit } = createForm();
const originalValue = input.value;
new DirtySubmitForm(form); // eslint-disable-line no-new
expect(submit.disabled).toBe(true);
return setInput(input, `${originalValue} changes`)
.then(() => expect(submit.disabled).toBe(false))
.then(() => setInput(input, originalValue))
.then(() => expect(submit.disabled).toBe(true))
.then(done)
.catch(done.fail);
});
});
import DirtySubmitForm from '~/dirty_submit/dirty_submit_form';
import setTimeoutPromiseHelper from '../helpers/set_timeout_promise_helper';
export function setInput(element, value) {
element.value = value;
element.dispatchEvent(
new Event('input', {
bubbles: true,
cancelable: true,
}),
);
return setTimeoutPromiseHelper(DirtySubmitForm.THROTTLE_DURATION);
}
export function createForm() {
const form = document.createElement('form');
form.innerHTML = `
<input type="text" value="original" class="js-input" name="input" />
<button type="submit" class="js-dirty-submit"></button>
`;
const input = form.querySelector('.js-input');
const submit = form.querySelector('.js-dirty-submit');
return {
form,
input,
submit,
};
}
...@@ -17,6 +17,16 @@ describe 'Groups (JavaScript fixtures)', type: :controller do ...@@ -17,6 +17,16 @@ describe 'Groups (JavaScript fixtures)', type: :controller do
sign_in(admin) sign_in(admin)
end end
describe GroupsController, '(JavaScript fixtures)', type: :controller do
it 'groups/edit.html.raw' do |example|
get :edit,
id: group
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
end
describe Groups::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do describe Groups::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do
it 'groups/ci_cd_settings.html.raw' do |example| it 'groups/ci_cd_settings.html.raw' do |example|
get :show, get :show,
......
/* eslint-disable no-var, one-var, no-return-assign, vars-on-top, jasmine/no-unsafe-spy */ /* eslint-disable no-var, one-var, no-return-assign, vars-on-top */
import $ from 'jquery'; import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
...@@ -94,33 +94,5 @@ import Sidebar from '~/right_sidebar'; ...@@ -94,33 +94,5 @@ import Sidebar from '~/right_sidebar';
}); });
}); });
}); });
describe('sidebarToggleClicked', () => {
const event = jasmine.createSpyObj('event', ['preventDefault']);
beforeEach(() => {
spyOn($.fn, 'hasClass').and.returnValue(false);
});
afterEach(() => {
gl.lazyLoader = undefined;
});
it('calls loadCheck if lazyLoader is set', () => {
gl.lazyLoader = jasmine.createSpyObj('lazyLoader', ['loadCheck']);
Sidebar.prototype.sidebarToggleClicked(event);
expect(gl.lazyLoader.loadCheck).toHaveBeenCalled();
});
it('does not throw if lazyLoader is not defined', () => {
gl.lazyLoader = undefined;
const toggle = Sidebar.prototype.sidebarToggleClicked.bind(null, event);
expect(toggle).not.toThrow();
});
});
}); });
}).call(window); }).call(window);
import $ from 'jquery';
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
describe('Settings Panels', () => { describe('Settings Panels', () => {
preloadFixtures('projects/ci_cd_settings.html.raw'); preloadFixtures('groups/edit.html.raw');
beforeEach(() => { beforeEach(() => {
loadFixtures('projects/ci_cd_settings.html.raw'); loadFixtures('groups/edit.html.raw');
}); });
describe('initSettingsPane', () => { describe('initSettingsPane', () => {
...@@ -13,17 +14,32 @@ describe('Settings Panels', () => { ...@@ -13,17 +14,32 @@ describe('Settings Panels', () => {
}); });
it('should expand linked hash fragment panel', () => { it('should expand linked hash fragment panel', () => {
window.location.hash = '#autodevops-settings'; window.location.hash = '#js-general-settings';
const pipelineSettingsPanel = document.querySelector('#autodevops-settings'); const panel = document.querySelector('#js-general-settings');
// Our test environment automatically expands everything so we need to clear that out first // Our test environment automatically expands everything so we need to clear that out first
pipelineSettingsPanel.classList.remove('expanded'); panel.classList.remove('expanded');
expect(pipelineSettingsPanel.classList.contains('expanded')).toBe(false); expect(panel.classList.contains('expanded')).toBe(false);
initSettingsPanels(); initSettingsPanels();
expect(pipelineSettingsPanel.classList.contains('expanded')).toBe(true); expect(panel.classList.contains('expanded')).toBe(true);
}); });
}); });
it('does not change the text content of triggers', () => {
const panel = document.querySelector('#js-general-settings');
const trigger = panel.querySelector('.js-settings-toggle-trigger-only');
const originalText = trigger.textContent;
initSettingsPanels();
expect(panel.classList.contains('expanded')).toBe(true);
$(trigger).click();
expect(panel.classList.contains('expanded')).toBe(false);
expect(trigger.textContent).toEqual(originalText);
});
}); });
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::BlobHelper do
include FakeBlobHelpers
let(:project) { create(:project) }
let(:blob) { fake_blob(path: 'file.txt') }
let(:large_blob) { fake_blob(path: 'test.pdf', size: 2.megabytes, binary: true) }
describe '#extname' do
it 'returns the extension' do
expect(blob.extname).to eq('.txt')
end
end
describe '#known_extension?' do
it 'returns true' do
expect(blob.known_extension?).to be_truthy
end
end
describe '#viewable' do
it 'returns true' do
expect(blob.viewable?).to be_truthy
end
it 'returns false' do
expect(large_blob.viewable?).to be_falsey
end
end
describe '#large?' do
it 'returns false' do
expect(blob.large?).to be_falsey
end
it 'returns true' do
expect(large_blob.large?).to be_truthy
end
end
describe '#binary?' do
it 'returns true' do
expect(large_blob.binary?).to be_truthy
end
it 'returns false' do
expect(blob.binary?).to be_falsey
end
end
describe '#text?' do
it 'returns true' do
expect(blob.text?).to be_truthy
end
it 'returns false' do
expect(large_blob.text?).to be_falsey
end
end
describe '#image?' do
it 'returns false' do
expect(blob.image?).to be_falsey
end
end
describe '#mime_type' do
it 'returns text/plain' do
expect(blob.mime_type).to eq('text/plain')
end
it 'returns application/pdf' do
expect(large_blob.mime_type).to eq('application/pdf')
end
end
describe '#binary_mime_type?' do
it 'returns false' do
expect(blob.binary_mime_type?).to be_falsey
end
end
describe '#lines' do
it 'returns the payload in an Array' do
expect(blob.lines).to eq(['foo'])
end
end
describe '#content_type' do
it 'returns text/plain' do
expect(blob.content_type).to eq('text/plain; charset=utf-8')
end
it 'returns text/plain' do
expect(large_blob.content_type).to eq('application/pdf')
end
end
describe '#encoded_newlines_re' do
it 'returns a regular expression' do
expect(blob.encoded_newlines_re).to eq(/\r\n|\r|\n/)
end
end
describe '#ruby_encoding' do
it 'returns UTF-8' do
expect(blob.ruby_encoding).to eq('UTF-8')
end
end
describe '#encoding' do
it 'returns UTF-8' do
expect(blob.ruby_encoding).to eq('UTF-8')
end
end
describe '#empty?' do
it 'returns false' do
expect(blob.empty?).to be_falsey
end
end
end
...@@ -267,11 +267,6 @@ FILE ...@@ -267,11 +267,6 @@ FILE
it 'includes the full content of the conflict' do it 'includes the full content of the conflict' do
expect(conflict_file.as_json(full_content: true)).to have_key(:content) expect(conflict_file.as_json(full_content: true)).to have_key(:content)
end end
it 'includes the detected language of the conflict file' do
expect(conflict_file.as_json(full_content: true)[:blob_ace_mode])
.to eq('ruby')
end
end end
end end
end end
# encoding: UTF-8
require "spec_helper"
describe Gitlab::Git::BlobSnippet, :seed_helper do
describe '#data' do
context 'empty lines' do
let(:snippet) { Gitlab::Git::BlobSnippet.new('master', nil, nil, nil) }
it { expect(snippet.data).to be_nil }
end
context 'present lines' do
let(:snippet) { Gitlab::Git::BlobSnippet.new('master', %w(wow much), 1, 'wow.rb') }
it { expect(snippet.data).to eq("wow\nmuch") }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::LanguageData do
describe '#extensions' do
before do
described_class.clear_extensions!
end
it 'loads the extensions once' do
expect(YAML).to receive(:load_file).once.and_call_original
2.times do
expect(described_class.extensions).to be_a(Set)
expect(described_class.extensions.count).to be > 0
# Sanity check for known extensions
expect(described_class.extensions).to include(*%w(.rb .yml .json))
end
end
end
end
...@@ -2372,4 +2372,15 @@ describe Repository do ...@@ -2372,4 +2372,15 @@ describe Repository do
end end
end end
end end
describe '#merge_base' do
set(:project) { create(:project, :repository) }
subject(:repository) { project.repository }
it 'only makes one gitaly call' do
expect(Gitlab::GitalyClient).to receive(:call).once.and_call_original
repository.merge_base('master', 'fix')
end
end
end end
...@@ -468,7 +468,7 @@ describe API::Repositories do ...@@ -468,7 +468,7 @@ describe API::Repositories do
describe 'GET :id/repository/merge_base' do describe 'GET :id/repository/merge_base' do
let(:refs) do let(:refs) do
%w(304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209) %w(304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209 570e7b2abdd848b95f2f578043fc23bd6f6fd24d)
end end
subject(:request) do subject(:request) do
...@@ -534,7 +534,7 @@ describe API::Repositories do ...@@ -534,7 +534,7 @@ describe API::Repositories do
request request
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq('Provide exactly 2 refs') expect(json_response['message']).to eq('Provide at least 2 refs')
end end
end end
end end
......
...@@ -13,7 +13,10 @@ describe API::Settings, 'Settings' do ...@@ -13,7 +13,10 @@ describe API::Settings, 'Settings' do
expect(json_response['default_projects_limit']).to eq(42) expect(json_response['default_projects_limit']).to eq(42)
expect(json_response['password_authentication_enabled_for_web']).to be_truthy expect(json_response['password_authentication_enabled_for_web']).to be_truthy
expect(json_response['repository_storages']).to eq(['default']) expect(json_response['repository_storages']).to eq(['default'])
<<<<<<< HEAD
expect(json_response['password_authentication_enabled']).to be_truthy expect(json_response['password_authentication_enabled']).to be_truthy
=======
>>>>>>> upstream/master
expect(json_response['plantuml_enabled']).to be_falsey expect(json_response['plantuml_enabled']).to be_falsey
expect(json_response['plantuml_url']).to be_nil expect(json_response['plantuml_url']).to be_nil
expect(json_response['default_project_visibility']).to be_a String expect(json_response['default_project_visibility']).to be_a String
......
# frozen_string_literal: true
require "spec_helper" require "spec_helper"
describe ::Applications::CreateService do describe ::Applications::CreateService do
include TestRequestHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:params) { attributes_for(:application) } let(:params) { attributes_for(:application) }
let(:request) do
if Gitlab.rails5?
ActionController::TestRequest.new({ remote_ip: "127.0.0.1" }, ActionController::TestSession.new)
else
ActionController::TestRequest.new(remote_ip: "127.0.0.1")
end
end
subject { described_class.new(user, params) } subject { described_class.new(user, params) }
it { expect { subject.execute(request) }.to change { Doorkeeper::Application.count }.by(1) } it { expect { subject.execute(test_request) }.to change { Doorkeeper::Application.count }.by(1) }
end end
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::Applications::CreateService do
include TestRequestHelpers
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:user) { create(:user) }
let(:params) { { application: 'helm' } }
let(:service) { described_class.new(cluster, user, params) }
describe '#execute' do
before do
allow(ClusterInstallAppWorker).to receive(:perform_async)
end
subject { service.execute(test_request) }
it 'creates an application' do
expect do
subject
cluster.reload
end.to change(cluster, :application_helm)
end
it 'schedules an install via worker' do
expect(ClusterInstallAppWorker).to receive(:perform_async).with('helm', anything).once
subject
end
context 'jupyter application' do
let(:params) do
{
application: 'jupyter',
hostname: 'example.com'
}
end
before do
allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
end
it 'creates the application' do
expect do
subject
cluster.reload
end.to change(cluster, :application_jupyter)
end
it 'sets the hostname' do
expect(subject.hostname).to eq('example.com')
end
it 'sets the oauth_application' do
expect(subject.oauth_application).to be_present
end
end
context 'invalid application' do
let(:params) { { application: 'non-existent' } }
it 'raises an error' do
expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
end
end
end
end
...@@ -10,14 +10,13 @@ describe Clusters::Applications::ScheduleInstallationService do ...@@ -10,14 +10,13 @@ describe Clusters::Applications::ScheduleInstallationService do
expect(ClusterInstallAppWorker).not_to receive(:perform_async) expect(ClusterInstallAppWorker).not_to receive(:perform_async)
count_before = count_scheduled count_before = count_scheduled
expect { service.execute(application) }.to raise_error(StandardError) expect { service.execute }.to raise_error(StandardError)
expect(count_scheduled).to eq(count_before) expect(count_scheduled).to eq(count_before)
end end
end end
describe '#execute' do describe '#execute' do
let(:project) { double(:project) } let(:service) { described_class.new(application) }
let(:service) { described_class.new(project, nil) }
context 'when application is installable' do context 'when application is installable' do
let(:application) { create(:clusters_applications_helm, :installable) } let(:application) { create(:clusters_applications_helm, :installable) }
...@@ -25,7 +24,7 @@ describe Clusters::Applications::ScheduleInstallationService do ...@@ -25,7 +24,7 @@ describe Clusters::Applications::ScheduleInstallationService do
it 'make the application scheduled' do it 'make the application scheduled' do
expect(ClusterInstallAppWorker).to receive(:perform_async).with(application.name, kind_of(Numeric)).once expect(ClusterInstallAppWorker).to receive(:perform_async).with(application.name, kind_of(Numeric)).once
expect { service.execute(application) }.to change { application.class.with_status(:scheduled).count }.by(1) expect { service.execute }.to change { application.class.with_status(:scheduled).count }.by(1)
end end
end end
......
...@@ -77,6 +77,15 @@ shared_examples 'issuable record that supports quick actions in its description ...@@ -77,6 +77,15 @@ shared_examples 'issuable record that supports quick actions in its description
expect(issuable.labels).to eq [label_bug] expect(issuable.labels).to eq [label_bug]
expect(issuable.milestone).to eq milestone expect(issuable.milestone).to eq milestone
end end
it 'removes the quick action from note and explains it in the preview' do
preview_note("Awesome!\n\n/close")
expect(page).to have_content 'Awesome!'
expect(page).not_to have_content '/close'
issuable_name = issuable.is_a?(Issue) ? 'issue' : 'merge request'
expect(page).to have_content "Closes this #{issuable_name}."
end
end end
context 'with a note containing only commands' do context 'with a note containing only commands' do
......
# frozen_string_literal: true
module TestRequestHelpers
def test_request(remote_ip: '127.0.0.1')
if Gitlab.rails5?
ActionController::TestRequest.new({ remote_ip: remote_ip }, ActionController::TestSession.new)
else
ActionController::TestRequest.new(remote_ip: remote_ip)
end
end
end
shared_examples 'dirty submit form' do |selector_args|
selectors = selector_args.is_a?(Array) ? selector_args : [selector_args]
selectors.each do |selector|
it "disables #{selector[:form]} submit until there are changes", :js do
form = find(selector[:form])
submit = form.first('.js-dirty-submit')
input = form.first(selector[:input])
original_value = input.value
expect(submit.disabled?).to be true
input.set("#{original_value} changes")
form.find('.js-dirty-submit:not([disabled])', match: :first)
expect(submit.disabled?).to be false
input.set(original_value)
form.find('.js-dirty-submit[disabled]', match: :first)
expect(submit.disabled?).to be true
end
end
end
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment