From d12f2cb87f78b7b6e75e8c0083e45960251818b6 Mon Sep 17 00:00:00 2001 From: Reb Date: Wed, 8 Feb 2017 04:00:20 +0000 Subject: [PATCH 01/73] Fix typo in auth0.md doc Removed spurious character from Omnibus example --- doc/integration/auth0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md index 212b4854dd737e..c39d7ab57c66e9 100644 --- a/doc/integration/auth0.md +++ b/doc/integration/auth0.md @@ -54,7 +54,7 @@ for initial settings. gitlab_rails['omniauth_providers'] = [ { "name" => "auth0", - "args" => { client_id: 'YOUR_AUTH0_CLIENT_ID'', + "args" => { client_id: 'YOUR_AUTH0_CLIENT_ID', client_secret: 'YOUR_AUTH0_CLIENT_SECRET', namespace: 'YOUR_AUTH0_DOMAIN' } -- GitLab From 7bb753fbcf140c12e2dbe8288b70d0777f933d68 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 3 Jan 2017 16:31:43 +0000 Subject: [PATCH 02/73] Removed jQuery UI highlight & autocomplete In an effort to tackle #18437 this removes 2 of the jQuery UI plugins. Highlight & autocomplete, both used once in our code. Highlight was just removed easily, autocomplete was replaced with GL dropdown --- app/assets/javascripts/application.js | 2 -- app/assets/javascripts/milestone.js | 1 - app/assets/javascripts/new_branch_form.js | 30 ++++++++++++++--- .../protected_branch_edit.js.es6 | 6 ++++ app/assets/stylesheets/application.scss | 1 - app/assets/stylesheets/framework/jquery.scss | 11 ------- .../personal_access_tokens/index.html.haml | 2 -- app/views/projects/branches/new.html.haml | 8 +++-- app/views/projects/pipelines/new.html.haml | 6 +++- .../unreleased/remove-jquery-ui-plugins.yml | 4 +++ features/project/commits/branches.feature | 8 ++--- features/steps/project/commits/branches.rb | 24 +++++++------- .../projects/pipelines/pipelines_spec.rb | 33 ++++++++++--------- 13 files changed, 76 insertions(+), 60 deletions(-) create mode 100644 changelogs/unreleased/remove-jquery-ui-plugins.yml diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 7cca655562eaaa..f41345d003d688 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -9,9 +9,7 @@ function requireAll(context) { return context.keys().map(context); } window.$ = window.jQuery = require('jquery'); -require('jquery-ui/ui/autocomplete'); require('jquery-ui/ui/draggable'); -require('jquery-ui/ui/effect-highlight'); require('jquery-ui/ui/sortable'); require('jquery-ujs'); require('vendor/jquery.endless-scroll'); diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 7fbaeec7882c0d..38c673e890782b 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -78,7 +78,6 @@ } else { $(element).find('.assignee-icon').empty(); } - return $(element).effect('highlight'); }; function Milestone() { diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index cb24f212c66e81..3f678b93f73115 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */ (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; @@ -20,15 +20,35 @@ }; NewBranchForm.prototype.init = function() { - if (this.name.val().length > 0) { + if (this.name.length && this.name.val().length > 0) { return this.name.trigger('blur'); } }; NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) { - return this.ref.autocomplete({ - source: availableRefs, - minLength: 1 + var $branchSelect = $('.js-branch-select'); + + $branchSelect.glDropdown({ + data: availableRefs, + filterable: true, + filterByText: true, + remote: false, + fieldName: $branchSelect.data('field-name'), + selectable: true, + isSelectable: function(branch, $el) { + return !$el.hasClass('is-active'); + }, + text: function(branch) { + return branch; + }, + id: function(branch) { + return branch; + }, + toggleLabel: function(branch) { + if (branch) { + return branch; + } + } }); }; diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 index 649abb411e2fea..e16df3c0502d19 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 @@ -70,6 +70,9 @@ formData[`${accessLevelName}_attributes`] = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName); } + this.$allowedToMergeDropdown.disable(); + this.$allowedToPushDropdown.disable(); + return $.ajax({ type: 'POST', url: this.$wrap.data('url'), @@ -93,6 +96,9 @@ $.scrollTo(0); new Flash('Failed to update branch!'); } + }).always(() => { + this.$allowedToMergeDropdown.enable(); + this.$allowedToPushDropdown.enable(); }); } diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 1dcd1f8a6fc32f..83a8eeaafdeda1 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -2,7 +2,6 @@ * This is a manifest file that'll automatically include all the stylesheets available in this directory * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at * the top of the compiled file, but it's generally better to create a new file per style scope. - *= require jquery-ui/autocomplete *= require jquery.atwho *= require select2 *= require_self diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index d335fedefe2a35..300ba4f2de62cd 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -2,17 +2,6 @@ font-family: $regular_font; font-size: $font-size-base; - &.ui-autocomplete { - border-color: $jq-ui-border; - padding: 0; - margin-top: 2px; - z-index: 1001; - - .ui-menu-item a { - padding: 4px 10px; - } - } - .ui-state-default { border: 1px solid $white-light; background: $white-light; diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index b10f5fc08e2bdd..903b957c26b32b 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -101,5 +101,3 @@ $("#created-personal-access-token").click(function() { this.select(); }); - - $("#created-personal-access-token").effect('highlight', { color: '#ffff99' }, 2000); diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index e63bdb38bd8739..d3c3e40d5185a3 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -12,12 +12,16 @@ .form-group = label_tag :branch_name, nil, class: 'control-label' .col-sm-10 - = text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control js-branch-name' + = text_field_tag :branch_name, params[:branch_name], required: true, autofocus: true, class: 'form-control js-branch-name' .help-block.text-danger.js-branch-name-error .form-group = label_tag :ref, 'Create from', class: 'control-label' .col-sm-10 - = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control' + = hidden_field_tag :ref, params[:ref] || @project.default_branch + = dropdown_tag(params[:ref] || @project.default_branch, + options: { toggle_class: 'js-branch-select wide', + filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches", + data: { selected: params[:ref] || @project.default_branch, field_name: 'ref' } }) .help-block Existing branch name, tag, or commit SHA .form-actions = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index 55202725b9ee74..14a270a3039dc1 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -9,7 +9,11 @@ .form-group = f.label :ref, 'Create for', class: 'control-label' .col-sm-10 - = f.text_field :ref, required: true, tabindex: 2, class: 'form-control js-branch-name ui-autocomplete-input', autocomplete: :false, id: :ref + = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch + = dropdown_tag(params[:ref] || @project.default_branch, + options: { toggle_class: 'js-branch-select wide', + filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches", + data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } }) .help-block Existing branch name, tag .form-actions = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3 diff --git a/changelogs/unreleased/remove-jquery-ui-plugins.yml b/changelogs/unreleased/remove-jquery-ui-plugins.yml new file mode 100644 index 00000000000000..c768f702ba29fd --- /dev/null +++ b/changelogs/unreleased/remove-jquery-ui-plugins.yml @@ -0,0 +1,4 @@ +--- +title: Removed jQuery UI highlight & autocomplete +merge_request: +author: diff --git a/features/project/commits/branches.feature b/features/project/commits/branches.feature index 88fef674c0cf7c..c57376aecffb85 100644 --- a/features/project/commits/branches.feature +++ b/features/project/commits/branches.feature @@ -13,6 +13,7 @@ Feature: Project Commits Branches Given I visit project protected branches page Then I should see "Shop" protected branches list + @javascript Scenario: I create a branch Given I visit project branches page And I click new branch link @@ -33,12 +34,7 @@ Feature: Project Commits Branches And I submit new branch form with invalid name Then I should see new an error that branch is invalid - Scenario: I create a branch with invalid reference - Given I visit project branches page - And I click new branch link - And I submit new branch form with invalid reference - Then I should see new an error that ref is invalid - + @javascript Scenario: I create a branch that already exists Given I visit project branches page And I click new branch link diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb index 5f9b9e0445e859..ccaf3237815eb0 100644 --- a/features/steps/project/commits/branches.rb +++ b/features/steps/project/commits/branches.rb @@ -34,25 +34,19 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps step 'I submit new branch form' do fill_in 'branch_name', with: 'deploy_keys' - fill_in 'ref', with: 'master' + select_branch('master') click_button 'Create branch' end step 'I submit new branch form with invalid name' do fill_in 'branch_name', with: '1.0 stable' - fill_in 'ref', with: 'master' - click_button 'Create branch' - end - - step 'I submit new branch form with invalid reference' do - fill_in 'branch_name', with: 'foo' - fill_in 'ref', with: 'foo' + select_branch('master') click_button 'Create branch' end step 'I submit new branch form with branch that already exists' do fill_in 'branch_name', with: 'master' - fill_in 'ref', with: 'master' + select_branch('master') click_button 'Create branch' end @@ -65,10 +59,6 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps expect(page).to have_content "can't contain spaces" end - step 'I should see new an error that ref is invalid' do - expect(page).to have_content 'Invalid reference name' - end - step 'I should see new an error that branch already exists' do expect(page).to have_content 'Branch already exists' end @@ -88,4 +78,12 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps step "I should not see branch 'improve/awesome'" do expect(page.all(visible: true)).not_to have_content 'improve/awesome' end + + def select_branch(branch_name) + click_button 'master' + + page.within '#new-branch-form .dropdown-menu' do + click_link branch_name + end + end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 592dc4483d2a84..6b780194401e71 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -359,8 +359,14 @@ visit new_namespace_project_pipeline_path(project.namespace, project) end - context 'for valid commit' do - before { fill_in('pipeline[ref]', with: 'master') } + context 'for valid commit', js: true do + before do + click_button project.default_branch + + page.within '.dropdown-menu' do + click_link 'master' + end + end context 'with gitlab-ci.yml' do before { stub_ci_pipeline_to_return_yaml_file } @@ -377,15 +383,6 @@ it { expect(page).to have_content('Missing .gitlab-ci.yml file') } end end - - context 'for invalid commit' do - before do - fill_in('pipeline[ref]', with: 'invalid-reference') - click_on 'Create pipeline' - end - - it { expect(page).to have_content('Reference not found') } - end end describe 'Create pipelines' do @@ -397,18 +394,22 @@ describe 'new pipeline page' do it 'has field to add a new pipeline' do - expect(page).to have_field('pipeline[ref]') + expect(page).to have_selector('.js-branch-select') + expect(find('.js-branch-select')).to have_content project.default_branch expect(page).to have_content('Create for') end end describe 'find pipelines' do it 'shows filtered pipelines', js: true do - fill_in('pipeline[ref]', with: 'fix') - find('input#ref').native.send_keys(:keydown) + click_button project.default_branch - within('.ui-autocomplete') do - expect(page).to have_selector('li', text: 'fix') + page.within '.dropdown-menu' do + find('.dropdown-input-field').native.send_keys('fix') + + page.within '.dropdown-content' do + expect(page).to have_content('fix') + end end end end -- GitLab From 266197421f05f6aee4d9666d502da2f8bb4313b9 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Feb 2017 18:00:37 +0800 Subject: [PATCH 03/73] Initial implementation for default artifacts expiration TODO: Add tests and screenshots --- .../admin/application_settings_controller.rb | 1 + app/models/application_setting.rb | 19 ++++++++++++++++-- app/models/ci/build.rb | 11 ++++++++++ .../application_settings/_form.html.haml | 9 ++++++++- ...acts_expiration_to_application_settings.rb | 11 ++++++++++ .../settings/continuous_integration.md | 20 +++++++++---------- lib/api/settings.rb | 7 +++++-- lib/ci/api/builds.rb | 2 +- spec/models/ci/build_spec.rb | 2 +- 9 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index e2f02d8824d60d..7de0c35b4cc1a5 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -92,6 +92,7 @@ def application_setting_params_ce :akismet_api_key, :akismet_enabled, :container_registry_token_expire_delay, + :default_artifacts_expiration, :default_branch_protection, :default_group_visibility, :default_project_visibility, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 3a7ba34a7ca819..22053db0ce9f1a 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -78,6 +78,14 @@ class ApplicationSetting < ActiveRecord::Base numericality: { only_integer: true, greater_than: 0 } validates :repository_size_limit, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :max_artifacts_size, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :default_artifacts_expiration, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } @@ -187,6 +195,7 @@ def self.defaults_ce after_sign_up_text: nil, akismet_enabled: false, container_registry_token_expire_delay: 5, + default_artifacts_expiration: 30, default_branch_protection: Settings.gitlab['default_branch_protection'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_projects_limit: Settings.gitlab['default_projects_limit'], @@ -220,9 +229,9 @@ def self.defaults_ce sign_in_text: nil, signin_enabled: Settings.gitlab['signin_enabled'], signup_enabled: Settings.gitlab['signup_enabled'], + terminal_max_session_time: 0, two_factor_grace_period: 48, - user_default_external: false, - terminal_max_session_time: 0 + user_default_external: false } end @@ -323,6 +332,12 @@ def sidekiq_throttling_enabled? sidekiq_throttling_enabled end + def default_artifacts_expire_in + if default_artifacts_expiration.nonzero? + "#{default_artifacts_expiration} days" + end + end + private def check_repository_storages diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 9d60bb24660fb8..6ea190b62f5102 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -489,6 +489,17 @@ def artifacts_expire_in=(value) end end + def set_artifacts_expire_in(expire_in) + value = + if expire_in + expire_in + else + ApplicationSetting.current.default_artifacts_expire_in + end + + self.artifacts_expire_in = value + end + def has_expiring_artifacts? artifacts_expire_at.present? end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 03f8728eea1838..a53cb6b6eaceb9 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -232,8 +232,15 @@ .col-sm-10 = f.number_field :max_artifacts_size, class: 'form-control' .help-block - Set the maximum file size each jobs's artifacts can have + Set the maximum file size for each job's artifacts = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "maximum-artifacts-size") + .form-group + = f.label :default_artifacts_expiration, 'Default artifacts expiration (days)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :default_artifacts_expiration, class: 'form-control' + .help-block + Set the default expiration time for each job's artifacts (0 as never expired) + = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "default-artifacts-expiration") - if Gitlab.config.registry.enabled %fieldset diff --git a/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb new file mode 100644 index 00000000000000..728d581251c55d --- /dev/null +++ b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb @@ -0,0 +1,11 @@ +class AddDefaultArtifactsExpirationToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :application_settings, + :default_artifacts_expiration, + :integer, default: 0, null: false + end +end diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index 699634521422ab..9ffe7e8562ed8c 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -10,29 +10,29 @@ this setting is set for each job. ![Admin area settings button](img/admin_area_settings_button.png) -1. Change the value of the maximum artifacts size (in MB): +1. Change the value of maximum artifacts size (in MB): ![Admin area maximum artifacts size](img/admin_area_maximum_artifacts_size.png) 1. Hit **Save** for the changes to take effect. -## Shared Runners build minutes quota +## Default artifacts expiration > [Introduced][ee-1078] in GitLab Enterprise Edition 8.16. -If you have enabled shared Runners for your GitLab instance, you can limit their -usage by setting a maximum number of build minutes that a group can use on -shared Runners per month. Set 0 to grant unlimited build minutes. -While build limits are stored as minutes, the counting is done in seconds. +The default expiration time of the [job artifacts][art-yml] can be set in +the Admin area of your GitLab instance. The syntax of duration is described +in [artifacts:expire_in][duration-syntax]. The default is `30 days`. Note that +this setting is set for each job. Set it to 0 if you don't want default +expiration. -1. Go to the **Admin area ➔ Settings** (`/admin/application_settings`). +1. Go to **Admin area > Settings** (`/admin/application_settings`). ![Admin area settings button](img/admin_area_settings_button.png) -1. Navigate to the **Continuous Integration** block and enable the Shared - Runners setting. Then set the build minutes quota limit. +1. Change the value of default expiration time (in days): - ![Shared Runners build minutes quota](img/ci_shared_runners_build_minutes_quota.png) + ![Admin area default artifacts expiration](img/admin_area_default_artifacts_expiration.png) 1. Hit **Save** for the changes to take effect. diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 3ee50bf186ee59..1d04b7cbeb22e5 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -56,7 +56,8 @@ def current_settings given shared_runners_enabled: ->(val) { val } do requires :shared_runners_text, type: String, desc: 'Shared runners text ' end - optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have" + optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts" + optional :default_artifacts_expiration, type: Integer, desc: "Set the default expiration time for each job's artifacts" optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics' @@ -130,7 +131,9 @@ def current_settings :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled, :after_sign_up_text, :signin_enabled, :require_two_factor_authentication, :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text, - :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay, + :shared_runners_enabled, :max_artifacts_size, + :default_artifacts_expiration, :max_pages_size, + :container_registry_token_expire_delay, :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled, :akismet_enabled, :admin_notification_email, :sentry_enabled, :repository_checks_enabled, :koding_enabled, :housekeeping_enabled, :terminal_max_session_time, :plantuml_enabled, diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 8b939663ffd972..7aad6c50b7bb4c 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -167,7 +167,7 @@ class Builds < Grape::API build.artifacts_file = artifacts build.artifacts_metadata = metadata - build.artifacts_expire_in = params['expire_in'] + build.set_artifacts_expire_in(params['expire_in']) if build.save present(build, with: Entities::BuildDetails) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 63b6c3c65a6efb..401d4c83dbfd40 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -162,7 +162,7 @@ is_expected.to be_nil end - it 'when resseting value' do + it 'when resetting value' do build.artifacts_expire_in = nil is_expected.to be_nil -- GitLab From 09fac4a1e7b4d2a6cba72f586965f87c7efddac2 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Feb 2017 19:17:36 +0800 Subject: [PATCH 04/73] ApplicationSetting.current doesn't work well in tests Therefore we prefer `Gitlab::CurrentSettings.current_application_settings` and fix the tests by setting default_artifacts_expire_in to 0 to restore the original behaviour. --- app/models/ci/build.rb | 3 ++- spec/requests/ci/api/builds_spec.rb | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 6ea190b62f5102..a3c686bca5a7ee 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -494,7 +494,8 @@ def set_artifacts_expire_in(expire_in) if expire_in expire_in else - ApplicationSetting.current.default_artifacts_expire_in + Gitlab::CurrentSettings.current_application_settings + .default_artifacts_expire_in end self.artifacts_expire_in = value diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 444258e312dac3..2834137b9bd28b 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -638,6 +638,8 @@ def force_patch_the_trace end before do + stub_application_setting(default_artifacts_expiration: 0) + post(post_url, post_data, headers_with_token) end -- GitLab From 97627d654f6bd61167c48e430ae663829293e356 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Feb 2017 19:24:57 +0800 Subject: [PATCH 05/73] Add changelog entry --- .../unreleased/27762-add-default-artifacts-expiration.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/27762-add-default-artifacts-expiration.yml diff --git a/changelogs/unreleased/27762-add-default-artifacts-expiration.yml b/changelogs/unreleased/27762-add-default-artifacts-expiration.yml new file mode 100644 index 00000000000000..27fa77ed04dd16 --- /dev/null +++ b/changelogs/unreleased/27762-add-default-artifacts-expiration.yml @@ -0,0 +1,4 @@ +--- +title: Add admin setting for default artifacts expiration +merge_request: 9219 +author: -- GitLab From 4e440dd84cbcff29ec5a8b08f8886c7da39b1aba Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Feb 2017 19:34:07 +0800 Subject: [PATCH 06/73] Test build API if expire_in not set, set to app default --- spec/requests/ci/api/builds_spec.rb | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 2834137b9bd28b..33d56ae52e9ee7 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -630,6 +630,7 @@ def force_patch_the_trace context 'with an expire date' do let!(:artifacts) { file_upload } + let(:default_artifacts_expiration) { 0 } let(:post_data) do { 'file.path' => artifacts.path, @@ -638,7 +639,8 @@ def force_patch_the_trace end before do - stub_application_setting(default_artifacts_expiration: 0) + stub_application_setting( + default_artifacts_expiration: default_artifacts_expiration) post(post_url, post_data, headers_with_token) end @@ -650,7 +652,8 @@ def force_patch_the_trace build.reload expect(response).to have_http_status(201) expect(json_response['artifacts_expire_at']).not_to be_empty - expect(build.artifacts_expire_at).to be_within(5.minutes).of(Time.now + 7.days) + expect(build.artifacts_expire_at). + to be_within(5.minutes).of(7.days.from_now) end end @@ -663,6 +666,18 @@ def force_patch_the_trace expect(json_response['artifacts_expire_at']).to be_nil expect(build.artifacts_expire_at).to be_nil end + + context 'with application default' do + let(:default_artifacts_expiration) { 5 } + + it 'sets to application default' do + build.reload + expect(response).to have_http_status(201) + expect(json_response['artifacts_expire_at']).not_to be_empty + expect(build.artifacts_expire_at). + to be_within(5.minutes).of(5.days.from_now) + end + end end end -- GitLab From 2f45602a429bdfef16d351eececc32c54f0302e2 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Feb 2017 22:54:46 +0800 Subject: [PATCH 07/73] Use the same syntax for default expiration Feedback: * https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23343951 * https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23344036 * https://gitlab.com/gitlab-org/gitlab-ce/issues/27762#note_23344797 --- .../admin/application_settings_controller.rb | 2 +- app/models/application_setting.rb | 20 +++++++++---------- app/models/ci/build.rb | 12 ----------- .../application_settings/_form.html.haml | 10 +++++----- ...acts_expiration_to_application_settings.rb | 4 ++-- lib/api/settings.rb | 4 ++-- lib/ci/api/builds.rb | 5 ++++- spec/requests/ci/api/builds_spec.rb | 6 +++--- 8 files changed, 27 insertions(+), 36 deletions(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 7de0c35b4cc1a5..78b199c4c91727 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -92,7 +92,7 @@ def application_setting_params_ce :akismet_api_key, :akismet_enabled, :container_registry_token_expire_delay, - :default_artifacts_expiration, + :default_artifacts_expire_in, :default_branch_protection, :default_group_visibility, :default_project_visibility, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 22053db0ce9f1a..f43b8fd334f771 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -85,9 +85,7 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than: 0 } - validates :default_artifacts_expiration, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validate :check_default_artifacts_expire_in validates :container_registry_token_expire_delay, presence: true, @@ -195,7 +193,7 @@ def self.defaults_ce after_sign_up_text: nil, akismet_enabled: false, container_registry_token_expire_delay: 5, - default_artifacts_expiration: 30, + default_artifacts_expire_in: '30 days', default_branch_protection: Settings.gitlab['default_branch_protection'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_projects_limit: Settings.gitlab['default_projects_limit'], @@ -332,12 +330,6 @@ def sidekiq_throttling_enabled? sidekiq_throttling_enabled end - def default_artifacts_expire_in - if default_artifacts_expiration.nonzero? - "#{default_artifacts_expiration} days" - end - end - private def check_repository_storages @@ -345,4 +337,12 @@ def check_repository_storages errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless invalid.empty? end + + def check_default_artifacts_expire_in + ChronicDuration.parse(default_artifacts_expire_in) + true + rescue ChronicDuration::DurationParseError => e + errors.add(:default_artifacts_expire_in, ": #{e.message}") + false + end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index a3c686bca5a7ee..9d60bb24660fb8 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -489,18 +489,6 @@ def artifacts_expire_in=(value) end end - def set_artifacts_expire_in(expire_in) - value = - if expire_in - expire_in - else - Gitlab::CurrentSettings.current_application_settings - .default_artifacts_expire_in - end - - self.artifacts_expire_in = value - end - def has_expiring_artifacts? artifacts_expire_at.present? end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index a53cb6b6eaceb9..9a9a273f3e8a5c 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -233,14 +233,14 @@ = f.number_field :max_artifacts_size, class: 'form-control' .help-block Set the maximum file size for each job's artifacts - = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "maximum-artifacts-size") + = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size') .form-group - = f.label :default_artifacts_expiration, 'Default artifacts expiration (days)', class: 'control-label col-sm-2' + = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'control-label col-sm-2' .col-sm-10 - = f.number_field :default_artifacts_expiration, class: 'form-control' + = f.text_field :default_artifacts_expire_in, class: 'form-control' .help-block - Set the default expiration time for each job's artifacts (0 as never expired) - = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "default-artifacts-expiration") + Set the default expiration time for each job's artifacts + = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') - if Gitlab.config.registry.enabled %fieldset diff --git a/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb index 728d581251c55d..34905570739a29 100644 --- a/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb +++ b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb @@ -5,7 +5,7 @@ class AddDefaultArtifactsExpirationToApplicationSettings < ActiveRecord::Migrati def change add_column :application_settings, - :default_artifacts_expiration, - :integer, default: 0, null: false + :default_artifacts_expire_in, + :string, null: true end end diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 1d04b7cbeb22e5..0c24576831b1e4 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -57,7 +57,7 @@ def current_settings requires :shared_runners_text, type: String, desc: 'Shared runners text ' end optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts" - optional :default_artifacts_expiration, type: Integer, desc: "Set the default expiration time for each job's artifacts" + optional :default_artifacts_expire_in, type: Integer, desc: "Set the default expiration time for each job's artifacts" optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics' @@ -132,7 +132,7 @@ def current_settings :after_sign_up_text, :signin_enabled, :require_two_factor_authentication, :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text, :shared_runners_enabled, :max_artifacts_size, - :default_artifacts_expiration, :max_pages_size, + :default_artifacts_expire_in, :max_pages_size, :container_registry_token_expire_delay, :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled, :akismet_enabled, :admin_notification_email, :sentry_enabled, diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 7aad6c50b7bb4c..2018191c4bd4f9 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -167,7 +167,10 @@ class Builds < Grape::API build.artifacts_file = artifacts build.artifacts_metadata = metadata - build.set_artifacts_expire_in(params['expire_in']) + build.artifacts_expire_in = + params['expire_in'] || + Gitlab::CurrentSettings.current_application_settings + .default_artifacts_expire_in if build.save present(build, with: Entities::BuildDetails) diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 33d56ae52e9ee7..c7284be09b7841 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -630,7 +630,7 @@ def force_patch_the_trace context 'with an expire date' do let!(:artifacts) { file_upload } - let(:default_artifacts_expiration) { 0 } + let(:default_artifacts_expire_in) {} let(:post_data) do { 'file.path' => artifacts.path, @@ -640,7 +640,7 @@ def force_patch_the_trace before do stub_application_setting( - default_artifacts_expiration: default_artifacts_expiration) + default_artifacts_expire_in: default_artifacts_expire_in) post(post_url, post_data, headers_with_token) end @@ -668,7 +668,7 @@ def force_patch_the_trace end context 'with application default' do - let(:default_artifacts_expiration) { 5 } + let(:default_artifacts_expire_in) { '5 days' } it 'sets to application default' do build.reload -- GitLab From 6e29ec73aff2141d976a7a3e1c9162a26f1bc2db Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Feb 2017 23:44:05 +0800 Subject: [PATCH 08/73] rubocop: Align the operands of an expression C: Style/MultilineOperationIndentation: Align the operands of an expression in an assignment spanning multiple lines. --- lib/ci/api/builds.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 2018191c4bd4f9..0e17ac24d5a957 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -169,8 +169,8 @@ class Builds < Grape::API build.artifacts_metadata = metadata build.artifacts_expire_in = params['expire_in'] || - Gitlab::CurrentSettings.current_application_settings - .default_artifacts_expire_in + Gitlab::CurrentSettings.current_application_settings + .default_artifacts_expire_in if build.save present(build, with: Entities::BuildDetails) -- GitLab From d10290be88ea0bec83faa6011203f0a2f3bbd535 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Feb 2017 23:47:23 +0800 Subject: [PATCH 09/73] Check default_artifacts_expire_in only if existed --- app/models/application_setting.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index f43b8fd334f771..2a3537a8fcc01b 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -339,7 +339,8 @@ def check_repository_storages end def check_default_artifacts_expire_in - ChronicDuration.parse(default_artifacts_expire_in) + ChronicDuration.parse(default_artifacts_expire_in) if + default_artifacts_expire_in true rescue ChronicDuration::DurationParseError => e errors.add(:default_artifacts_expire_in, ": #{e.message}") -- GitLab From 84531eadbdecf8f6350e310b1d28fc0c7874c5b9 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 15 Feb 2017 00:19:36 +0800 Subject: [PATCH 10/73] Add a few more tests and make sure empty value sets to nil --- app/models/application_setting.rb | 8 ++++++++ spec/models/application_setting_spec.rb | 22 ++++++++++++++++++++++ spec/requests/api/settings_spec.rb | 11 +++++++++-- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 2a3537a8fcc01b..a9fe4191bd0115 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -310,6 +310,14 @@ def repository_storage=(value) self.repository_storages = [value] end + def default_artifacts_expire_in=(value) + if value.present? + super(value.strip) + else + super(nil) + end + end + # Choose one of the available repository storage options. Currently all have # equal weighting. def pick_repository_storage diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 118a57c2909eb6..40768b35326332 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -35,6 +35,28 @@ it { is_expected.not_to allow_value(['test']).for(:disabled_oauth_sign_in_sources) } end + describe 'default_artifacts_expire_in' do + it 'sets an error if it is invalid' do + setting.update(default_artifacts_expire_in: 'a') + + expect(setting).to be_invalid + end + + it 'sets the value if it is valid' do + setting.update(default_artifacts_expire_in: '30 days') + + expect(setting).to be_valid + expect(setting.default_artifacts_expire_in).to eq('30 days') + end + + it 'does not set it if it is blank' do + setting.update(default_artifacts_expire_in: ' ') + + expect(setting).to be_valid + expect(setting.default_artifacts_expire_in).to be_nil + end + end + it { is_expected.to validate_presence_of(:max_attachment_size) } it do diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 91e3c333a02e63..411905edb49c4a 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -30,8 +30,14 @@ it "updates application settings" do put api("/application/settings", admin), - default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com', - plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com' + default_projects_limit: 3, + signin_enabled: false, + repository_storage: 'custom', + koding_enabled: true, + koding_url: 'http://koding.example.com', + plantuml_enabled: true, + plantuml_url: 'http://plantuml.example.com', + default_artifacts_expire_in: '2 days' expect(response).to have_http_status(200) expect(json_response['default_projects_limit']).to eq(3) expect(json_response['signin_enabled']).to be_falsey @@ -41,6 +47,7 @@ expect(json_response['koding_url']).to eq('http://koding.example.com') expect(json_response['plantuml_enabled']).to be_truthy expect(json_response['plantuml_url']).to eq('http://plantuml.example.com') + expect(json_response['default_artifacts_expire_in']).to eq('2 days') end end -- GitLab From 55059c662f2858b75c85adeda76a63c0575f3f9f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 15 Feb 2017 15:31:25 +0800 Subject: [PATCH 11/73] Fix tests and disallow 0 to make it consistent with .gitlab-ci.yml --- app/models/application_setting.rb | 12 +++++++++--- lib/api/entities.rb | 1 + lib/api/settings.rb | 2 +- spec/models/application_setting_spec.rb | 6 ++++++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index a9fe4191bd0115..4a542f146fb8e7 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -347,9 +347,15 @@ def check_repository_storages end def check_default_artifacts_expire_in - ChronicDuration.parse(default_artifacts_expire_in) if - default_artifacts_expire_in - true + return true unless default_artifacts_expire_in + + if ChronicDuration.parse(default_artifacts_expire_in).nil? + errors.add(:default_artifacts_expire_in, + "can't be 0. Leave it blank for unlimited") + false + else + true + end rescue ChronicDuration::DurationParseError => e errors.add(:default_artifacts_expire_in, ": #{e.message}") false diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 3cdb95f0b4d113..4700c8fbd0eb6e 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -610,6 +610,7 @@ class ApplicationSetting < Grape::Entity expose :default_project_visibility expose :default_snippet_visibility expose :default_group_visibility + expose :default_artifacts_expire_in expose :domain_whitelist expose :domain_blacklist_enabled expose :domain_blacklist diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 0c24576831b1e4..e2d99cb1e24cf1 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -57,7 +57,7 @@ def current_settings requires :shared_runners_text, type: String, desc: 'Shared runners text ' end optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts" - optional :default_artifacts_expire_in, type: Integer, desc: "Set the default expiration time for each job's artifacts" + optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts" optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics' diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 40768b35326332..a99bfee9572625 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -42,6 +42,12 @@ expect(setting).to be_invalid end + it 'does not allow 0' do + setting.update(default_artifacts_expire_in: '0') + + expect(setting).to be_invalid + end + it 'sets the value if it is valid' do setting.update(default_artifacts_expire_in: '30 days') -- GitLab From f05545cab222a128aaa56b8b0305d14218af87b0 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 15 Feb 2017 16:03:58 +0800 Subject: [PATCH 12/73] Update docs to reflect current behaviour --- app/models/application_setting.rb | 2 +- app/views/admin/application_settings/_form.html.haml | 4 +++- doc/user/admin_area/settings/continuous_integration.md | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 4a542f146fb8e7..cd8020aa49e9b2 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -351,7 +351,7 @@ def check_default_artifacts_expire_in if ChronicDuration.parse(default_artifacts_expire_in).nil? errors.add(:default_artifacts_expire_in, - "can't be 0. Leave it blank for unlimited") + "can't be 0. Leave it blank for no expiration") false else true diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 9a9a273f3e8a5c..687bba3c55e8cc 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -240,7 +240,9 @@ = f.text_field :default_artifacts_expire_in, class: 'form-control' .help-block Set the default expiration time for each job's artifacts - = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') + = surround '(', ')' do + = link_to 'syntax', help_page_path('ci/yaml/README', anchor: 'artifactsexpire_in') + = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration-time') - if Gitlab.config.registry.enabled %fieldset diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index 9ffe7e8562ed8c..524267a305b902 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -70,5 +70,5 @@ the group. ![Group pipelines quota](img/group_pipelines_quota.png) -[art-yml]: ../../../administration/job_artifacts.md -[ee-1078]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1078 +[art-yml]: ../../../administration/build_artifacts +[duration-syntax]: ../../../ci/yaml/README#artifactsexpire_in -- GitLab From 82e98b2431f952e0a541ffb44f2b514a8a747927 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 16 Feb 2017 11:00:35 +0000 Subject: [PATCH 13/73] Update schema --- db/schema.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/db/schema.rb b/db/schema.rb index 42eb45d9af2178..2ff0e49696e384 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -121,6 +121,7 @@ t.integer "repository_size_limit", limit: 8, default: 0 t.integer "terminal_max_session_time", default: 0, null: false t.integer "minimum_mirror_sync_time", default: 15, null: false + t.string "default_artifacts_expire_in", limit: 255 end create_table "approvals", force: :cascade do |t| -- GitLab From ddedbdfee47f30db7fd24d7afa064c8347f66ca2 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 16 Feb 2017 19:27:37 +0800 Subject: [PATCH 14/73] Use static error message and don't give booleans in validation. Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23437431 https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23437868 --- app/models/application_setting.rb | 13 ++++--------- spec/models/application_setting_spec.rb | 4 ++++ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index cd8020aa49e9b2..93fdba5c71e2e6 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -347,17 +347,12 @@ def check_repository_storages end def check_default_artifacts_expire_in - return true unless default_artifacts_expire_in - - if ChronicDuration.parse(default_artifacts_expire_in).nil? - errors.add(:default_artifacts_expire_in, + if default_artifacts_expire_in && + ChronicDuration.parse(default_artifacts_expire_in).nil? + errors.add(:default_artifacts_expiration, "can't be 0. Leave it blank for no expiration") - false - else - true end rescue ChronicDuration::DurationParseError => e - errors.add(:default_artifacts_expire_in, ": #{e.message}") - false + errors.add(:default_artifacts_expiration, "is invalid") end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index a99bfee9572625..5716e27c5974fd 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -40,12 +40,16 @@ setting.update(default_artifacts_expire_in: 'a') expect(setting).to be_invalid + expect(setting.errors.messages) + .to have_key(:default_artifacts_expiration) end it 'does not allow 0' do setting.update(default_artifacts_expire_in: '0') expect(setting).to be_invalid + expect(setting.errors.messages) + .to have_key(:default_artifacts_expiration) end it 'sets the value if it is valid' do -- GitLab From eb07c60ec29cd9807c624a995c914353cd23aaf1 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 16 Feb 2017 19:33:30 +0800 Subject: [PATCH 15/73] Use squish to also compact the string Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23436980 --- app/models/application_setting.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 93fdba5c71e2e6..778439bfe45756 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -312,7 +312,7 @@ def repository_storage=(value) def default_artifacts_expire_in=(value) if value.present? - super(value.strip) + super(value.squish) else super(nil) end -- GitLab From dae36f983969bc10b826d72b29ffd59e22062a9d Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 16 Feb 2017 22:29:29 +0800 Subject: [PATCH 16/73] The exception was no longer used --- app/models/application_setting.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 778439bfe45756..4be05161d9feec 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -352,7 +352,7 @@ def check_default_artifacts_expire_in errors.add(:default_artifacts_expiration, "can't be 0. Leave it blank for no expiration") end - rescue ChronicDuration::DurationParseError => e + rescue ChronicDuration::DurationParseError errors.add(:default_artifacts_expiration, "is invalid") end end -- GitLab From 007324b71710bd18398c74cf2a2421a70d858688 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 16 Feb 2017 23:40:13 +0800 Subject: [PATCH 17/73] 0 for unlimited, disallow blank, feedback: https://gitlab.com/gitlab-org/gitlab-ce/issues/27762#note_23520780 --- app/models/application_setting.rb | 16 +++--------- app/models/ci/build.rb | 2 +- .../application_settings/_form.html.haml | 5 ++-- ...acts_expiration_to_application_settings.rb | 4 +-- db/schema.rb | 2 +- spec/models/application_setting_spec.rb | 26 ++++++++++--------- spec/models/ci/build_spec.rb | 6 +++++ 7 files changed, 31 insertions(+), 30 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 4be05161d9feec..b8918b8266f0da 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -310,14 +310,6 @@ def repository_storage=(value) self.repository_storages = [value] end - def default_artifacts_expire_in=(value) - if value.present? - super(value.squish) - else - super(nil) - end - end - # Choose one of the available repository storage options. Currently all have # equal weighting. def pick_repository_storage @@ -347,10 +339,10 @@ def check_repository_storages end def check_default_artifacts_expire_in - if default_artifacts_expire_in && - ChronicDuration.parse(default_artifacts_expire_in).nil? - errors.add(:default_artifacts_expiration, - "can't be 0. Leave it blank for no expiration") + if default_artifacts_expire_in.blank? + errors.add(:default_artifacts_expiration, "is not presented") + else + ChronicDuration.parse(default_artifacts_expire_in) end rescue ChronicDuration::DurationParseError errors.add(:default_artifacts_expiration, "is invalid") diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 9d60bb24660fb8..a46f628e61f14b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -485,7 +485,7 @@ def artifacts_expire_in def artifacts_expire_in=(value) self.artifacts_expire_at = if value - Time.now + ChronicDuration.parse(value) + ChronicDuration.parse(value)&.seconds&.from_now end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 687bba3c55e8cc..983ffb860873f7 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -239,10 +239,11 @@ .col-sm-10 = f.text_field :default_artifacts_expire_in, class: 'form-control' .help-block - Set the default expiration time for each job's artifacts + Set the default expiration time for each job's artifacts. + 0 for unlimited. = surround '(', ')' do = link_to 'syntax', help_page_path('ci/yaml/README', anchor: 'artifactsexpire_in') - = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration-time') + = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') - if Gitlab.config.registry.enabled %fieldset diff --git a/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb index 34905570739a29..e0e3ff8957a05f 100644 --- a/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb +++ b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb @@ -5,7 +5,7 @@ class AddDefaultArtifactsExpirationToApplicationSettings < ActiveRecord::Migrati def change add_column :application_settings, - :default_artifacts_expire_in, - :string, null: true + :default_artifacts_expire_in, :string, + null: false, default: '0' end end diff --git a/db/schema.rb b/db/schema.rb index 2ff0e49696e384..bbc1f893fb3e67 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -121,7 +121,7 @@ t.integer "repository_size_limit", limit: 8, default: 0 t.integer "terminal_max_session_time", default: 0, null: false t.integer "minimum_mirror_sync_time", default: 15, null: false - t.string "default_artifacts_expire_in", limit: 255 + t.string "default_artifacts_expire_in", default: '0', null: false end create_table "approvals", force: :cascade do |t| diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 5716e27c5974fd..22e54ed6856888 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -36,20 +36,16 @@ end describe 'default_artifacts_expire_in' do - it 'sets an error if it is invalid' do + it 'sets an error if it cannot parse' do setting.update(default_artifacts_expire_in: 'a') - expect(setting).to be_invalid - expect(setting.errors.messages) - .to have_key(:default_artifacts_expiration) + expect_invalid end - it 'does not allow 0' do - setting.update(default_artifacts_expire_in: '0') + it 'sets an error if it is blank' do + setting.update(default_artifacts_expire_in: ' ') - expect(setting).to be_invalid - expect(setting.errors.messages) - .to have_key(:default_artifacts_expiration) + expect_invalid end it 'sets the value if it is valid' do @@ -59,11 +55,17 @@ expect(setting.default_artifacts_expire_in).to eq('30 days') end - it 'does not set it if it is blank' do - setting.update(default_artifacts_expire_in: ' ') + it 'sets the value if it is 0' do + setting.update(default_artifacts_expire_in: '0') expect(setting).to be_valid - expect(setting.default_artifacts_expire_in).to be_nil + expect(setting.default_artifacts_expire_in).to eq('0') + end + + def expect_invalid + expect(setting).to be_invalid + expect(setting.errors.messages) + .to have_key(:default_artifacts_expiration) end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 401d4c83dbfd40..4251cb06a2d41a 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -167,6 +167,12 @@ is_expected.to be_nil end + + it 'when setting to 0' do + build.artifacts_expire_in = '0' + + is_expected.to be_nil + end end describe '#commit' do -- GitLab From 5e3a78c4366610b556535ddf6312b5106855f5dd Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Mon, 20 Feb 2017 13:48:16 +0600 Subject: [PATCH 18/73] fixes line number copy issue for unfolded parallel view diff --- app/views/projects/blob/diff.html.haml | 4 ++-- .../28367-fix-unfold-diff-line-number-copy-paste.yml | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index d1f7f65bf53a3d..c48d9dd982cdf6 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -19,10 +19,10 @@ = line_content - when :parallel %td.old_line.diff-line-num{ data: { linenumber: line_old } } - = link_to raw(line_old), "##{line_old}" + %a{ href: "##{line_old}", data: { linenumber: line_old } } = line_content %td.new_line.diff-line-num{ data: { linenumber: line_new } } - = link_to raw(line_new), "##{line_new}" + %a{ href: "##{line_new}", data: { linenumber: line_new } } = line_content - if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size diff --git a/changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml b/changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml new file mode 100644 index 00000000000000..6fc89fd91dd8d0 --- /dev/null +++ b/changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml @@ -0,0 +1,4 @@ +--- +title: Fixes includes line number during unfold copy n paste in parallel diff view +merge_request: 9365 +author: -- GitLab From 475a8bfed0f30507f2bd85371d4cba8133e590c2 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 20 Feb 2017 08:22:44 -0500 Subject: [PATCH 19/73] Remove markup showing in tooltip for renamed files in diff view --- app/views/projects/diffs/_file_header.html.haml | 4 ++-- .../unreleased/28366-renamed-file-tooltip-contains-html.yml | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index 1dbfe830d52bd0..f809c52c3678e5 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -10,10 +10,10 @@ - if diff_file.renamed_file - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) - %strong.file-title-name.has-tooltip{ data: { title: old_path, container: 'body' } } + %strong.file-title-name.has-tooltip{ data: { title: diff_file.old_path, container: 'body' } } = old_path → - %strong.file-title-name.has-tooltip{ data: { title: new_path, container: 'body' } } + %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } } = new_path - else %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } } diff --git a/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml b/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml new file mode 100644 index 00000000000000..faf1e89ed94082 --- /dev/null +++ b/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml @@ -0,0 +1,4 @@ +--- +title: Remove markup that was showing in tooltip for renamed files +merge_request: 9374 +author: -- GitLab From fe57e5d6abfb3b1f96b31389b4ed6e7f4b3323f5 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 21 Feb 2017 02:25:35 +0800 Subject: [PATCH 20/73] Update error message and check with presence: true Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23762243 https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23762268 --- app/models/application_setting.rb | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index b8918b8266f0da..3883b5e549e9b9 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -85,6 +85,7 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than: 0 } + validates :default_artifacts_expire_in, presence: true validate :check_default_artifacts_expire_in validates :container_registry_token_expire_delay, @@ -263,6 +264,14 @@ def elasticsearch_host read_attribute(:elasticsearch_host).split(',').map(&:strip) end + def self.human_attribute_name(attr, _options = {}) + if attr == :default_artifacts_expire_in + 'Default artifacts expiration' + else + super + end + end + def home_page_url_column_exist ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url) end @@ -339,12 +348,8 @@ def check_repository_storages end def check_default_artifacts_expire_in - if default_artifacts_expire_in.blank? - errors.add(:default_artifacts_expiration, "is not presented") - else - ChronicDuration.parse(default_artifacts_expire_in) - end + ChronicDuration.parse(default_artifacts_expire_in) rescue ChronicDuration::DurationParseError - errors.add(:default_artifacts_expiration, "is invalid") + errors.add(:default_artifacts_expiration, "is not a correct duration") end end -- GitLab From aad6d6d6cf578613760538bf667a414d2cf5125c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 21 Feb 2017 02:28:21 +0800 Subject: [PATCH 21/73] Add screenshot for default artifacts expiration, and update screenshot for maximum artifacts size. --- ...dmin_area_default_artifacts_expiration.png | Bin 0 -> 16484 bytes .../img/admin_area_maximum_artifacts_size.png | Bin 3447 -> 12917 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png diff --git a/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png b/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png new file mode 100644 index 0000000000000000000000000000000000000000..c6425ad32cd991ff361a9d40c4147d237e37ec82 GIT binary patch literal 16484 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 Rz)H{-D^FKHmvv4FO#loOpCAAL literal 0 HcmV?d00001 diff --git a/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png b/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png index b7d6671902ad980d5eb7497626fffa08d3fe68e0..33fd29e2039a20adb0f2e49b3c7313fe4b957e87 100644 GIT binary patch literal 12917 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 literal 3447 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 Date: Tue, 21 Feb 2017 02:36:11 +0800 Subject: [PATCH 22/73] Update doc according to the new syntax --- doc/user/admin_area/settings/continuous_integration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index 524267a305b902..518e4f699fdc70 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -30,7 +30,7 @@ expiration. ![Admin area settings button](img/admin_area_settings_button.png) -1. Change the value of default expiration time (in days): +1. Change the value of default expiration time ([syntax][duration-syntax]): ![Admin area default artifacts expiration](img/admin_area_default_artifacts_expiration.png) -- GitLab From 2e83e1e12e95ba96c4dae8e5e733d368b239180d Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 21 Feb 2017 18:46:52 +0800 Subject: [PATCH 23/73] Fix tests due to error key changed --- app/models/application_setting.rb | 2 +- spec/models/application_setting_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 3883b5e549e9b9..d75b0abe32b352 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -350,6 +350,6 @@ def check_repository_storages def check_default_artifacts_expire_in ChronicDuration.parse(default_artifacts_expire_in) rescue ChronicDuration::DurationParseError - errors.add(:default_artifacts_expiration, "is not a correct duration") + errors.add(:default_artifacts_expire_in, "is not a correct duration") end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 22e54ed6856888..cf9bb8ff539781 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -65,7 +65,7 @@ def expect_invalid expect(setting).to be_invalid expect(setting.errors.messages) - .to have_key(:default_artifacts_expiration) + .to have_key(:default_artifacts_expire_in) end end -- GitLab From 3f0cb448bef75f6849aee160ab05e7d7f8cfb0d1 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 22 Feb 2017 23:48:49 +0800 Subject: [PATCH 24/73] Remove syntax help link and update screenshot Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23943262 --- .../application_settings/_form.html.haml | 2 -- ...dmin_area_default_artifacts_expiration.png | Bin 16484 -> 14656 bytes 2 files changed, 2 deletions(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 983ffb860873f7..f65956d28cd752 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -241,8 +241,6 @@ .help-block Set the default expiration time for each job's artifacts. 0 for unlimited. - = surround '(', ')' do - = link_to 'syntax', help_page_path('ci/yaml/README', anchor: 'artifactsexpire_in') = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') - if Gitlab.config.registry.enabled diff --git a/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png b/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png index c6425ad32cd991ff361a9d40c4147d237e37ec82..50a86ede56ba0a2ca1268bdd1e748c84f51e8c7a 100644 GIT binary patch delta 11606 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 delta 13448 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 -- GitLab From 2cbed59fdb40ef9e4457ebbd9fc4470541ca2ec1 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Wed, 22 Feb 2017 21:08:54 +0000 Subject: [PATCH 25/73] clarify custom domain details --- doc/administration/pages/index.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 1c444cf0d50051..62b0468da7962b 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -26,22 +26,24 @@ it works. --- -In the case of custom domains, the Pages daemon needs to listen on ports `80` -and/or `443`. For that reason, there is some flexibility in the way which you -can set it up: +In the case of [custom domains](#custom-domains) (but not +[wildcard domains](#wildcard-domains)), the Pages daemon needs to listen on +ports `80` and/or `443`. For that reason, there is some flexibility in the way +which you can set it up: -1. Run the pages daemon in the same server as GitLab, listening on a secondary IP. -1. Run the pages daemon in a separate server. In that case, the +1. Run the Pages daemon in the same server as GitLab, listening on a secondary IP. +1. Run the Pages daemon in a separate server. In that case, the [Pages path](#change-storage-path) must also be present in the server that - the pages daemon is installed, so you will have to share it via network. -1. Run the pages daemon in the same server as GitLab, listening on the same IP + the Pages daemon is installed, so you will have to share it via network. +1. Run the Pages daemon in the same server as GitLab, listening on the same IP but on different ports. In that case, you will have to proxy the traffic with a loadbalancer. If you choose that route note that you should use TCP load balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the pages will not be able to be served with user provided certificates. For HTTP it's OK to use HTTP or TCP load balancing. -In this document, we will proceed assuming the first option. +In this document, we will proceed assuming the first option. If you are not +supporting custom domains a secondary IP is not needed. ## Prerequisites @@ -54,6 +56,7 @@ Before proceeding with the Pages configuration, you will need to: serve Pages under HTTPS. 1. (Optional but recommended) Enable [Shared runners](../../ci/runners/README.md) so that your users don't have to bring their own. +1. (Only for custom domains) Have a **secondary IP**. ### DNS configuration @@ -150,7 +153,7 @@ that without TLS certificates. > URL scheme: `http://page.example.io` and `http://domain.com` -In that case, the pages daemon is running, Nginx still proxies requests to +In that case, the Pages daemon is running, Nginx still proxies requests to the daemon but the daemon is also able to receive requests from the outside world. Custom domains are supported, but no TLS. @@ -179,7 +182,7 @@ world. Custom domains are supported, but no TLS. > URL scheme: `https://page.example.io` and `https://domain.com` -In that case, the pages daemon is running, Nginx still proxies requests to +In that case, the Pages daemon is running, Nginx still proxies requests to the daemon but the daemon is also able to receive requests from the outside world. Custom domains and TLS are supported. -- GitLab From d863ad50b99b859623897a7a1bf6a08294ecd481 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Thu, 23 Feb 2017 12:36:23 +0100 Subject: [PATCH 26/73] Disable unused tags count cache for Projects, Builds and Runners + remove complete leftover when Issues were tagged using acts_as_taggable --- app/models/issue.rb | 2 -- app/models/project.rb | 3 +-- changelogs/unreleased/27989-disable-counting-tags.yml | 4 ++++ config/initializers/acts_as_taggable.rb | 5 +++++ 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/27989-disable-counting-tags.yml create mode 100644 config/initializers/acts_as_taggable.rb diff --git a/app/models/issue.rb b/app/models/issue.rb index 6e66dca56c20cb..b1f6a5b9e94c13 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -21,8 +21,6 @@ class Issue < ActiveRecord::Base DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze - ActsAsTaggableOn.strict_case_match = true - belongs_to :project belongs_to :moved_to, class_name: 'Issue' diff --git a/app/models/project.rb b/app/models/project.rb index e2ca179ce12d9a..9f155c830956d8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -64,8 +64,7 @@ def set_last_activity_at after_validation :check_pending_delete - ActsAsTaggableOn.strict_case_match = true - acts_as_taggable_on :tags + acts_as_taggable attr_accessor :new_default_branch attr_accessor :old_path_with_namespace diff --git a/changelogs/unreleased/27989-disable-counting-tags.yml b/changelogs/unreleased/27989-disable-counting-tags.yml new file mode 100644 index 00000000000000..988785ac4543c1 --- /dev/null +++ b/changelogs/unreleased/27989-disable-counting-tags.yml @@ -0,0 +1,4 @@ +--- +title: Disable unused tags count cache for Projects, Builds and Runners +merge_request: +author: diff --git a/config/initializers/acts_as_taggable.rb b/config/initializers/acts_as_taggable.rb new file mode 100644 index 00000000000000..c564c0cab11342 --- /dev/null +++ b/config/initializers/acts_as_taggable.rb @@ -0,0 +1,5 @@ +ActsAsTaggableOn.strict_case_match = true + +# tags_counter enables caching count of tags which results in an update whenever a tag is added or removed +# since the count is not used anywhere its better performance wise to disable this cache +ActsAsTaggableOn.tags_counter = false -- GitLab From 9504ca8b2006ec9acdd36c49c80f2c89c1c7d148 Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Mon, 13 Feb 2017 16:10:39 +0600 Subject: [PATCH 27/73] fixes job dropdown action button error --- .../javascripts/vue_pipelines_index/stage.js.es6 | 16 ++++++++++------ ...0-fix-job-dropdown-pipeline-console-error.yml | 4 ++++ 2 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 index 8cc417a9966120..cfe6ea10140861 100644 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -37,15 +37,19 @@ return flash; }); }, + /** + * When the user right clicks or cmd/ctrl + click in the job name or action icon, + * the dropdown should not be closed and the link should open in another tab. + * If the target is a svg we stop propagation in order to prevent + * the default behavior of the dropdown. + */ keepGraph(e) { const { target } = e; + const svgClassName = target.getAttribute('class'); + const svgParentClassName = target.parentElement && target.parentElement.getAttribute('class'); - if (target.className.indexOf('js-ci-action-icon') >= 0) return null; - - if ( - target.parentElement && - (target.parentElement.className.indexOf('js-ci-action-icon') >= 0) - ) return null; + if (svgClassName && svgClassName.indexOf('js-ci-action-icon') >= 0) return null; + if (svgParentClassName && svgParentClassName.indexOf('js-ci-action-icon') >= 0) return null; return e.stopPropagation(); }, diff --git a/changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml b/changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml new file mode 100644 index 00000000000000..4436b4bee685e6 --- /dev/null +++ b/changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml @@ -0,0 +1,4 @@ +--- +title: Fixes job dropdown action throws error in js console +merge_request: 9182 +author: -- GitLab From 13196dfcc0b939b4a8d45afa8014851b8eed7b77 Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Mon, 20 Feb 2017 19:37:02 +0600 Subject: [PATCH 28/73] adds mount function with prevent and removes keepGraph function --- .../vue_pipelines_index/stage.js.es6 | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 index cfe6ea10140861..7082df2fcea9bb 100644 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -23,6 +23,19 @@ required: true, }, }, + mounted() { + /** + * When the user right clicks or cmd/ctrl + click in the job name or action icon, + * the dropdown should not be closed and the link should open in another tab. + * If the target is a svg we stop propagation in order to prevent + * the default behavior of the dropdown. + */ + console.log('I am called'); + $('.js-builds-dropdown-list').on('click', (e) => { + console.log('i am in event'); + e.stopPropagation(); + }); + }, methods: { fetchBuilds(e) { const areaExpanded = e.currentTarget.attributes['aria-expanded']; @@ -37,22 +50,6 @@ return flash; }); }, - /** - * When the user right clicks or cmd/ctrl + click in the job name or action icon, - * the dropdown should not be closed and the link should open in another tab. - * If the target is a svg we stop propagation in order to prevent - * the default behavior of the dropdown. - */ - keepGraph(e) { - const { target } = e; - const svgClassName = target.getAttribute('class'); - const svgParentClassName = target.parentElement && target.parentElement.getAttribute('class'); - - if (svgClassName && svgClassName.indexOf('js-ci-action-icon') >= 0) return null; - if (svgParentClassName && svgParentClassName.indexOf('js-ci-action-icon') >= 0) return null; - - return e.stopPropagation(); - }, }, computed: { buildsOrSpinner() { @@ -80,13 +77,13 @@ template: `
+
+
+
+
+

+ Customize your experience +

+

+ Change syntax themes, default project pages, and more in preferences. +

+ Check it out +
+
+
`; + class UserCallout { constructor() { this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE); + this.userCalloutBody = $(userCalloutElementName); + this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName); + $(userCalloutElementName).removeAttr(userCalloutSvgAttrName); this.init(); - this.toggleUserCallout(); } init() { - $(document) - .on('click', closeButton, () => this.dismissCallout()) - .on('click', userCalloutBtn, () => this.dismissCallout()); + const $template = $(USER_CALLOUT_TEMPLATE); + if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') { + $template.find('.svg-container').append(this.userCalloutSvg); + this.userCalloutBody.append($template); + $template.find(closeButton).on('click', e => this.dismissCallout(e)); + $template.find(userCalloutBtn).on('click', e => this.dismissCallout(e)); + } } - dismissCallout() { + dismissCallout(e) { Cookies.set(USER_CALLOUT_COOKIE, 'true'); - } - - toggleUserCallout() { - if (!this.isCalloutDismissed) { - $(userCalloutElementName).show(); + const $currentTarget = $(e.currentTarget); + if ($currentTarget.hasClass('close-user-callout')) { + this.userCalloutBody.empty(); } } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index dd252bf1e57c65..aad1a8986b0fc3 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -279,7 +279,6 @@ table.u2f-registrations { } .user-callout { - display: none; margin: 24px auto 0; .bordered-box { diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index 8276cce693c3dc..b82b933c3ad78a 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -5,7 +5,7 @@ - page_title "Projects" - header_title "Projects", dashboard_projects_path -= render partial: 'shared/user_callout' +.user-callout{ 'callout-svg' => custom_icon('icon_customization') } - if @projects.any? || params[:filter_projects] = render 'dashboard/projects_head' diff --git a/app/views/shared/_user_callout.html.haml b/app/views/shared/_user_callout.html.haml deleted file mode 100644 index 3f31025156841b..00000000000000 --- a/app/views/shared/_user_callout.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -.user-callout - .bordered-box.landing.content-block - %button.btn.btn-default.close.close-user-callout{ type: "button" } - = icon("times", class: "dismiss-icon") - .row - .col-sm-3.col-xs-12.svg-container - = custom_icon('icon_customization') - .col-sm-8.col-xs-12.inner-content - %h4 - Customize your experience - %p - Change syntax themes, default project pages, and more in preferences. - - = link_to "Check it out", profile_preferences_path, class: 'btn user-callout-btn' diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 6203c668ff5259..c130f3d9e17998 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -96,9 +96,9 @@ %li.js-snippets-tab = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do Snippets - + %div{ class: container_class } - = render partial: 'shared/user_callout' + .user-callout{ 'callout-svg' => custom_icon('icon_customization') } .tab-content #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs diff --git a/spec/javascripts/fixtures/user_callout.html.haml b/spec/javascripts/fixtures/user_callout.html.haml index ad564469a270c3..275359bde0ae10 100644 --- a/spec/javascripts/fixtures/user_callout.html.haml +++ b/spec/javascripts/fixtures/user_callout.html.haml @@ -1,13 +1,2 @@ -.user-callout - .bordered-box.landing.content-block - %button.btn.btn-default.close.close-user-callout{ type: "button" } - %i.fa.fa-times.dismiss-icon - .row - .col-sm-3.col-xs-12.svg-container - .col-sm-8.col-xs-12.inner-content - %h4 - Customize your experience - %p - Change syntax themes, default project pages, and more in preferences. - %a{ href: 'foo', class:'user-callout-btn' } - Check it out +.user-callout{ 'callout-svg' => custom_icon('icon_customization') } + diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js index 097368db6e5ebe..e174fdf35de622 100644 --- a/spec/javascripts/user_callout_spec.js +++ b/spec/javascripts/user_callout_spec.js @@ -11,22 +11,22 @@ describe('UserCallout', function () { loadFixtures(fixtureName); this.userCallout = new UserCallout(); this.closeButton = $('.close-user-callout'); - this.userCalloutContainer = $('.user-callout'); this.userCalloutBtn = $('.user-callout-btn'); + this.userCalloutContainer = $('.user-callout'); Cookie.set(USER_CALLOUT_COOKIE, 'false'); }); - it('shows when cookie is set to false', () => { + fit('shows when cookie is set to false', () => { expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeDefined(); expect(this.userCalloutContainer.is(':visible')).toBe(true); }); - it('hides when user clicks on the dismiss-icon', () => { + fit('hides when user clicks on the dismiss-icon', () => { this.closeButton.click(); expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); }); - it('hides when user clicks on the "check it out" button', () => { + fit('hides when user clicks on the "check it out" button', () => { this.userCalloutBtn.click(); expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); }); -- GitLab From 54ac0815c2369e3881bd051f3b25032b483e6748 Mon Sep 17 00:00:00 2001 From: Oswaldo Date: Wed, 22 Feb 2017 14:37:13 -0300 Subject: [PATCH 47/73] Return 202 with JSON body on async removals on V4 API --- ...rectly-return-json-on-delete-responses.yml | 4 ++++ doc/api/v3_to_v4.md | 2 ++ lib/api/branches.rb | 2 +- lib/api/helpers.rb | 4 ++++ lib/api/projects.rb | 2 ++ lib/api/v3/branches.rb | 7 +++++++ spec/requests/api/branches_spec.rb | 6 ++++-- spec/requests/api/projects_spec.rb | 8 +++++-- spec/requests/api/v3/branches_spec.rb | 21 +++++++++++++++++++ 9 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml diff --git a/changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml b/changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml new file mode 100644 index 00000000000000..4a4932288b4a23 --- /dev/null +++ b/changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml @@ -0,0 +1,4 @@ +--- +title: Return 202 with JSON body on async removals on V4 API +merge_request: +author: diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 5905a0cc04b7e5..4c8818da1c5e97 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -42,3 +42,5 @@ changes are in V4: - Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736) - Remove the ProjectGitHook API. Use the ProjectPushRule API instead [!1301](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1301) - Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384) +- Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449) + diff --git a/lib/api/branches.rb b/lib/api/branches.rb index c65de90cca29c2..34f136948c2392 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -137,7 +137,7 @@ class Branches < Grape::API delete ":id/repository/merged_branches" do DeleteMergedBranchesService.new(user_project, current_user).async_execute - status(200) + accepted! end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 9021cf55fc23e8..a6b5ab62f91dec 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -216,6 +216,10 @@ def no_content! render_api_error!('204 No Content', 204) end + def accepted! + render_api_error!('202 Accepted', 202) + end + def render_validation_error!(model) if model.errors.any? render_api_error!(model.errors.messages || '400 Bad Request', 400) diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 3e7bebde388331..ef2199d95e14e9 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -288,6 +288,8 @@ def present_projects(projects, options = {}) delete ":id" do authorize! :remove_project, user_project ::Projects::DestroyService.new(user_project, current_user, {}).async_execute + + accepted! end desc 'Mark this project as forked from another' diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb index 733c6b21be5d50..51eb566cf7d620 100644 --- a/lib/api/v3/branches.rb +++ b/lib/api/v3/branches.rb @@ -18,6 +18,13 @@ class Branches < Grape::API present branches, with: ::API::Entities::RepoBranch, project: user_project end + + desc 'Delete all merged branches' + delete ":id/repository/merged_branches" do + DeleteMergedBranchesService.new(user_project, current_user).async_execute + + status(200) + end end end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 404311d2372a41..55f62d5832132a 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -373,9 +373,11 @@ allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) end - it 'returns 200' do + it 'returns 202 with json body' do delete api("/projects/#{project.id}/repository/merged_branches", user) - expect(response).to have_http_status(200) + + expect(response).to have_http_status(202) + expect(json_response['message']).to eql('202 Accepted') end it 'returns a 403 error if guest' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 496dd4438bac7a..50fb6c8732716c 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1272,7 +1272,9 @@ context 'when authenticated as user' do it 'removes project' do delete api("/projects/#{project.id}", user) - expect(response).to have_http_status(200) + + expect(response).to have_http_status(202) + expect(json_response['message']).to eql('202 Accepted') end it 'does not remove a project if not an owner' do @@ -1296,7 +1298,9 @@ context 'when authenticated as admin' do it 'removes any existing project' do delete api("/projects/#{project.id}", admin) - expect(response).to have_http_status(200) + + expect(response).to have_http_status(202) + expect(json_response['message']).to eql('202 Accepted') end it 'does not remove a non existing project' do diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb index 0e4c6bc3bc63c6..a3e1581fcc5e50 100644 --- a/spec/requests/api/v3/branches_spec.rb +++ b/spec/requests/api/v3/branches_spec.rb @@ -20,4 +20,25 @@ expect(branch_names).to match_array(project.repository.branch_names) end end + + describe "DELETE /projects/:id/repository/merged_branches" do + before do + allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) + end + + it 'returns 200' do + delete v3_api("/projects/#{project.id}/repository/merged_branches", user) + + expect(response).to have_http_status(200) + end + + it 'returns a 403 error if guest' do + user_b = create :user + create(:project_member, :guest, user: user_b, project: project) + + delete v3_api("/projects/#{project.id}/repository/merged_branches", user_b) + + expect(response).to have_http_status(403) + end + end end -- GitLab From 47ae1619a70da5fec1924f105fe725ead3f322ee Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 17:40:36 -0600 Subject: [PATCH 48/73] remove require.context from profile_bundle --- app/assets/javascripts/profile/profile_bundle.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js index d7f3c9fd37e167..15d32825583cb9 100644 --- a/app/assets/javascripts/profile/profile_bundle.js +++ b/app/assets/javascripts/profile/profile_bundle.js @@ -1,3 +1,2 @@ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!profile_bundle).*\.(js|es6)$/)); +require('./gl_crop'); +require('./profile'); -- GitLab From f19c3ec11c6fd30e7a8d33cba9ceeb99747ffba1 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 17:46:10 -0600 Subject: [PATCH 49/73] remove require.context from protected_branches_bundle --- .../protected_branches/protected_branches_bundle.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js index ffb66caf5f45da..849c1e31623bc8 100644 --- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js +++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js @@ -1,3 +1,5 @@ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!protected_branches_bundle).*\.(js|es6)$/)); +require('./protected_branch_access_dropdown'); +require('./protected_branch_create'); +require('./protected_branch_dropdown'); +require('./protected_branch_edit'); +require('./protected_branch_edit_list'); -- GitLab From 7f822eb910ff10c34a92d99a8463b457de193c67 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 17:53:01 -0600 Subject: [PATCH 50/73] remove require.context from snippet_bundle --- app/assets/javascripts/snippet/snippet_bundle.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index 89822246bb82b1..a98403f4cf2a4f 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,10 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */ /* global ace */ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!snippet_bundle).*\.(js|es6)$/)); - (function() { $(function() { var editor = ace.edit("editor"); -- GitLab From bb739cd93860660af5010ec556410e9f9d5e1706 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 23 Feb 2017 17:55:01 -0600 Subject: [PATCH 51/73] Use Namespace#full_path instead of #path where appropriate --- app/assets/javascripts/milestone_select.js | 5 +- app/helpers/issuables_helper.rb | 2 +- app/models/project.rb | 10 +-- app/models/user.rb | 2 +- app/views/devise/shared/_signup_box.html.haml | 2 +- app/views/projects/edit.html.haml | 2 +- app/views/shared/_group_form.html.haml | 2 +- app/views/shared/issuable/_sidebar.html.haml | 2 +- lib/gitlab/import_export.rb | 2 +- lib/gitlab/regex.rb | 11 +-- spec/controllers/blob_controller_spec.rb | 8 +-- .../projects/blame_controller_spec.rb | 4 +- .../projects/blob_controller_spec.rb | 12 ++-- .../projects/boards/issues_controller_spec.rb | 6 +- .../projects/boards/lists_controller_spec.rb | 10 +-- .../projects/boards_controller_spec.rb | 8 +-- .../projects/branches_controller_spec.rb | 28 ++++---- .../projects/commit_controller_spec.rb | 52 +++++++------- .../projects/commits_controller_spec.rb | 8 +-- .../projects/compare_controller_spec.rb | 32 ++++----- .../cycle_analytics_controller_spec.rb | 8 +-- .../projects/find_file_controller_spec.rb | 8 +-- .../projects/forks_controller_spec.rb | 12 ++-- .../projects/graphs_controller_spec.rb | 2 +- .../projects/group_links_controller_spec.rb | 12 ++-- .../projects/imports_controller_spec.rb | 18 ++--- .../projects/issues_controller_spec.rb | 54 +++++++------- .../projects/labels_controller_spec.rb | 16 ++--- .../projects/mattermosts_controller_spec.rb | 4 +- .../merge_requests_controller_spec.rb | 72 ++++++++++--------- .../projects/pipelines_controller_spec.rb | 8 +-- .../protected_branches_controller_spec.rb | 2 +- .../projects/raw_controller_spec.rb | 8 +-- .../projects/refs_controller_spec.rb | 4 +- .../projects/releases_controller_spec.rb | 6 +- .../projects/repositories_controller_spec.rb | 6 +- .../projects/snippets_controller_spec.rb | 36 +++++----- .../projects/tags_controller_spec.rb | 4 +- .../projects/templates_controller_spec.rb | 8 +-- .../projects/todo_controller_spec.rb | 8 +-- .../projects/tree_controller_spec.rb | 6 +- .../projects/uploads_controller_spec.rb | 8 +-- .../projects/variables_controller_spec.rb | 8 +-- spec/controllers/projects_controller_spec.rb | 72 +++++++++---------- .../project_member_activity_index_spec.rb | 2 +- .../features/projects/members/sorting_spec.rb | 2 +- spec/helpers/application_helper_spec.rb | 4 +- spec/javascripts/fixtures/branches.rb | 2 +- spec/javascripts/fixtures/builds.rb | 2 +- spec/javascripts/fixtures/issues.rb | 2 +- spec/javascripts/fixtures/merge_requests.rb | 2 +- spec/javascripts/fixtures/projects.rb | 2 +- spec/javascripts/fixtures/todos.rb | 4 +- .../project_url_constrainer_spec.rb | 4 +- spec/lib/gitlab/conflict/file_spec.rb | 2 +- spec/lib/gitlab/regex_spec.rb | 4 +- spec/models/ci/build_spec.rb | 4 +- spec/models/concerns/routable_spec.rb | 4 +- spec/models/namespace_spec.rb | 4 +- .../project_services/drone_ci_service_spec.rb | 2 +- spec/models/project_spec.rb | 16 ++--- spec/requests/api/commits_spec.rb | 10 +++ spec/requests/api/groups_spec.rb | 2 +- spec/requests/api/v3/commits_spec.rb | 2 +- ...issuables_list_metadata_shared_examples.rb | 2 +- spec/support/markdown_feature.rb | 5 +- spec/support/test_env.rb | 4 +- spec/workers/repository_fork_worker_spec.rb | 20 +++--- 68 files changed, 358 insertions(+), 347 deletions(-) diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 8df1c8e7f9463c..51fa5c828b38c8 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -39,7 +39,7 @@ $value = $block.find('.value'); $loading = $block.find('.block-loading').fadeOut(); if (issueUpdateURL) { - milestoneLinkTemplate = _.template('<%- title %>'); + milestoneLinkTemplate = _.template('<%- title %>'); milestoneLinkNoneTemplate = 'None'; collapsedSidebarLabelTemplate = _.template(' <%- title %> '); } @@ -181,8 +181,7 @@ $selectbox.hide(); $value.css('display', ''); if (data.milestone != null) { - data.milestone.namespace = _this.currentProject.namespace; - data.milestone.path = _this.currentProject.path; + data.milestone.full_path = _this.currentProject.full_path; data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date); $value.html(milestoneLinkTemplate(data.milestone)); return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index faed1f16e948d7..00c59aa7142bd3 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -52,7 +52,7 @@ def template_dropdown_tag(issuable, &block) field_name: 'issuable_template', selected: selected_template(issuable), project_path: ref_project.path, - namespace_path: ref_project.namespace.path + namespace_path: ref_project.namespace.full_path } } diff --git a/app/models/project.rb b/app/models/project.rb index 9f155c830956d8..25e95dd720eb9a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -398,7 +398,7 @@ def sort(method) end def reference_pattern - name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR + name_pattern = Gitlab::Regex::FULL_NAMESPACE_REGEX_STR %r{ ((?#{name_pattern})\/)? @@ -957,10 +957,6 @@ def url_to_repo gitlab_shell.url_to_repo(path_with_namespace) end - def namespace_dir - namespace.try(:path) || '' - end - def repo_exists? @repo_exists ||= repository.exists? rescue @@ -1014,8 +1010,8 @@ def personal? def rename_repo path_was = previous_changes['path'].first - old_path_with_namespace = File.join(namespace_dir, path_was) - new_path_with_namespace = File.join(namespace_dir, path) + old_path_with_namespace = File.join(namespace.full_path, path_was) + new_path_with_namespace = File.join(namespace.full_path, path) Rails.logger.error "Attempting to rename #{old_path_with_namespace} -> #{new_path_with_namespace}" diff --git a/app/models/user.rb b/app/models/user.rb index 094827123ec465..7fb7adbead14fc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -364,7 +364,7 @@ def reference_prefix def reference_pattern %r{ #{Regexp.escape(reference_prefix)} - (?#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR}) + (?#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR}) }x end diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 5a44ec45b7b70a..30e63d991bb0b9 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -8,7 +8,7 @@ = f.text_field :name, class: "form-control top", required: true, title: "This field is required." .username.form-group = f.label :username - = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, required: true, title: 'Please create a username with only alphanumeric characters.' + = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, required: true, title: 'Please create a username with only alphanumeric characters.' %p.validation-error.hide Username is already taken. %p.validation-success.hide Username is available. %p.validation-pending.hide Checking username availability... diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 638bad82e20a5a..2223ef1b570e5e 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -131,7 +131,7 @@ .form-group - if @project.avatar? .avatar-container.s160 - = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160') + = project_icon(@project.full_path, alt: '', class: 'avatar project-avatar s160') %p.light - if @project.avatar_in_git Project avatar in repository: #{ @project.avatar_in_git } diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index efb207b9916c23..02b7b2447ed5ca 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -17,7 +17,7 @@ %strong= parent.full_path + '/' = f.text_field :path, placeholder: 'open-source', class: 'form-control', autofocus: local_assigns[:autofocus] || false, required: true, - pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, + pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, title: 'Please choose a group name with no special characters.' - if parent = f.hidden_field :parent_id, value: parent.id diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index c883468afb2b71..6397f28e817955 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -200,7 +200,7 @@ :javascript gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}'); new gl.IssuableTimeTracking("#{escape_javascript(serialize_issuable(issuable))}"); - new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); + new MilestoneSelect('{"full_path":"#{@project.full_path}"}'); new LabelsSelect(); new WeightSelect(); new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index f1d1af8eee5b1c..8b327cfc226171 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -35,7 +35,7 @@ def version_filename end def export_filename(project:) - basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{project.namespace.full_path}_#{project.path}" + basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{project.full_path.tr('/', '_')}" "#{basename[0..FILENAME_LIMIT]}_export.tar.gz" end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index c77fe2d8bdc586..5e5f5ff1589b31 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -5,17 +5,18 @@ module Regex # The namespace regex is used in Javascript to validate usernames in the "Register" form. However, Javascript # does not support the negative lookbehind assertion (?" end @@ -68,7 +68,7 @@ def stub_action_name(value) allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true) avatar_url = "http://#{Gitlab.config.gitlab.host}#{namespace_project_avatar_path(project.namespace, project)}" - expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).to match( + expect(helper.project_icon(project.full_path).to_s).to match( image_tag(avatar_url)) end end diff --git a/spec/javascripts/fixtures/branches.rb b/spec/javascripts/fixtures/branches.rb index 0e7c2351b6668a..a059818145b9b4 100644 --- a/spec/javascripts/fixtures/branches.rb +++ b/spec/javascripts/fixtures/branches.rb @@ -20,7 +20,7 @@ it 'branches/new_branch.html.raw' do |example| get :new, namespace_id: project.namespace.to_param, - project_id: project.to_param + project_id: project expect(response).to be_success store_frontend_fixture(response, example.description) diff --git a/spec/javascripts/fixtures/builds.rb b/spec/javascripts/fixtures/builds.rb index 978e25a1c32f46..320de791b08654 100644 --- a/spec/javascripts/fixtures/builds.rb +++ b/spec/javascripts/fixtures/builds.rb @@ -24,7 +24,7 @@ it 'builds/build-with-artifacts.html.raw' do |example| get :show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: build_with_artifacts.to_param expect(response).to be_success diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb index 06f708f9e151a5..88e3f86080984e 100644 --- a/spec/javascripts/fixtures/issues.rb +++ b/spec/javascripts/fixtures/issues.rb @@ -41,7 +41,7 @@ def render_issue(fixture_file_name, issue) get :show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: issue.to_param expect(response).to be_success diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index 62984097099f83..ee893b76c84405 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -27,7 +27,7 @@ def render_merge_request(fixture_file_name, merge_request) get :show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: merge_request.to_param expect(response).to be_success diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb index 56513219e1ec52..6c33b240e5cb08 100644 --- a/spec/javascripts/fixtures/projects.rb +++ b/spec/javascripts/fixtures/projects.rb @@ -20,7 +20,7 @@ it 'projects/dashboard.html.raw' do |example| get :show, namespace_id: project.namespace.to_param, - id: project.to_param + id: project expect(response).to be_success store_frontend_fixture(response, example.description) diff --git a/spec/javascripts/fixtures/todos.rb b/spec/javascripts/fixtures/todos.rb index 2c08b06ea9e856..a81ef8c5492ce4 100644 --- a/spec/javascripts/fixtures/todos.rb +++ b/spec/javascripts/fixtures/todos.rb @@ -39,8 +39,8 @@ it 'todos/todos.json' do |example| post :create, - namespace_id: namespace.path, - project_id: project.path, + namespace_id: namespace, + project_id: project, issuable_type: 'issue', issuable_id: issue_2.id, format: 'json' diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb index a5251e9a8c25cd..4f25ad88960500 100644 --- a/spec/lib/constraints/project_url_constrainer_spec.rb +++ b/spec/lib/constraints/project_url_constrainer_spec.rb @@ -6,7 +6,7 @@ describe '#matches?' do context 'valid request' do - let(:request) { build_request(namespace.path, project.path) } + let(:request) { build_request(namespace.full_path, project.path) } it { expect(subject.matches?(request)).to be_truthy } end @@ -19,7 +19,7 @@ end context "project id ending with .git" do - let(:request) { build_request(namespace.path, project.path + '.git') } + let(:request) { build_request(namespace.full_path, project.path + '.git') } it { expect(subject.matches?(request)).to be_falsey } end diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb index 7e5531d92dc514..780ac0ad97ed43 100644 --- a/spec/lib/gitlab/conflict/file_spec.rb +++ b/spec/lib/gitlab/conflict/file_spec.rb @@ -251,7 +251,7 @@ def default_regex describe '#as_json' do it 'includes the blob path for the file' do expect(conflict_file.as_json[:blob_path]). - to eq("/#{project.namespace.to_param}/#{merge_request.project.to_param}/blob/#{our_commit.oid}/files/ruby/regex.rb") + to eq("/#{project.full_path}/blob/#{our_commit.oid}/files/ruby/regex.rb") end it 'includes the blob icon for the file' do diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 089ec4e2737a8d..ba45e2d758ceac 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -51,8 +51,8 @@ it { is_expected.not_to match('foo-') } end - describe 'NAMESPACE_REF_REGEX_STR' do - subject { %r{\A#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR}\z} } + describe 'FULL_NAMESPACE_REGEX_STR' do + subject { %r{\A#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR}\z} } it { is_expected.to match('gitlab.org') } it { is_expected.to match('gitlab.org/gitlab-git') } diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 4251cb06a2d41a..b963ca4e542cf6 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1245,8 +1245,8 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now) { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true }, { key: 'CI_PROJECT_NAME', value: project.path, public: true }, - { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true }, - { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true }, + { key: 'CI_PROJECT_PATH', value: project.full_path, public: true }, + { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true } ] diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index e008ec28fa4518..677e60e1282297 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -86,7 +86,7 @@ let(:nested_group) { create(:group, parent: group) } it { expect(group.full_path).to eq(group.path) } - it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") } + it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") } end describe '#full_name' do @@ -102,7 +102,7 @@ describe '#full_path' do let(:project) { build_stubbed(:empty_project) } - it { expect(project.full_path).to eq "#{project.namespace.path}/#{project.path}" } + it { expect(project.full_path).to eq "#{project.namespace.full_path}/#{project.path}" } end describe '#full_name' do diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 3ad74251203925..77a3e6eaea42ef 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -36,7 +36,7 @@ end describe '#to_param' do - it { expect(namespace.to_param).to eq(namespace.path) } + it { expect(namespace.to_param).to eq(namespace.full_path) } end describe '#human_name' do @@ -163,7 +163,7 @@ describe :rm_dir do let!(:project) { create(:empty_project, namespace: namespace) } - let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.path) } + let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.full_path) } it "removes its dirs when deleted" do namespace.destroy diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb index 47ca802ebc2af0..044737c6026dd7 100644 --- a/spec/models/project_services/drone_ci_service_spec.rb +++ b/spec/models/project_services/drone_ci_service_spec.rb @@ -28,7 +28,7 @@ shared_context :drone_ci_service do let(:drone) { DroneCiService.new } let(:project) { create(:project, :repository, name: 'project') } - let(:path) { "#{project.namespace.path}/#{project.path}" } + let(:path) { project.full_path } let(:drone_url) { 'http://drone.example.com' } let(:sha) { '2ab7834c' } let(:branch) { 'dev' } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 719436a3350864..be25f9935f7182 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -414,7 +414,7 @@ let(:project) { create(:empty_project, path: "somewhere") } it 'returns the full web URL for this repo' do - expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.path}/somewhere") + expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere") end end @@ -975,7 +975,7 @@ end let(:avatar_path) do - "/#{project.namespace.name}/#{project.path}/avatar" + "/#{project.full_path}/avatar" end it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" } @@ -1367,16 +1367,14 @@ def create_pipeline end it 'renames a repository' do - ns = project.namespace_dir - expect(gitlab_shell).to receive(:mv_repository). ordered. - with(project.repository_storage_path, "#{ns}/foo", "#{ns}/#{project.path}"). + with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}"). and_return(true) expect(gitlab_shell).to receive(:mv_repository). ordered. - with(project.repository_storage_path, "#{ns}/foo.wiki", "#{ns}/#{project.path}.wiki"). + with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki"). and_return(true) expect_any_instance_of(SystemHooksService). @@ -1385,7 +1383,7 @@ def create_pipeline expect_any_instance_of(Gitlab::UploadsTransfer). to receive(:rename_project). - with('foo', project.path, ns) + with('foo', project.path, project.namespace.full_path) expect(project).to receive(:expire_caches_before_rename) @@ -1817,7 +1815,7 @@ def create_build(new_pipeline = pipeline, name = 'test') it 'schedules a RepositoryForkWorker job' do expect(RepositoryForkWorker).to receive(:perform_async). with(project.id, forked_from_project.repository_storage_path, - forked_from_project.path_with_namespace, project.namespace.path) + forked_from_project.path_with_namespace, project.namespace.full_path) project.add_import_job end @@ -2141,7 +2139,7 @@ def create_build(new_pipeline = pipeline, name = 'test') describe 'inside_path' do let!(:project1) { create(:empty_project) } let!(:project2) { create(:empty_project) } - let!(:path) { project1.namespace.path } + let!(:path) { project1.namespace.full_path } it { expect(Project.inside_path(path)).to eq([project1]) } end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 3144baad232844..5190fcca2d1f2d 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -146,6 +146,16 @@ expect(response).to have_http_status(400) end + + context 'with project path in URL' do + let(:url) { "/projects/#{project.full_path.gsub('/', '%2F')}/repository/commits" } + + it 'a new file in project repo' do + post api(url, user), valid_c_params + + expect(response).to have_http_status(201) + end + end end context :delete do diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index b65381da310513..83e6d6761487b1 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -556,7 +556,7 @@ describe "POST /groups/:id/projects/:project_id" do let(:project) { create(:empty_project) } - let(:project_path) { "#{project.namespace.path}%2F#{project.path}" } + let(:project_path) { project.full_path.gsub('/', '%2F') } before(:each) do allow_any_instance_of(Projects::TransferService). diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb index 2d7584c3e5934f..e298ef055e1b8e 100644 --- a/spec/requests/api/v3/commits_spec.rb +++ b/spec/requests/api/v3/commits_spec.rb @@ -148,7 +148,7 @@ end context 'with project path in URL' do - let(:url) { "/projects/#{project.namespace.path}%2F#{project.path}/repository/commits" } + let(:url) { "/projects/#{project.full_path.gsub('/', '%2F')}/repository/commits" } it 'a new file in project repo' do post v3_api(url, user), valid_c_params diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb index 4644c7a6b86f1f..4c0f556e73631c 100644 --- a/spec/support/issuables_list_metadata_shared_examples.rb +++ b/spec/support/issuables_list_metadata_shared_examples.rb @@ -22,7 +22,7 @@ if action get action else - get :index, namespace_id: project.namespace.path, project_id: project.path + get :index, namespace_id: project.namespace, project_id: project end meta_data = assigns(:issuable_meta_data) diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index a79386b5db9618..5980d14c8ae58c 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -79,8 +79,9 @@ def milestone def xproject @xproject ||= begin - namespace = create(:namespace, name: 'cross-reference') - create(:project, namespace: namespace) do |project| + group = create(:group, name: 'cross-reference') + group2 = create(:group, parent: group, name: 'nested-group') + create(:project, namespace: group2) do |project| project.team << [user, :developer] end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 69eec21fbc4bad..705067f4b60662 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -137,7 +137,7 @@ def setup_repo(repo_path, repo_path_bare, repo_name, branch_sha) def copy_repo(project) base_repo_path = File.expand_path(factory_repo_path_bare) - target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.namespace.path}/#{project.path}.git") + target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git") FileUtils.mkdir_p(target_repo_path) FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) FileUtils.chmod_R 0755, target_repo_path @@ -154,7 +154,7 @@ def backup_path def copy_forked_repo_with_submodules(project) base_repo_path = File.expand_path(forked_repo_path_bare) - target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.namespace.path}/#{project.path}.git") + target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git") FileUtils.mkdir_p(target_repo_path) FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) FileUtils.chmod_R 0755, target_repo_path diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index 60605460adbebd..87521ae408e90b 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -15,24 +15,24 @@ it "creates a new repository from a fork" do expect(shell).to receive(:fork_repository).with( '/test/path', - project.path_with_namespace, + project.full_path, project.repository_storage_path, - fork_project.namespace.path + fork_project.namespace.full_path ).and_return(true) subject.perform( project.id, '/test/path', - project.path_with_namespace, - fork_project.namespace.path) + project.full_path, + fork_project.namespace.full_path) end it 'flushes various caches' do expect(shell).to receive(:fork_repository).with( '/test/path', - project.path_with_namespace, + project.full_path, project.repository_storage_path, - fork_project.namespace.path + fork_project.namespace.full_path ).and_return(true) expect_any_instance_of(Repository).to receive(:expire_emptiness_caches). @@ -41,8 +41,8 @@ expect_any_instance_of(Repository).to receive(:expire_exists_cache). and_call_original - subject.perform(project.id, '/test/path', project.path_with_namespace, - fork_project.namespace.path) + subject.perform(project.id, '/test/path', project.full_path, + fork_project.namespace.full_path) end it "handles bad fork" do @@ -53,8 +53,8 @@ subject.perform( project.id, '/test/path', - project.path_with_namespace, - fork_project.namespace.path) + project.full_path, + fork_project.namespace.full_path) end end end -- GitLab From 4c3052a6b007228f0508a52f0e68ec3424fb4549 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 17:57:53 -0600 Subject: [PATCH 52/73] remove require.context from users_bundle --- app/assets/javascripts/users/users_bundle.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js index 4cad60a59b1065..580e2d84be58d9 100644 --- a/app/assets/javascripts/users/users_bundle.js +++ b/app/assets/javascripts/users/users_bundle.js @@ -1,3 +1 @@ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!users_bundle).*\.(js|es6)$/)); +require('./calendar'); -- GitLab From 65d600e4f6966c9a626f4af916cb039fe26e42b9 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 23 Feb 2017 19:22:26 -0500 Subject: [PATCH 53/73] Fix failing specs --- ...er_callout_spec.js => user_callout_spec.js.es6} | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) rename spec/javascripts/{user_callout_spec.js => user_callout_spec.js.es6} (71%) diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js.es6 similarity index 71% rename from spec/javascripts/user_callout_spec.js rename to spec/javascripts/user_callout_spec.js.es6 index e174fdf35de622..6ee63f56a26a94 100644 --- a/spec/javascripts/user_callout_spec.js +++ b/spec/javascripts/user_callout_spec.js.es6 @@ -3,11 +3,11 @@ const UserCallout = require('~/user_callout'); const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; const Cookie = window.Cookies; -describe('UserCallout', function () { +describe('UserCallout', () => { const fixtureName = 'static/user_callout.html.raw'; preloadFixtures(fixtureName); - beforeEach(() => { + beforeEach(function () { loadFixtures(fixtureName); this.userCallout = new UserCallout(); this.closeButton = $('.close-user-callout'); @@ -16,17 +16,21 @@ describe('UserCallout', function () { Cookie.set(USER_CALLOUT_COOKIE, 'false'); }); - fit('shows when cookie is set to false', () => { + afterEach(function () { + Cookie.set(USER_CALLOUT_COOKIE, 'false'); + }); + + it('shows when cookie is set to false', function () { expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeDefined(); expect(this.userCalloutContainer.is(':visible')).toBe(true); }); - fit('hides when user clicks on the dismiss-icon', () => { + it('hides when user clicks on the dismiss-icon', function () { this.closeButton.click(); expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); }); - fit('hides when user clicks on the "check it out" button', () => { + it('hides when user clicks on the "check it out" button', function () { this.userCalloutBtn.click(); expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); }); -- GitLab From 12fbffb9662237e413944ff7bd2d82b78fd388a2 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 23:05:07 -0600 Subject: [PATCH 54/73] add missing require statement and don't require self --- app/assets/javascripts/application.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index b8cc82d38e45bb..4cedcd99bd6be6 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -104,8 +104,8 @@ require('./droplab/droplab_filter'); require('./abuse_reports'); require('./activities'); require('./admin'); +require('./ajax_loading_spinner'); require('./api'); -require('./application'); require('./aside'); require('./autosave'); require('./awards_handler'); -- GitLab From a78674a384c11be472b39f2e8ac599c5dae307b1 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 23:31:14 -0600 Subject: [PATCH 55/73] remove unused StatGraph class --- .../javascripts/graphs/graphs_bundle.js | 1 - app/assets/javascripts/graphs/stat_graph.js | 18 ----------------- spec/javascripts/graphs/stat_graph_spec.js | 20 ------------------- 3 files changed, 39 deletions(-) delete mode 100644 app/assets/javascripts/graphs/stat_graph.js delete mode 100644 spec/javascripts/graphs/stat_graph_spec.js diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index 086dcb34571d8c..230e0706500ddb 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,4 +1,3 @@ require('./stat_graph_contributors_graph'); require('./stat_graph_contributors_util'); require('./stat_graph_contributors'); -require('./stat_graph'); diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js deleted file mode 100644 index 75a53aae33cac9..00000000000000 --- a/app/assets/javascripts/graphs/stat_graph.js +++ /dev/null @@ -1,18 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, max-len */ -(function() { - this.StatGraph = (function() { - function StatGraph() {} - - StatGraph.log = {}; - - StatGraph.get_log = function() { - return this.log; - }; - - StatGraph.set_log = function(data) { - return this.log = data; - }; - - return StatGraph; - })(); -}).call(window); diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js deleted file mode 100644 index 876c23361bc790..00000000000000 --- a/spec/javascripts/graphs/stat_graph_spec.js +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable quotes */ -/* global StatGraph */ - -require('~/graphs/stat_graph'); - -describe("StatGraph", function () { - describe("#get_log", function () { - it("returns log", function () { - StatGraph.log = "test"; - expect(StatGraph.get_log()).toBe("test"); - }); - }); - - describe("#set_log", function () { - it("sets the log", function () { - StatGraph.set_log("test"); - expect(StatGraph.log).toBe("test"); - }); - }); -}); -- GitLab From db01ddf0cc284474f8534ee9a9354d4f1122a28e Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 23:28:54 -0600 Subject: [PATCH 56/73] refactor stat_graph_contributors_util to es6 module syntax --- .../javascripts/graphs/graphs_bundle.js | 1 - .../graphs/stat_graph_contributors.js | 3 +- .../graphs/stat_graph_contributors_util.js | 265 +++++++++--------- .../stat_graph_contributors_util_spec.js | 3 +- 4 files changed, 135 insertions(+), 137 deletions(-) diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index 230e0706500ddb..a455cebc0f6315 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,3 +1,2 @@ require('./stat_graph_contributors_graph'); -require('./stat_graph_contributors_util'); require('./stat_graph_contributors'); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index bbfb467ad5079a..3023e97fd747f4 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -2,7 +2,8 @@ /* global ContributorsGraph */ /* global ContributorsAuthorGraph */ /* global ContributorsMasterGraph */ -/* global ContributorsStatGraphUtil */ +import ContributorsStatGraphUtil from './stat_graph_contributors_util'; + /* global d3 */ window.d3 = require('d3'); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js index 7954c583598fc5..c583757f3f2f44 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js @@ -1,138 +1,137 @@ /* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */ -(function() { - window.ContributorsStatGraphUtil = { - parse_log: function(log) { - var by_author, by_email, data, entry, i, len, total, normalized_email; - total = {}; - by_author = {}; - by_email = {}; - for (i = 0, len = log.length; i < len; i += 1) { - entry = log[i]; - if (total[entry.date] == null) { - this.add_date(entry.date, total); - } - normalized_email = entry.author_email.toLowerCase(); - data = by_author[entry.author_name] || by_email[normalized_email]; - if (data == null) { - data = this.add_author(entry, by_author, by_email); - } - if (!data[entry.date]) { - this.add_date(entry.date, data); - } - this.store_data(entry, total[entry.date], data[entry.date]); - } - total = _.toArray(total); - by_author = _.toArray(by_author); - return { - total: total, - by_author: by_author - }; - }, - add_date: function(date, collection) { - collection[date] = {}; - return collection[date].date = date; - }, - add_author: function(author, by_author, by_email) { - var data, normalized_email; - data = {}; - data.author_name = author.author_name; - data.author_email = author.author_email; - normalized_email = author.author_email.toLowerCase(); - by_author[author.author_name] = data; - by_email[normalized_email] = data; - return data; - }, - store_data: function(entry, total, by_author) { - this.store_commits(total, by_author); - this.store_additions(entry, total, by_author); - return this.store_deletions(entry, total, by_author); - }, - store_commits: function(total, by_author) { - this.add(total, "commits", 1); - return this.add(by_author, "commits", 1); - }, - add: function(collection, field, value) { - if (collection[field] == null) { - collection[field] = 0; - } - return collection[field] += value; - }, - store_additions: function(entry, total, by_author) { - if (entry.additions == null) { - entry.additions = 0; + +export default { + parse_log: function(log) { + var by_author, by_email, data, entry, i, len, total, normalized_email; + total = {}; + by_author = {}; + by_email = {}; + for (i = 0, len = log.length; i < len; i += 1) { + entry = log[i]; + if (total[entry.date] == null) { + this.add_date(entry.date, total); } - this.add(total, "additions", entry.additions); - return this.add(by_author, "additions", entry.additions); - }, - store_deletions: function(entry, total, by_author) { - if (entry.deletions == null) { - entry.deletions = 0; + normalized_email = entry.author_email.toLowerCase(); + data = by_author[entry.author_name] || by_email[normalized_email]; + if (data == null) { + data = this.add_author(entry, by_author, by_email); } - this.add(total, "deletions", entry.deletions); - return this.add(by_author, "deletions", entry.deletions); - }, - get_total_data: function(parsed_log, field) { - var log, total_data; - log = parsed_log.total; - total_data = this.pick_field(log, field); - return _.sortBy(total_data, function(d) { - return d.date; - }); - }, - pick_field: function(log, field) { - var total_data; - total_data = []; - _.each(log, function(d) { - return total_data.push(_.pick(d, [field, 'date'])); - }); - return total_data; - }, - get_author_data: function(parsed_log, field, date_range) { - var author_data, log; - if (date_range == null) { - date_range = null; - } - log = parsed_log.by_author; - author_data = []; - _.each(log, (function(_this) { - return function(log_entry) { - var parsed_log_entry; - parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range); - if (!_.isEmpty(parsed_log_entry.dates)) { - return author_data.push(parsed_log_entry); - } - }; - })(this)); - return _.sortBy(author_data, function(d) { - return d[field]; - }).reverse(); - }, - parse_log_entry: function(log_entry, field, date_range) { - var parsed_entry; - parsed_entry = {}; - parsed_entry.author_name = log_entry.author_name; - parsed_entry.author_email = log_entry.author_email; - parsed_entry.dates = {}; - parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0; - _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) { - return function(value, key) { - if (_this.in_range(value.date, date_range)) { - parsed_entry.dates[value.date] = value[field]; - parsed_entry.commits += value.commits; - parsed_entry.additions += value.additions; - return parsed_entry.deletions += value.deletions; - } - }; - })(this)); - return parsed_entry; - }, - in_range: function(date, date_range) { - var ref; - if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) { - return true; - } else { - return false; + if (!data[entry.date]) { + this.add_date(entry.date, data); } + this.store_data(entry, total[entry.date], data[entry.date]); + } + total = _.toArray(total); + by_author = _.toArray(by_author); + return { + total: total, + by_author: by_author + }; + }, + add_date: function(date, collection) { + collection[date] = {}; + return collection[date].date = date; + }, + add_author: function(author, by_author, by_email) { + var data, normalized_email; + data = {}; + data.author_name = author.author_name; + data.author_email = author.author_email; + normalized_email = author.author_email.toLowerCase(); + by_author[author.author_name] = data; + by_email[normalized_email] = data; + return data; + }, + store_data: function(entry, total, by_author) { + this.store_commits(total, by_author); + this.store_additions(entry, total, by_author); + return this.store_deletions(entry, total, by_author); + }, + store_commits: function(total, by_author) { + this.add(total, "commits", 1); + return this.add(by_author, "commits", 1); + }, + add: function(collection, field, value) { + if (collection[field] == null) { + collection[field] = 0; + } + return collection[field] += value; + }, + store_additions: function(entry, total, by_author) { + if (entry.additions == null) { + entry.additions = 0; + } + this.add(total, "additions", entry.additions); + return this.add(by_author, "additions", entry.additions); + }, + store_deletions: function(entry, total, by_author) { + if (entry.deletions == null) { + entry.deletions = 0; + } + this.add(total, "deletions", entry.deletions); + return this.add(by_author, "deletions", entry.deletions); + }, + get_total_data: function(parsed_log, field) { + var log, total_data; + log = parsed_log.total; + total_data = this.pick_field(log, field); + return _.sortBy(total_data, function(d) { + return d.date; + }); + }, + pick_field: function(log, field) { + var total_data; + total_data = []; + _.each(log, function(d) { + return total_data.push(_.pick(d, [field, 'date'])); + }); + return total_data; + }, + get_author_data: function(parsed_log, field, date_range) { + var author_data, log; + if (date_range == null) { + date_range = null; + } + log = parsed_log.by_author; + author_data = []; + _.each(log, (function(_this) { + return function(log_entry) { + var parsed_log_entry; + parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range); + if (!_.isEmpty(parsed_log_entry.dates)) { + return author_data.push(parsed_log_entry); + } + }; + })(this)); + return _.sortBy(author_data, function(d) { + return d[field]; + }).reverse(); + }, + parse_log_entry: function(log_entry, field, date_range) { + var parsed_entry; + parsed_entry = {}; + parsed_entry.author_name = log_entry.author_name; + parsed_entry.author_email = log_entry.author_email; + parsed_entry.dates = {}; + parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0; + _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) { + return function(value, key) { + if (_this.in_range(value.date, date_range)) { + parsed_entry.dates[value.date] = value[field]; + parsed_entry.commits += value.commits; + parsed_entry.additions += value.additions; + return parsed_entry.deletions += value.deletions; + } + }; + })(this)); + return parsed_entry; + }, + in_range: function(date, date_range) { + var ref; + if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) { + return true; + } else { + return false; } - }; -}).call(window); + } +}; diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index b15764abe8c3ca..9b47ab6218170f 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,7 +1,6 @@ /* eslint-disable quotes, no-var, camelcase, object-property-newline, comma-dangle, max-len, vars-on-top, quote-props */ -/* global ContributorsStatGraphUtil */ -require('~/graphs/stat_graph_contributors_util'); +import ContributorsStatGraphUtil from '~/graphs/stat_graph_contributors_util'; describe("ContributorsStatGraphUtil", function () { describe("#parse_log", function () { -- GitLab From 9c90ad7ca19db757f776c9c6ebbba8cd29d44531 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 23:48:25 -0600 Subject: [PATCH 57/73] refactor stat_graph_contributors_graph to es6 module syntax --- .../javascripts/graphs/graphs_bundle.js | 1 - .../graphs/stat_graph_contributors.js | 5 +- .../graphs/stat_graph_contributors_graph.js | 542 +++++++++--------- .../stat_graph_contributors_graph_spec.js | 8 +- 4 files changed, 274 insertions(+), 282 deletions(-) diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index a455cebc0f6315..914716a5147ab8 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,2 +1 @@ -require('./stat_graph_contributors_graph'); require('./stat_graph_contributors'); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index 3023e97fd747f4..a11ee5967e7749 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -1,7 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign */ -/* global ContributorsGraph */ -/* global ContributorsAuthorGraph */ -/* global ContributorsMasterGraph */ + +import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; import ContributorsStatGraphUtil from './stat_graph_contributors_util'; /* global d3 */ diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index 228771da4eed55..521bc77db662de 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -1,276 +1,272 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return */ -/* global d3 */ -/* global ContributorsGraph */ - -window.d3 = require('d3'); - -(function() { - var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, - extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; - - this.ContributorsGraph = (function() { - function ContributorsGraph() {} - - ContributorsGraph.prototype.MARGIN = { - top: 20, - right: 20, - bottom: 30, - left: 50 - }; - - ContributorsGraph.prototype.x_domain = null; - - ContributorsGraph.prototype.y_domain = null; - - ContributorsGraph.prototype.dates = []; - - ContributorsGraph.set_x_domain = function(data) { - return ContributorsGraph.prototype.x_domain = data; - }; - - ContributorsGraph.set_y_domain = function(data) { - return ContributorsGraph.prototype.y_domain = [ - 0, d3.max(data, function(d) { - return d.commits = d.commits || d.additions || d.deletions; - }) - ]; - }; - - ContributorsGraph.init_x_domain = function(data) { - return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) { - return d.date; - }); - }; - - ContributorsGraph.init_y_domain = function(data) { - return ContributorsGraph.prototype.y_domain = [ - 0, d3.max(data, function(d) { - return d.commits = d.commits || d.additions || d.deletions; - }) - ]; - }; - - ContributorsGraph.init_domain = function(data) { - ContributorsGraph.init_x_domain(data); - return ContributorsGraph.init_y_domain(data); - }; - - ContributorsGraph.set_dates = function(data) { - return ContributorsGraph.prototype.dates = data; - }; - - ContributorsGraph.prototype.set_x_domain = function() { - return this.x.domain(this.x_domain); - }; - - ContributorsGraph.prototype.set_y_domain = function() { - return this.y.domain(this.y_domain); - }; - - ContributorsGraph.prototype.set_domain = function() { - this.set_x_domain(); - return this.set_y_domain(); - }; - - ContributorsGraph.prototype.create_scale = function(width, height) { - this.x = d3.time.scale().range([0, width]).clamp(true); - return this.y = d3.scale.linear().range([height, 0]).nice(); - }; - - ContributorsGraph.prototype.draw_x_axis = function() { - return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis); - }; - - ContributorsGraph.prototype.draw_y_axis = function() { - return this.svg.append("g").attr("class", "y axis").call(this.y_axis); - }; - - ContributorsGraph.prototype.set_data = function(data) { - return this.data = data; - }; - - return ContributorsGraph; - })(); - - this.ContributorsMasterGraph = (function(superClass) { - extend(ContributorsMasterGraph, superClass); - - function ContributorsMasterGraph(data1) { - this.data = data1; - this.update_content = bind(this.update_content, this); - this.width = $('.content').width() - 70; - this.height = 200; - this.x = null; - this.y = null; - this.x_axis = null; - this.y_axis = null; - this.area = null; - this.svg = null; - this.brush = null; - this.x_max_domain = null; +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ + +import d3 from 'd3'; + +const bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; +const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; +const hasProp = {}.hasOwnProperty; + +export const ContributorsGraph = (function() { + function ContributorsGraph() {} + + ContributorsGraph.prototype.MARGIN = { + top: 20, + right: 20, + bottom: 30, + left: 50 + }; + + ContributorsGraph.prototype.x_domain = null; + + ContributorsGraph.prototype.y_domain = null; + + ContributorsGraph.prototype.dates = []; + + ContributorsGraph.set_x_domain = function(data) { + return ContributorsGraph.prototype.x_domain = data; + }; + + ContributorsGraph.set_y_domain = function(data) { + return ContributorsGraph.prototype.y_domain = [ + 0, d3.max(data, function(d) { + return d.commits = d.commits || d.additions || d.deletions; + }) + ]; + }; + + ContributorsGraph.init_x_domain = function(data) { + return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) { + return d.date; + }); + }; + + ContributorsGraph.init_y_domain = function(data) { + return ContributorsGraph.prototype.y_domain = [ + 0, d3.max(data, function(d) { + return d.commits = d.commits || d.additions || d.deletions; + }) + ]; + }; + + ContributorsGraph.init_domain = function(data) { + ContributorsGraph.init_x_domain(data); + return ContributorsGraph.init_y_domain(data); + }; + + ContributorsGraph.set_dates = function(data) { + return ContributorsGraph.prototype.dates = data; + }; + + ContributorsGraph.prototype.set_x_domain = function() { + return this.x.domain(this.x_domain); + }; + + ContributorsGraph.prototype.set_y_domain = function() { + return this.y.domain(this.y_domain); + }; + + ContributorsGraph.prototype.set_domain = function() { + this.set_x_domain(); + return this.set_y_domain(); + }; + + ContributorsGraph.prototype.create_scale = function(width, height) { + this.x = d3.time.scale().range([0, width]).clamp(true); + return this.y = d3.scale.linear().range([height, 0]).nice(); + }; + + ContributorsGraph.prototype.draw_x_axis = function() { + return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis); + }; + + ContributorsGraph.prototype.draw_y_axis = function() { + return this.svg.append("g").attr("class", "y axis").call(this.y_axis); + }; + + ContributorsGraph.prototype.set_data = function(data) { + return this.data = data; + }; + + return ContributorsGraph; +})(); + +export const ContributorsMasterGraph = (function(superClass) { + extend(ContributorsMasterGraph, superClass); + + function ContributorsMasterGraph(data1) { + this.data = data1; + this.update_content = bind(this.update_content, this); + this.width = $('.content').width() - 70; + this.height = 200; + this.x = null; + this.y = null; + this.x_axis = null; + this.y_axis = null; + this.area = null; + this.svg = null; + this.brush = null; + this.x_max_domain = null; + } + + ContributorsMasterGraph.prototype.process_dates = function(data) { + var dates; + dates = this.get_dates(data); + this.parse_dates(data); + return ContributorsGraph.set_dates(dates); + }; + + ContributorsMasterGraph.prototype.get_dates = function(data) { + return _.pluck(data, 'date'); + }; + + ContributorsMasterGraph.prototype.parse_dates = function(data) { + var parseDate; + parseDate = d3.time.format("%Y-%m-%d").parse; + return data.forEach(function(d) { + return d.date = parseDate(d.date); + }); + }; + + ContributorsMasterGraph.prototype.create_scale = function() { + return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height); + }; + + ContributorsMasterGraph.prototype.create_axes = function() { + this.x_axis = d3.svg.axis().scale(this.x).orient("bottom"); + return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); + }; + + ContributorsMasterGraph.prototype.create_svg = function() { + return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + }; + + ContributorsMasterGraph.prototype.create_area = function(x, y) { + return this.area = d3.svg.area().x(function(d) { + return x(d.date); + }).y0(this.height).y1(function(d) { + d.commits = d.commits || d.additions || d.deletions; + return y(d.commits); + }).interpolate("basis"); + }; + + ContributorsMasterGraph.prototype.create_brush = function() { + return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content); + }; + + ContributorsMasterGraph.prototype.draw_path = function(data) { + return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area); + }; + + ContributorsMasterGraph.prototype.add_brush = function() { + return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height); + }; + + ContributorsMasterGraph.prototype.update_content = function() { + ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent()); + return $("#brush_change").trigger('change'); + }; + + ContributorsMasterGraph.prototype.draw = function() { + this.process_dates(this.data); + this.create_scale(); + this.create_axes(); + ContributorsGraph.init_domain(this.data); + this.x_max_domain = this.x_domain; + this.set_domain(); + this.create_area(this.x, this.y); + this.create_svg(); + this.create_brush(); + this.draw_path(this.data); + this.draw_x_axis(); + this.draw_y_axis(); + return this.add_brush(); + }; + + ContributorsMasterGraph.prototype.redraw = function() { + this.process_dates(this.data); + ContributorsGraph.set_y_domain(this.data); + this.set_y_domain(); + this.svg.select("path").datum(this.data); + this.svg.select("path").attr("d", this.area); + return this.svg.select(".y.axis").call(this.y_axis); + }; + + return ContributorsMasterGraph; +})(ContributorsGraph); + +export const ContributorsAuthorGraph = (function(superClass) { + extend(ContributorsAuthorGraph, superClass); + + function ContributorsAuthorGraph(data1) { + this.data = data1; + // Don't split graph size in half for mobile devices. + if ($(window).width() < 768) { + this.width = $('.content').width() - 80; + } else { + this.width = ($('.content').width() / 2) - 100; } - - ContributorsMasterGraph.prototype.process_dates = function(data) { - var dates; - dates = this.get_dates(data); - this.parse_dates(data); - return ContributorsGraph.set_dates(dates); - }; - - ContributorsMasterGraph.prototype.get_dates = function(data) { - return _.pluck(data, 'date'); - }; - - ContributorsMasterGraph.prototype.parse_dates = function(data) { + this.height = 200; + this.x = null; + this.y = null; + this.x_axis = null; + this.y_axis = null; + this.area = null; + this.svg = null; + this.list_item = null; + } + + ContributorsAuthorGraph.prototype.create_scale = function() { + return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height); + }; + + ContributorsAuthorGraph.prototype.create_axes = function() { + this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8); + return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); + }; + + ContributorsAuthorGraph.prototype.create_area = function(x, y) { + return this.area = d3.svg.area().x(function(d) { var parseDate; parseDate = d3.time.format("%Y-%m-%d").parse; - return data.forEach(function(d) { - return d.date = parseDate(d.date); - }); - }; - - ContributorsMasterGraph.prototype.create_scale = function() { - return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height); - }; - - ContributorsMasterGraph.prototype.create_axes = function() { - this.x_axis = d3.svg.axis().scale(this.x).orient("bottom"); - return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); - }; - - ContributorsMasterGraph.prototype.create_svg = function() { - return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); - }; - - ContributorsMasterGraph.prototype.create_area = function(x, y) { - return this.area = d3.svg.area().x(function(d) { - return x(d.date); - }).y0(this.height).y1(function(d) { - d.commits = d.commits || d.additions || d.deletions; - return y(d.commits); - }).interpolate("basis"); - }; - - ContributorsMasterGraph.prototype.create_brush = function() { - return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content); - }; - - ContributorsMasterGraph.prototype.draw_path = function(data) { - return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area); - }; - - ContributorsMasterGraph.prototype.add_brush = function() { - return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height); - }; - - ContributorsMasterGraph.prototype.update_content = function() { - ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent()); - return $("#brush_change").trigger('change'); - }; - - ContributorsMasterGraph.prototype.draw = function() { - this.process_dates(this.data); - this.create_scale(); - this.create_axes(); - ContributorsGraph.init_domain(this.data); - this.x_max_domain = this.x_domain; - this.set_domain(); - this.create_area(this.x, this.y); - this.create_svg(); - this.create_brush(); - this.draw_path(this.data); - this.draw_x_axis(); - this.draw_y_axis(); - return this.add_brush(); - }; - - ContributorsMasterGraph.prototype.redraw = function() { - this.process_dates(this.data); - ContributorsGraph.set_y_domain(this.data); - this.set_y_domain(); - this.svg.select("path").datum(this.data); - this.svg.select("path").attr("d", this.area); - return this.svg.select(".y.axis").call(this.y_axis); - }; - - return ContributorsMasterGraph; - })(ContributorsGraph); - - this.ContributorsAuthorGraph = (function(superClass) { - extend(ContributorsAuthorGraph, superClass); - - function ContributorsAuthorGraph(data1) { - this.data = data1; - // Don't split graph size in half for mobile devices. - if ($(window).width() < 768) { - this.width = $('.content').width() - 80; - } else { - this.width = ($('.content').width() / 2) - 100; - } - this.height = 200; - this.x = null; - this.y = null; - this.x_axis = null; - this.y_axis = null; - this.area = null; - this.svg = null; - this.list_item = null; - } - - ContributorsAuthorGraph.prototype.create_scale = function() { - return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height); - }; - - ContributorsAuthorGraph.prototype.create_axes = function() { - this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8); - return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); - }; - - ContributorsAuthorGraph.prototype.create_area = function(x, y) { - return this.area = d3.svg.area().x(function(d) { - var parseDate; - parseDate = d3.time.format("%Y-%m-%d").parse; - return x(parseDate(d)); - }).y0(this.height).y1((function(_this) { - return function(d) { - if (_this.data[d] != null) { - return y(_this.data[d]); - } else { - return y(0); - } - }; - })(this)).interpolate("basis"); - }; - - ContributorsAuthorGraph.prototype.create_svg = function() { - this.list_item = d3.selectAll(".person")[0].pop(); - return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); - }; - - ContributorsAuthorGraph.prototype.draw_path = function(data) { - return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area); - }; - - ContributorsAuthorGraph.prototype.draw = function() { - this.create_scale(); - this.create_axes(); - this.set_domain(); - this.create_area(this.x, this.y); - this.create_svg(); - this.draw_path(this.dates); - this.draw_x_axis(); - return this.draw_y_axis(); - }; - - ContributorsAuthorGraph.prototype.redraw = function() { - this.set_domain(); - this.svg.select("path").datum(this.dates); - this.svg.select("path").attr("d", this.area); - this.svg.select(".x.axis").call(this.x_axis); - return this.svg.select(".y.axis").call(this.y_axis); - }; - - return ContributorsAuthorGraph; - })(ContributorsGraph); -}).call(window); + return x(parseDate(d)); + }).y0(this.height).y1((function(_this) { + return function(d) { + if (_this.data[d] != null) { + return y(_this.data[d]); + } else { + return y(0); + } + }; + })(this)).interpolate("basis"); + }; + + ContributorsAuthorGraph.prototype.create_svg = function() { + this.list_item = d3.selectAll(".person")[0].pop(); + return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + }; + + ContributorsAuthorGraph.prototype.draw_path = function(data) { + return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area); + }; + + ContributorsAuthorGraph.prototype.draw = function() { + this.create_scale(); + this.create_axes(); + this.set_domain(); + this.create_area(this.x, this.y); + this.create_svg(); + this.draw_path(this.dates); + this.draw_x_axis(); + return this.draw_y_axis(); + }; + + ContributorsAuthorGraph.prototype.redraw = function() { + this.set_domain(); + this.svg.select("path").datum(this.dates); + this.svg.select("path").attr("d", this.area); + this.svg.select(".x.axis").call(this.x_axis); + return this.svg.select(".y.axis").call(this.y_axis); + }; + + return ContributorsAuthorGraph; +})(ContributorsGraph); diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index a954bb60560e82..861f26e162ff1c 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -1,9 +1,7 @@ -/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var, max-len */ -/* global d3 */ -/* global ContributorsGraph */ -/* global ContributorsMasterGraph */ +/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var */ -require('~/graphs/stat_graph_contributors_graph'); +import d3 from 'd3'; +import { ContributorsGraph, ContributorsMasterGraph } from '~/graphs/stat_graph_contributors_graph'; describe("ContributorsGraph", function () { describe("#set_x_domain", function () { -- GitLab From 5ba95e62c7ec206f9fdc70536c387e6444f8dd93 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 23:55:34 -0600 Subject: [PATCH 58/73] refactor stat_graph_contributors to es6 module syntax --- .../javascripts/graphs/graphs_bundle.js | 5 +- .../graphs/stat_graph_contributors.js | 197 +++++++++--------- 2 files changed, 100 insertions(+), 102 deletions(-) diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index 914716a5147ab8..ea5afbd9d2943c 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1 +1,4 @@ -require('./stat_graph_contributors'); +import ContributorsStatGraph from './stat_graph_contributors'; + +// export to global scope +window.ContributorsStatGraph = ContributorsStatGraph; diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index a11ee5967e7749..c6be4c9e8feb18 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -1,116 +1,111 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, no-shadow */ +import d3 from 'd3'; import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; import ContributorsStatGraphUtil from './stat_graph_contributors_util'; -/* global d3 */ +export default (function() { + function ContributorsStatGraph() {} -window.d3 = require('d3'); + ContributorsStatGraph.prototype.init = function(log) { + var author_commits, total_commits; + this.parsed_log = ContributorsStatGraphUtil.parse_log(log); + this.set_current_field("commits"); + total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); + author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field); + this.add_master_graph(total_commits); + this.add_authors_graph(author_commits); + return this.change_date_header(); + }; -(function() { - this.ContributorsStatGraph = (function() { - function ContributorsStatGraph() {} + ContributorsStatGraph.prototype.add_master_graph = function(total_data) { + this.master_graph = new ContributorsMasterGraph(total_data); + return this.master_graph.draw(); + }; - ContributorsStatGraph.prototype.init = function(log) { - var author_commits, total_commits; - this.parsed_log = ContributorsStatGraphUtil.parse_log(log); - this.set_current_field("commits"); - total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); - author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field); - this.add_master_graph(total_commits); - this.add_authors_graph(author_commits); - return this.change_date_header(); - }; + ContributorsStatGraph.prototype.add_authors_graph = function(author_data) { + var limited_author_data; + this.authors = []; + limited_author_data = author_data.slice(0, 100); + return _.each(limited_author_data, (function(_this) { + return function(d) { + var author_graph, author_header; + author_header = _this.create_author_header(d); + $(".contributors-list").append(author_header); + _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates); + return author_graph.draw(); + }; + })(this)); + }; - ContributorsStatGraph.prototype.add_master_graph = function(total_data) { - this.master_graph = new ContributorsMasterGraph(total_data); - return this.master_graph.draw(); - }; + ContributorsStatGraph.prototype.format_author_commit_info = function(author) { + var commits; + commits = $('', { + "class": 'graph-author-commits-count' + }); + commits.text(author.commits + " commits"); + return $('').append(commits); + }; - ContributorsStatGraph.prototype.add_authors_graph = function(author_data) { - var limited_author_data; - this.authors = []; - limited_author_data = author_data.slice(0, 100); - return _.each(limited_author_data, (function(_this) { - return function(d) { - var author_graph, author_header; - author_header = _this.create_author_header(d); - $(".contributors-list").append(author_header); - _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates); - return author_graph.draw(); - }; - })(this)); - }; + ContributorsStatGraph.prototype.create_author_header = function(author) { + var author_commit_info, author_commit_info_span, author_email, author_name, list_item; + list_item = $('
  • ', { + "class": 'person', + style: 'display: block;' + }); + author_name = $('

    ' + author.author_name + '

    '); + author_email = $('

    ' + author.author_email + '

    '); + author_commit_info_span = $('', { + "class": 'commits' + }); + author_commit_info = this.format_author_commit_info(author); + author_commit_info_span.html(author_commit_info); + list_item.append(author_name); + list_item.append(author_email); + list_item.append(author_commit_info_span); + return list_item; + }; - ContributorsStatGraph.prototype.format_author_commit_info = function(author) { - var commits; - commits = $('', { - "class": 'graph-author-commits-count' - }); - commits.text(author.commits + " commits"); - return $('').append(commits); - }; + ContributorsStatGraph.prototype.redraw_master = function() { + var total_data; + total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); + this.master_graph.set_data(total_data); + return this.master_graph.redraw(); + }; - ContributorsStatGraph.prototype.create_author_header = function(author) { - var author_commit_info, author_commit_info_span, author_email, author_name, list_item; - list_item = $('
  • ', { - "class": 'person', - style: 'display: block;' - }); - author_name = $('

    ' + author.author_name + '

    '); - author_email = $('

    ' + author.author_email + '

    '); - author_commit_info_span = $('', { - "class": 'commits' - }); - author_commit_info = this.format_author_commit_info(author); - author_commit_info_span.html(author_commit_info); - list_item.append(author_name); - list_item.append(author_email); - list_item.append(author_commit_info_span); - return list_item; - }; + ContributorsStatGraph.prototype.redraw_authors = function() { + var author_commits, x_domain; + $("ol").html(""); + x_domain = ContributorsGraph.prototype.x_domain; + author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain); + return _.each(author_commits, (function(_this) { + return function(d) { + _this.redraw_author_commit_info(d); + $(_this.authors[d.author_name].list_item).appendTo("ol"); + _this.authors[d.author_name].set_data(d.dates); + return _this.authors[d.author_name].redraw(); + }; + })(this)); + }; - ContributorsStatGraph.prototype.redraw_master = function() { - var total_data; - total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); - this.master_graph.set_data(total_data); - return this.master_graph.redraw(); - }; + ContributorsStatGraph.prototype.set_current_field = function(field) { + return this.field = field; + }; - ContributorsStatGraph.prototype.redraw_authors = function() { - var author_commits, x_domain; - $("ol").html(""); - x_domain = ContributorsGraph.prototype.x_domain; - author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain); - return _.each(author_commits, (function(_this) { - return function(d) { - _this.redraw_author_commit_info(d); - $(_this.authors[d.author_name].list_item).appendTo("ol"); - _this.authors[d.author_name].set_data(d.dates); - return _this.authors[d.author_name].redraw(); - }; - })(this)); - }; + ContributorsStatGraph.prototype.change_date_header = function() { + var print, print_date_format, x_domain; + x_domain = ContributorsGraph.prototype.x_domain; + print_date_format = d3.time.format("%B %e %Y"); + print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]); + return $("#date_header").text(print); + }; - ContributorsStatGraph.prototype.set_current_field = function(field) { - return this.field = field; - }; + ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) { + var author_commit_info, author_list_item; + author_list_item = $(this.authors[author.author_name].list_item); + author_commit_info = this.format_author_commit_info(author); + return author_list_item.find("span").html(author_commit_info); + }; - ContributorsStatGraph.prototype.change_date_header = function() { - var print, print_date_format, x_domain; - x_domain = ContributorsGraph.prototype.x_domain; - print_date_format = d3.time.format("%B %e %Y"); - print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]); - return $("#date_header").text(print); - }; - - ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) { - var author_commit_info, author_list_item; - author_list_item = $(this.authors[author.author_name].list_item); - author_commit_info = this.format_author_commit_info(author); - return author_list_item.find("span").html(author_commit_info); - }; - - return ContributorsStatGraph; - })(); -}).call(window); + return ContributorsStatGraph; +})(); -- GitLab From 99d00a3e310f583bb08f2db5b4327cb4cff90541 Mon Sep 17 00:00:00 2001 From: Lukas Raska Date: Fri, 24 Feb 2017 07:50:45 +0100 Subject: [PATCH 59/73] Use correct GitLab Prometheus exporter name in docs --- .../monitoring/prometheus/gitlab_monitor_exporter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md index 86ef9d167e294d..edb9c911aac45c 100644 --- a/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md +++ b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md @@ -13,7 +13,7 @@ To enable the GitLab monitor exporter: 1. Add or find and uncomment the following line, making sure it's set to `true`: ```ruby - gitlab_monitor_exporter['enable'] = true + gitlab_monitor['enable'] = true ``` 1. Save the file and [reconfigure GitLab][reconfigure] for the changes to -- GitLab From 80e9cbfd4567f7ef792a760d877d2b4d10f75cd1 Mon Sep 17 00:00:00 2001 From: Lukas Raska Date: Fri, 24 Feb 2017 08:34:48 +0100 Subject: [PATCH 60/73] Use persistent name identifier instead of transient in SAML2 documentation --- doc/integration/saml.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/integration/saml.md b/doc/integration/saml.md index c7cbe3b421dbb9..195084d3352e6f 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -74,7 +74,7 @@ in your SAML IdP: idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_sso_target_url: 'https://login.example.com/idp', issuer: 'https://gitlab.example.com', - name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' }, label: 'Company Login' # optional label for SAML login button, defaults to "Saml" } @@ -91,7 +91,7 @@ in your SAML IdP: idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_sso_target_url: 'https://login.example.com/idp', issuer: 'https://gitlab.example.com', - name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' }, label: 'Company Login' # optional label for SAML login button, defaults to "Saml" } @@ -172,7 +172,7 @@ tell GitLab which groups are external via the `external_groups:` element: idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_sso_target_url: 'https://login.example.com/idp', issuer: 'https://gitlab.example.com', - name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' } } ``` @@ -251,7 +251,7 @@ args: { idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_sso_target_url: 'https://login.example.com/idp', issuer: 'https://gitlab.example.com', - name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', attribute_statements: { email: ['EmailAddress'] } } ``` @@ -269,7 +269,7 @@ args: { idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_sso_target_url: 'https://login.example.com/idp', issuer: 'https://gitlab.example.com', - name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', attribute_statements: { email: ['EmailAddress'] }, allowed_clock_drift: 1 # for one second clock drift } -- GitLab From 38a82c57b446390ddbed4ac0f46431f90b9adf9a Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 24 Feb 2017 17:15:25 +0800 Subject: [PATCH 61/73] Test against default to '0', it should not set --- spec/requests/ci/api/builds_spec.rb | 30 +++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index c7284be09b7841..9948d1a9ea021d 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -668,14 +668,28 @@ def force_patch_the_trace end context 'with application default' do - let(:default_artifacts_expire_in) { '5 days' } - - it 'sets to application default' do - build.reload - expect(response).to have_http_status(201) - expect(json_response['artifacts_expire_at']).not_to be_empty - expect(build.artifacts_expire_at). - to be_within(5.minutes).of(5.days.from_now) + context 'default to 5 days' do + let(:default_artifacts_expire_in) { '5 days' } + + it 'sets to application default' do + build.reload + expect(response).to have_http_status(201) + expect(json_response['artifacts_expire_at']) + .not_to be_empty + expect(build.artifacts_expire_at) + .to be_within(5.minutes).of(5.days.from_now) + end + end + + context 'default to 0' do + let(:default_artifacts_expire_in) { '0' } + + it 'does not set expire_in' do + build.reload + expect(response).to have_http_status(201) + expect(json_response['artifacts_expire_at']).to be_nil + expect(build.artifacts_expire_at).to be_nil + end end end end -- GitLab From 9b633f9ab4bfb50dcacc46b4e0bd4c58b2b078f9 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 24 Feb 2017 17:28:24 +0800 Subject: [PATCH 62/73] Introduce DurationValidator, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_24032923 --- app/models/application_setting.rb | 9 +-------- app/validators/duration_validator.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 app/validators/duration_validator.rb diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index d75b0abe32b352..f7fe245cfa4ee4 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -85,8 +85,7 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than: 0 } - validates :default_artifacts_expire_in, presence: true - validate :check_default_artifacts_expire_in + validates :default_artifacts_expire_in, presence: true, duration: true validates :container_registry_token_expire_delay, presence: true, @@ -346,10 +345,4 @@ def check_repository_storages errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless invalid.empty? end - - def check_default_artifacts_expire_in - ChronicDuration.parse(default_artifacts_expire_in) - rescue ChronicDuration::DurationParseError - errors.add(:default_artifacts_expire_in, "is not a correct duration") - end end diff --git a/app/validators/duration_validator.rb b/app/validators/duration_validator.rb new file mode 100644 index 00000000000000..10ff44031c6616 --- /dev/null +++ b/app/validators/duration_validator.rb @@ -0,0 +1,17 @@ +# DurationValidator +# +# Validate the format conforms with ChronicDuration +# +# Example: +# +# class ApplicationSetting < ActiveRecord::Base +# validates :default_artifacts_expire_in, presence: true, duration: true +# end +# +class DurationValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + ChronicDuration.parse(value) + rescue ChronicDuration::DurationParseError + record.errors.add(attribute, "is not a correct duration") + end +end -- GitLab From 8c2f2191eaa48424ec2ea96fa82588c66fd281e9 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Fri, 24 Feb 2017 09:25:53 +0100 Subject: [PATCH 63/73] API: Use parameter to get owned groups instead of dedicated endpoint --- lib/api/groups.rb | 16 ++++------------ spec/requests/api/groups_spec.rb | 14 ++------------ 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 77410dd9e02ad0..190a8e96042942 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -44,12 +44,15 @@ def present_groups(groups, options = {}) optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list' optional :all_available, type: Boolean, desc: 'Show all group that you have access to' optional :search, type: String, desc: 'Search for a specific group' + optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user' optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path' optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)' use :pagination end get do - groups = if current_user.admin + groups = if params[:owned] + current_user.owned_groups + elsif current_user.admin Group.all elsif params[:all_available] GroupsFinder.new.execute(current_user) @@ -64,17 +67,6 @@ def present_groups(groups, options = {}) present_groups groups, statistics: params[:statistics] && current_user.is_admin? end - desc 'Get list of owned groups for authenticated user' do - success Entities::Group - end - params do - use :pagination - use :statistics_params - end - get '/owned' do - present_groups current_user.owned_groups, statistics: params[:statistics] - end - desc 'Create a group. Available only for users who can create groups.' do success Entities::Group end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 83e6d6761487b1..0b5032ef696d7d 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -164,20 +164,10 @@ expect(response_groups).to eq([group1.name, group3.name]) end end - end - - describe 'GET /groups/owned' do - context 'when unauthenticated' do - it 'returns authentication error' do - get api('/groups/owned') - - expect(response).to have_http_status(401) - end - end - context 'when authenticated as group owner' do + context 'when using owned in the request' do it 'returns an array of groups the user owns' do - get api('/groups/owned', user2) + get api('/groups', user2), owned: true expect(response).to have_http_status(200) expect(response).to include_pagination_headers -- GitLab From 8ad3f94ba84dd28ca762f1fa823cde83a628b635 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Fri, 24 Feb 2017 09:29:53 +0100 Subject: [PATCH 64/73] Backport groups API to V3 --- lib/api/api.rb | 1 + lib/api/v3/groups.rb | 38 +++++++++++++++++++++++++++++ spec/requests/api/v3/groups_spec.rb | 35 ++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 lib/api/v3/groups.rb create mode 100644 spec/requests/api/v3/groups_spec.rb diff --git a/lib/api/api.rb b/lib/api/api.rb index ddd3a63cbdf0a7..dd799e2bdbd58f 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -10,6 +10,7 @@ class API < Grape::API mount ::API::V3::Commits mount ::API::V3::DeployKeys mount ::API::V3::Files + mount ::API::V3::Groups mount ::API::V3::Issues mount ::API::V3::Labels mount ::API::V3::Members diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb new file mode 100644 index 00000000000000..c826bc4fe0b2bb --- /dev/null +++ b/lib/api/v3/groups.rb @@ -0,0 +1,38 @@ +module API + module V3 + class Groups < Grape::API + include PaginationParams + + before { authenticate! } + + helpers do + params :statistics_params do + optional :statistics, type: Boolean, default: false, desc: 'Include project statistics' + end + + def present_groups(groups, options = {}) + options = options.reverse_merge( + with: ::API::Entities::Group, + current_user: current_user, + ) + + groups = groups.with_statistics if options[:statistics] + present paginate(groups), options + end + end + + resource :groups do + desc 'Get list of owned groups for authenticated user' do + success ::API::Entities::Group + end + params do + use :pagination + use :statistics_params + end + get '/owned' do + present_groups current_user.owned_groups, statistics: params[:statistics] + end + end + end + end +end diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb new file mode 100644 index 00000000000000..8b29ad037376f9 --- /dev/null +++ b/spec/requests/api/v3/groups_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe API::V3::Groups, api: true do + include ApiHelpers + include UploadHelpers + + let(:user2) { create(:user) } + let!(:group2) { create(:group, :private) } + let!(:project2) { create(:empty_project, namespace: group2) } + + before do + group2.add_owner(user2) + end + + describe 'GET /groups/owned' do + context 'when unauthenticated' do + it 'returns authentication error' do + get v3_api('/groups/owned') + + expect(response).to have_http_status(401) + end + end + + context 'when authenticated as group owner' do + it 'returns an array of groups the user owns' do + get v3_api('/groups/owned', user2) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(group2.name) + end + end + end +end -- GitLab From 4d1d25aea8763587862a8982802bac80bc2a757d Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Fri, 24 Feb 2017 09:37:38 +0100 Subject: [PATCH 65/73] Update documentation --- changelogs/unreleased/api-remove-owned-groups.yml | 4 ++++ doc/api/groups.md | 15 +-------------- doc/api/v3_to_v4.md | 2 +- 3 files changed, 6 insertions(+), 15 deletions(-) create mode 100644 changelogs/unreleased/api-remove-owned-groups.yml diff --git a/changelogs/unreleased/api-remove-owned-groups.yml b/changelogs/unreleased/api-remove-owned-groups.yml new file mode 100644 index 00000000000000..cf0301b7fe0adf --- /dev/null +++ b/changelogs/unreleased/api-remove-owned-groups.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Remove /groups/owned endpoint' +merge_request: 9505 +author: Robert Schilling diff --git a/doc/api/groups.md b/doc/api/groups.md index 04e06a809f0244..5daee06f1d6044 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -14,6 +14,7 @@ Parameters: | `order_by` | string | no | Order groups by `name` or `path`. Default is `name` | | `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` | | `statistics` | boolean | no | Include group statistics (admins only) | +| `owned` | boolean | no | Limit by groups owned by the current user | ``` GET /groups @@ -40,20 +41,6 @@ GET /groups You can search for groups by name or path, see below. -## List owned groups - -Get a list of groups which are owned by the authenticated user. - -``` -GET /groups/owned -``` - -Parameters: - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `statistics` | boolean | no | Include group statistics | - ## List a group's projects Get a list of projects in this group. diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 4c8818da1c5e97..b347616b1d7c80 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -43,4 +43,4 @@ changes are in V4: - Remove the ProjectGitHook API. Use the ProjectPushRule API instead [!1301](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1301) - Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384) - Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449) - +- Remove `GET /groups/owned`. Use `GET /groups?owned=true` instead [!9505](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9505) -- GitLab From 9a5cf07bbed4c4f8400f173749d2b7ae4c3edee3 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Fri, 24 Feb 2017 11:24:40 +0100 Subject: [PATCH 66/73] Simplyfy variables validation in triggers API --- lib/api/triggers.rb | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 87a717ba751257..ea0ad85263383d 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -21,14 +21,9 @@ class Triggers < Grape::API unauthorized! unless trigger.project == project # validate variables - variables = params[:variables] - if variables - unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) } - render_api_error!('variables needs to be a map of key-valued strings', 400) - end - - # convert variables from Mash to Hash - variables = variables.to_h + variables = params[:variables].to_h + unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) } + render_api_error!('variables needs to be a map of key-valued strings', 400) end # create request and trigger builds -- GitLab From 0bd35195e1f8441603fa4673a7da15c779b5d6e2 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 11 Nov 2016 11:57:43 +0530 Subject: [PATCH 67/73] Deleting a user shouldn't delete associated issues. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "Associated" issues are issues the user has created + issues that the user is assigned to. - Issues that a user owns are transferred to a "Ghost User" (just a regular user with `state = 'ghost'` that is created when `User.ghost` is called). - Issues that a user is assigned to are moved to the "Unassigned" state. - Fix a spec failure in `profile_spec` — a spec was asserting that when a user is deleted, `User.count` decreases by 1. After this change, deleting a user creates (potentially) a ghost user, causing `User.count` not to change. The spec has been updated to look for the relevant user in the assertion. --- app/models/user.rb | 2 ++ spec/models/user_spec.rb | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 7fb7adbead14fc..34089e77eb9752 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -197,6 +197,8 @@ def inactive_message "administrator if you think this is an error." end end + + state :ghost end mount_uploader :avatar, AvatarUploader diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index abacfe22d01845..fcba4ce1aa2b2f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1717,4 +1717,41 @@ def add_user(access) end end end + + describe '.ghost' do + it "creates a ghost user if one isn't already present" do + ghost = User.ghost + + expect(ghost).to be_ghost + expect(ghost).to be_persisted + end + + it "does not create a second ghost user if one is already present" do + expect do + User.ghost + User.ghost + end.to change { User.count }.by(1) + expect(User.ghost).to eq(User.ghost) + end + + context "when a regular user exists with the username 'ghost'" do + it "creates a ghost user with a non-conflicting username" do + create(:user, username: 'ghost') + ghost = User.ghost + + expect(ghost).to be_persisted + expect(ghost.username).to eq('ghost0') + end + end + + context "when a regular user exists with the email 'ghost@example.com'" do + it "creates a ghost user with a non-conflicting email" do + create(:user, email: 'ghost@example.com') + ghost = User.ghost + + expect(ghost).to be_persisted + expect(ghost.email).to eq('ghost0@example.com') + end + end + end end -- GitLab From ba4fe08c432bf5d296367405061491193b91cf76 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 6 Feb 2017 17:37:05 +0530 Subject: [PATCH 68/73] Use a `ghost` boolean to track ghost users. Rather than using a separate `ghost` state. This lets us have the benefits of both ghost and blocked users (ghost: true, state: blocked) without having to rewrite a number of queries to include cases for `state: ghost`. --- app/models/user.rb | 2 -- spec/models/user_spec.rb | 2 -- 2 files changed, 4 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 34089e77eb9752..7fb7adbead14fc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -197,8 +197,6 @@ def inactive_message "administrator if you think this is an error." end end - - state :ghost end mount_uploader :avatar, AvatarUploader diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index fcba4ce1aa2b2f..19b27e4df19af9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -220,14 +220,12 @@ it 'does not allow a non-blocked ghost user' do user = build(:user, :ghost) user.state = 'active' - expect(user).to be_invalid end it 'allows a blocked ghost user' do user = build(:user, :ghost) user.state = 'blocked' - expect(user).to be_valid end end -- GitLab From 1c0b0d70cbcbb00ba86cc652edf97e2743d755e4 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 24 Feb 2017 12:46:03 +0100 Subject: [PATCH 69/73] Fix broken links in CI admin area docs [ci skip] --- doc/user/admin_area/settings/continuous_integration.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index 518e4f699fdc70..ad2a97b9a770e8 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -36,6 +36,7 @@ expiration. 1. Hit **Save** for the changes to take effect. +<<<<<<< ba4fe08c432bf5d296367405061491193b91cf76 --- While the setting in the Admin area has a global effect, as an admin you can @@ -70,5 +71,5 @@ the group. ![Group pipelines quota](img/group_pipelines_quota.png) -[art-yml]: ../../../administration/build_artifacts -[duration-syntax]: ../../../ci/yaml/README#artifactsexpire_in +[art-yml]: ../../../administration/job_artifacts.md +[duration-syntax]: ../../../ci/yaml/README.md#artifactsexpire_in -- GitLab From 058aa9031bafc2b0fc38162440219b36ecdbdf70 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 24 Feb 2017 08:13:57 -0600 Subject: [PATCH 70/73] Fix spec --- spec/lib/gitlab/import_export/import_export_spec.rb | 2 +- spec/support/markdown_feature.rb | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/lib/gitlab/import_export/import_export_spec.rb b/spec/lib/gitlab/import_export/import_export_spec.rb index 20743811dab9b3..f3fd0d82875924 100644 --- a/spec/lib/gitlab/import_export/import_export_spec.rb +++ b/spec/lib/gitlab/import_export/import_export_spec.rb @@ -10,7 +10,7 @@ end it 'contains the namespace path' do - expect(described_class.export_filename(project: project)).to include(project.namespace.full_path) + expect(described_class.export_filename(project: project)).to include(project.namespace.full_path.tr('/', '_')) end it 'does not go over a certain length' do diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index 5980d14c8ae58c..dea0015f105fdd 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -79,9 +79,8 @@ def milestone def xproject @xproject ||= begin - group = create(:group, name: 'cross-reference') - group2 = create(:group, parent: group, name: 'nested-group') - create(:project, namespace: group2) do |project| + group = create(:group, :nested) + create(:project, namespace: group) do |project| project.team << [user, :developer] end end -- GitLab From 436017f8d5758c8e90908aca2b27914c852740ef Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Fri, 24 Feb 2017 11:31:11 -0600 Subject: [PATCH 71/73] Corrected indentation on the template string Also removed eslint disabled rules --- app/assets/javascripts/user_callout.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index 300d825ec9ae76..74b869502a4a37 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -1,4 +1,3 @@ -/* eslint-disable class-methods-use-this */ /* global Cookies */ const userCalloutElementName = '.user-callout'; @@ -16,15 +15,15 @@ const USER_CALLOUT_TEMPLATE = `
    -
    -

    - Customize your experience -

    -

    - Change syntax themes, default project pages, and more in preferences. -

    - Check it out -
    +
    +

    + Customize your experience +

    +

    + Change syntax themes, default project pages, and more in preferences. +

    + Check it out +
    `; -- GitLab From d0f059b410d8ae9db8e2253127a0a2573c6880d6 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 24 Feb 2017 17:42:21 -0600 Subject: [PATCH 72/73] Fix conflict resolution remnant --- doc/user/admin_area/settings/continuous_integration.md | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index ad2a97b9a770e8..9c1c0b745a4ece 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -36,7 +36,6 @@ expiration. 1. Hit **Save** for the changes to take effect. -<<<<<<< ba4fe08c432bf5d296367405061491193b91cf76 --- While the setting in the Admin area has a global effect, as an admin you can -- GitLab From e562593977a9012eeb7b0ec8b23a8c6c80aa0bc6 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 14 Feb 2017 19:52:44 -0600 Subject: [PATCH 73/73] Add MockCiService integration MR: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9250/ See https://gitlab.com/madlittlemods/gl-mock-ci-service --- app/controllers/concerns/service_params.rb | 1 + .../project_services/mock_ci_service.rb | 82 +++++++++++++++++++ app/models/service.rb | 5 +- changelogs/unreleased/mock-ci-service.yml | 4 + doc/api/services.md | 35 ++++++++ doc/development/ci_setup.md | 3 +- doc/user/project/integrations/mock_ci.md | 13 +++ lib/api/services.rb | 15 +++- 8 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 app/models/project_services/mock_ci_service.rb create mode 100644 changelogs/unreleased/mock-ci-service.yml create mode 100644 doc/user/project/integrations/mock_ci.md diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index abc2d3bac6008f..cbb0d34fe47733 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -33,6 +33,7 @@ module ServiceParams :issues_url, :jira_issue_transition_id, :merge_requests_events, + :mock_service_url, :namespace, :new_issue_url, :notify, diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb new file mode 100644 index 00000000000000..23964fca138abe --- /dev/null +++ b/app/models/project_services/mock_ci_service.rb @@ -0,0 +1,82 @@ +# For an example companion mocking service, see https://gitlab.com/gitlab-org/gitlab-mock-ci-service +class MockCiService < CiService + ALLOWED_STATES = %w[failed canceled running pending success success_with_warnings skipped not_found] + + prop_accessor :mock_service_url + validates :mock_service_url, presence: true, url: true, if: :activated? + + def title + 'MockCI' + end + + def description + 'Mock an external CI' + end + + def self.to_param + 'mock_ci' + end + + def fields + [ + { type: 'text', + name: 'mock_service_url', + placeholder: 'http://localhost:4004' }, + ] + end + + # Return complete url to build page + # + # Ex. + # http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c + # + def build_page(sha, ref) + url = [mock_service_url, + "#{project.namespace.path}/#{project.path}/status/#{sha}"] + + URI.join(*url).to_s + end + + # Return string with build status or :error symbol + # + # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped' + # + # + # Ex. + # @service.commit_status('13be4ac', 'master') + # # => 'success' + # + # @service.commit_status('2abe4ac', 'dev') + # # => 'running' + # + # + def commit_status(sha, ref) + response = HTTParty.get(commit_status_path(sha), verify: false) + read_commit_status(response) + rescue Errno::ECONNREFUSED + :error + end + + def commit_status_path(sha) + url = [mock_service_url, + "#{project.namespace.path}/#{project.path}/status/#{sha}.json"] + + URI.join(*url).to_s + end + + def read_commit_status(response) + return :error unless response.code == 200 || response.code == 404 + + status = if response.code == 404 + 'pending' + else + response['status'] + end + + if status.present? && ALLOWED_STATES.include?(status) + status + else + :error + end + end +end diff --git a/app/models/service.rb b/app/models/service.rb index 6cac1025bbc98c..2f95a857da9715 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -210,7 +210,7 @@ def issue_tracker? end def self.available_services_names - %w[ + service_names = %w[ asana assembla bamboo @@ -240,6 +240,9 @@ def self.available_services_names slack teamcity ] + service_names << 'mock_ci' if Rails.env.development? + + service_names.sort_by(&:downcase) end def self.build_from_template(project_id, template) diff --git a/changelogs/unreleased/mock-ci-service.yml b/changelogs/unreleased/mock-ci-service.yml new file mode 100644 index 00000000000000..24c6366177fc0f --- /dev/null +++ b/changelogs/unreleased/mock-ci-service.yml @@ -0,0 +1,4 @@ +--- +title: Add Mock CI service/integration for development +merge_request: +author: diff --git a/doc/api/services.md b/doc/api/services.md index 7864e7a16b1da7..fb02954346741b 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -881,3 +881,38 @@ GET /projects/:id/services/jenkins-deprecated [jira-doc]: ../user/project/integrations/jira.md [old-jira-api]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/api/services.md#jira + + +## MockCI + +Mock an external CI. See [`gitlab-org/gitlab-mock-ci-service`](https://gitlab.com/gitlab-org/gitlab-mock-ci-service) for an example of a companion mock service. + +This service is only available when your environment is set to development. + +### Create/Edit MockCI service + +Set MockCI service for a project. + +``` +PUT /projects/:id/services/mock-ci +``` + +Parameters: + +- `mock_service_url` (**required**) - http://localhost:4004 + +### Delete MockCI service + +Delete MockCI service for a project. + +``` +DELETE /projects/:id/services/mock-ci +``` + +### Get MockCI service settings + +Get MockCI service settings for a project. + +``` +GET /projects/:id/services/mock-ci +``` diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md index 2f49b3564ab493..b03216fec95880 100644 --- a/doc/development/ci_setup.md +++ b/doc/development/ci_setup.md @@ -2,11 +2,12 @@ This document describes what services we use for testing GitLab and GitLab CI. -We currently use three CI services to test GitLab: +We currently use four CI services to test GitLab: 1. GitLab CI on [GitHost.io](https://gitlab-ce.githost.io/projects/4/) for the [GitLab.com repo](https://gitlab.com/gitlab-org/gitlab-ce) 2. GitLab CI at ci.gitlab.org to test the private GitLab B.V. repo at dev.gitlab.org 3. [Semephore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for [GitHub.com repo](https://github.com/gitlabhq/gitlabhq) +4. [Mock CI Service](user/project/integrations/mock_ci.md) for local development | Software @ configuration being tested | GitLab CI (ci.gitlab.org) | GitLab CI (GitHost.io) | Semaphore | |---------------------------------------|---------------------------|---------------------------------------------------------------------------|-----------| diff --git a/doc/user/project/integrations/mock_ci.md b/doc/user/project/integrations/mock_ci.md new file mode 100644 index 00000000000000..6aefe5dbded69c --- /dev/null +++ b/doc/user/project/integrations/mock_ci.md @@ -0,0 +1,13 @@ +# Mock CI Service + +**NB: This service is only listed if you are in a development environment!** + +To setup the mock CI service server, respond to the following endpoints + +- `commit_status`: `#{project.namespace.path}/#{project.path}/status/#{sha}.json` + - Have your service return `200 { status: ['failed'|'canceled'|'running'|'pending'|'success'|'success_with_warnings'|'skipped'|'not_found'] }` + - If the service returns a 404, it is interpreted as `pending` +- `build_page`: `#{project.namespace.path}/#{project.path}/status/#{sha}` + - Just where the build is linked to, doesn't matter if implemented + +For an example of a mock CI server, see [`gitlab-org/gitlab-mock-ci-service`](https://gitlab.com/gitlab-org/gitlab-mock-ci-service) diff --git a/lib/api/services.rb b/lib/api/services.rb index 9b90b2426eaf4e..84985a1b756b83 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -611,7 +611,20 @@ class Services < Grape::API TeamcityService, JenkinsService, JenkinsDeprecatedService - ].freeze + ] + + if Rails.env.development? + services['mock-ci'] = [ + { + required: true, + name: :mock_service_url, + type: String, + desc: 'URL to the mock service' + } + ] + + service_classes << MockCiService + end trigger_services = { 'mattermost-slash-commands' => [ -- GitLab